Skip to main content
Developers

Webhooks

A webhook tells you something happened after it happened. This page is the contract: what arrives, how to prove it came from us, and what we do when your endpoint does not answer.

One transaction boundaryThree stacked elements — a record, a rule and a ledger posting — enclosed by a single boundary, indicating that all three commit together or not at all.ONE TRANSACTION BOUNDARYRecordwhat is trueRulewhat is permittedPostingwhat it did to the bookscommit together

What it changes

  1. What arrivesA POST with a JSON body of exactly four fields — eventId, eventType, occurredAt and data — carrying three headers: X-RWAOS-Signature, X-RWAOS-Event-Id and X-RWAOS-Event-Type. The event payload is nested under data rather than spread into the envelope, so an event carrying its own eventType cannot overwrite ours.
  2. Proving it came from usX-RWAOS-Signature is t=<unix-seconds>,v1=<hex>. The v1 value is HMAC-SHA256 of the literal string `${t}.${rawBody}`, keyed with your endpoint secret, computed over the raw bytes — parse the body after you verify, never before. The timestamp is inside the signed string rather than beside it, so an attacker replaying a captured delivery cannot move it. Reject anything more than 300 seconds old, and compare in constant time.
  3. Deduplicate on the event idDelivery is at-least-once. A delivery whose response was lost is retried with the same eventId, so treat that id as an idempotency key and discard a second arrival. This is the only thing that makes at-least-once safe for an event that moves money, and it is your side of the contract rather than ours.
  4. What we do when you do not answerAny 2xx is delivered. 408, 429, 5xx and a network failure are retried five times with backoff at 10s, 1m, 10m and 1h, then recorded as failed. Every other 4xx is not retried at all — your endpoint understood the request and refused it, and a revoked credential will be refused identically on the fourth attempt. 410 Gone deactivates the endpoint entirely: it is the one status that means stop sending, and we honour it.
  5. Where we will and will not sendEndpoints must be https on port 443 or 8443, with no credentials in the URL. We resolve the hostname and refuse delivery if any resolved address is private, loopback, link-local or carrier-grade NAT — every address, not merely the first, because a host answering with one public and one private answer is a rebinding setup. Redirects are not followed: a 302 to an internal address is the same request with one extra hop.
Ask, agree, then actThree steps. One person requests, a different person approves, and only then does the state change. The approval binds to what was requested, so editing it afterwards invalidates the sign-off.Requestone personApprovea different oneActnow it movesThe approval binds to what was asked.Edit it afterwards and the sign-off is void.
Every irreversible act on this platform takes this shape, whatever the subject of the page: one person asks, a different one agrees, and only then does anything move.

What it includes

Delivery historyEvery attempt records its response status and the reason it failed, so a disputed notice can be checked rather than argued about.
Frozen payloadsThe body is captured when the event commits, so a retry sends what happened rather than what the record looks like now.
One delivery per endpoint per eventA unique constraint on (webhook, event) means a relay that redelivers after a half-finished publish does not send you the same settlement twice from two rows.
Verification you do not have to writeThe signature check ships in @rwaos/api as verifySignature, so our tests and your integration check the same way.

Evidence, not assertion

Controls that have been verified running, rather than described.

  • Signature covers `${timestamp}.${body}`, not the body alone.
  • Constant-time comparison; a length mismatch returns false rather than throwing.
  • Resolved addresses are checked, not the hostname.
  • Redirects are never followed.
What counts as evidenceThree rows. A control described in a document is unticked; a control enforced by code and one refused by the database are ticked. Only the second and third are evidence that something holds.Described in a documentEnforced by codeRefused by the database
A control written down is not a control. Only the two lower rows are evidence that something holds, and both are what the list beside this describes.

Questions

Why does my signature check fail on a body that looks identical?

You are almost certainly hashing a re-serialised body. JSON.parse followed by JSON.stringify changes bytes — key order, whitespace, number formatting — and the signature covers the bytes we sent. Capture the raw request body before any parsing middleware touches it.

Do I need to respond quickly?

Yes. Return 2xx as soon as you have stored the event and do your work afterwards. A slow endpoint is retried, and a retry is a duplicate you then have to deduplicate.

What happens if I return 410 by accident?

The endpoint is deactivated and nothing further is sent to it until somebody reactivates it. It is the only status treated that way, precisely because it is the only one that means stop.

See it against something real.

Bring an instrument you are actually considering. Structuring it is the only way to judge whether the model fits.

Your instrument, against this modelAn unspecified asset on the left feeds four stages: structure, compliance policy, the offering book and the register. The asset is drawn as an outline because it is the reader's, not the platform's.Your assetwhatever it isStructurePolicyBookRegister