# Real-time data and webhooks

> Signed webhooks, how to verify them, and the status of the streaming endpoint.

Source: https://docs.botradix.com/api-realtime/

Webhooks deliver your wallet's account events to your server as soon as BotRadix indexes them from the chain, so you do not need to poll aggressively.

## Creating a webhook

Webhooks are managed with a developer session (see [API overview and authentication](https://docs.botradix.com/api/)):

```bash
curl -X POST https://app.botradix.com/api/dex/developer/webhooks \
  -H "Authorization: Bearer $DEVELOPER_SESSION" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/botradix/webhook","events":["order.filled","position.changed","liquidation"]}'
```

| Rule | Value |
|---|---|
| URL | HTTPS on port 443 or 8443, no embedded credentials, up to 512 characters, resolving to public IP addresses |
| Limit | Up to 8 active webhooks per wallet |
| Events | A list of event names, or `["*"]` for all. Default: `transaction.confirmed` and `transaction.reverted` |
| Secret | `signing_secret` (starts with `brx_whsec_`) is returned **only once**, when the webhook is created |
| Removal | `DELETE /api/dex/developer/webhooks/{webhook_id}` disables the subscription |

## Events

| Event | When |
|---|---|
| `order.accepted` | An order was accepted on chain |
| `order.filled` | An order was completely filled |
| `order.partially_filled` | An order was partly filled |
| `order.cancelled` | An order was cancelled |
| `position.changed` | A position was opened, increased, reduced, closed or inverted |
| `liquidation` | A position was liquidated |
| `funding.charged` / `funding.credited` | Funding was paid or received |
| `collateral.changed` | A deposit or withdrawal changed your collateral |
| `transaction.confirmed` / `transaction.reverted` | A transaction BotRadix knows about reached finality, successfully or not |
| `chain.reorg` | A chain reorganization changed already reported data |
| `transaction.pending` | Sent only as a correction after a reorganization (with `revoked_event_id`) |

`liquidation.warning` is accepted in subscriptions but reserved: it is not emitted today.

> [!NOTE]
> `transaction.*` events are sent for transactions BotRadix knows the hash of: those submitted through the relay or the delegated executor, and those whose hash you reported to `/api/dex/actions/receipt`.

## Delivery

Each delivery is an HTTPS `POST` with a JSON body:

```json
{ "id": "…", "type": "position.changed", "created_at": 1790950000, "data": { … } }
```

| Header | Content |
|---|---|
| `X-BotRadix-Event` | Event type |
| `X-BotRadix-Event-ID` | Unique, stable event ID (up to 96 characters, same as `id` in the body). Use it to deduplicate |
| `X-BotRadix-Timestamp` | Unix time (seconds) of this delivery attempt |
| `X-BotRadix-Signature` | `v1=` followed by the hex HMAC-SHA256 signature |
| `User-Agent` | `BotRadix-DEX-Webhook/1.0` |

- Delivery is **at least once**: the same event can arrive more than once. Deduplicate by `X-BotRadix-Event-ID`.
- Answer with any `2xx` status within 4 seconds. Redirects are not followed.
- A failed delivery is retried up to 4 attempts in total, waiting 2, 4 and 8 seconds.
- Events can arrive out of order; use `created_at` and the data itself to order them.

## Verifying the signature

The signature is an HMAC-SHA256 of `timestamp + "." + raw_body`, keyed with your full `signing_secret`. Always verify it against the **raw** body, before parsing the JSON, and reject old timestamps.

```javascript
// Node.js (Express: use express.raw({ type: "application/json" }) for this route)
import crypto from "node:crypto";

export function verifyBotRadixWebhook(rawBody, headers, secret, toleranceSeconds = 300) {
  const timestamp = headers["x-botradix-timestamp"];
  const received = headers["x-botradix-signature"] || "";
  if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false;
  const expected = "v1=" + crypto.createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex");
  return received.length === expected.length && crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}
```

```python
# Python
import hashlib, hmac, time

def verify_botradix_webhook(raw_body: bytes, headers, secret: str, tolerance: int = 300) -> bool:
    timestamp = headers.get("X-BotRadix-Timestamp", "")
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > tolerance:
        return False
    expected = "v1=" + hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(headers.get("X-BotRadix-Signature", ""), expected)
```

## Server-Sent Events stream

`GET /api/dex/v1/stream?pair_id={id}&interval=1m` is defined as a private stream with market snapshots, account snapshots and lifecycle events, a heartbeat every 12 seconds and resumption with `Last-Event-ID`. **It is not enabled on the public service yet** and answers `503 REALTIME_STREAM_ASYNC_WORKER_REQUIRED`. Use webhooks for account events and the read endpoints for market data in the meantime. This page will be updated when the stream is enabled.
