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": null,
"secret": "whsec_..."
}
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.
Store your signing secret
secret is your endpoint's signing secret, and this response is the only
time it is shown: GET /api/webhooks never returns it. Store it the way
you store API keys, one per environment. If you lose it, delete the
endpoint and register it again.
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
Land another simulated deposit on the virtual account from Receive a payment:
curl -s $BASE/api/sandbox/virtual_accounts/va_.../deposits \
-H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"amount": "10"}'
Your endpoint receives deposit.received, then the conversion and payout
events. Sandbox deliveries are signed exactly like live ones, so the
consumer you build here needs 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)"
The replay's envelope carries the same id plus "replayed": true.
Deduplicate on that envelope id, not on the webhook-id header, which
differs on a replay. Your handler should recognize the event and do
nothing. If it processes the event twice, fix that now: deliveries are at
least once, so duplicates happen in live.
Order by sequence, not arrival
Every event carries a sequence that only increases. Retries and parallel
delivery reorder arrivals, so order your processing by sequence, not by
arrival. The counter has gaps, so never wait for a missing number. When
the current state matters, read the object back with a GET. See
Webhooks overview.
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_transaction.created, wallet_operation.completed, wallet_operation.failed |
wallet_transaction.created (with kind: "deposit") 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: