Webhooks are optional. Pull sync is the canonical, complete, and recoverable source of truth. Start pull-only unless lower latency materially improves the integration.
Register an endpoint#
From the integration detail page, choose the environment, payload mode, one or more event types, and a public HTTPS URL. Up to three endpoints are supported per integration. Production endpoint configuration requires an approved or active integration and a fresh passkey confirmation.
The endpoint must not redirect and must resolve only to public addresses. BOT pins and rechecks DNS and rejects loopback, link-local, private, reserved, documentation, multicast, and other non-public destinations.
Ownership challenge#
Before an endpoint becomes active, BOT resolves every A/AAAA answer, rejects the whole hostname if any answer is non-public, selects one validated address, pins that address for the request, and still verifies TLS for the registered hostname. Registration accepts only a public HTTPS hostname on port 443, with no URL credentials or fragment, and does not follow redirects.
BOT then sends this unsigned setup request:
POST /your/registered/path HTTP/1.1
Content-Type: application/json
Accept: application/json
User-Agent: BoringOrderTracker-Webhook-Verification/1.0
{"type":"bot.webhook.verification","challenge":"<random-value>"}
Return a 2xx JSON response containing the exact challenge string. The minimal valid response is:
{"challenge":"<the-exact-random-value>"}
Do not log or transform the challenge. The current verification request has a five-second connection timeout, a ten-second total timeout, a 16 KiB response header limit and an 8 KiB response body limit. These setup limits are not a latency budget for normal delivery. The ownership challenge is not a regular event and does not carry the HTTP Message Signature headers described below. Only a successful challenge activates the endpoint.
Event types#
connection.createdresource.grantedresource.updatedresource.revokedconnection.revokedstatistics.updatedintegration.suspended
Revocation messages are minimal and never contain a rich snapshot/history. Pending rich-data deliveries are discarded after revocation.
Event envelope and payload modes#
Every signed event uses a CloudEvents-shaped JSON envelope:
{
"specversion": "1.0",
"id": "evt_opaque",
"type": "resource.updated",
"source": "https://api.integration.boringordertracker.com",
"time": "2026-09-01T12:34:56.123456Z",
"environment": "sandbox",
"data": {
"connection_id": "con_integration_specific",
"resource": {"id": "ord_integration_specific", "type": "order"}
}
}
notification mode sends only references needed to trigger a pull:
connection.created:connection_id;resource.granted/resource.updated:connection_idandresource;statistics.updated:generation_id;integration.suspended: an emptydataobject.
In full_payload mode, only resource.granted and resource.updated add the current snapshot plus history_complete. History pages are never embedded; retrieve them from the API. The payload is generated immediately before each attempt after rechecking the active endpoint, integration, connection, grant, resource alias, environment gate and published statistics generation. A retry can therefore carry a newer current snapshot under the same event ID; consumers must still treat the event ID as idempotent and use pull/revisions as truth.
resource.revoked contains connection_id, resource, revocation_id and deletion_deadline_at. connection.revoked contains connection_id, revocation_id and deletion_deadline_at. Neither contains snapshot or history. Delete every retained individual/derived copy, then acknowledge the revocation through POST /v1/revocations/{id}/ack; do not acknowledge receipt as if it proved deletion.
Verify before parsing#
BOT signs each delivery using Ed25519 HTTP Message Signatures and sends a Content-Digest, event ID, and event timestamp. Public verification keys are at https://integration.boringordertracker.com/.well-known/bot-integration-webhook-jwks.json.
The current signed request has these headers (HTTP field names are case-insensitive):
Content-Type: application/json
Content-Digest: sha-256=:<base64-SHA-256-of-the-exact-body-bytes>:
X-BOT-Event-ID: <event-id>
X-BOT-Event-Timestamp: <RFC3339-UTC-event-time>
Signature-Input: bot=("@method" "@target-uri" "content-digest" "x-bot-event-id" "x-bot-event-timestamp");created=<unix-seconds>;keyid="<kid>";alg="ed25519"
Signature: bot=:<base64-Ed25519-signature>:
User-Agent: BoringOrderTracker-Webhook/1.0
Use an RFC 9421 implementation where available. For implementations that expose the signature base, the bot label covers exactly these components in this order:
"@method": POST
"@target-uri": https://the-exact-registered.example/path?query
"content-digest": sha-256=:<base64-digest>:
"x-bot-event-id": <event-id>
"x-bot-event-timestamp": <event-time>
"@signature-params": ("@method" "@target-uri" "content-digest" "x-bot-event-id" "x-bot-event-timestamp");created=<unix-seconds>;keyid="<kid>";alg="ed25519"
@target-uri is the exact public URL registered in the portal, not an internal reverse-proxy URL. Preserve or reconstruct that external URL exactly. Do not normalize its path, query encoding, default port or trailing slash before verification. Select the JWK whose kid matches keyid; production keys have kty=OKP, crv=Ed25519, use=sig, and alg=EdDSA.
For every request:
- accept only
POSTover HTTPS on the exact registered route and read the raw body bytes with a strict local size limit; - parse
Signature-Inputstructurally—do not concatenate untrusted parameters into a verifier—and select itskeyidfrom a locally cached current JWKS; - calculate SHA-256 over the exact raw body bytes and verify
Content-Digestwith a constant-time comparison; - verify the Ed25519 signature over the exact component values and order above before parsing or using the JSON body;
- apply a bounded, documented receiver clock-skew/freshness policy to the signed
createdvalue, then use the signed event ID as the replay key; - parse JSON and require envelope
id/timeto equal the signed event-ID and event-timestamp headers, and require the expected source/environment/type; - durably enqueue or transactionally commit the event ID before returning a 2xx. If that ID was already committed, return a 2xx without applying it again.
BOT does not currently publish a numeric receiver clock-skew tolerance. Do not invent one or treat an undocumented number as a BOT guarantee. The signature's created value is generated for each delivery attempt and is the appropriate freshness input. The event timestamp represents when the event occurred and can legitimately be older after retries; use it for domain ordering, not as the only delivery-freshness test.
An unknown key ID should trigger a bounded JWKS refresh, not signature bypass. Never trust an event merely because it arrived over HTTPS.
Delivery and recovery#
Any 2xx response within the response-size policy completes the attempt. BOT retries transport failures, oversized responses, HTTP 408, 409, 425, 429, and 5xx; other HTTP statuses are terminal for that delivery. Retries use exponential backoff from 30 seconds with deterministic jitter, cap an individual delay at six hours, and stop at the earlier of the event's stored expires_at boundary and seven days after it occurred. The current delivery transport defaults to a five-second connection timeout, 15-second total timeout and 64 KiB maximum response; these worker settings can be tightened, so handlers should respond quickly with a small/empty body.
After the retry window, a failed delivery moves to the dead-letter queue. The portal exposes dead letters and a manual replay action. Return 429 only for temporary load shedding, not for signature failures or permanently invalid payloads.
Your handler must tolerate duplicate delivery and reordering. Use the API's monotonic revisions and run pull sync after downtime or ambiguity. A webhook is a prompt to synchronize, not proof that no other change occurred.
For notification, enqueue a pull and return. For full_payload, it is safe to use the verified current snapshot for latency, but still reconcile through pull. Never advance the API cursor merely because a webhook arrived.
Current rollout limitation#
The contract, portal registration/verification flow, signing implementation, outbox, retry, and dead-letter model are implemented. Scheduled public webhook, statistics, and retention workers are not yet enabled for production during the current sandbox rollout. Do not make webhook delivery a launch dependency until BOT explicitly enables it for the integration/environment.