# Orders, approvals and pay on account

When a company user places an order, the order carries the company's details: the purchase order number, the branch it is for, delivery instructions, and a cost centre and lead time on each line. A company can pay on account instead of by card. An order can also be held until someone in the company approves it. This page covers what those orders look like on the API, how to find them and how to act on them.

## What a company order carries

`GET /v1/orders/{id}` (`order:read`) and `GET /v1/orders` (`order:list`) return these fields on every order. They are `null` on orders that do not use them.

| Field | Type | Meaning |
|---|---|---|
| `companyId` | uuid or `null` | The company the order was placed under, recorded when it was placed. |
| `poNumber` | string, up to 64 characters, or `null` | The buyer's purchase order number. |
| `companyLocationId` | uuid or `null` | The branch (company location) the order is for. |
| `deliveryInstructions` | string or `null` | What the buyer typed for this delivery. The branch's standing instructions are on the [location](/guides/b2b-companies#locations); emails and documents print the branch's first, then the order's. |
| `paymentStatus` | string | `payment_due` on an order placed on account (or created unpaid): the money is owed, and the order is never cancelled for being unpaid. An on-account order held for approval is `payment_due` too, with `amountDue` `"0.00"` until it is approved. |
| `amountDue` | decimal string | What is still owed, such as `"292.96"`. Parse it before doing sums with it. |
| `exchangeRate` | decimal string or `null` | The rate from the store's primary currency to the order's currency, recorded when the order was placed: `"1.00000000"` for an order in the primary currency. |
| `balanceDueAt` | date-time or `null` | When an order placed on account or on payment terms falls due: `null` while it is held for approval, and on a `fulfillment` term until the order is fully fulfilled, when it is set to that moment. |
| `metadata.payment_flow` | string | `on_account` on orders placed on account, and on on-account orders held for approval. |
| `b2bInvoice` | object or `null` | The invoice that bills the order: `id`, `invoiceNumber`, `status`, `paidAt`. |

Each entry of `items` also carries:

| Field | Type | Meaning |
|---|---|---|
| `costCentreId` | uuid or `null` | The [cost centre](/guides/b2b-cost-centres) the line is charged to. |
| `costCentreCode`, `costCentreName` | string or `null` | The code and name as they were at placement, kept if the cost centre is renamed or archived later. |
| `leadTimeDays` | integer or `null` | Days until the line could ship when the order was placed: `0` in stock, `null` unknown. |
| `expectedDate` | date or `null` | When the line is expected to ship. |

## Finding company orders

`GET /v1/orders` (`order:list`, at most 100 per page) takes these filters, alongside `status`, `paymentStatus`, `fulfillmentStatus`, `customerId`, `dateFrom`, `dateTo`, `updatedFrom`, `externalId` and `includeArchived`:

| Parameter | Orders it keeps |
|---|---|
| `companyId` | placed under this company |
| `poNumber` | with exactly this PO number, ignoring case |
| `costCentreId` | with at least one line charged to this cost centre |
| `approvalState` | `awaiting`, `approved` or `declined` |

