Overview
C2C sends an HTTPPOST to your callback URL when an order reaches a final outcome, so you don’t have to poll. Webhooks are a notification. The order status endpoint remains the source of truth.
Configuring the callback URL
The URL is fixed when the order is created. Changing your portal setting later doesn’t affect orders that already exist. If neither URL is set, no webhook is sent for that order.
Use a publicly reachable endpoint that accepts
POST with a JSON body. callbackUrl must be an absolute http:// or https:// URL (any other scheme is rejected with 400). Use HTTPS in production.
The URL must resolve to a public internet address. Loopback, private, link-local (including 169.254.169.254) and similar addresses are refused when C2C delivers the webhook: that delivery fails and is retried and recorded like any other failure. Redirects aren’t followed, so point the URL at the final endpoint.
Events
Webhooks are sent for final outcomes only.
Example
reason values on order.failed: Checkout expired before payment, No eligible member became available in time, the member’s rejection reason, or the resolution note from C2C operations.
ManualReview doesn’t send a webhook. The order isn’t final: it’s being handled by C2C operations, and you can see it through status inquiry (status ManualReview). You receive order.completed or order.failed once it is resolved. Don’t refund or re-pay the customer in the meantime.Request format
X-C2C-Delivery-Id and X-C2C-Timestamp are informational and are not covered by the signature. Only the raw body is signed. Don’t use them as the only basis for a security decision.Payload: order.completed
Payload: order.failed
Verifying the signature
Webhooks are signed with your API key’s signing secret (sksec_...), the same secret you use to sign order requests. No separate webhook secret is needed. The algorithm is different, though: webhooks use HMAC-SHA256 over the raw body, as described below, while your order requests use the MD5 signature field.
Which key’s secret is used:
- The API key that created the order, while that key is still active.
- Otherwise (the key was revoked or rotated), your newest active API key.
X-C2C-Key-Prefix names the key, so if you hold several keys, or are mid-rotation, you can pick the matching secret instead of trying each one.
Every attempt is signed with the secret that is current at that attempt. A retry of a webhook first sent before a key rotation arrives signed with your new key’s secret. Deploy the new secret before the old key is revoked.
Legacy API keys issued without a signing secret get webhooks signed with a platform secret and no
X-C2C-Key-Prefix header. Ask C2C to regenerate the key to switch to per-merchant signing.- Read the raw body bytes before any JSON parsing.
- Compute
sha256=+ hex(HMAC-SHA256(signingSecret, rawBody)). - Compare with
X-C2C-Signatureusing a constant-time comparison. - Reject the request with
401if they don’t match, and don’t process it.
Responding and retries
- Return any
2xxstatus to acknowledge the event. The response body is ignored. - Respond within 5 seconds. Do the heavy work asynchronously after you acknowledge.
- A non-
2xxresponse, a timeout or a connection error counts as a failed delivery.
Each retry sends the identical body (same
timestamp) with the same X-C2C-Delivery-Id. Retries are processed once a minute, so an attempt can arrive up to a minute after the time shown. After the last attempt, reconcile with the status endpoint.
Newer events replace older ones
If an order produces a newer event while an earlier one is still being retried, the earlier delivery is superseded: its retries stop and only the newer event is delivered. For example, if an order fails and C2C operations then resolve it as completed, you receiveorder.completed, and the pending order.failed retries stop. A stale outcome never arrives after a newer one.
Resends by C2C operations
C2C operations can send a webhook again from the admin portal, for example after you fix your endpoint:- Retry now sends a failed delivery again immediately: the same body and the same
X-C2C-Delivery-Id. Only the latest delivery for an order can be retried. - Resend sends a new delivery for the order’s current final outcome. It has a new
X-C2C-Delivery-Idand a new bodytimestamp, but the sameorderIdandeventType.
Handling duplicates
You will sometimes receive the same event more than once, because of retries, network timeouts and resends by C2C operations. Make your handler idempotent:- Deduplicate on
orderId+eventType. Don’t deduplicate onX-C2C-Delivery-Idor the bodytimestampalone: a resend changes both. - Ignore duplicate events for orders you have already finalized, but still act on an
order.completedthat follows an earlierorder.failed(an order resolved by C2C operations). - Before fulfilling, re-check the status with Query order status. This also protects you if your signing secret is ever exposed.
Reconciliation
Run a periodic job, for example every 5–15 minutes, that queries the status of every order you created that:- is not yet final in your system, and
- is older than your expected completion time.
ManualReview and any delivery that never reached you.
Best practices
Verify every request
Verify every request
Reject unsigned or incorrectly signed callbacks with
401 and don’t act on them.Use HTTPS
Use HTTPS
Your callback URL should use HTTPS with a valid certificate.
Don't trust the payload alone for money movement
Don't trust the payload alone for money movement
Re-check the order status before you credit a customer or mark a payout as done.
Keep the secret server-side
Keep the secret server-side
The signing secret verifies webhooks and signs your API requests. Keep it in environment variables or a secret manager, never in code or logs.