Skip to main content
Meow POSTs an event to your URL when a transfer changes state or a deposit clears. The wire format is Standard Webhooks: the Python, Node, and Go verifier libraries work as-is.

You’ll need

  • An API key with webhooks:write and webhooks:read.
  • A public HTTPS URL. Private, loopback, and metadata IPs are blocked at both create time and every delivery.

1. Create a subscription

Save secret from the response. Meow returns it once. Every webhook POST requires an Idempotency-Key header, so a retried create never leaves you with two subscriptions. Generate a fresh key per operation: 1-50 printable ASCII characters, no spaces. Reusing one is rejected with a 400.
Response
event_types: null subscribes to everything. Pass an array to allowlist (empty array is rejected). You can only list events this subscription can receive: the rail events in the catalog plus your own onboarding events, application.* and identity_verification.* (so a global key can track an entity it is onboarding without polling). Only the delivery-meta events webhook.test and message.attempt.exhausted are rejected with a 400. They are always-on or on-demand, not subscribable.
Subscriptions belong to an entity, not to one account. Every enabled subscription for the entity receives each matching event. Use data.account_id to route crypto send and deposit events to the correct account.

2. Verify the signature

Three headers come with every delivery: Sign f"{webhook-id}.{webhook-timestamp}.{body}" with HMAC-SHA-256. The HMAC key is the base64-decoded body of your whsec_<base64> secret, not the raw string. Reject anything more than 5 minutes off. That is the replay window.
Use constant-time compare (hmac.compare_digest, crypto.timingSafeEqual, hmac.Equal). == leaks the secret one byte at a time.

3. Dispatch on payload.status

Event names stay coarse: {resource}.created and {resource}.updated. The lifecycle stage lives on the payload, so one handler covers the whole flow:
Per-event payload shapes are in the event catalog.

4. Pick a payload mode

Set payload_mode per subscription. Change it any time with PATCH.
data carries the full resource. No follow-up GET needed.

5. Handle out-of-order deliveries

Deliveries are not ordered. Retries, redrives, and concurrent workers mean an older state for a resource can land after a newer one. Two fields let you stay correct. Every resource event carries a sequence, a counter that increments once per resource, the same for every subscriber, stable across retries and redrives. Track the highest sequence you’ve applied per resource and advance it with a single atomic conditional write, so a stale delivery can’t overwrite newer state:
sequence is monotonic, not gapless. A hole (you see 4 then 7) is normal. Not every internal change emits an event, and a redelivered event keeps its original number. Never wait for a missing number or treat a gap as lost data; only the per-resource ordering is guaranteed.
sequence is absent on webhook.test and message.attempt.exhausted (they describe no resource). For everything else, pair it with deduplication on webhook-id (next section): sequence discards stale states, webhook-id discards exact duplicates.

6. Retries

Meow retries up to 10 times over ~91 hours.
  • Each non-zero delay gets up to 20% extra jitter.
  • Retry-After on a 429 or 503 is honored, up to 24 hours.
  • 5 failures in a row open a circuit breaker on the subscription. New deliveries wait out a cooldown (1 → 30 min) without burning an attempt. A success closes it.
  • After 10 failed attempts the delivery becomes failed_permanent, a message.attempt.exhausted event fires, and your business’s admins get an email that names the endpoint and event type. The subscription stays enabled, so an endpoint that rejects one event type keeps getting the rest.
  • If no delivery to the subscription succeeds for 3 days, Meow disables it (disabled_reason=retry_exhausted) and emails your admins. Deliveries that were still retrying are held, not dropped. Events that happen while the subscription is disabled are not sent to it.
New events enter the delivery queue on a one-minute fanout schedule. Provider callbacks can arrive after an on-chain transaction confirms. The event timestamp shows when Meow recorded the event, not the blockchain confirmation time. To recover after an outage:
  1. Fix your endpoint.
  2. Re-enable the subscription with PATCH /webhooks/subscriptions/{id} and {"is_enabled": true}. The held deliveries go out again. Re-enabling does not resend deliveries that already failed.
  3. Resend the deliveries that failed with POST /webhooks/subscriptions/{id}/recover?since=<timestamp>. Set since to when your endpoint went down. Every failed delivery created at or after that time restarts its retry schedule. Send an Idempotency-Key header. Each delivery keeps its webhook-id, so you can drop the ones you already processed.
To find failures first, filter GET /webhooks/deliveries by status=failed_permanent, subscription_id, or created_after. To replay one delivery, use POST /webhooks/deliveries/{id}/redrive.

7. Operate

Send a test event

Sends webhook.test to one subscription. Good for verifying a new receiver without waiting for real activity.

Inspect delivery history

Every attempt with HTTP status and response body excerpt.

Redrive a delivery

Resets the counter and re-queues. Works even after failed_permanent.

Rotate the secret

PATCH with rotate_secret: true. Both old and new secrets sign for 7 days; pick whichever your code recognizes. If the old secret leaked, also send expire_previous_secret: true. The old secret then stops signing at once, and your next delivery carries one signature.

8. Security checklist

Constant-time compare. == leaks the secret.
Otherwise a leaked payload can be replayed forever.
Same event can arrive twice (retries, redrives). webhook-id doesn’t change.
Advance the highest sequence per resource with an atomic conditional write so a stale delivery can’t overwrite newer state. See Handle out-of-order deliveries.

See also