Skip to main content

Overview

C2C sends an HTTP POST 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.
An order that failed can later be confirmed by C2C operations, for example after a dispute is resolved. You then receive order.completed after the earlier order.failed. Always act on the latest event, and confirm with a status query.

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:
  1. The API key that created the order, while that key is still active.
  2. 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.
To verify a webhook:
  1. Read the raw body bytes before any JSON parsing.
  2. Compute sha256= + hex(HMAC-SHA256(signingSecret, rawBody)).
  3. Compare with X-C2C-Signature using a constant-time comparison.
  4. Reject the request with 401 if they don’t match, and don’t process it.

Responding and retries

  • Return any 2xx status to acknowledge the event. The response body is ignored.
  • Respond within 5 seconds. Do the heavy work asynchronously after you acknowledge.
  • A non-2xx response, a timeout or a connection error counts as a failed delivery.
A failed delivery is retried with exponential backoff, up to 10 attempts in total: 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 receive order.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-Id and a new body timestamp, but the same orderId and eventType.
Both are signed with your current signing secret, like any other attempt. Because a resend is a new delivery, deduplicate on the order and its outcome, not on the delivery ID (see below).

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 on X-C2C-Delivery-Id or the body timestamp alone: a resend changes both.
  • Ignore duplicate events for orders you have already finalized, but still act on an order.completed that follows an earlier order.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.
That catches orders still in ManualReview and any delivery that never reached you.

Best practices

Reject unsigned or incorrectly signed callbacks with 401 and don’t act on them.
Your callback URL should use HTTPS with a valid certificate.
Re-check the order status before you credit a customer or mark a payout as done.
The signing secret verifies webhooks and signs your API requests. Keep it in environment variables or a secret manager, never in code or logs.