> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bilt.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Order Reporting

> Partner integration guide: sending Bilt each purchase and refund made by a linked member, and checking whether Bilt accepted it

<Info>
  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](/partners/account-linking/account-links-api) or
  [Bilt to Partner Account Linking](/partners/account-linking/bilt-to-partner-account-linking).
</Info>

## 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).

```mermaid theme={null}
sequenceDiagram
    participant PS as Your server
    participant WH as Bilt order webhook
    participant API as Bilt Orders API

    PS->>WH: POST /v1/orders  (x-api-key, one event)
    WH-->>PS: 200 OK (no body)
    Note over WH,API: Bilt checks the event within seconds
    PS->>API: GET /v1/orders/{orderId}  (Bearer token)
    API-->>PS: 200 { status: "ACCEPTED" }  — or REJECTED + reason
```

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

| Environment | Order webhook base URL | Orders API base URL |
| - | - | - |
| Production | `https://webhooks.bilt.com` | `https://partnerapi.biltrewards.com` |
| Staging | `https://webhooks.staging.bilt.dev` | `https://staging.partnerapi.biltrewards.com` |

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.

| To… | Send | Credential |
| - | - | - |
| Post an order or a batch (§7.1, §7.2) | `x-api-key: <order-webhook-key>` | Your order webhook API key |
| Read order status (§7.3, §7.4) | `Authorization: Bearer <your-access-token>` | Your partner OAuth client — the same token you use for `/v1/account-links` |

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`.

<Note>
  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.
</Note>

### 6.1 Purchase

| Field | Required | Notes |
| - | - | - |
| `type` | yes | `PURCHASE`. Any other value is `VALIDATION_FAILED` |
| `orderId` | yes | string, 1–128. Your own id. Unique within your integration, and never reused. A refund has its **own** `orderId` |
| `idempotencyKey` | no | string, up to 128. Proposed, not final: when present it replaces body comparison for the duplicate check (§8.1) |
| `partnerUserId` | yes | string, 1–256. An opaque value you generate for this customer — an id, a hash, or a token. Bilt only compares it and never reads anything into it. It must equal the value used when the customer linked: the `partnerUserId` you sent to `/v1/account-links` (Partner-to-Bilt), or the `partnerMemberId` you returned to Bilt (Bilt-to-Partner). You may report orders for all your customers. An order for a customer who has not linked still gets `200` on the `POST`; when you read it back (§7.3, §7.4) it is `REJECTED` with `CONSUMER_NOT_LINKED`, `retry: true` — resend it once the customer links (§8.2) |
| `currency` | yes | `USD`, `CAD`, `GBP` or `EUR`. One per event; every amount in the event is in it |
| `closedAt` | yes | ISO 8601 with `Z` or an offset. When the purchase was completed and charged. Not more than 1 hour in the future, not older than your lookback window (default 90 days) |
| `createdAt` | no | ISO 8601. When the order was placed, if that differs from `closedAt` |
| `source` | no | `{ channel?, name? }`. `channel` is `ONLINE`, `IN_STORE`, `PHONE`, `MARKETPLACE` or `OTHER`; `name` is free text up to 64 characters, for example `ios-app` or `kiosk` |
| `location` | no | `{ id?, name?, phone?, address? { line1?, line2?, city?, state?, postalCode?, country? }, merchantCategoryCode?, merchantMid? }`. `id` (up to 128) is your own stable id for the store; `country` is a two-letter code; `merchantCategoryCode` is the 4-digit MCC; `merchantMid` (up to 64) is the acquirer merchant id. See the note on locations below |
| `lineItems[]` | no | Up to 100. See §6.3 |
| `taxes[]` | no | Up to 20 order-level taxes not already on a line: `{ uid?, name?, percentage?, applied }`. `applied` is the decimal-string amount; `percentage` is a decimal string such as `"8.875"` |
| `discounts[]` | no | Up to 20 order-level discounts not already on a line: `{ uid?, name?, percentage?, applied }`, same shape as `taxes[]` |
| `serviceCharges[]` | no | Up to 20 charges that are not products: `{ uid?, name, kind?, taxable?, applied }`. `kind` is `DELIVERY`, `SHIPPING`, `SERVICE`, `SMALL_ORDER`, `BAG`, `PACKAGING`, `REGULATORY` or `OTHER`. A tip is not a service charge; it goes on the payment |
| `fulfillments[]` | no | Up to 5 of `{ type, state? }`. `type` is `PICKUP`, `CURBSIDE`, `DELIVERY`, `SHIPMENT`, `IN_STORE` or `DIGITAL`; `state` is `COMPLETED` or `CANCELED` |
| `payments[]` | yes | 1 to 10 entries, one per tender (a split tender is two entries). See §6.4 |
| `netAmounts` | yes | The totals block. See §6.5 |
| `receiptUrl`, `receiptImageUrl`, `barcodeUrl` | no | `https` URL, up to 2 048 characters. `barcodeUrl` is an image of the receipt barcode |
| `metadata` | no | Up to 50 string key-value pairs (key up to 40 characters, value up to 500). Stored and returned to you as-is; Bilt does not read it |

### 6.2 Refund

A refund is its own event that points at the purchase it refunds.

| Field | Required | Notes |
| - | - | - |
| `type` | yes | `REFUND` |
| `orderId` | yes | string, 1–128. The refund's **own** id, never the purchase's |
| `idempotencyKey` | no | As in §6.1 |
| `originalOrderId` | yes | string, 1–128. The `orderId` of the purchase being refunded. It must be an accepted purchase of yours for the same customer, otherwise `ORIGINAL_ORDER_NOT_FOUND` (§8.2) |
| `partnerUserId` | yes | As in §6.1 |
| `currency` | yes | As in §6.1 |
| `createdAt` | yes | ISO 8601. When the refund was issued. Same window rule as `closedAt` on a purchase |
| `amount` | yes | Decimal string, **positive**. What was returned to the customer |
| `reason` | no | Free text, up to 256 characters |
| `returnLineItems[]` | no | Up to 100 of `{ uid?, sku?, upc?, name?, quantity, total }` — which lines came back. Same meaning as on the purchase |
| `payments[]` | no | Which payment was refunded: `{ type, amount, cardDetails? }`, same shapes as §6.4 |
| `metadata` | no | As in §6.1 |

### 6.3 `lineItems[]` entry fields

| Field | Required | Notes |
| - | - | - |
| `quantity` | yes | JSON number greater than zero with at most 3 decimal places (`1.25` for weighed goods). This is the only number in the event; every amount is a string |
| `total` | yes | Decimal string. What this line cost after discount, including its tax: `grossSales − discount + tax` |
| `uid` | no | Your id for this line, up to 64. Useful when you check what Bilt stored |
| `sku`, `upc` | no | Your product codes, up to 64 |
| `name` | no | Up to 256 |
| `variationName` | no | Up to 64, for example `Large` |
| `brand`, `category` | no | Up to 64 |
| `basePrice` | no | Decimal string. Price for one unit before discounts |
| `grossSales` | no | Decimal string. `basePrice × quantity` |
| `discount` | no | Decimal string. Discount applied to this line |
| `tax` | no | Decimal string. Tax applied to this line |

### 6.4 `payments[]` entry fields

| Field | Required | Notes |
| - | - | - |
| `type` | yes | `CARD`, `CASH`, `GIFT_CARD`, `WALLET`, `BUY_NOW_PAY_LATER`, `BANK_ACCOUNT` or `OTHER`. A PayPal or similar payment is `WALLET` |
| `amount` | yes | Decimal string. What was charged to this tender, **not** including `tip` |
| `uid` | no | Your id for this payment, up to 64 |
| `tip` | no | Decimal string. Tip paid on this tender |
| `cashBack` | no | Decimal string. Debit cash back at the register: included in this entry's `amount`, not part of any line item |
| `cardDetails` | no | Only for `type: CARD`. `{ brand?, cardType?, last4?, bin?, isVirtual?, entryMethod?, authResultCode?, arn?, paymentAccountReference? }`. See below |

`cardDetails` fields:

| Field | Notes |
| - | - |
| `brand` | `VISA`, `MASTERCARD`, `AMERICAN_EXPRESS`, `DISCOVER` or `OTHER` |
| `cardType` | `CREDIT`, `DEBIT` or `PREPAID` |
| `last4` | Exactly 4 digits |
| `bin` | 6 to 8 digits |
| `isVirtual` | JSON boolean |
| `entryMethod` | `KEYED`, `SWIPED`, `EMV`, `CONTACTLESS` or `ON_FILE` |
| `authResultCode` | 6 letters or digits |
| `arn` | Acquirer reference number, exactly 23 digits |
| `paymentAccountReference` | string, up to 29 characters |

### 6.5 `netAmounts`

The totals for the whole order, stated by you. Only `total` is required; send the others when you
have them.

| Field | Required | Notes |
| - | - | - |
| `total` | yes | Decimal string. What the customer paid, all in: `subtotal − discount + tax + serviceCharge + tip` |
| `subtotal` | no | Products before discounts: the sum of `lineItems[].grossSales` |
| `discount` | no | All discounts: line-level plus `discounts[]` |
| `tax` | no | All tax: line-level plus `taxes[]` |
| `serviceCharge` | no | The sum of `serviceCharges[].applied` |
| `tip` | no | The sum of `payments[].tip` |

### 6.6 How the amounts add up

When you send the parts, they have to agree with the totals:

```text theme={null}
lineItems[].total       = grossSales − discount + tax          (per line, when those are sent)
netAmounts.subtotal     = Σ lineItems[].grossSales
netAmounts.discount     = Σ lineItems[].discount + Σ discounts[].applied
netAmounts.tax          = Σ lineItems[].tax      + Σ taxes[].applied
netAmounts.serviceCharge = Σ serviceCharges[].applied
netAmounts.tip          = Σ payments[].tip
netAmounts.total        = subtotal − discount + tax + serviceCharge + tip
Σ payments[].amount + Σ payments[].tip = netAmounts.total
```

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**

```http theme={null}
POST /v1/orders
Host: webhooks.bilt.com
x-api-key: <order-webhook-key>
Content-Type: application/json

