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