Payload Format
Every webhook delivery is a POST with a JSON body. Headers carry metadata for replay protection, signature verification, and retry tracking.
Body
Every event uses the same envelope:
{
"id": "evt_8c1f2a3b4d",
"type": "strategy.deployed",
"created_at": "2026-04-29T12:00:00.000Z",
"data": { ... }
}
| Field | Type | Description |
|---|---|---|
id | string | Unique event identifier, format evt_<10-12 hex>. Use this for idempotent deduplication. |
type | string | Event type; one of the values enumerated below. |
created_at | string | ISO 8601 UTC timestamp with millisecond precision indicating when the event was generated server-side. |
data | object | Event-type-specific payload. Schemas are documented per type below. |
Headers
| Header | Description |
|---|---|
X-PipAI-Event-Id | Unique event ID. Identical to body.id. Use for idempotency. |
X-PipAI-Event-Type | Event type. Identical to body.type. Lets you route without parsing the body. |
X-PipAI-Webhook-Id | The webhook subscription that this delivery belongs to (e.g. wh_2a4f10). Useful when one endpoint serves multiple subscriptions. |
X-PipAI-Delivery-Id | Unique per-delivery identifier. The same event_id redelivered as a retry will reuse this delivery ID across attempts. |
X-PipAI-Delivery-Attempt | Integer attempt counter starting at 1. Increments to 10 over the retry schedule before delivery is given up. |
X-PipAI-Timestamp | Unix milliseconds at delivery time. Used for replay-window math during signature verification. |
X-PipAI-Signature | Hex-encoded HMAC-SHA256(webhook_secret, "{timestamp}.{raw_body}"). See Signature Verification. |
Event types
Wildcard subscriptions (strategy.*, backtest.*, account.*, or *) are also accepted at registration time — see Create Webhook.
| Type | Description | Data schema |
|---|---|---|
strategy.created | A strategy was created in draft status. | strategy.created |
strategy.deployed | A strategy was deployed to production (or paper-trade if dry_run). | strategy.deployed |
strategy.paused | A strategy was paused; no new positions will open. | strategy.paused |
strategy.stopped | A strategy was stopped (terminal, not resumable). | strategy.stopped |
strategy.position_opened | A new position was opened by the strategy. | strategy.position_opened |
strategy.position_closed | An open position was closed; PnL is final. | strategy.position_closed |
strategy.order_filled | An order placed by the strategy was filled (full or partial). | strategy.order_filled |
strategy.error | The strategy hit an unrecoverable error and halted. | strategy.error |
backtest.job_done | A backtest job finished successfully. | backtest.job_done |
backtest.job_failed | A backtest job aborted with an error. | backtest.job_failed |
account.balance_low | A monitored asset balance fell below the configured threshold. | account.balance_low |
account.api_key_rotated | An API key on the account was rotated. | account.api_key_rotated |
Per-event data schemas
strategy.created
{
"strategy_id": "strat_8f2a1b",
"name": "BTC grid 1h",
"status": "draft"
}
strategy.deployed
{
"strategy_id": "strat_8f2a1b",
"name": "BTC grid 1h",
"capital": "10000.00",
"leverage": 3,
"deployed_at": "2026-04-29T12:00:00.000Z"
}
strategy.paused
{
"strategy_id": "strat_8f2a1b",
"status": "paused"
}
strategy.stopped
{
"strategy_id": "strat_8f2a1b",
"status": "stopped"
}
strategy.position_opened
{
"strategy_id": "strat_8f2a1b",
"position_id": "pos_19acb2",
"symbol": "BTCUSDT",
"side": "long",
"entry_price": "67234.50",
"qty": "0.05",
"leverage": 3,
"opened_at": "2026-04-29T12:00:01.245Z"
}
strategy.position_closed
{
"strategy_id": "strat_8f2a1b",
"position_id": "pos_19acb2",
"symbol": "BTCUSDT",
"side": "long",
"entry_price": "67234.50",
"exit_price": "68110.00",
"qty": "0.05",
"exit_qty": "0.05",
"pnl": "43.78",
"leverage": 3,
"opened_at": "2026-04-29T12:00:01.245Z",
"closed_at": "2026-04-29T13:42:18.880Z"
}
strategy.order_filled
{
"strategy_id": "strat_8f2a1b",
"order_id": "ord_3c9df4",
"symbol": "BTCUSDT",
"side": "buy",
"type": "limit",
"price": "67234.50",
"qty": "0.05",
"filled_at": "2026-04-29T12:00:01.180Z"
}
strategy.error
{
"strategy_id": "strat_8f2a1b",
"error_code": "ORDER_REJECTED",
"message": "Exchange rejected order: insufficient margin"
}
backtest.job_done
{
"job_id": "bt_4d2e7c1f",
"metrics": {
"total_return": "0.184",
"sharpe": "1.62",
"max_drawdown": "-0.073",
"win_rate": "0.541",
"total_trades": 312,
"profit_factor": "1.78"
}
}
See the Backtesting metrics reference for the full metric definitions.
backtest.job_failed
{
"job_id": "bt_4d2e7c1f",
"error_code": "TIMEOUT",
"message": "Job exceeded maximum runtime of 30 minutes"
}
account.balance_low
{
"asset": "USDT",
"balance": "12.50",
"threshold": "100.00"
}
The threshold is configured per webhook via balance_low_threshold at creation time — see Create Webhook.
account.api_key_rotated
{
"key_id": "key_7a91c2",
"rotated_at": "2026-04-29T12:00:00.000Z"
}
Acknowledgement
Your endpoint must respond with a 2xx HTTP status within 10 seconds of receiving the request. Any other outcome — non-2xx response, slow response, TLS error, or connection failure — is treated as a delivery failure and queued for retry per the schedule in the Overview.
The response body is ignored. An empty 200 OK is fine.
If you need to perform expensive work in response to an event, acknowledge synchronously and dispatch the work to a background queue. Holding the connection open while you process events will starve the delivery worker and trigger unnecessary retries.