Skip to main content
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 scheme v1a: 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.
Node.js (version 18 or later):
Python, with the 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.
  1. Register. Send Bilt your whpk_ public key (or ask Bilt to generate the pair). Bilt returns a key id of the form bk_<environment>_<22 letters and digits>, for example bk_prod_…. Send it in the X-Bilt-Key-Id header of every request signed with that key.
  2. 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.
  3. 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.
Rotating on a schedule is optional. Revoking is required whenever a private key may have leaked.

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:
The body part is exactly the bytes you send. Serialize your JSON once, sign that string, and send the same string. Re-serializing after signing (for example, letting your HTTP client encode the object again) changes the bytes and breaks the signature.

4.3 Steps

  1. Serialize the body to a JSON string.
  2. 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.
  3. Take the current Unix time in seconds as webhook-timestamp.
  4. Sign {webhook-id}.{webhook-timestamp}.{body} with your Ed25519 private key.
  5. Send the body with the four headers above.
On a retry, repeat steps 3 to 5: keep the 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:
Python, with the 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-Id names a key that is registered for you, not revoked, and not past its 7-day rotation window.
  • webhook-timestamp is within 300 seconds (5 minutes) of when Bilt received the request, in either direction. Keep your server clock synchronized.
  • At least one v1a, entry in webhook-signature is a valid signature of {webhook-id}.{webhook-timestamp}.{body} for that key.
A request that fails verification is not processed and is not retried by Bilt. You get no error response for it, because the 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.