For engineering teams at partner companies. This page covers how you prove that an
order-reporting request comes from you. It applies only to the
Order Reporting routes. Account linking keeps
its existing OAuth sign-in and does not change.
1. How signing works
Every request you send to the order-reporting routes is signed with a private key that only you hold. Bilt stores only the matching public key, so Bilt never holds a secret that could be used to send requests as you. Bilt follows the Standard Webhooks specification, using its asymmetric schemev1a: an Ed25519 (Edwards-curve Digital Signature
Algorithm on Curve25519) signature over the request. If you already verify webhooks with Standard
Webhooks, this is the same scheme used in the other direction.
The routes it covers, on the hosts listed in
Order Reporting §3:
2. Your key pair
An Ed25519 key pair has two halves:
You can get a key pair in one of two ways. Pick one:
- Generate it yourself (recommended). Run one of the snippets below and send Bilt only the
whpk_value. - Ask Bilt to generate it. Bilt shows you the private key once, at creation, and does not store it. If you lose it, register a new key.
cryptography package:
3. Key lifecycle
Key registration, rotation and revocation are done by Bilt’s team on your request. Contact your Bilt integration manager for each step. Keys are per environment: staging and production each need their own key pair.- Register. Send Bilt your
whpk_public key (or ask Bilt to generate the pair). Bilt returns a key id of the formbk_<environment>_<22 letters and digits>, for examplebk_prod_…. Send it in theX-Bilt-Key-Idheader of every request signed with that key. - Rotate. When you want a new key, ask Bilt to rotate: send the new pair’s
whpk_public key (or ask Bilt to generate the new pair). Bilt returns a new key id. The old key keeps working for 7 days, so you can switch over without downtime. After 7 days, requests signed with the old key are rejected. - Revoke. If your private key may have been exposed, ask Bilt to revoke it. Revocation takes effect immediately: from then on every request signed with that key is rejected. Register a new key to keep sending.
4. Signing a request
4.1 Headers
Every request carries these headers. All are required.webhook-signature may hold several space-separated entries, for example one per key while you
rotate. Bilt accepts the request when any one v1a, entry is valid. Entries for other schemes,
such as v1,, are ignored.
4.2 What you sign
Sign these bytes with your private key:4.3 Steps
- Serialize the body to a JSON string.
- Pick the
webhook-id: a new one for a new request, or the one you used before if this is a retry of the same body. - Take the current Unix time in seconds as
webhook-timestamp. - Sign
{webhook-id}.{webhook-timestamp}.{body}with your Ed25519 private key. - Send the body with the four headers above.
webhook-id and the body, and sign again with the
current time.
4.4 Examples
The official Standard Webhooks libraries (standardwebhooks for JavaScript and Python) only
support the symmetric v1 scheme today, not v1a. These examples use each language’s standard
Ed25519 support instead. Any Standard Webhooks library that supports v1a signing produces the
same headers.
Node.js (version 18 or later), no extra packages:
cryptography and requests packages:
4.5 Check your code
Ed25519 signatures are deterministic, so you can test your signing code against these fixed values. They use a published example key: never use it for real requests.
Expected header:
5. What Bilt checks
Bilt checks a request in two stages. Right away, before answering:
Shortly after, Bilt verifies the request. It is processed only when all of these hold:
X-Bilt-Key-Idnames a key that is registered for you, not revoked, and not past its 7-day rotation window.webhook-timestampis within 300 seconds (5 minutes) of when Bilt received the request, in either direction. Keep your server clock synchronized.- At least one
v1a,entry inwebhook-signatureis a valid signature of{webhook-id}.{webhook-timestamp}.{body}for that key.
202 was already sent. Bilt records it as a rejected request with one
of these reasons:
Looking up rejected requests: available on request from Bilt. Ask your Bilt integration manager,
giving the
webhook-id. A self-serve API is planned and not available yet.
A rejected request was never processed. Fix the cause and send it again.
Retries are safe. A request with a webhook-id Bilt already has, and the same body, is
ignored. The same webhook-id with a different body is rejected with IDEMPOTENCY_KEY_REUSED, and
the first request does not change. Retry after a timeout or a 5xx with the same webhook-id and
body, signed again with the current time (§4.3). The 300-second window also limits how long anyone
who captured a signed request could replay it. This covers network retries of one request. How to
resend an event that Bilt rejected after checking its content is in
Order Reporting.
6. Security
- Keep the
whsk_private key in a secret manager, server-side only. Never put it in a browser, a mobile app, a log or a support ticket. - Never send your private key to Bilt. Bilt only ever needs the
whpk_public key. - If a private key may have leaked, ask Bilt to revoke it right away, then register a new one.