ROOKDocs
5 min readUpdated August 2026

Dedicated Webhook Event Catalog

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=<hex> 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):

{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

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

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)

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.

Was this page helpful?