Skip to navigation

Integration Examples

Working webhook handlers for Node.js and Python, and payloads for the chargeback, funding and statement events. The Webhooks Overview explains the envelope, the signature, duplicates and retries.

Each handler does four things, in this order:

  • Checks the signature against the raw body and responds 401 if it doesn’t match.
  • Skips the event if its metadata.idempotency_key has already been handled.
  • Saves the event, then records the key. If saving fails, it responds 500 so the event is retried.
  • Responds 200 straight away. Do any slow work later, from what you saved.

isValidSignature and is_valid_signature are the functions from Verifying Signatures. The alreadyHandled, saveEvent and markHandled functions stand in for your own storage.

Node.js / Express

const express = require("express");
const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
// isValidSignature: see Verifying Signatures in the Webhooks Overview.
// Keep the raw body: the signature is computed over the exact bytes sent.
app.post("/webhooks/run", express.raw({ type: "*/*" }), async (req, res) => {
if (!isValidSignature(req.body, req.get("X-Webhook-Signature-256"), WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const webhook = JSON.parse(req.body.toString("utf8"));
const key = webhook.metadata.idempotency_key;
if (await alreadyHandled(key)) {
return res.sendStatus(200);
}
try {
await saveEvent(webhook);
await markHandled(key);
} catch (err) {
console.error("Could not save webhook", err);
return res.sendStatus(500);
}
res.sendStatus(200);
});
app.listen(3000);

Python / Flask

import json
import os
from flask import Flask, request
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["WEBHOOK_SECRET"]
# is_valid_signature: see Verifying Signatures in the Webhooks Overview.
@app.post("/webhooks/run")
def handle_webhook():
raw_body = request.get_data() # raw bytes, before JSON parsing
if not is_valid_signature(raw_body, request.headers.get("X-Webhook-Signature-256"), WEBHOOK_SECRET):
return "", 401
webhook = json.loads(raw_body)
key = webhook["metadata"]["idempotency_key"]
if already_handled(key):
return "", 200
try:
save_event(webhook)
mark_handled(key)
except Exception:
app.logger.exception("Could not save webhook")
return "", 500
return "", 200

Reporting Event Payloads

These are the payload objects for the reporting events. They arrive in the standard envelope, and source_id is the Run merchant ID. Dates in these payloads are strings in MM/DD/YYYY format.

chargeback.entered

Sent when a chargeback is recorded for a merchant.

{
"event_table_pk": 82043,
"event_type": "chargeback.entered",
"mid": "800000000000",
"case_number": "700000000001",
"card_number": "51XXXXXXXXXX0000",
"card_brand_id": 2,
"transaction_date": "10/01/2026",
"transaction_amount": 44.00,
"chargeback_date": "10/23/2026",
"chargeback_amount": 44.00,
"chargeback_reason": "No Cardholder Authorization",
"_timestamp": "2026-10-23T04:16:57.310+00:00"
}
FieldData TypeComments
event_table_pkIntegerRun’s ID for the chargeback record.
event_typeStringAlways chargeback.entered.
midStringMerchant ID assigned by the processor.
case_numberStringChargeback case number from the processor.
card_numberStringMasked card number.
card_brand_idIntegerCard brand of the disputed transaction.
transaction_dateStringDate of the original transaction (MM/DD/YYYY).
transaction_amountNumberAmount of the original transaction.
chargeback_dateStringDate of the chargeback (MM/DD/YYYY).
chargeback_amountNumberAmount charged back.
chargeback_reasonStringReason given for the chargeback.
_timestampStringWhen the event was created, in ISO 8601 format with a UTC offset.

funding.entered

Sent when funding information is recorded for a merchant.

{
"event_table_pk": 5242032,
"event_type": "funding.entered",
"mid": "800000000000",
"funding_master_id": "20000001",
"dda_number": "XXXXX12345",
"aba_number": "011000015",
"funding_date": "11/02/2026",
"dba_name": "Example Coffee Co",
"net_sales_amount": 1250.00,
"third_party_amount": 0,
"adjustments_amount": 0,
"interchange_fees_amount": -21.40,
"service_charges_amount": -3.15,
"fee_amount": -7.65,
"reversals_amount": 0,
"other_adjustments_amount": 0,
"funding_amount": 1217.80,
"_timestamp": "2026-11-02T04:16:57.309+00:00"
}
FieldData TypeComments
event_table_pkIntegerRun’s ID for the funding record.
event_typeStringAlways funding.entered.
midStringMerchant ID assigned by the processor.
funding_master_idStringProcessor’s ID for the funding batch.
dda_numberStringNumber of the bank account funded, masked to its last 5 digits.
aba_numberStringRouting number of the bank account funded. Sometimes masked.
funding_dateStringDate of the deposit (MM/DD/YYYY).
dba_nameStringMerchant’s DBA name.
net_sales_amountNumberNet sales in this funding.
third_party_amountNumberAmount paid by third parties.
adjustments_amountNumberAdjustments.
interchange_fees_amountNumberInterchange fees.
service_charges_amountNumberService charges.
fee_amountNumberFees.
reversals_amountNumberReversals.
other_adjustments_amountNumberOther adjustments.
funding_amountNumberTotal amount deposited. Can be negative.
_timestampStringWhen the event was created, in ISO 8601 format with a UTC offset.

statement.entered

Sent when a merchant statement is available.

{
"event_table_pk": 31500,
"event_type": "statement.entered",
"mid": "800000000000",
"statement_date": "04/01/2026",
"statement_filename": "800000000000_0000_B000_APR-01-2026.pdf",
"_timestamp": "2026-04-01T22:16:31.924+00:00"
}
FieldData TypeComments
event_table_pkIntegerRun’s ID for the statement record.
event_typeStringAlways statement.entered.
midStringMerchant ID assigned by the processor.
statement_dateStringStatement date (MM/DD/YYYY).
statement_filenameStringFile name of the statement.
_timestampStringWhen the event was created, in ISO 8601 format with a UTC offset.