Webhooks
When a payment happens, successfully or unsuccessfully -- we'll send a webhook to your server to let you know. Webhooks are the best way to stay on top of what's happening with your payments, and you should use them as your source of truth for payment status.
Setting things up
You set up webhook subscriptions through Basecamp. For each subscription you need:
- A target URL: The HTTPS endpoint on your server where we should send events.
- An HMAC secret: A shared secret we use to sign payloads so you can verify they came from us.
Your target URL needs to be publicly reachable over HTTPS. We can't send webhooks to private IPs - your webhook url must be accessible via the public internet.
What events you'll get
Here are the payment events that trigger a webhook:
| Event | What happened |
|---|---|
authorisation | A payment was authorised (or turned down). |
capture | A payment was captured. |
cancellation | A payment was cancelled. |
cancel.or.refund | A reversal went through (either a cancel or a refund). |
refund | A refund was processed. |
refund.with.data | A refund was processed, with extra data attached. |
chargeback | A chargeback came in. |
notification.of.fraud | A fraud notification was flagged. |
Payments made through the ECOM API have "paymentSource": "Ecom" in their metadata, so you can tell them apart from other payment types.
Choosing which notifications you receive
Webhook subscriptions can optionally be created with an eventCode so you only receive the notifications you care about.
- If
eventCodeis omitted: you'll receive all events for that webhook type (legacy behaviour). - If
eventCodeis set: you'll only receive events matching that code.
For payment webhooks, eventCode matches the event names in the table above (e.g. authorisation, refund, chargeback).
Merchant alerts (merchant-level webhooks)
In addition to payment webhooks (which are created per-site), we also support merchant-level webhooks for operational alerts.\n\nThese use webhook type MERCHANT_ALERT and can optionally be filtered by eventCode (for example CHARGEBACK, MOTO_LIMIT_REACHED, HIGH_VALUE_PAYMENT, etc.). If eventCode is omitted, you'll receive all merchant alert events.
What a webhook looks like
We send each webhook as an HTTP POST to your target URL with a JSON body.
Headers we send:
| Header | What it is |
|---|---|
Content-Type | application/json |
User-Agent | Yetipay-Dispatch/1.0 |
X-Webhook-Id | A unique ID for this particular webhook delivery. |
X-Webhook-Timestamp | Unix timestamp (in seconds) of when we signed the payload. |
X-Webhook-HMAC-Signature | The HMAC-SHA256 signature. Looks like sha256={hex}. |
X-Webhook-Delivery-Attempt | Which attempt this is (starts at 1). |
X-Webhook-Payload-Version | The payload format version. |
Example payload:
{
"live": true,
"notificationItems": [
{
"NotificationRequestItem": {
"eventCode": "AUTHORISATION",
"success": "true",
"pspReference": "8835612345678901",
"merchantReference": "order-12345",
"amount": {
"value": 2500,
"currency": "GBP"
},
"paymentMethod": "visa",
"additionalData": {
"paymentSource": "Ecom",
"tokenization.store.operationType": "created",
"tokenization.storedPaymentMethodId": "8415xxxxxxxxxx",
"recurring.shopperReference": "guest-98765"
}
}
}
]
}
Checking that a webhook is genuine
Before you do anything with a webhook, make sure it actually came from us. We sign every payload with your HMAC secret so you can verify it hasn't been tampered with.
How to verify
- Grab the
X-Webhook-TimestampandX-Webhook-HMAC-Signatureheaders. - Build the signed string:
{timestamp}.{raw request body}. - Compute the HMAC-SHA256 of that string using your HMAC secret.
- Compare it with the signature in the header (after removing the
sha256=prefix). Use a constant-time comparison to be safe.
Node.js example
const crypto = require('crypto');
function isWebhookGenuine(rawBody, headers, hmacSecret) {
const timestamp = headers['x-webhook-timestamp'];
const receivedSignature = headers['x-webhook-hmac-signature'];
if (!timestamp || !receivedSignature) {
return false;
}
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = crypto.createHmac('sha256', hmacSecret).update(signedPayload, 'utf8').digest('hex');
const expected = `sha256=${expectedSignature}`;
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(receivedSignature));
}
Python example
import hmac
import hashlib
def is_webhook_genuine(raw_body: str, headers: dict, hmac_secret: str) -> bool:
timestamp = headers.get("x-webhook-timestamp")
received_signature = headers.get("x-webhook-hmac-signature")
if not timestamp or not received_signature:
return False
signed_payload = f"{timestamp}.{raw_body}"
expected = hmac.new(
hmac_secret.encode("utf-8"),
signed_payload.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", received_signature)
If we can't reach you
If your server doesn't come back with a 2xx status code, we'll try again:
- Up to 5 attempts in total.
- 15-second timeout on each attempt.
The X-Webhook-Delivery-Attempt header tells you which attempt you're on. The X-Webhook-Id stays the same across retry attempts for the same delivery, so you can use it to deduplicate. If all 5 fail, we stop trying - sorry.
Tips
- Always check the signature before doing anything with the payload. See Checking that a webhook is genuine.
- Reply quickly. If you need to do something slow, pop the event onto a queue and return
200straight away. If your endpoint takes too long, we might time out and retry -- which means you could end up processing the same event twice. - Handle duplicates. Because of retries, you might get the same event more than once. Use the
X-Webhook-IdorpspReferenceto spot duplicates. - Treat webhooks as the source of truth. The response you get from a payment request tells you the initial result, but the webhook confirms what actually happened. Always reconcile your records against webhooks.