5. Set up webhooks
Register an endpoint, verify a signature, and replay an event to prove your dedupe works.
Webhooks are how you learn that anything happened: a decision, a deposit, a delivery, a customer being paid at a wallet address you never called an API about.
Prerequisites
- An HTTPS endpoint reachable from the internet. A tunnel to your laptop is fine for this guide.
- Somewhere to store processed event ids.
- Access to the raw request body in your handler, before JSON parsing.
Register the endpoint
curl -s $BASE/api/webhooks \
-H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"url": "https://yourapp.example/rasto/webhook"}' | jq .
{
"id": "whk_033y9PqRsTuVwXyZaBcDe",
"object": "webhook",
"url": "https://yourapp.example/rasto/webhook",
"event_kinds": ["*"]
}
Omitting event_kinds subscribes to everything, which is the right default
while you are building. Endpoints are per environment: this registration
does not carry over to live.
Get your signing secret
curl -s $BASE/api/webhooks/portal -H "Api-Key: $KEY" | jq -r '.url'
The delivery portal shows your endpoint's signing secret, plus every delivery attempt with its response code and body. Store the secret the way you store API keys, one per environment.
Verify before you parse
Compute the HMAC over the raw body and compare it in constant time, then parse. Full code in both Python and TypeScript is on Verifying signatures.
The two mistakes that cost the most time:
- Parsing before verifying. Re-serializing parsed JSON does not reproduce the signed bytes. Capture the raw body first.
- Skipping the timestamp check. Without it, a captured delivery can be replayed against you forever, because its signature stays valid.
Trigger a real event
Anything you did in the previous guides emits one. The quickest is a wallet deposit:
curl -s $BASE/api/sandbox/simulate/wallet_deposit \
-H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"wallet_id": "wal_...", "asset": "usdc", "amount": "10.00"}'
Your endpoint receives wallet.deposit_received. Sandbox deliveries are
really delivered and really signed, so the consumer you build here is
production ready with no environment branches.
Prove your dedupe works
List what we emitted, then replay one:
curl -s $BASE/api/webhook_events -H "Api-Key: $KEY" | jq '.data[0]'
curl -s $BASE/api/webhook_events/wev_.../replay \
-H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json"
The replay carries the same id plus "replayed": true. Your handler
should recognize it and do nothing. If it processes the event twice, fix
that now: real deliveries are at least once, so this will happen in
production.
Order by sequence, not arrival
Every event carries a monotonically increasing per-partner sequence.
Retries and parallel delivery reorder arrivals, and a projection built on
arrival order will eventually apply a stale event over a fresh one.
Store sequence alongside your projection and ignore anything lower than
what you have already applied.
What to wire first
| If you use | Subscribe to first |
|---|---|
| Onboarding | endorsement.approved, endorsement.request_for_information, endorsement.rejected |
| Virtual accounts | deposit.received, conversion.completed, payout.completed |
| Wallets | wallet.deposit_received |
wallet.deposit_received is the one people forget. Because a wallet is
funded by a plain on-chain send, it is the only way you learn a customer
was paid: there is no API call to observe instead.
You are done
You have run the whole product end to end. Next: