Webhooks

Real-time notifications

Overview

CrissCross uses webhooks to notify your application about events that occur within your payment processing. These notifications are delivered via HTTP POST requests to your specified endpoint, allowing you to automate workflows and keep your systems synchronized with payment activities.

Event Types

CrissCross sends different types of webhook events for both transactions (payments in) and payouts (payments out).

Transaction event types:

Event TypeDescription
transaction.completedTransaction was successfully completed
transaction.failedTransaction attempt failed authorisation
transaction.erroredTransaction encountered an error
transaction.cancelledTransaction was cancelled
transaction.expiredTransaction expired
transaction.settledTransaction settlement completed

Payout event types:

Event TypeDescription
payout.completedPayout was successfully delivered to the recipient
payout.failedPayout failed (e.g. invalid recipient account)
payout.erroredPayout encountered a retryable error
payout.cancelledPayout was cancelled
payout.expiredPayout expired

Transaction and payout webhooks share the same payload schema — it’s the same object returned by the Retrieve Payment Status endpoint. Whatever fields you see when polling for status, you’ll get the same fields in the webhook. The batchPayoutId field will additionally be present if the payout is part of a batch.

Payload Fields

The payload field is identical to the response body of the Retrieve Payment Status endpoint, so you can reuse the same parser for both. For convenience, the fields are:

FieldTypePresenceDescription
transactionIdstringAlwaysUnique transaction identifier
statusstringAlwaysCurrent status of the transaction or payout
messagestringAlwaysHuman-readable status message
merchantReferencestringOptionalMerchant reference from the original session
sessionIdstring (uuid)OptionalSession ID associated with the transaction
paymentMethodIdstringOptionalPayment method identifier
identifiersobjectOptionalAdditional identifiers related to the transaction
paymentAttributesobjectOptionalPayment-specific attributes and metadata
processorNamestringOptionalName of the payment processor
processorReferencestringOptionalPayment processor reference
financialTransactionReferencestringOptionalFinancial transaction reference returned by the processor
currentAttemptIdstringOptionalCurrent attempt identifier
batchPayoutIdstringOptionalBatch payout identifier (present for batch payouts)
authStateobjectOptionalAuthentication state with state, transitionedAt, and message

Example Webhook Payloads

Here’s an example of a completed transaction:

1{
2 "eventType": "transaction.completed",
3 "eventId": "evt_1234567890",
4 "timestamp": "2025-07-21T10:30:00Z",
5 "payload": {
6 "transactionId": "019b024f-8c57-777f-a97c-fa21a2bdbb40",
7 "status": "COMPLETED",
8 "message": "Transaction completed",
9 "merchantReference": "ORDER-2025-001",
10 "sessionId": "5f1a3b6c-2d4e-4f8a-9b1c-7d2e3f4a5b6c",
11 "paymentMethodId": "card",
12 "processorName": "example-processor",
13 "processorReference": "PROC_REF_123",
14 "identifiers": {},
15 "paymentAttributes": {}
16 }
17}

Here’s an example of a completed payout:

1{
2 "eventType": "payout.completed",
3 "eventId": "evt_9876543210",
4 "timestamp": "2025-07-21T11:00:00Z",
5 "payload": {
6 "transactionId": "019b024f-8c57-777f-a97c-fa21a2bdbb40",
7 "status": "COMPLETED",
8 "message": "Payout completed",
9 "merchantReference": "PAYOUT_550e8400-e29b-41d4-a716-446655440000",
10 "paymentMethodId": "mobilemoney",
11 "identifiers": {},
12 "paymentAttributes": {},
13 "processorReference": "LK_REF_123",
14 "currentAttemptId": "ATTEMPT_456",
15 "authState": {
16 "state": "completed",
17 "transitionedAt": "2025-07-21T11:00:00Z",
18 "message": "Payout completed"
19 }
20 }
21}

Getting Started with Webhooks

To integrate webhooks into your application, follow these steps:

  1. Create a webhook subscription - Configure the HTTPS URL where you want to receive notifications
  2. Implement signature verification - Verify webhook authenticity using the provided secret
  3. Process webhooks idempotently - Handle webhook events while avoiding duplicate processing
  4. Acknowledge receipt - Return HTTP 200 response for successful webhook receipt

Creating a Webhook Subscription

You can create and configure your webhook subscriptions from the webhooks section of your CrissCross dashboard.

  1. Open the webhooks dashboard
  2. Click “Add Endpoint”
  3. Enter your HTTPS endpoint URL
  4. Select the events you want to subscribe to
  5. Save your webhook configuration

If you don’t see the webhooks section in your dashboard, contact your CrissCross account manager.

Securing Webhooks

Every webhook delivered by CrissCross includes a signature header that you should verify before processing the event. Signatures are HMAC-SHA256 over the message ID, timestamp, and raw payload.

The example below uses the open-source svix npm package, which implements the same signature scheme. If you’d rather not pull in a dependency, see Manual Webhook Verification further down for a hand-rolled equivalent.

1import { Webhook } from "svix";
2import bodyParser from "body-parser";
3
4const secret = "YOUR_WEBHOOK_SECRET"; // From subscription creation
5
6app.post(
7 "/webhooks",
8 bodyParser.raw({ type: "application/json" }),
9 async (req, res) => {
10 const payload = req.body;
11 const headers = req.headers;
12
13 const wh = new Webhook(secret);
14 let msg;
15
16 try {
17 msg = wh.verify(payload, headers);
18 } catch (err) {
19 return res.status(400).json({
20 message: err.toString()
21 });
22 }
23
24 const event = JSON.parse(payload);
25
26 switch(event.eventType) {
27 case 'transaction.completed':
28 await handleTransactionCompleted(event.payload);
29 break;
30 case 'transaction.failed':
31 await handleTransactionFailed(event.payload);
32 break;
33 case 'payout.completed':
34 await handlePayoutCompleted(event.payload);
35 break;
36 // Handle other event types
37 }
38
39 res.json({ received: true });
40 }
41);

Manual Webhook Verification

If you’d prefer not to use the svix library, you can verify webhook signatures manually with any HMAC-SHA256 implementation. Here’s how it works:

1. Required Headers

Each webhook includes three critical headers:

  • svix-id: Unique identifier for the webhook message
  • svix-timestamp: Timestamp in seconds since epoch
  • svix-signature: Base64 encoded list of signatures (space delimited)

2. Constructing the Signed Content

Concatenate the ID, timestamp, and payload with periods:

1const signedContent = `${svix_id}.${svix_timestamp}.${body}`;

⚠️ Important: Use the raw request body. Any modification (even whitespace) will invalidate the signature.

3. Calculating the Signature

Use HMAC-SHA256 to verify the signature:

1const crypto = require('crypto');
2
3function verifyWebhook(payload, headers, secret) {
4 const svix_id = headers['svix-id'];
5 const svix_timestamp = headers['svix-timestamp'];
6 const svix_signature = headers['svix-signature'];
7
8 // Construct the signed content
9 const signedContent = `${svix_id}.${svix_timestamp}.${payload}`;
10
11 // Extract and decode the secret
12 const secretBytes = Buffer.from(secret.split('_')[1], "base64");
13
14 // Calculate expected signature
15 const expectedSignature = crypto
16 .createHmac('sha256', secretBytes)
17 .update(signedContent)
18 .digest('base64');
19
20 // Get received signatures (can be multiple)
21 const receivedSignatures = svix_signature.split(' ').map(sig => {
22 // Remove version prefix (e.g., "v1,")
23 return sig.split(',')[1];
24 });
25
26 // Verify if any signature matches
27 return receivedSignatures.includes(expectedSignature);
28}

4. Timestamp Verification

Always verify the timestamp to prevent replay attacks:

1function isTimestampValid(timestamp, toleranceInSeconds = 300) {
2 const now = Math.floor(Date.now() / 1000);
3 return Math.abs(now - timestamp) <= toleranceInSeconds;
4}

Example Implementation

Here’s a complete example combining all verification steps:

1function verifyAndProcessWebhook(req, res) {
2 const secret = process.env.WEBHOOK_SECRET;
3 const payload = req.body;
4 const headers = req.headers;
5
6 try {
7 // 1. Verify timestamp
8 if (!isTimestampValid(headers['svix-timestamp'])) {
9 return res.status(400).json({ error: 'Invalid timestamp' });
10 }
11
12 // 2. Verify signature
13 if (!verifyWebhook(payload, headers, secret)) {
14 return res.status(400).json({ error: 'Invalid signature' });
15 }
16
17 // 3. Process webhook
18 const event = JSON.parse(payload);
19 processWebhookEvent(event);
20
21 res.json({ received: true });
22 } catch (err) {
23 res.status(400).json({ error: err.message });
24 }
25}

🔒 Security Note: Always use constant-time string comparison when comparing signatures to prevent timing attacks.