{
  "type": "PURCHASE",
  "orderId": "ORD-10042",
  "partnerUserId": "cust_88213",
  "currency": "USD",
  "createdAt": "2026-09-28T17:12:00Z",
  "closedAt": "2026-09-28T17:42:00Z",
  "source": { "channel": "ONLINE", "name": "ios-app" },
  "location": {
    "id": "store-0042", "name": "Example Store — Downtown", "phone": "+12125550100",
    "address": { "line1": "100 Main St", "city": "New York", "state": "NY", "postalCode": "10001", "country": "US" },
    "merchantCategoryCode": "5200", "merchantMid": "MID-448201"
  },
  "lineItems": [
    { "uid": "l1", "sku": "SKU-74219", "upc": "012345678905", "name": "Cordless Drill Kit", "brand": "ExampleBrand",
      "category": "tools", "quantity": 1, "basePrice": "149.00", "grossSales": "149.00", "discount": "20.00",
      "tax": "11.45", "total": "140.45" },
    { "uid": "l2", "sku": "SKU-12087", "name": "Lawn Fertilizer 5,000 sq ft", "brand": "Example Brand", "category": "garden",
      "quantity": 2, "basePrice": "22.98", "grossSales": "45.96", "discount": "0.00", "tax": "4.08", "total": "50.04" }
  ],
  "discounts": [ { "uid": "d1", "name": "WELCOME20", "applied": "5.00" } ],
  "serviceCharges": [ { "uid": "s1", "name": "Pickup fee", "kind": "SERVICE", "taxable": false, "applied": "2.38" } ],
  "fulfillments": [ { "type": "PICKUP", "state": "COMPLETED" } ],
  "payments": [
    { "uid": "p1", "type": "CARD", "amount": "187.87", "tip": "0.00",
      "cardDetails": { "brand": "VISA", "cardType": "CREDIT", "last4": "4412", "bin": "471234", "isVirtual": false,
        "entryMethod": "ON_FILE", "authResultCode": "A1B2C3", "arn": "74119875123456789012345",
        "paymentAccountReference": "V0010013820399948564925814" } }
  ],
  "netAmounts": { "subtotal": "194.96", "discount": "25.00", "tax": "15.53", "serviceCharge": "2.38", "tip": "0.00", "total": "187.87" },
  "receiptUrl": "https://receipts.example-partner.com/store-0042/10042",
  "barcodeUrl": "https://receipts.example-partner.com/store-0042/10042/barcode.png",
  "metadata": { "registerId": "07" }
}
```

**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`:

