# Import invoices

Operation ID: `importInvoices`

Group: Accounting › Invoices

[Open in the interactive reference](https://docs.api.lensing.ai/lensing/v1/endpoints/importInvoices)

`POST /accounting/invoices/import`

Retry-safe: A write that accepts Idempotency-Key: repeating a request with the same key replays the stored answer instead of executing twice.

Imports invoices from another system. Use it for sales orders that another system invoices, where externalizedInvoicing is true on GET accounting/sales-orders/:id.

Each invoice is created exactly as you send it. It doesn't group lines into invoices, calculate tax, or apply one-time discounts for these orders.

For each line, send its total and its tax. For a tax-exempt line, send an empty taxLines array. Include any one-time discount in the line's totalPrice.

Each line is matched to its planned invoicing line on the sales order by salesOrderProductLineId and plannedDate. If the sales order has no planned line for that date and its invoicing schedule is still open, one is created.

If any invoice can't be imported, none are. The response lists the new invoices' ids in the order you sent them. To read one, use GET accounting/invoices/:id.

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Api-Version` | header | string · 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 this system is refused with SALES_ORDER_NOT_EXTERNALISED. See id on GET accounting/sales-orders/:id. Example: `"8c1f4a92-3e77-4d21-9b05-6a2f1c8e4d33"`. |
| `invoices` | array<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. Example: `[{"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"}]}]}]`. |
| `invoices[].externalInvoiceId` | string | no | The id of this invoice in the source system, stamped on the created invoice for reconciliation. |
| `invoices[].invoiceDate` | date | yes | The date to stamp on this invoice. Format YYYY-MM-DD. |
| `invoices[].lines` | array<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` | 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` | 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` | 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` | 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` | 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` | 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` | any | no | Whether totalPrice already includes tax. One of: `"TaxInclusive"`, `"TaxExclusive"`. |
| `invoices[].lines[].taxLines` | array<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` | decimal | yes | The tax amount for this jurisdiction, in the sales order's currency. |
| `invoices[].lines[].taxLines[].rate` | 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` | decimal | no | The pre-tax amount the rate was applied to, in the sales order's currency. |
| `invoices[].lines[].taxLines[].exemptAmount` | 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` | any | no | The VAT treatment for this jurisdiction. When supplied, the built-in VAT determination is bypassed. One of: `"salesTax"`, `"domesticStandardRate"`, `"domesticReducedRate"`, `"domesticSalesZeroRated"`, `"domesticSalesExempt"`, `"vatIntraEuStandardRate"`, `"vatIntraEuReducedRate"`, `"nonEuExempt"`, `"reverseCharge"`. |

```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<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. Example: `["f3b9c1d4-2a68-4e07-8c15-9d0b7e5a3f62"]`. |

```json
{
  "data": {
    "ids": [
      "f3b9c1d4-2a68-4e07-8c15-9d0b7e5a3f62"
    ]
  },
  "diagnostics": [
    {
      "code": "INVOICES_IMPORTED",
      "message": "<message>",
      "severity": "error",
      "path": [
        "<path>"
      ],
      "details": {
        "count": 0
      }
    }
  ]
}
```

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

## Advisories

Warnings and information a successful response can carry under `diagnostics`:

- `INVOICES_IMPORTED`

## 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` — The request carries no valid 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 from this client address. The response has no body and no `Retry-After` header.
- `500` — Internal server error
