Quickstart5. Set Up Webhooks

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 useSubscribe to first
Onboardingendorsement.approved, endorsement.request_for_information, endorsement.rejected
Virtual accountsdeposit.received, conversion.completed, payout.completed
Walletswallet.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: