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

# Receive events on your server

> Register an https endpoint, verify the signature over the raw bytes, and read the delivery log when your handler goes quiet.

Your dashboard should never poll. Register an https endpoint and the platform POSTs a signed JSON
body to it every time one of your firm's events fires — registrations linking, accounts resetting,
balances recording, risk locking and unlocking.

Everything here runs with your partner key against `/api/partner/` routes.

<Steps>
  <Step title="Register your endpoint">
    [Create a webhook](/api-reference/webhooks/create-a-webhook) takes an `https` URL of at most 500
    characters. Omit `events` to receive everything, including event types added later; name a
    subset to filter.

    ```bash theme={null}
    curl -X POST "$TRDRS_API_BASE_URL/api/partner/webhooks" \
      -H "Authorization: Bearer $TRDRS_PARTNER_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://ops.yourfirm.example/hooks/trdrs",
        "events": ["registration.linked", "risk.locked", "balance.recorded"]
      }'
    ```

    Five endpoints per firm — a sixth answers `409`, so delete one first. Plain `http` is refused
    with a `400`, as is an unknown event type, and the error names the valid types. A host that
    resolves to a private address is refused at delivery time.

    The response carries the signing `secret`. It is shown on every read, not once, because it
    authenticates us to your endpoint and grants no access here — so
    [listing your webhooks](/api-reference/webhooks/list-your-webhooks) later will show it again.
  </Step>

  <Step title="Prove the wiring with a test delivery">
    [Create a test delivery](/api-reference/webhooks/create-a-test-delivery) sends a `ping` to one
    of your endpoints immediately, signed exactly like a real event. This is the call to run while
    you are building the receiver: it exercises the real signature, the real headers, and the real
    network path.

    ```bash theme={null}
    curl -X POST "$TRDRS_API_BASE_URL/api/partner/webhooks/test" \
      -H "Authorization: Bearer $TRDRS_PARTNER_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "id": "whk_4d19c2" }'
    ```

    An endpoint that refuses the ping is still a `200` here, with `ok: false`, the `status` your
    endpoint returned, and a `detail` saying why. The request succeeded at what it was asked to do;
    reporting your endpoint's failure as our error would tell you the wrong thing. A test delivery
    is not retried and is not written to the log.
  </Step>

  <Step title="Verify every delivery over the raw bytes">
    Each delivery carries a `trdrs-signature` header of the form `t=<epoch seconds>,v1=<64 hex
            characters>`. `v1` is an HMAC-SHA256, keyed with your endpoint's signing secret, over the string
    `<t>.<raw request body>`.

    ```ts theme={null}
    import { createHmac, timingSafeEqual } from 'node:crypto'

    // rawBody is the exact bytes we sent — verify before JSON.parse, not after.
    function verify(secret: string, header: string, rawBody: string): boolean {
      const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header)
      if (!m) return false
      if (Math.abs(Math.floor(Date.now() / 1000) - Number(m[1])) > 300) return false // stale
      const expected = createHmac('sha256', secret).update(`${m[1]}.${rawBody}`).digest()
      const given = Buffer.from(m[2], 'hex')
      return expected.length === given.length && timingSafeEqual(expected, given)
    }
    ```

    Two rules make this work. The timestamp is inside the signed material, so a captured body
    cannot be replayed under a fresh clock — reject anything outside a five-minute window. And you
    must verify against the bytes you received: parsing the JSON and re-serializing it changes the
    whitespace, and the signature will not match.
  </Step>

  <Step title="Answer 2xx fast, and tolerate a duplicate">
    Answer `2xx` to accept a delivery. Anything else — including a timeout — is retried with
    backoff for about nine hours in total, so your endpoint may see the same event twice.

    Key your handler on the delivery `id`, or on the natural key in the payload: `referenceId` on
    balance and reset events, `registrationId` on registration events, `accountNumber` on risk
    events. Acknowledge first and do your processing afterwards; a handler that finishes its work
    before responding will time out and be retried even though it succeeded.
  </Step>

  <Step title="Read the log when something goes quiet">
    [List webhook deliveries](/api-reference/webhooks/list-webhook-deliveries) is the log for one
    endpoint, newest first: what we sent, what your endpoint answered, and what is still queued.

    ```bash theme={null}
    curl "$TRDRS_API_BASE_URL/api/partner/webhooks/deliveries?id=whk_4d19c2&limit=100" \
      -H "Authorization: Bearer $TRDRS_PARTNER_KEY"
    ```

    `limit` is 1 to 200 and defaults to 50. Out of range is a `400`, never a silent clamp.
  </Step>
</Steps>

## The event catalog

| Event                  | Fires when                                                                                                  |
| ---------------------- | ----------------------------------------------------------------------------------------------------------- |
| `registration.linked`  | A trader signed in and linked an account your firm registered.                                              |
| `registration.revoked` | Your firm revoked a pending registration.                                                                   |
| `account.reset`        | An evaluation account your firm issued was reset.                                                           |
| `balance.recorded`     | A credit, debit, or adjustment landed on one of your accounts.                                              |
| `risk.locked`          | Trading was locked on one of your accounts — a risk control firing, an evaluation breach, or your own halt. |
| `risk.unlocked`        | That lock cleared.                                                                                          |

Every delivery uses the same envelope: a `type`, a `createdAt`, and a `data` object carrying the
event's own fields.

```json theme={null}
{
  "type": "risk.locked",
  "createdAt": "2026-08-24T19:41:07Z",
  "data": {
    "accountNumber": "EVAL-7C21A9",
    "reason": "eval_breach"
  }
}
```

An endpoint registered without an `events` filter receives every type in this table, and every type
added to it later. Name the events explicitly if you would rather opt in deliberately.

## Reading a delivery row

| Field                         | What it tells you                                                                                                            |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `status`                      | `delivered` is done. `pending` is still inside the retry schedule. `failed` means the schedule was exhausted and we stopped. |
| `attempts`                    | How many times we have dialed.                                                                                               |
| `responseStatus`              | The HTTP status your endpoint returned, or null when the dial never completed.                                               |
| `lastError`                   | The reason for the most recent failure.                                                                                      |
| `createdAt` / `lastAttemptAt` | When the event fired, and when we last tried.                                                                                |
| `payload`                     | The exact JSON body we sent, or will send. An event your receiver dropped can be replayed from here rather than lost.        |

Terminal rows age out after 60 days. A pending row is never swept.

## Deleting an endpoint

[Delete a webhook](/api-reference/webhooks/delete-a-webhook) stops delivery immediately and takes
the endpoint's delivery log with it, so read anything you still need first. Deleting one endpoint
never affects another.

```bash theme={null}
curl -X DELETE "$TRDRS_API_BASE_URL/api/partner/webhooks?id=whk_4d19c2" \
  -H "Authorization: Bearer $TRDRS_PARTNER_KEY"
```

## Where to go deeper

* [Events and webhooks](/partner-platform/operator-concepts/events-and-webhooks) — the retry schedule attempt by attempt, and the delivery contract in full.
* [Follow registrations](/guides/handle-registration-events) — the two onboarding events, and reconciling them against the registration list.
* [Govern account risk](/guides/govern-account-risk) — what puts a `risk.locked` on the wire.
* [Record balance operations](/guides/record-balance-operations) — what puts a `balance.recorded` on it.
