ROOKDocs
Events & Webhooks Guide

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.

HeaderValue
Rook-TimestampUnix time in seconds when the signature was computed.
Rook-Signaturev1= of the HMAC-SHA256 digest. Multiple v1= values may be comma-separated during secret rotation.
Signed material is the timestamp, a literal ASCII dot, then the raw request body bytes (no re-encoding):
Code
{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.

bash
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.

TryDelay before this try
1Immediate
215 seconds
31 minute
45 minutes
530 minutes
62 hours
76 hours
812 hours
After the last try the attempt status is 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).

typePayload schema
wallet.createdWalletCreatedPayload (Wallet)
wallet.updatedWalletUpdatedPayload (Wallet)
wallet_entity.verification.updatedWalletEntityVerificationUpdatedPayload (WalletEntity)
financial_account.createdFinancialAccountCreatedPayload (FinancialAccount)
financial_account.status.updatedFinancialAccountStatusUpdatedPayload (FinancialAccount; status_reason set)
application.status.updatedApplicationStatusUpdatedPayload (Application)
card.createdCardCreatedPayload (Card)
card.updatedCardUpdatedPayload (Card)
card.shippedCardShippedPayload (Card; shipping.status is SHIPPED)
transaction.createdTransactionCreatedPayload (Transaction of any type)
transaction.updatedTransactionUpdatedPayload (Transaction of any type; also fires for payment, internal transfer, external payment, and management operation lifecycle changes)
dispute.updatedDisputeUpdatedPayload (Dispute)
statement.createdStatementCreatedPayload (Statement)
balance.updatedBalanceUpdatedPayload (Balance)
tokenization.updatedTokenizationUpdatedPayload (Tokenization)
external_bank_account.updatedExternalBankAccountUpdatedPayload (ExternalBankAccount)
authorization_rule.updatedAuthorizationRuleUpdatedPayload
monitoring_case.updatedMonitoringCaseUpdatedPayload (MonitoringCase)
Send a signed sample of any type with POST /v1/webhook-subscriptions/{webhook_subscription_id}/send-example. The example is not appended to the event log.