Webhooks

Webhooks are optional and additive — they are never required for correctness. A webhook outage has zero effect on order fulfillment, and GET /orders/{order_id} always reflects the true current state independently of whether a webhook ever arrived.

Current status: management works today; automatic delivery does not yet. You can register, list, and revoke endpoints, and send a synchronous test ping, right now. Automatic delivery on real order-lifecycle events is not yet active — a registered endpoint will not receive a push notification when one of your orders actually changes state. Until automatic delivery is activated, poll GET /orders/{order_id} (see Orders) to find out what happened to an order. This page documents the full delivery/retry/dead-letter design so you can integrate against it now and be ready the moment it's switched on — the Testing section below is unaffected and works today.

Setup

Register a callback URL and the events you want through Account → Developer in your Brenoz account, or via the API itself once you already hold a key with the webhooks:manage scope:

POST /api/v1/reseller/webhooks
{
  "url": "https://example.com/webhooks/brenoz",
  "events": ["order.completed", "order.failed"]
}

The URL must be HTTPS and publicly reachable. It is re-validated on every delivery attempt, not only at registration — a URL that later re-resolves to a private address is rejected before any attempt, protecting against DNS-rebinding.

Events

Event Fires when
order.created An order is successfully created
order.processing The order moves from debited to actively fulfilling
order.completed The order is fulfilled
order.failed The order fails terminally
order.refunded A failed order is refunded
order.needs_attention The order is held for a supplier/manual-fulfillment reason

Signing

Every delivery carries:

Brenoz-Signature: t=<unix_timestamp>,v1=<hex_hmac_sha256>

The signature is HMAC-SHA256(secret, "<timestamp>.<raw_body>"), keyed by the signing secret shown exactly once at registration. To verify:

  1. Reconstruct "<timestamp>.<raw_request_body>" exactly as received (no re-serialization).
  2. Compute the HMAC-SHA256 with your stored secret.
  3. Compare it to the v1 value using a constant-time comparison.
  4. Reject anything where t is more than a few minutes old (replay protection) or whose event_id you've already processed (at-least-once delivery — duplicates can happen).

Retries & dead-letter

Delivery is at-least-once with bounded exponential backoff. A delivery that exhausts every attempt moves to a dead-letter state, visible on GET /webhooks/{id}/deliveries. A dead-lettered delivery can be manually replayed exactly once per request — never a runaway retry storm.

Testing

POST /webhooks/{id}/test sends a synthetic test.ping immediately and synchronously — useful to confirm your endpoint is reachable and your signature verification is correct before relying on it for real order events. It never creates a delivery-history row.

Rate limits on test-ping and replay

Sending a test ping (available on both the public API and the Account → Developer dashboard) or manually replaying a dead-lettered delivery (public API only — there is no replay action on the dashboard today) each make one real outbound HTTP request to your endpoint on demand. Both actions share a single limit: 5 requests per 300 seconds, per account — not per endpoint, and shared across however many webhooks or surfaces you use, since a request through any of them is the same real-world outbound traffic. Exceeding it returns:

HTTP/1.1 429 Too Many Requests
Retry-After: <seconds>

{"error": {"code": "reseller_api.webhook_outbound_rate_limited", "message": "Too many test pings or replays for this endpoint. Please slow down.", ...}}

Wait at least Retry-After seconds before retrying. This limit is independent of, and in addition to, the general per-key/per-account request budgets described in Errors & Rate Limits — it exists specifically to bound how much outbound traffic a test-ping/replay loop can generate, not to limit normal API usage.

Rotating or revoking a signing secret

There is no secret-rotation-with-overlap in v1 — only revoke-and-register. If a signing secret is exposed or you simply want a new one:

  1. Register a new webhook endpoint with the same URL and events.
  2. Update your receiver to verify against the new secret (shown once, at registration).
  3. Revoke the old endpoint.

Revocation takes effect immediately — a revoked endpoint's pending deliveries dead-letter without ever attempting to send. Two things do not carry over to the new endpoint: its id (any configuration you keyed on the old endpoint id must be updated) and its delivery history (GET /webhooks/{id}/deliveries is scoped to one endpoint; the new endpoint starts with none). To avoid a delivery gap, do steps 1–2 before step 3.