```http
GET /v1/orders?companyId=6f2a1c3e-9d4b-4a7f-8e2c-1b3f5a7c9d0e&paymentStatus=payment_due&includeArchived=true&limit=100 HTTP/1.1
Host: api.myorbitcommerce.net
Authorization: Bearer oc_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

### Credit in use

The API keeps no running balance for a company. The credit it is using is counted in the store's primary currency, as `creditLimit` is: for each of its orders whose `paymentStatus` is `payment_due`, archived orders included, leaving out any whose `status` is `canceled`, take `amountDue` divided by `exchangeRate` (an order in the primary currency counts at face value, and a missing, zero or negative rate counts as 1), and add them up. The query above lists those orders: `GET /v1/orders` leaves archived orders out unless you send `includeArchived=true`, so keep that parameter, and read every page. `amountDue` and `exchangeRate` are decimal strings, so parse them before doing sums. The company's `creditLimit` is on the [company](/guides/b2b-companies#company-fields).

## Pay on account

With pay on account, the buyer places the order without paying and the store bills the company from its own accounts system.

At storefront checkout, a user may pay on account when all of these hold:
- pay on account is on for the company (the store setting, or `payOnAccountEnabled` in its `b2bSettings`);
- the company's `status` is `active` and `onStop` is `false`;
- the user's `status` is `active` and their `payOnAccount` is not `false`;
- the order's branch has an active payment term, or failing that the location Orbit falls back to does: the company's default location, or, with per-user limits on, the user's default delivery location when they may use it, else the default among their allowed locations, else the company's default location. Any active term will do, whatever its type;
- the order fits in the credit left, or the company has no `creditLimit`.

At checkout, an order that would go over the limit follows the company's `overCreditLimit`. With `approval`, while order approval is on for the company, the order is [held for approval](#approvals), with `approvalTrigger: "credit_limit"` when no earlier trigger matched, and placed on account only once it is approved. Otherwise pay on account is refused with `errorCode: "B2B_CREDIT_LIMIT_EXCEEDED"` and the buyer pays another way. Credit is counted as a checkout or an approval places the order, under a lock on the company, so two buyers placing orders at once cannot both spend the same credit. An order created through `POST /v1/orders` is never checked against the limit, but once created unpaid it counts towards the credit in use like any other.

An order placed on account:
- is `processing`, with `paymentStatus: "payment_due"`;
- has `amountDue` equal to its total, and `balanceDueAt` set from its payment term: the invoice date for a `receipt` term, the invoice date plus `netDays` for a `net` or `external` term, the term's `dueDate` for a `fixed` term. The invoice date is the calendar day the order was placed in the store's time zone, so `balanceDueAt` is the due day at 00:00 UTC. On a `fulfillment` term it starts as `null`: once the order is fully fulfilled, its invoice takes the store's calendar day of that moment as its due date, and `balanceDueAt` takes the moment itself;
- carries `metadata.payment_flow: "on_account"`;
- has a B2B invoice whose `termType` is `external` and whose `invoiceNumber` is `INV-` followed by the order number.

No card is charged and no payment link is sent. The topics `order.created`, `b2b_invoice.created`, `order.confirmed` and `order.placed_on_account` fire. When the merchant records the payment against the invoice, under **Invoices** in the dashboard, the invoice and the order are settled together: `order.paid` and `b2b_invoice.paid` fire and `paymentStatus` becomes `paid`. Marking the order itself paid settles its invoice too: when `PATCH /v1/orders/{id}` sets `paymentStatus: "paid"`, or the merchant marks the order paid on the dashboard order page or with the bulk action, `order.paid` fires and the order's open or past-due invoice becomes `paid`, with `amountPaid` equal to `amountDue` and `paidAt` set. `b2b_invoice.paid` follows, with its usual payload, and no second `order.paid`. An invoice already settled is left as it is.

### Payment terms for accounts the store invoices itself

`POST /v1/payment-terms` (`payment-term:create`) with a `name`, `type: "external"` and `netDays` creates the term the dashboard calls "Invoiced externally". Orbit records the invoice and its due date, but never charges a card or emails a payment link for it. Pay on account does not need this type: an order placed on account takes any active term, and its invoice is always typed `external`. Assign it to a location with `PATCH /v1/companies/{companyId}/locations/{locationId}/payment-term`.

## Creating company orders through the API

`POST /v1/orders` (`order:create`) accepts `poNumber`, `companyLocationId` and `deliveryInstructions`, and on each item `costCentreCode`.

It is a trusted create: the order is never held for approval, and no spending limit, credit limit, pay on account rule or PO number rule applies to it, so your system is trusted with the PO number and instructions as sent. Its `paymentStatus` defaults to `payment_due`, so unless you send another, the order counts towards the company's [credit in use](#credit-in-use). It gets no B2B invoice and no `balanceDueAt`.

The order's company comes only from `customerId`. Send the `customerId` of the company user the order is for: every [company user](/guides/b2b-companies#company-users) object carries it. An `email` alone never links an order to a company.

`currencyId` takes the currency's id or its ISO 4217 code, such as `GBP`, when the store has that currency enabled; any other code answers `400` with `code: "ORDER_CURRENCY_NOT_ENABLED"`.

`companyLocationId` must be a location of that company, and each `costCentreCode` must name one of its cost centres (matched ignoring case); otherwise the request answers `400` with `B2B_LOCATION_NOT_IN_COMPANY` or `B2B_COST_CENTRE_NOT_FOUND`. An order without a company (no `customerId`, or a customer who is not a company user) has no locations or cost centres, so a branch or a cost centre on it is refused the same way.

```http
POST /v1/orders HTTP/1.1
Host: api.myorbitcommerce.net
Authorization: Bearer oc_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