```json theme={null}
{
  "type": "REFUND",
  "orderId": "ORD-10042-R1",
  "originalOrderId": "ORD-10042",
  "partnerUserId": "cust_88213",
  "currency": "USD",
  "createdAt": "2026-10-10T14:05:00Z",
  "amount": "140.45",
  "reason": "Item returned",
  "returnLineItems": [ { "uid": "l1", "sku": "SKU-74219", "quantity": 1, "total": "140.45" } ],
  "payments": [ { "type": "CARD", "amount": "140.45", "cardDetails": { "brand": "VISA", "last4": "4412" } } ],
  "metadata": { "returnReason": "defective" }
}
```

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).

| Status | What it tells you |
| - | - |
| 401 | Key missing, unknown or disabled. Nothing was stored |
| 413 | Body over 1 MiB. Nothing was stored |
| 500 | Bilt could not store the event. **Retry the same body** with backoff |

### 7.2 Report a batch — `POST /v1/orders/batch`

Up to 100 events in one call, 1 MiB in total.

**Request**

```http theme={null}
POST /v1/orders/batch
Host: webhooks.bilt.com
x-api-key: <order-webhook-key>
Content-Type: application/json

{ "events": [ { "type": "PURCHASE", "orderId": "ORD-10050", ... },
              { "type": "REFUND",  "orderId": "ORD-10050-R1", "originalOrderId": "ORD-10050", ... } ] }
```

