# Record a sale

> The whole flow of a sale, with every request and reply - read the catalogue, find or create the customer, issue the invoice, and take the payment at issue or later.

A sale through the API takes four steps. Each step below shows the full request and the full reply, made with a test key against the sandbox.

1. Read the catalogue: the branch, the items and the payment method.
2. Find the customer, or create them.
3. Issue the invoice, with the payment or without it.
4. Take the payment later, when the invoice was not paid at issue.

The key needs `branches:read`, `items:read`, `payment_methods:read`, `customers:write`, `sales_invoices:write` and, for step 4, `payments:write`.

The ids in these examples follow the [test mode fixtures](https://docs.kosoor.com.sa/test-mode.md#fixtures) where one exists. The invoice and receipt `number` values are made up: each merchant sets its own numbering.

## 1. Read the catalogue

An invoice names its branch, each line's item and the payment method by id. Take those ids from the catalogue, never from a guess:

| Endpoint | Permission | Gives you |
|---|---|---|
| `GET /v1/branches` | `branches:read` | the branches your key may use: `branchId` |
| `GET /v1/items` | `items:read` | the items the merchant sells: `lines[].itemId` |
| `GET /v1/payment-methods` | `payment_methods:read` | the merchant's payment methods: `payment.paymentMethodId` |

The sandbox keeps the live ids of the branches, items and payment methods it copied, so ids you map in the sandbox hold on live data.

## 2. Find or create the customer

Every invoice names a customer: the API makes no walk-in sale. Look the customer up by phone, or by the `referenceId` you gave them:

```bash
curl "https://api.kosoor.com.sa/v1/customers?phone=%2B966500000001" \
  -H "Authorization: Bearer ksr_test_..."
```

When `data` is empty, create the customer with `POST /v1/customers`. If two of your requests race and the phone was taken in between, the create answers `409 phone_taken` and `existingId` names the customer to use. [Customers](https://docs.kosoor.com.sa/customers.md) has both requests and every rule.

The examples below use two sandbox fixtures: **Sandbox Person** (`...0001`), a person, and **Sandbox Standard Co** (`...0002`), a company.

## 3. Issue the invoice

`POST /v1/sales-invoices` creates and issues the invoice in one call; there is no draft. Every `POST` carries an `Idempotency-Key`: if the reply is lost, send the same request with the same key, and you get the same invoice back instead of a second one. See [Retries and idempotency](https://docs.kosoor.com.sa/idempotency.md).

### A person who pays in full

The customer is a person, so the invoice is simplified. `payment` without `amount` pays the whole invoice. Here the price includes VAT, so `pricesIncludeVat` is `true`.

```bash
curl https://api.kosoor.com.sa/v1/sales-invoices \
  -H "Authorization: Bearer ksr_test_..." \
  -H "Idempotency-Key: 4e9f2a7c-1b3d-4c5e-8f6a-9b0c1d2e3f4a" \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "5a4db0c5-0000-4000-8000-000000000001",
    "branchId": "9a0b1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
    "pricesIncludeVat": true,
    "lines": [
      { "itemId": "7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f", "quantity": 1, "unitPrice": 115 }
    ],
    "payment": { "paymentMethodId": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9" },
    "referenceId": "order-20417"
  }'
```

`201 Created`:

```json
{
  "id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
  "object": "salesInvoice",
  "number": "INV-1042",
  "issueDate": "2026-11-07",
  "supplyDate": null,
  "dueDate": null,
  "branchId": "9a0b1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
  "customerId": "5a4db0c5-0000-4000-8000-000000000001",
  "currency": "SAR",
  "subtotal": 100,
  "discountTotal": 0,
  "vatTotal": 15,
  "total": 115,
  "amountPaid": 115,
  "amountDue": 0,
  "lines": [
    {
      "itemId": "7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
      "name": "Full wash",
      "description": null,
      "quantity": 1,
      "unitPrice": 100,
      "unitDiscount": 0,
      "vatRate": 15,
      "vatAmount": 15,
      "lineTotal": 115
    }
  ],
  "notes": null,
  "costCentreId": null,
  "referenceId": "order-20417",
  "metadata": {},
  "createdVia": "api",
  "zatca": {
    "kind": "simplified",
    "status": "pending",
    "uuid": null,
    "counter": null,
    "hash": null,
    "qr": null,
    "submittedAt": null,
    "clearedAt": null,
    "reportedAt": null,
    "errors": []
  }
}
```

Read these from the reply:

- **Line prices are before VAT.** The 115 you sent including VAT is shown as `unitPrice: 100` with `vatAmount: 15`.
- **`total` is what the customer pays.** Compare it with what your system expected: a discount below an item's minimum price is reduced, and a price that includes VAT can move the total by a halala. See [Sales invoices](https://docs.kosoor.com.sa/sales-invoices.md).
- **`zatca.status` is `pending`.** A simplified invoice is reported to ZATCA after the reply; it turns `reported`. The document fields stay `null` until Kosoor signs it. See [ZATCA results](https://docs.kosoor.com.sa/zatca.md).

The money stays on the invoice (`amountPaid`): a sale to a person paid at issue creates no separate payment.

### A company on account

The customer is a company, so the invoice is standard, and Kosoor has ZATCA clear it before replying. Without `payment`, the whole invoice goes on the company's account. Here prices are sent before VAT.

```bash
curl https://api.kosoor.com.sa/v1/sales-invoices \
  -H "Authorization: Bearer ksr_test_..." \
  -H "Idempotency-Key: 0c5b7d3e-9f2a-4b1c-8d6e-3a4f5b6c7d8e" \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "5a4db0c5-0000-4000-8000-000000000002",
    "branchId": "9a0b1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
    "pricesIncludeVat": false,
    "lines": [
      { "itemId": "8d9e0f1a-2b3c-4d4e-9f5a-6b7c8d9e0f1a", "quantity": 2, "unitPrice": 100, "description": "Plates ABC 1234 and XYZ 5678" }
    ],
    "dueDate": "2026-12-07",
    "notes": "Fleet contract 2026-14",
    "referenceId": "order-20418",
    "metadata": { "fleetContract": "2026-14" }
  }'
```

`201 Created`:

```json
{
  "id": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
  "object": "salesInvoice",
  "number": "INV-1043",
  "issueDate": "2026-11-07",
  "supplyDate": null,
  "dueDate": "2026-12-07",
  "branchId": "9a0b1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
  "customerId": "5a4db0c5-0000-4000-8000-000000000002",
  "currency": "SAR",
  "subtotal": 200,
  "discountTotal": 0,
  "vatTotal": 30,
  "total": 230,
  "amountPaid": 0,
  "amountDue": 230,
  "lines": [
    {
      "itemId": "8d9e0f1a-2b3c-4d4e-9f5a-6b7c8d9e0f1a",
      "name": "Fleet wash",
      "description": "Plates ABC 1234 and XYZ 5678",
      "quantity": 2,
      "unitPrice": 100,
      "unitDiscount": 0,
      "vatRate": 15,
      "vatAmount": 30,
      "lineTotal": 230
    }
  ],
  "notes": "Fleet contract 2026-14",
  "costCentreId": null,
  "referenceId": "order-20418",
  "metadata": { "fleetContract": "2026-14" },
  "createdVia": "api",
  "zatca": {
    "kind": "standard",
    "status": "cleared",
    "uuid": "e7f8a9b0-c1d2-4e3f-a4b5-c6d7e8f9a0b1",
    "counter": 18,
    "hash": "zXab+32ksuccBzrDiE27f65AmOvbZ2ixbnSSRnoRKzA=",
    "qr": "ARBTYW5kYm94IE1lcmNoYW50Ag8zOTk5OTk5OTk5MDAwMDMDFDIwMjYtMTEtMDdUMDk6NDE6MjJaBAYyMzAuMDAFBTMwLjAw",
    "submittedAt": "2026-11-07T09:41:21.884Z",
    "clearedAt": "2026-11-07T09:41:22.517Z",
    "reportedAt": null,
    "errors": []
  }
}
```

`zatca.status` is `cleared` and `zatca.qr` holds the QR code ZATCA returned. If ZATCA does not answer in time, the reply is still `201`, with `status: "pending"`; Kosoor clears it later. If ZATCA rejects it, nothing is saved and the call answers `422 zatca_rejected`. Both are covered in [ZATCA results](https://docs.kosoor.com.sa/zatca.md).

Only a company can buy on account. For a person, an invoice without `payment`, or with a `payment.amount` below the total, answers `422 on_account_not_allowed`.

### Taking part of the money at issue

`payment.amount` below the total pays that much at issue, and the rest stays on the company's account:

```json
{
  "paymentMethodId": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
  "amount": 100
}
```

An amount above the total answers `400 invalid_parameter`. To pay in full, leave `amount` out rather than sending your own total.

## 4. Take the payment later

When the company pays, record the money with `POST /v1/payments` and allocate it to the invoice:

```bash
curl https://api.kosoor.com.sa/v1/payments \
  -H "Authorization: Bearer ksr_test_..." \
  -H "Idempotency-Key: 7a1b2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d" \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "5a4db0c5-0000-4000-8000-000000000002",
    "branchId": "9a0b1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
    "paymentMethodId": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
    "amount": 230,
    "date": "2026-11-09",
    "allocations": [
      { "invoiceId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f", "amount": 230 }
    ],
    "referenceId": "bank-tx-55190"
  }'
```

`201 Created`:

```json
{
  "id": "d3e4f5a6-b7c8-4d9e-8f0a-2b3c4d5e6f7a",
  "object": "payment",
  "number": "RCT-0031",
  "date": "2026-11-09",
  "customerId": "5a4db0c5-0000-4000-8000-000000000002",
  "branchId": "9a0b1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
  "paymentMethodId": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
  "amount": 230,
  "discount": 0,
  "allocations": [
    { "invoiceId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f", "amount": 230 }
  ],
  "unallocatedAmount": 0,
  "notes": null,
  "referenceId": "bank-tx-55190",
  "metadata": {},
  "createdVia": "api"
}
```

After it, `GET /v1/sales-invoices/c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f` shows `amountPaid: 230` and `amountDue: 0`. Money you do not allocate stays as the customer's credit, shown in `unallocatedAmount`. [Payments](https://docs.kosoor.com.sa/payments.md) has every rule.

## 5. Give the customer the invoice

`GET /v1/sales-invoices/{id}/pdf` returns the invoice on A4. A simplified invoice's PDF is ready at once. A standard invoice's PDF answers `409 invoice_not_cleared` until ZATCA clears it.

## What can go wrong

| Error | What it means | What to do |
|---|---|---|
| `422 customer_address_incomplete` | a standard invoice needs the company's full address and VAT number or ID; `missing` lists the gaps | fill them with `PATCH /v1/customers/{id}`, then send the invoice again |
| `422 on_account_not_allowed` | a person was not paid in full | send `payment` without `amount` |
| `422 credit_limit_exceeded` | the company's account would pass its credit limit | take payment at issue, or ask the merchant |
| `422 customer_inactive` | the merchant disabled the customer | ask the merchant |
| `422 item_not_sellable` | the item is a subscription, or not for sale | use another item |
| `400 missing_parameter` on `lines[i].unitPrice` | the item is priced per car size | send `unitPrice` |
| `403 branch_not_allowed` | the key is limited to other branches | use a branch from `GET /v1/branches` |
| `422 branch_not_onboarded` | the branch is not registered with ZATCA, so it cannot issue a standard invoice | ask the merchant |
| `422 zatca_rejected` | ZATCA rejected a standard invoice; nothing was saved | read `zatcaErrors`, fix the data, send it again under a new `Idempotency-Key` |

Every code is in [Error codes](https://docs.kosoor.com.sa/errors.md).

Source: https://docs.kosoor.com.sa/record-a-sale
