> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thedecard.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook configuration

> How to register the callback URL, recompute and verify X-Signature with sortedCompactJson, deduplicate by webhookId, and what the delivery and retry windows are.

## 📄 Guide

Organization onboarding, employee creation, card issuing, transactions and deposits are all asynchronous: the platform pushes the final state to the callback URL you registered as soon as the event happens, so you never have to poll. Your receiver only needs **one** endpoint — every event shares the same envelope, so you read `webhookType` and then parse `data`. The full event list and each event's `data` structure are in [Events and data structures](./events-and-schema).

## 1. Registering the callback URL

The callback URL is configured for you by platform operations; there is no self-service endpoint, so contact your DCS representative to change it. **Sandbox and production are configured separately** and must both be registered.

<Warning>
  With no callback URL configured, any path that needs your participation fails outright — a 3DS challenge cannot be forwarded to you and the transaction is declined. Configure the callback URL before going live with OOB or with `otpSendMode=ENTERPRISE`.
</Warning>

### Requirements for the callback URL

| Requirement | Detail                                                                                                           |
| :---------- | :--------------------------------------------------------------------------------------------------------------- |
| Protocol    | HTTPS, with a certificate that validates publicly                                                                |
| Method      | Accept `POST` with a JSON body                                                                                   |
| Response    | HTTP `2xx` counts as a successful delivery; the platform **does not parse the response body**                    |
| Latency     | Return quickly — persist first and process asynchronously, never do long-running work inside the callback        |
| Idempotency | The same `webhookId` may arrive more than once; deduplicate on it and **still return `2xx`** after deduplicating |

## 2. Verifying the signature (HMAC-SHA256)

Every delivery carries `X-Signature`:

```
X-Signature = Hex( HMAC-SHA256( SK, sortedCompactJson(entire payload) ) )
```

`SK` is the webhook signing key issued to you specifically, isolated per partner. It is **not** the same key you use for API request signing.

### Headers

| Header        | Detail                               |
| :------------ | :----------------------------------- |
| `X-Signature` | The signature, lowercase hexadecimal |
| `Accept`      | `application/json`                   |

The event type, timestamp and IDs all live in the body — **never in headers**.

<Warning>
  **You cannot verify against the raw bytes.** `sortedCompactJson` means: sort the fields at **every level** of the payload by field name, then serialise compactly with no whitespace. You must re-serialise the same way before comparing, otherwise the signature will never match.
</Warning>

### Verification example

```python theme={null}
import hmac, hashlib, json

def sorted_compact_json(obj):
    # every level sorted by field name, then compact serialisation
    return json.dumps(obj, sort_keys=True, separators=(',', ':'), ensure_ascii=False)

def verify(payload_dict: dict, header_signature: str, sk: str) -> bool:
    body = sorted_compact_json(payload_dict).encode('utf-8')
    expected = hmac.new(sk.encode('utf-8'), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header_signature.lower())
```

Discard and alert on a failed verification; never process it as business data.

## 3. Delivery, retries and ordering

| Item            | Contract                                                                                                                                         |
| :-------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- |
| Success         | HTTP `2xx`; the response body is not parsed                                                                                                      |
| Retry trigger   | Any non-`2xx`, or a timeout                                                                                                                      |
| Retry cadence   | Queued for redelivery and swept roughly once a minute; at most **3** retries, about 1 minute apart, **no exponential backoff**                   |
| Total window    | First attempt plus retries is at most **4** deliveries, finishing within about **3 minutes**                                                     |
| Still failing   | The platform alerts and follows up manually                                                                                                      |
| Idempotency key | `webhookId` is globally unique and **reused across every retry** of the same event                                                               |
| Ordering        | **Not guaranteed.** Order by `webhookId` (a monotonically increasing snowflake ID) plus the resource's current state; never assume arrival order |
| Fallback        | If nothing arrives within the window, reconcile through the corresponding query endpoint                                                         |

<Warning>
  The retry window is only about 3 minutes. A receiver that is down longer than that will miss events, so you **must** reconcile through the query endpoints — do not rely on webhooks alone.
</Warning>

## Next steps

* The full event list, envelope fields and each event's `data`: [Events and data structures](./events-and-schema)
* Receiving and replying to 3DS challenges: [3DS challenges](../3ds-challenges)