**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**

```http theme={null}
GET /v1/orders/ORD-10042
Host: partnerapi.biltrewards.com
Authorization: Bearer <your-access-token>
```

**Response `200`, accepted**

```json theme={null}
{
  "orderId": "ORD-10042",
  "type": "PURCHASE",
  "status": "ACCEPTED",
  "receivedAt": "2026-09-28T17:42:06Z",
  "processedAt": "2026-09-28T17:42:08Z",
  "duplicateCount": 0,
  "event": { "type": "PURCHASE", "orderId": "ORD-10042", "netAmounts": { "total": "187.87", "...": "..." }, "...": "..." }
}
```

**Response `200`, rejected**

```json theme={null}
{
  "orderId": "ORD-10077",
  "type": "PURCHASE",
  "status": "REJECTED",
  "reason": "CONSUMER_NOT_LINKED",
  "retry": true,
  "receivedAt": "2026-09-28T17:50:01Z",
  "processedAt": "2026-09-28T17:50:03Z"
}
```

| Field | Notes |
| - | - |
| `status` | `ACCEPTED` or `REJECTED` |
| `reason` | On `REJECTED`: one of the codes in §9 |
| `retry` | On `REJECTED`: `true` when sending the **same** body again later can succeed (§8.2) |
| `violations[]` | On `VALIDATION_FAILED`: `[{ code, field, detail }]` — what was wrong and where, for example `{ "code": "INVALID_VALUE", "field": "netAmounts.total", "detail": "does not equal subtotal − discount + tax + serviceCharge + tip" }` |
| `duplicateCount` | On `ACCEPTED`: how many times you have resent exactly the same event since |
| `event` | On `ACCEPTED`: the event as Bilt stored it (timestamps in UTC) |
| `receivedAt`, `processedAt` | When Bilt received your `POST`, and when it checked the event |

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.

```json theme={null}
{
  "items": [
    { "orderId": "ORD-10042", "type": "PURCHASE", "status": "REJECTED", "reason": "ORDER_CONFLICT", "retry": false,
      "receivedAt": "2026-09-29T08:15:31Z", "processedAt": "2026-09-29T08:15:33Z" },
    { "orderId": "ORD-10042", "type": "PURCHASE", "status": "ACCEPTED",
      "receivedAt": "2026-09-28T17:42:06Z", "processedAt": "2026-09-28T17:42:08Z", "duplicateCount": 2,
      "event": { "type": "PURCHASE", "orderId": "ORD-10042", "netAmounts": { "total": "187.87", "...": "..." }, "...": "..." } }
  ]
}
```

**Failure modes**

