# Webhooks

> Be told about breaches, passes and fills instead of polling for them, with signed and verifiable deliveries.

Source: https://www.propexecutor.com/docs/webhooks
Whole reference in one file: https://www.propexecutor.com/doc.md

Register a URL and we will tell you when something happens instead of you asking. For a firm watching hundreds of accounts this is the difference between knowing about a breach in seconds and burning your whole [read budget](https://www.propexecutor.com/docs/rate-limits) discovering that nothing happened.

## Register an endpoint

`POST /v1/webhooks` · **Planned** · scope `accounts:write`

<!-- curl -->
```bash
curl -X POST https://api.propexecutor.com/v1/webhooks \
  -H "Authorization: Bearer $PFX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.apexprop.example/hooks/propexecutor",
    "events": ["account.breached", "account.passed"]
  }'
```

HTTPS only. An empty `events` array means everything, so an endpoint registered without thinking still receives rather than silently getting nothing. The response carries a `secret` — store it, it is shown once.

## Events

| Event | Fires when |
| --- | --- |
| `account.breached` | A rule was violated. Carries the rule, the reason and the equity state at the moment it fired. |
| `account.passed` | The account hit its profit target and satisfied every other objective. |
| `account.flagged` | A rule with a flag action fired — worth review, trading continues. |
| `account.created` | An account was provisioned, whether from the API or the panel. |
| `account.assigned` | An account's trader changed. Previous sessions are already revoked. |
| `account.archived` | An account was taken out of circulation. |
| `position.opened` | A position filled, including one from a resting order triggering. |
| `position.closed` | A position closed — manually, or on its stop or target. |
| `order.rejected` | A pre-order rule refused an order, with which rule and why. |
| `credits.low` | Remaining account credits fell below a threshold you set. |

## Payload

<!-- POST to your URL -->
```json
{
  "id": "evt_9f2c1b7a4e",
  "type": "account.breached",
  "created_at": "2026-09-27T14:02:19Z",
  "organization_id": "11111111-…",
  "data": {
    "account": 10000042,
    "trader_id": "3d21…",
    "status": "breached",
    "reason": "daily drawdown 5.2% exceeded the 5% limit",
    "equity": 9480,
    "balance": 9480,
    "peak_equity": 10310.55
  }
}
```

## Verifying a delivery

Every request carries a signature over the **raw body** and a timestamp:

<!-- Request headers -->
```http
X-PropExecutor-Id: evt_9f2c1b7a4e
X-PropExecutor-Timestamp: 1790503647
X-PropExecutor-Signature: v1=5d41402abc4b2a76b9719d911017c592…
```

<!-- Node -->
```javascript
import crypto from "node:crypto";

function verify(rawBody, headers, secret) {
  const ts = headers["x-propexecutor-timestamp"];
  const sig = headers["x-propexecutor-signature"];

  // Reject anything older than five minutes, so a captured delivery cannot be
  // replayed against you later.
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const expected =
    "v1=" +
    crypto
      .createHmac("sha256", secret)
      .update(`${ts}.${rawBody}`)
      .digest("hex");

  // Constant-time, so a caller cannot time their way to a valid signature.
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
```

> **Verify over the raw bytes**
> Compute the HMAC on the body exactly as received, before any JSON parsing or re-serialising. Parsing and re-encoding changes key order and whitespace, and the signature will never match.

## Delivery rules

- **Answer 2xx quickly.** Anything else, or a timeout, counts as a failure. Acknowledge first and do your work after — a slow handler looks identical to a broken one.
- **At least once, not exactly once.** A retry can deliver an event you already processed. Deduplicate on `id`.
- **Retries back off** and eventually stop, rather than hammering a dead host forever. An endpoint that keeps failing is marked and stops being delivered to.
- **Order is not guaranteed.** Use `created_at` if sequence matters, and treat each event as a statement about a moment rather than a step in a sequence.
- **An event is a notification, never authority.** The decision behind it is already durably written. If you need certainty, read the account back.

## Debugging

`GET /v1/webhooks/{id}/deliveries` · **Planned** · scope `accounts:read`

`POST /v1/webhooks/{id}/test` · **Planned** · scope `accounts:write`

Deliveries are kept with their response status and error, so you can see exactly what we sent and what came back. The test endpoint sends a signed event with the same shape as a real one — use it to check your signature verification before you rely on it.

## Managing endpoints

`GET /v1/webhooks` · **Planned** · scope `accounts:read`

`DELETE /v1/webhooks/{id}` · **Planned** · scope `accounts:write`
