Skip to main content
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. You POST 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 200 is a receipt, not an answer. Read the order back to learn whether Bilt accepted it.
  • Your orderId is the key. Bilt does not give you an id. Every read uses the orderId you 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-key value 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)
You give Bilt:
  • The currency or currencies you will report in
  • Whether you will send lineItems, location and the optional parts of payments (all recommended)
  • A technical contact and an escalation path for production incidents
Go-live gate: your integration stays disabled on the Bilt side until onboarding completes. While disabled, both the webhook and the read API answer 401.

5. Authentication

You use two credentials: one to post, one to read. Rules:
  • The credential identifies you. There is no partnerId in any URL or body. Two partners can never see each other’s orders.
  • A missing, unknown or disabled key is 401 with 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/json on anything with a body. You may send an x-request-id header; it comes back as error.requestId on 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:
Each rule is checked only when the fields on both sides are present. A partner that cannot send line items can still send a complete 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", not 187.43. Zero or more, with no more decimals than the currency allows ("20.49" for USD). quantity and 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 orderId with a different body is REJECTED with ORDER_CONFLICT (§8.1).
  • Locations. Bilt keeps one location record per location.id. If you send the same id with a new name or address, Bilt updates the record — but only from events newer than the last one it saw for that id, so an old order that arrives late never overwrites newer details. Without an id, the location is only kept with that order.

7. API reference

7.1 Report an order — POST /v1/orders

Request
Response 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:
If the purchase has not been accepted yet, the refund is 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
Response 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
Response 200, accepted
Response 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.
Failure modes

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
Items are the §7.3 shape without 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

Your orderId 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 shows ACCEPTED.
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 separate POSTs 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, including metadata.
  • Card data belongs only in payments[].cardDetails, never in metadata or line items.
  • Bilt stores the event as you sent it and returns it to you on GET. Bilt does not read metadata.
  • Both credentials are server-to-server secrets. Never put them in a browser, mobile app or log.