{
  "email": "jane.armitage@beaconhardware.example",
  "customerId": "4e8a2c6f-3b1d-4f7a-9c2e-5d6b7a8c9e0f",
  "currencyId": "GBP",
  "shippingAmount": 0,
  "poNumber": "PO-48213",
  "companyLocationId": "2b7d4e6f-1a3c-4b5d-8e9f-0a1b2c3d4e5f",
  "deliveryInstructions": "Goods in at the rear, 8am to 4pm",
  "items": [
    { "sku": "HB-2201", "name": "Hinge, brass", "quantity": 40, "price": 2.15, "costCentreCode": "MAINT" }
  ]
}
```

## Approvals

When order approval is on for the company (the store setting, or `orderApprovalEnabled` in its `b2bSettings`), an order placed at storefront checkout is held instead of being paid or placed. Orders created through the API or in the dashboard are never held. A card, manual payment, payment terms or on-account checkout can be held; a deposit, instalment or gift card checkout that matches a trigger is refused with `errorCode: "B2B_APPROVAL_PAYMENT_UNSUPPORTED"`. The first trigger that matches decides, and `approvalTrigger` records which:

1. `spending_limit`: the order is over the buyer's `spendingLimit` and their `overSpendingLimit` (else the company's) is `approval`.
2. `order_value`: the total is over the company's `approvalThreshold`.
3. `non_contract`: `approvalNonContract` is on, the company has a catalogue, and a line is priced from the store's standard price.
4. `credit_limit`: the order is on account, it would go over the `creditLimit`, and `overCreditLimit` is `approval`.

When approval is off, only the spending limit applies, and checkout refuses an order over it with `errorCode: "B2B_SPENDING_LIMIT_EXCEEDED"`.

Every limit is in the store's primary currency: the company's `creditLimit` and `approvalThreshold`, and a user's `spendingLimit` and `approvalLimit`. An order is compared with them at its total divided by its `exchangeRate`, to the penny (a missing, zero or negative rate counts as 1), and at checkout the basket is converted at the rate the order will record.

A held order:
- is `pending`, with `approvalState: "awaiting"` and `approvalRequestedAt` set;
- has its stock reserved, nothing charged, and is never cancelled for being unpaid;
- has `paymentStatus` `payment_due` for an on-account order and `pending` for every other flow, and `amountDue` `"0.00"` either way: an on-account order gets its invoice and `balanceDueAt` only when it is approved;
- has no ship-by date and starts no CRM automations yet: both start when it is approved;
- names who was asked: its `authoriserContactId` and `secondAuthoriserContactId` are the pair the request or the latest reminder reached, and both are `null` when nobody at the company was asked, a reminder to the store team included.

Do not send an order with `approvalState: "awaiting"` to fulfilment or to your accounts system: wait for `order.approved`. `PATCH /v1/orders/{id}` refuses a `status` or `paymentStatus` that would change a held order with `409` and `errorCode: "ORDER_AWAITING_APPROVAL"`, and writes nothing. Sending the values the order already has is accepted, so a re-sync is not refused, and `notes`, `metadata` and `archived` still apply.

While an order is held, Orbit keeps what its decision needs in `metadata`: `approval_payment_flow`, `approval_staff_only` and `approval_payment_term_id`. Like `payment_flow`, these keys belong to the platform: `PATCH /v1/orders/{id}` ignores them and `POST /v1/orders` drops them.

### Who decides

Any active contact of the order's company who has `canApproveOrders` and whose own `approvalLimit` covers the order total may decide it. There is no exemption for the second authoriser or for company managers: a manager without `canApproveOrders` cannot decide, and an approver whose limit is below the total cannot either. An `approvalLimit` of `null` means no limit.

Routing decides only who is emailed, under the same rule. Orbit asks the company's authoriser (`authoriserContactId`) when they can decide the order and copies in the `secondAuthoriserContactId` when they can decide it too. When the authoriser cannot decide it, the second authoriser is asked instead. When neither can, or none is named, Orbit asks up to two of the company's other users who can decide it: the one whose `approvalLimit` is the smallest that covers the total first, then users with no limit, then the longest-standing user, never the buyer who placed the order. The order's `authoriserContactId` and `secondAuthoriserContactId` then name them. When nobody but the buyer can decide it, no company user is emailed, and when nobody at the company can, only store staff can.

Store staff can always decide, in the dashboard or through the routes below. Once the company's `approvalReminderHours` have passed (else the store's, 24 by default), checked every hour, every held order gets one reminder. Who receives it is worked out again at that moment: the authoriser when they can still decide the order (with the second authoriser copied when they can too); else the first who can of the order's second authoriser and the company's current authoriser and second authoriser; else up to two of the company's other users who can decide it, chosen as above; else the store team at the store's new-order email address. So an order whose request reached nobody is reminded to whoever can decide it by then. A reminder that fails to send is tried again the next hour, whether it went to company users or to the store team; one that staff send by hand from the dashboard is not.

### Approving and declining through the API

`POST /v1/orders/{id}/approve` and `POST /v1/orders/{id}/decline` (`order:update`) take an optional `reason`, up to 500 characters (a longer one answers a plain validation `400`), which is stored on the order and sent to the buyer when declining.
- Approving sets `approvalState: "approved"`. A card order then waits for the buyer to pay with the link Orbit emails them, which works for 30 days from the decision. An order paid by manual payment (a bank transfer and the like) waits for its payment, like any manual order, with `paymentStatus: "awaiting_payment"`. An on-account order is placed on account at once. An order on payment terms gets its invoice, due by its term, and stays `pending` until the invoice is paid; when the buyer can no longer pay on payment terms, approving it, by any route, answers `403` with `errorCode: "B2B_PAYMENT_TERMS_UNAVAILABLE"` and the message "This buyer can no longer pay on payment terms, so the order cannot be placed on terms.", and the order stays awaiting approval. Storefront checkout refuses an order on payment terms from such a buyer with the same code.
- Approving an on-account order checks it again at that moment, under a lock on the company. The call answers `403` with `errorCode: "B2B_PAY_ON_ACCOUNT_UNAVAILABLE"` when the buyer is no longer a user of the company (or belongs to another one), the company is suspended or on stop, the user is switched off, pay on account is off for the store, the company or the user, or no active payment term can take the order. It answers `403` with `errorCode: "B2B_CREDIT_LIMIT_EXCEEDED"` when the credit the company's other orders already use, plus this order, is over its `creditLimit`, unless order approval is on for the company and its `overCreditLimit` is `approval`. Either way the order stays awaiting approval, and the approval links already emailed still work.
- An approved on-account order is placed on the payment term checkout validated. Only when that term is gone or inactive does it fall back to the branch's term, then the company's default location's.
- Declining sets `approvalState: "declined"`, cancels the order with your reason, releases its stock and emails the buyer.
- Both answer the order as `GET /v1/orders/{id}` returns it. An order that is not awaiting approval answers `409` with `errorCode: "ORDER_NOT_AWAITING_APPROVAL"`.

```http
POST /v1/orders/3f9c1b7a-2d4e-4c6f-8a1b-9e2d3c4b5a6f/decline HTTP/1.1
Host: api.myorbitcommerce.net
Authorization: Bearer oc_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

{ "reason": "Over this quarter's budget" }
```

The decision is recorded on the order:
- `approvalDecidedAt`;
- `approvalDecidedVia`: `account`, `email`, `dashboard` or `api`;
- `approvalReason`;
- `approvalDecidedByContactId` when a company user decided;
- `approvalDecidedByUserId` when store staff did.

### What fires when

A held order is not announced as an order: there is no `order.created` until it is approved. Subscribers hear `order.approval_requested`, and `checkout.completed` too, because the buyer did finish checking out, so read the order's `approvalState` before acting on `checkout.completed`: a held order reads `awaiting`. Nothing else fires until the order is decided. A held order that store staff cancel instead fires no further topic: it reads `status: "canceled"` with `approvalState` still `awaiting`, and approving or declining it answers `409` with `ORDER_NOT_AWAITING_APPROVAL`.

| Moment | Topics |
|---|---|
| Placed on account (never held) | `order.created`, `checkout.completed`, `b2b_invoice.created`, `order.confirmed`, `order.placed_on_account` |
| Placed on payment terms (never held) | `order.created`, `b2b_invoice.created`, `checkout.completed` |
| Held for approval | `order.approval_requested`, `checkout.completed` (the order reads `approvalState: "awaiting"`) |
| Approved, card order | `order.created`, `order.approved`; the payment topics follow when the buyer pays |
| Approved, manual payment | `payment.awaiting_manual`, then `order.created` and `order.approved`; `order.paid` follows when the store records the payment |
| Approved, on account | `b2b_invoice.created`, `order.confirmed`, `order.placed_on_account`, then `order.created` and `order.approved` |
| Approved, payment terms | `b2b_invoice.created`, then `order.created` and `order.approved`; `order.paid` and `b2b_invoice.paid` follow when the invoice is paid or the order is marked paid |
| Declined | `order.declined` |
| Invoice paid: recorded under **Invoices** in the dashboard, or a payment-terms invoice paid through Stripe | `order.paid` (when the order was still unpaid), `b2b_invoice.paid` |
| Order marked paid: `PATCH /v1/orders/{id}`, the dashboard order page or the bulk action | `order.paid`, then `b2b_invoice.paid` when the order had an open or past-due invoice |
| Order cancelled | `order.canceled`, or `order.updated` when `PATCH /v1/orders/{id}` sets `status: "canceled"`; then `b2b_invoice.voided`, with `reason: "order_canceled"`, for each open or past-due invoice it had |

A payment-terms invoice that Stripe has issued has a hosted invoice the buyer can pay (its `hostedInvoiceUrl`). When Orbit settles the invoice itself (a payment recorded under **Invoices**, or the order marked paid) or voids it (in the dashboard, or by cancelling the order), it closes that hosted invoice, so the buyer can no longer pay it: Stripe records it as paid outside Stripe, or voids it, or deletes it while it is still a draft. A hosted invoice Stripe already shows as paid or void is left as it is. If Orbit voids an invoice the buyer has already paid in Stripe, nothing is refunded automatically: refund the payment from Stripe if it should not stand. On-account invoices are never sent to Stripe, so none of this applies to them.

An approved manual order's `payment.awaiting_manual` carries `notifyCustomer: false`: the buyer gets the approval email rather than the payment instructions email.

A decision's topics go out only once the decision is saved: the placement's first, then `order.created` and `order.approved`. Deliveries are not guaranteed in that order, so `order.placed_on_account` may arrive before or after `order.created` and `order.approved`. Read the order's `approvalState` and `paymentStatus` from `/v1` rather than relying on the sequence.

## Next steps

- A company's orders and balance as a document: [Company statements](/guides/b2b-statements).
- The company and user fields an order draws on: [Companies, buyers and locations](/guides/b2b-companies).
- Every topic an order fires, with its payload: [Webhooks](/guides/webhooks#topic-catalog).
