# Records invoices billed by an external system.

`POST /accounting/invoices/import`

Group: Accounting › Invoices · Operation ID: `importInvoices`

Records invoices that another system decided, against a sales order flagged for externalized billing. The response contains only the ids of the invoices created, in the same order as the entries you sent, so entry N and id N are the same invoice. Read each one back with GET accounting/invoices/:id.

You supply the grouping, each line's total and the tax. This path calls no tax engine. It resolves each line to its invoice-plan line by salesOrderProductLineId and plannedDate, creating one where none exists and the schedule is still open, then creates the invoices exactly as you grouped them. Two consequences follow. A tax-exempt line sends an empty taxLines array rather than omitting it. Any one-time discount must already be netted into each line's totalPrice, because the built-in discount allocation is skipped for externalized sales orders.

This endpoint serves only externalized sales orders, ones whose invoices are decided elsewhere. A sales order whose billing is scheduled in Everest is refused with SALES_ORDER_NOT_EXTERNALISED, because its invoices are created by the built-in billing run, and there is no API endpoint for that. Which applies is a property of the sales order, not a choice.

The call runs in one transaction. If any entry fails, no invoice from the batch is recorded.

[Open in the interactive reference](https://docs.api.lensing.ai/?op=importInvoices)

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Api-Version` | header | `"v1"` | yes | The API version this request targets, named by its public identifier. Required on every request; omitting it, or naming a version this company does not have, is answered with 400. This document describes version `v1`. |
| `Idempotency-Key` | header | string | no | An opaque caller-chosen string (1-255 characters) that makes this request safe to retry. Repeating the same key with an identical request body replays the original response instead of re-executing - but only once that original request succeeded; a failed attempt re-executes on retry. Repeating the key with a different body is answered with 422. A key still in flight is answered with 409 - as is a key whose original request was interrupted before it finished, for a few minutes afterwards, until that attempt is presumed dead. A completed response stays replayable for 24 hours. The key is scoped to the caller, not to the action: reusing it for a different action is answered with 409 or 422 rather than treated independently. On read actions this header is ignored. On write actions that cannot offer the guarantee it is answered with `400 IDEMPOTENCY_NOT_SUPPORTED` rather than silently ignored. |

## Request body

Required, `application/json`: importInvoicesInput.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `salesOrderId` | string | yes | The sales order these invoices bill. It must be flagged for externalized billing. One whose billing is scheduled in Everest is refused with SALES_ORDER_NOT_EXTERNALISED. See id on GET accounting/sales-orders/:id. |
| `invoices` | array of object | yes | The invoices to record, one entry each, exactly as the source system grouped them. Every entry yields one invoice, and the ids come back in this order. |
| `invoices[].externalInvoiceId` | string | no | The id of this invoice in the source system, stamped on the created invoice for reconciliation. |
| `invoices[].invoiceDate` | string (date) | yes | The date to stamp on this invoice. Format YYYY-MM-DD. |
| `invoices[].lines` | array of object | yes | The lines of this invoice. Each resolves to one invoice-plan line by salesOrderProductLineId and plannedDate. |
| `invoices[].lines[].salesOrderProductLineId` | string | yes | The unique identifier for the sales-order product line this invoice line bills against. There is no endpoint that returns a single sales-order product line. |
| `invoices[].lines[].plannedDate` | string (date) | yes | The planned billing date of the target invoice-plan line. Format YYYY-MM-DD. When no plan line exists for this date and the schedule is still open, one is created using this as both its service start date and its invoice date. |
| `invoices[].lines[].serviceEndDate` | string (date) | no | The last day of the service period, for a plan line created on an open schedule. Format YYYY-MM-DD. Ignored when the line matches an existing plan line. |
| `invoices[].lines[].totalPrice` | string (decimal) | yes | The pre-tax line total, in the sales order's currency. It must match the resolved plan line's total price. Any one-time discount must already be netted in. |
| `invoices[].lines[].dutiesAndCustomsAmount` | string (decimal) | no | The duties and customs charged on this line, in the sales order's currency. Requires the entity's Duties and Customs feature to be enabled. |
| `invoices[].lines[].taxAmount` | string (decimal) | no | The total tax on this line, in the sales order's currency. When given, it must equal the sum of taxLines[].amount. When omitted, it is computed from that sum. |
| `invoices[].lines[].taxCode` | string | no | The line-level tax code, as the source tax engine issued it. |
| `invoices[].lines[].taxDate` | string (date) | no | The date the line's tax was determined on. Format YYYY-MM-DD. Typically the service end date or the invoice date. |
| `invoices[].lines[].taxExemptCode` | string | no | The exempt code applied to this line. |
| `invoices[].lines[].exceptionNumber` | string | no | The exception number for the tax exemption, as the source tax engine issued it. |
| `invoices[].lines[].taxMode` | string, one of `"TaxInclusive"`, `"TaxExclusive"` | no | Whether totalPrice already includes tax. |
| `invoices[].lines[].taxLines` | array of object | yes | The per-jurisdiction tax for this line. You supply it, because no tax engine is called on this path. Send an empty array for a tax-exempt line. |
| `invoices[].lines[].taxLines[].name` | string | yes | The name of the tax as it should appear on the document, for example 'CA Sales Tax' or 'VAT'. |
| `invoices[].lines[].taxLines[].amount` | string (decimal) | yes | The tax amount for this jurisdiction, in the sales order's currency. |
| `invoices[].lines[].taxLines[].rate` | string (decimal) | no | The tax rate as a decimal fraction. For example, 0.21 is 21%. A rate, not an amount, so it has no currency. |
| `invoices[].lines[].taxLines[].taxableAmount` | string (decimal) | no | The pre-tax amount the rate was applied to, in the sales order's currency. |
| `invoices[].lines[].taxLines[].exemptAmount` | string (decimal) | no | The portion of the taxable base that was exempted, in the sales order's currency. |
| `invoices[].lines[].taxLines[].taxType` | string | no | The engine classification of the tax, for example 'Sales' or 'VAT'. |
| `invoices[].lines[].taxLines[].rateType` | string | no | The VAT rate classification, for example 'Standard', 'Reduced' or 'Zero'. |
| `invoices[].lines[].taxLines[].vatCode` | string | no | The VAT code for this jurisdiction. |
| `invoices[].lines[].taxLines[].isItemTaxable` | boolean | no | Whether the item is taxable in this jurisdiction. |
| `invoices[].lines[].taxLines[].taxTreatment` | string, one of `"salesTax"`, `"domesticStandardRate"`, `"domesticReducedRate"`, `"domesticSalesZeroRated"`, `"domesticSalesExempt"`, `"vatIntraEuStandardRate"`, `"vatIntraEuReducedRate"`, `"nonEuExempt"`, `"reverseCharge"` | no | The VAT treatment for this jurisdiction. When supplied, the built-in VAT determination is bypassed. |

Example:

```json
{
  "salesOrderId": "8c1f4a92-3e77-4d21-9b05-6a2f1c8e4d33",
  "invoices": [
    {
      "externalInvoiceId": "NS-INV-88213",
      "invoiceDate": "2026-09-01",
      "lines": [
        {
          "salesOrderProductLineId": "b2d7f309-6c14-4a85-9f37-0e8c5d1b7a26",
          "plannedDate": "2026-09-01",
          "serviceEndDate": "2026-09-30",
          "totalPrice": "1000.00",
          "taxAmount": "210.00",
          "taxCode": "NL-VAT-STD",
          "taxMode": "TaxExclusive",
          "taxLines": [
            {
              "name": "NL VAT",
              "amount": "210.00",
              "rate": "0.21",
              "taxableAmount": "1000.00",
              "taxType": "VAT",
              "rateType": "Standard",
              "taxTreatment": "domesticStandardRate"
            }
          ]
        }
      ]
    }
  ]
}
```

## Response

`200` Success

Under `data` (importInvoicesOutput):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `ids` | array of string | yes | The ids of the invoices recorded, one per requested entry and in the same order. Read each back with GET accounting/invoices/:id. |

## Response headers

- `Idempotency-Replayed` — Present and `true` only when this response replays a previously stored one for the same Idempotency-Key, rather than reflecting a live execution. Absent on a live response - never sent as `false`.

## Errors

| Code | Status |
| --- | --- |
| `SALES_ORDER_NOT_FOUND` | 400 |
| `SALES_ORDER_NOT_EXTERNALISED` | 400 |
| `SALES_ORDER_NOT_APPROVED` | 400 |
| `NO_INVOICES_REQUESTED` | 400 |
| `NO_LINES` | 400 |
| `IDEMPOTENCY_KEY_CONFLICT` | 409 |
| `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 |

## Statuses

- `400` — Invalid request or business failure. Codes: SALES_ORDER_NOT_FOUND, SALES_ORDER_NOT_EXTERNALISED, SALES_ORDER_NOT_APPROVED, NO_INVOICES_REQUESTED, NO_LINES
- `401` — Missing or invalid access token
- `403` — Not authorized
- `404` — Not found
- `409` — The Idempotency-Key is still being processed by another request.
- `422` — The Idempotency-Key was already used with a different request payload.
- `429` — Too many requests
- `500` — Internal server error