Best Practices

  1. Verify Signatures

    • Always verify webhook signatures using your webhook secret
    • Reject requests with invalid signatures
  2. Handle Events Idempotently

    • Store processed webhook IDs to prevent duplicate processing
    • Use event IDs for deduplication
  3. Implement Proper Error Handling

    • Return 2xx status codes for successful receipt
    • Return 4xx for invalid requests
    • Return 5xx for processing errors
  4. Monitor Webhook Health

    • Track failed deliveries in the CrissCross dashboard
    • Set up alerts for repeated failures

Testing Webhooks

For development and testing:

  1. Use the webhooks dashboard to view delivery history and inspect individual attempts
  2. Test signature verification with sample payloads
  3. Use the Resend action on any past message to redeliver it to your endpoint while iterating
  4. If you don’t have an endpoint ready yet, Svix Play gives you a temporary public URL that captures incoming webhooks so you can inspect them in the browser

Webhook Retries

If your endpoint returns a non-2xx response code or is unreachable, CrissCross automatically retries delivery on an exponential backoff schedule. Each delay is measured from the failure of the previous attempt:

AttemptDelay after previous failure
Initial attemptImmediately when the event is generated
Retry 15 seconds
Retry 25 minutes
Retry 330 minutes
Retry 42 hours
Retry 55 hours
Retry 610 hours
Retry 710 hours

After all retries are exhausted (roughly 28 hours after the initial attempt), the message is marked as Failed and no further automatic delivery attempts will be made. You can still recover it manually — see Resending and Replaying Webhooks.

A successful delivery at any point ends the retry sequence. A 2xx response from your endpoint is treated as success; anything else (including 3xx redirects and timeouts) counts as a failure.

Delivery Failure Notifications

CrissCross emits two additional notifications you can subscribe to in order to monitor the health of your webhook integration:

EventWhen it fires
message.attempt.exhaustedA message exhausted all automatic retries and is now in the Failed state.
endpoint.disabledAn endpoint was automatically disabled after sustained delivery failures (see below).

These are configured the same way as your other event subscriptions from the webhooks dashboard.

Endpoint Auto-Disabling

If an endpoint experiences sustained delivery failures, CrissCross will automatically disable it to avoid sending traffic to a known-broken receiver. The criteria are:

  • All delivery attempts to the endpoint have failed for at least 5 consecutive days, and
  • Multiple deliveries failed within a 24-hour span, with at least 12 hours between the first and last failure (this prevents brief outages from triggering disablement).

When this happens, an endpoint.disabled notification is sent. Re-enable the endpoint from the webhooks dashboard once the underlying issue is resolved. Use recover or replay-missing to backfill any messages that were not delivered while the endpoint was disabled.

Resending and Replaying Webhooks

CrissCross supports four manual recovery operations from the webhooks dashboard, covering anything from a one-off resend to backfilling an endpoint that was down for hours.

OperationUse it when
Resend a single messageYou want to redeliver one specific event to one endpoint — for example, after fixing a bug in your handler.
Replay missing messagesAn endpoint was down or misconfigured and you want to deliver only the messages that never succeeded since a given timestamp. Messages already delivered successfully are skipped.
Recover failed messagesYou want to redeliver every message that ended in the Failed state since a given timestamp. Messages that eventually succeeded (even after retries) are not resent.
Bulk replayYou want to redeliver every message — successful and failed — since a given timestamp. Useful when rebuilding downstream state from scratch. Be aware your endpoint will receive duplicates of events it has already processed.

How to run a recovery

  1. Open the webhooks dashboard and select the endpoint you want to recover.
  2. For a single message, find the message in the delivery log and click Resend.
  3. For the bulk operations, open the endpoint’s actions menu, choose Recover or Replay, and pick the start timestamp. The dashboard will show progress and a count of messages re-queued.

Because these operations can deliver the same eventId more than once, handlers must be idempotent — see the idempotency guidance and deduplicate on eventId.

ℹ️ If you need to recover messages older than the retention window, contact your CrissCross account manager.

IP Allowlisting

If your infrastructure requires IP allowlisting for incoming webhook traffic, contact your CrissCross account manager for the current list of source IP ranges. The list is updated periodically, so we recommend confirming it before any allowlist change is rolled out to production.

Webhook Security Checklist

✅ Use HTTPS endpoints only
✅ Verify webhook signatures
✅ Store webhook secrets securely
✅ Process events idempotently
✅ Monitor failed deliveries
✅ Implement proper error handling