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.
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"
}
}
}
]
}
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. 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.