Using webhooks
Subscribe an HTTPS endpoint, verify each delivery, and consume events
idempotently. The POST body of every delivery is an Event: id,
object (event), type, payload, created_at, and updated_at.
List the same objects on GET /v1/events when a delivery is delayed or
missed.
Register endpoints with POST /v1/webhook-subscriptions. url must be
https. event_types is the filter: Rook POSTs only when the event
type is in that list. Pause a subscription by setting disabled true.
Signature verification
Each endpoint has a secret that starts with whsec_. Use the entire
secret string, including the prefix, as the HMAC key.
| Header | Value |
|---|---|
Rook-Timestamp | Unix time in seconds when the signature was computed. |
Rook-Signature | v1= of the HMAC-SHA256 digest. Multiple v1= values may be comma-separated during secret rotation. |
{timestamp}.{body}
Verify every delivery:
1. Read Rook-Timestamp and Rook-Signature. Reject the request if
either is missing.
2. Reject the delivery if the timestamp is more than 300 seconds from
the current time (replay window).
3. Compute HMAC-SHA256(secret, "{timestamp}.{body}") and hex-encode the
digest (lowercase).
4. Compare the digest to each v1= value in Rook-Signature using a
constant-time equality check. Succeed if any matches.
signed_payload="${timestamp}.${body}"
expected=$(printf '%s' "$signed_payload" | openssl dgst -sha256 -hmac "$webhook_secret" | awk '{print $2}')
Respond with 2xx only after the signature verifies. If verification
fails, respond with 401 and do not act on the event.
Getting Started → Webhook signing covers the same scheme.
Secret rotation
POST /v1/webhook-subscriptions/{webhook_subscription_id}/secret/rotate
issues a new whsec_ secret. The previous secret remains valid for
24 hours (previous_secret_expires_at). During that overlap,
deliveries include two v1= signatures, comma-separated. Accept the
request if either digest matches. After the window, verify only the
new secret.
Retrieve the current value with
GET /v1/webhook-subscriptions/{webhook_subscription_id}/secret. Treat
the secret as a credential.
Retry schedule
Rook retries failed deliveries on an exponential schedule for about
24 hours. A delivery is successful when the endpoint returns 2xx.
Any other HTTP status, a timeout, or a connection error is a failed try.
| Try | Delay before this try |
|---|---|
| 1 | Immediate |
| 2 | 15 seconds |
| 3 | 1 minute |
| 4 | 5 minutes |
| 5 | 30 minutes |
| 6 | 2 hours |
| 7 | 6 hours |
| 8 | 12 hours |
FAILED. Use
POST /v1/webhook-subscriptions/{webhook_subscription_id}/recover with
begin to resend failed deliveries since that instant. Use
POST .../replay with begin and end to re-deliver every matching
event in a created_at window, including events that already succeeded.
Use POST /v1/events/{event_id}/resend to send one event to every
matching subscription or to a single subscription_id.
Respond quickly (within 10 seconds) and process work asynchronously so retries are not triggered by slow handlers.
Ordering
Deliveries are at-least-once and are not ordered. A later event may arrive before an earlier one for the same resource, and a retry may overlap a newer event.
Do not assume card.created arrives before card.updated for the same
card, or that transaction.created arrives before transaction.updated.
Order on created_at, then id, in your own store. The event log from
GET /v1/events is newest first; walk it with page / page_size for a
catch-up.
Idempotent consumption
Persist id and skip a delivery whose id you already processed.
Retries, recover, replay, and resend reuse the same event id with a
new webhook attempt. Acting twice on the same id duplicates work.
Webhook attempts (GET /v1/events/{event_id}/attempts and
GET /v1/webhook-subscriptions/{webhook_subscription_id}/attempts)
record status (PENDING, SUCCESS, FAILED), response_code, a
truncated response_body, and attempted_at. They are a delivery
receipt, not a substitute for storing event ids.
Event catalogue
Discriminate on type. payload is the resource snapshot named below.
Retrieve always includes payload. List includes it when with_content
is true (the default).
type | Payload schema |
|---|---|
wallet.created | WalletCreatedPayload (Wallet) |
wallet.updated | WalletUpdatedPayload (Wallet) |
wallet_entity.verification.updated | WalletEntityVerificationUpdatedPayload (WalletEntity) |
financial_account.created | FinancialAccountCreatedPayload (FinancialAccount) |
financial_account.status.updated | FinancialAccountStatusUpdatedPayload (FinancialAccount; status_reason set) |
application.status.updated | ApplicationStatusUpdatedPayload (Application) |
card.created | CardCreatedPayload (Card) |
card.updated | CardUpdatedPayload (Card) |
card.shipped | CardShippedPayload (Card; shipping.status is SHIPPED) |
transaction.created | TransactionCreatedPayload (Transaction of any type) |
transaction.updated | TransactionUpdatedPayload (Transaction of any type; also fires for payment, internal transfer, external payment, and management operation lifecycle changes) |
dispute.updated | DisputeUpdatedPayload (Dispute) |
statement.created | StatementCreatedPayload (Statement) |
balance.updated | BalanceUpdatedPayload (Balance) |
tokenization.updated | TokenizationUpdatedPayload (Tokenization) |
external_bank_account.updated | ExternalBankAccountUpdatedPayload (ExternalBankAccount) |
authorization_rule.updated | AuthorizationRuleUpdatedPayload |
monitoring_case.updated | MonitoringCaseUpdatedPayload (MonitoringCase) |
POST /v1/webhook-subscriptions/{webhook_subscription_id}/send-example.
The example is not appended to the event log.