Skip to main content

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:

EventWhat happened
authorisationA payment was authorised (or turned down).
captureA payment was captured.
cancellationA payment was cancelled.
cancel.or.refundA reversal went through (either a cancel or a refund).
refundA refund was processed.
refund.with.dataA refund was processed, with extra data attached.
chargebackA chargeback came in.
notification.of.fraudA 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 eventCode is omitted: you'll receive all events for that webhook type (legacy behaviour).
  • If eventCode is 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:

HeaderWhat it is
Content-Typeapplication/json
User-AgentYetipay-Dispatch/1.0
X-Webhook-IdA unique ID for this particular webhook delivery.
X-Webhook-TimestampUnix timestamp (in seconds) of when we signed the payload.
X-Webhook-HMAC-SignatureThe HMAC-SHA256 signature. Looks like sha256={hex}.
X-Webhook-Delivery-AttemptWhich attempt this is (starts at 1).
X-Webhook-Payload-VersionThe 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

  1. Grab the X-Webhook-Timestamp and X-Webhook-HMAC-Signature headers.
  2. Build the signed string: {timestamp}.{raw request body}.
  3. Compute the HMAC-SHA256 of that string using your HMAC secret.
  4. 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 200 straight 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-Id or pspReference to 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.