For engineering teams at partner companies. This page covers the order webhook you post to and
the
/v1/orders API you read from. It assumes your customers are already linked to Bilt through
Partner to Bilt Account Linking or
Bilt to Partner Account Linking.1. What order reporting is
When a linked Bilt member buys something from you, or gets a refund, you tell Bilt. Bilt uses the event to reward the member. You send one event per order, in one JSON format. Bilt finds the member through the account link, stores the order, and lets you check what happened to it. All traffic goes from you to Bilt. Bilt never calls you.2. How it works: post, then read
Reporting is asynchronous. YouPOST an event, Bilt answers 200 right away, and checks the event
a few seconds later. The 200 only means “Bilt has it” — it does not mean the order was accepted.
To find out, read the order back (§7.3) or list rejected orders (§7.4).
Two things to design for:
- A
200is a receipt, not an answer. Read the order back to learn whether Bilt accepted it. - Your
orderIdis the key. Bilt does not give you an id. Every read uses theorderIdyou sent, so it must be unique and never change.
3. Environments
You post to the webhook host and read from the API host. The paths are the same on both:
/v1/orders. Like every Bilt partner API the path is versioned — a breaking change would ship as
/v2/orders next to /v1/orders, never by changing /v1. Within /v1 Bilt may add optional
request fields, response fields and new status, reason or type values; ignore response
fields and values you do not know.
Credentials are per environment and never shared across environments. Develop and test against
staging first. All requests must use TLS 1.2 or higher.
4. Onboarding checklist
Your Bilt partnership contact drives this; nothing here is self-service. Bilt gives you, per environment:- An order webhook API key — the
x-api-keyvalue for posting orders (§7.1, §7.2). It is only for order reporting and is not your account-links key - A partner OAuth client, if you do not already have one from account linking — this is what you read with (§5)
- Your lookback window, if it differs from the default 90 days (§6,
closedAt)
- The currency or currencies you will report in
- Whether you will send
lineItems,locationand the optional parts ofpayments(all recommended) - A technical contact and an escalation path for production incidents
401.
5. Authentication
You use two credentials: one to post, one to read.
Rules:
- The credential identifies you. There is no
partnerIdin any URL or body. Two partners can never see each other’s orders. - A missing, unknown or disabled key is
401with no further detail, and nothing is stored. - Treat both as secrets. Keep them in a secret manager, server-to-server only, never in a browser, mobile app or log.
- Keys can be rotated without a contract change. Make the key a configuration value.
- Send
Content-Type: application/jsonon anything with a body. You may send anx-request-idheader; it comes back aserror.requestIdon an error response (success responses do not carry it).
6. The order event
One JSON object per order. The shape follows the pattern most order APIs use: what was bought (lineItems, taxes, discounts, serviceCharges, fulfillments) on one side, how it was
paid (payments) on the other, and netAmounts tying the two together.
Unknown fields are not allowed: an unknown key anywhere in the event makes it REJECTED with
VALIDATION_FAILED. Anything not listed here goes in metadata. Fixed-choice values are written in
UPPER_SNAKE_CASE.
Three details are still being decided and may change before launch: whether
idempotencyKey
stays in /v1, whether the sums in “How the amounts add up” reject an event or are only flagged,
and the final list of currency values.6.1 Purchase
6.2 Refund
A refund is its own event that points at the purchase it refunds.6.3 lineItems[] entry fields
6.4 payments[] entry fields
cardDetails fields:
6.5 netAmounts
The totals for the whole order, stated by you. Only total is required; send the others when you
have them.
6.6 How the amounts add up
When you send the parts, they have to agree with the totals:netAmounts. Whether a mismatch is REJECTED with
VALIDATION_FAILED or accepted and flagged is still being decided (see the note at the top of §6).
Notes:
- Amounts are strings, never numbers —
"187.43", not187.43. Zero or more, with no more decimals than the currency allows ("20.49"for USD).quantityand true/false values are the only non-string scalars. - One event is at most 64 KiB.
- Accepted orders cannot be changed. If you reported wrong facts, contact Bilt. Resending the
same
orderIdwith a different body isREJECTEDwithORDER_CONFLICT(§8.1). - Locations. Bilt keeps one location record per
location.id. If you send the sameidwith a new name or address, Bilt updates the record — but only from events newer than the last one it saw for thatid, so an old order that arrives late never overwrites newer details. Without anid, the location is only kept with that order.
7. API reference
7.1 Report an order — POST /v1/orders
Request
200 — no body. Bilt has the event and will check it shortly.
A refund is the same call with type: "REFUND", its own orderId, originalOrderId pointing at
the purchase, and a positive amount:
REJECTED with ORIGINAL_ORDER_NOT_FOUND,
retry: true; resend it after the purchase shows ACCEPTED (§8.3).
Failure modes
These are the only errors this endpoint returns directly. Everything about the content of the
event is reported when you read it back (§7.3).
7.2 Report a batch — POST /v1/orders/batch
Up to 100 events in one call, 1 MiB in total.
Request
200 — no body. Same failure modes as §7.1.
Bilt checks the events in array order, one at a time. A refund in position 1 can see the
purchase in position 0, and one bad event does not affect the others. Use a batch when the order
of events matters to you (§8.3).
A batch with no events, more than 100, or a body that is not that shape is stored as one
submission with orderId: null, REJECTED with VALIDATION_FAILED, and shows up on the list
(§7.4).
7.3 Read an order — GET /v1/orders/{orderId}
Returns what Bilt did with the order you reported under that orderId. If Bilt accepted an
order with that id, you get the accepted order — even if you later sent a different body that
Bilt rejected as ORDER_CONFLICT. Otherwise you get your latest attempt.
Request
200, accepted
200, rejected
Add
?history=true to get every submission for that orderId as items[], newest first, each
in the shape above. This is where a rejected ORDER_CONFLICT attempt shows up; it also appears in
GET /v1/orders?status=REJECTED (§7.4). The accepted order itself never changes.
7.4 List orders by status — GET /v1/orders
Lists the orders Bilt rejected (or accepted) in a time window, so you do not have to read every
order one by one.
Request
Response
200
event; read the order for its event. Items are ordered by
receivedAt within the window, and nextCursor is absent on the last page. If Bilt could not
read a body at all, it has orderId: null and shows up only in this list — §7.3 cannot find
it.
Bad parameters (an unknown status, from/to more than 31 days apart, limit out of range, an
unreadable cursor) are 400 VALIDATION_FAILED in the error envelope (§9): this is a query, not
an order.
8. Resending and retrying
8.1 Resending the same order
YourorderId is the deduplication key. It is safe to resend after a timeout or a 500.
If
idempotencyKey is adopted (§6, still being decided): when you send one, Bilt compares the key
instead of the body — same orderId and same key is a duplicate, same orderId and a different
key is ORDER_CONFLICT.
8.2 Retrying a rejected order
retry: true means the same body can be accepted later, once something outside the event changes:
CONSUMER_NOT_LINKED— the customer was not linked to Bilt when the event was checked. Resend after they link. Most partners queue these and resend daily for 30 days.ORIGINAL_ORDER_NOT_FOUND— the refund arrived before its purchase, or the purchase was rejected. Resend after the purchase showsACCEPTED.
retry: false means resending the same body will be rejected again. Fix the body
(VALIDATION_FAILED) or contact Bilt (ORDER_CONFLICT, PARTNER_DISABLED).
8.3 Ordering
Two separatePOSTs are not guaranteed to be checked in the order you sent them. The only case
where you can notice this is a refund checked before its purchase: it is REJECTED with
ORIGINAL_ORDER_NOT_FOUND and retry: true. If you need the purchase to go first, send both in
one batch (§7.2) — a batch is checked in array order.
9. Status and reason codes
status is ACCEPTED or REJECTED. On REJECTED, reason is one of:
HTTP errors on the read API use the same error envelope (
error.code, error.message,
error.retry, error.requestId) as Partner to Bilt Account Linking §9.
The HTTP codes these four routes answer:
10. Security and data handling
- Send only your own identifiers:
orderId,partnerUserId,location.id,sku. Do not put customer email addresses or phone numbers anywhere in the event, includingmetadata. - Card data belongs only in
payments[].cardDetails, never inmetadataor line items. - Bilt stores the event as you sent it and returns it to you on
GET. Bilt does not readmetadata. - Both credentials are server-to-server secrets. Never put them in a browser, mobile app or log.