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:
- Read
Rook-TimestampandRook-Signature. Reject the request if either is missing. - Reject the delivery if the timestamp is more than 300 seconds from the current time (replay window).
- Compute
HMAC-SHA256(secret, "{timestamp}.{body}")and hex-encode the digest (lowercase). - Compare the digest to each
v1=value inRook-Signatureusing 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.