| Status | `code` | What it tells you |
| - | - | - |
| 401 | `UNAUTHORIZED` | Bad or disabled credential |
| 404 | `ORDER_NOT_FOUND` | Bilt has nothing under this `orderId`. This includes an event you posted a moment ago that has not been checked yet — right after a `POST`, treat `404` as "not yet" and read again after a few seconds |

```json theme={null}
{
  "error": {
    "code": "ORDER_NOT_FOUND",
    "message": "No order ORD-10099 for this partner",
    "retry": false,
    "requestId": "c2d4e6f8-0a2b-4c4d-8e6f-0a2b4c6d8eb5"
  }
}
```

### 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**

```http theme={null}
GET /v1/orders?status=REJECTED&from=2026-09-28T00:00:00Z&to=2026-09-29T00:00:00Z&limit=200
Host: partnerapi.biltrewards.com
Authorization: Bearer <your-access-token>
```

| Parameter | Required | Notes |
| - | - | - |
| `status` | yes | `ACCEPTED` or `REJECTED` |
| `reason` | no | Only `REJECTED` orders with this code (§9) |
| `from`, `to` | no | ISO 8601, on `receivedAt`. Default: the last 24 hours. At most 31 days apart |
| `limit` | no | 1–200, default 50 |
| `cursor` | no | The `nextCursor` from the previous page |

**Response `200`**

```json theme={null}
{
  "items": [
    { "orderId": "ORD-10077", "type": "PURCHASE", "status": "REJECTED",
      "reason": "CONSUMER_NOT_LINKED", "retry": true,
      "receivedAt": "2026-09-28T17:50:01Z", "processedAt": "2026-09-28T17:50:03Z" },
    { "orderId": null, "status": "REJECTED", "reason": "VALIDATION_FAILED", "retry": false,
      "violations": [ { "code": "INVALID_BODY", "field": "", "detail": "body is not valid JSON" } ],
      "receivedAt": "2026-09-28T18:02:11Z", "processedAt": "2026-09-28T18:02:12Z" }
  ],
  "nextCursor": "eyJyZWNlaXZlZEF0Ijoi..."
}
```

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`.

| You resend… | Bilt does |
| - | - |
| The same `orderId` with exactly the same body | Nothing new; `duplicateCount` goes up on the accepted order |
| The same `orderId` with a **different** body | `REJECTED` with `ORDER_CONFLICT`. The accepted order does not change |
| An `orderId` that was `REJECTED` before | Checks it again from scratch — this is how you retry (§8.2) |

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 `POST`s 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:

| `reason` | `retry` | Meaning |
| - | - | - |
| `VALIDATION_FAILED` | No | The body does not match §6; `violations[]` says what and where. Includes a body that is not JSON, an unknown key, an unknown `type`, an amount with too many decimals for the currency, a `closedAt` (or refund `createdAt`) outside the window, amounts that do not add up (§6.6, only if Bilt decides to reject these), an event over 64 KiB, or a batch with no `events` or too many |
| `CONSUMER_NOT_LINKED` | Yes | `partnerUserId` had no active Bilt link when the event was checked. The `POST` still answered `200`; you only see this when you read the order back. This is expected volume, not an error condition, if you report orders for all your customers |
| `ORIGINAL_ORDER_NOT_FOUND` | Yes | The refund's `originalOrderId` is not an accepted purchase of yours for the same member |
| `ORDER_CONFLICT` | No | This `orderId` was already accepted with a different body. Accepted orders cannot be changed |
| `PARTNER_DISABLED` | No | Your integration was disabled when the event was checked. Contact Bilt |

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](/partners/account-linking/account-links-api) §9.
The HTTP codes these four routes answer:

| Code | Status | When |
| - | - | - |
| `UNAUTHORIZED` | 401 | Missing, unknown or disabled credential; nothing stored |
| `ORDER_NOT_FOUND` | 404 | `GET /v1/orders/{orderId}`: nothing under this `orderId` (§7.3) |
| `VALIDATION_FAILED` | 400 | `GET /v1/orders` only: bad query parameters (§7.4). Never for an order body |
| — | 413 | Body over 1 MiB on a `POST`; no envelope |
| `INTERNAL_ERROR` | 500 | Retry with backoff; on a `POST`, retry the same body |

***

## 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.