# Lensing Public API v1 Reference # Accounting Period Statuses `GET /accounting/period-status` Group: Accounting › Accounting Period Status · Operation ID: `accountingPeriodStatuses` Returns a list of accounting period statuses, one per (entity, book, period). To read one with its close checklist, use GET accounting/period-status/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=accountingPeriodStatuses) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `accountingPeriodId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `bookId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `periodName` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `fiscalYear` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `fiscalQuarter` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `startDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `endDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `reopenedByCascade` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `postable` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, periodName, fiscalYear, fiscalQuarter, startDate, endDate, status, postable, lastModifiedDate - Selectable: id, accountingPeriodId, entityId, entityName, bookId, bookName, periodName, fiscalYear, fiscalQuarter, startDate, endDate, status, reopeningReason, reopenedByCascade, postable, postableReason, lastModifiedDate ## Response `200` Success Under `data`, an array of accountingPeriodStatusesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for this entity-book period status row. This identifies the (entity, book, period) combination, not the shared accounting period. | | `accountingPeriodId` | string or null | no | The identifier of the underlying accounting period, shared by every entity that uses this fiscal calendar. See id on GET accounting/accounting-periods/:id. | | `entityId` | string or null | no | The identifier of the legal entity this status belongs to. See id on GET base/entities. Absent only when the entity-book link is broken. | | `entityName` | string or null | no | The name of the legal entity this status belongs to. | | `bookId` | string or null | no | The identifier of the accounting book this status belongs to. A period is opened and closed per book. See id on GET accounting/books/:id. | | `bookName` | string or null | no | The name of the accounting book this status belongs to. | | `periodName` | string or null | no | The display name of the period as its fiscal calendar defines it, for example 'Jan 2026'. The format varies by calendar, so do not parse it. Match on dates instead. | | `fiscalYear` | string or null | no | The fiscal year label this period belongs to, for example 'FY2024'. Set by the calendar, so it need not match the calendar year of startDate. | | `fiscalQuarter` | string or null | no | The fiscal quarter label this period belongs to, for example 'Q1 2026'. | | `startDate` | string (date) or null | no | The first day of the period, inclusive. Format YYYY-MM-DD. Absent only when the underlying period was deleted. | | `endDate` | string (date) or null | no | The last day of the period, inclusive. Format YYYY-MM-DD. | | `status` | string | no | The status this entity and book have put the period in. Possible values: 'created' (generated, not yet opened), 'open', 'closed', 'reopened'. Only 'open' and 'reopened' accept postings. | | `reopeningReason` | string or null | no | The free-text reason recorded when the period was reopened. Set only when status is 'reopened'. | | `reopenedByCascade` | boolean or null | no | Whether this period was reopened as a side effect of reopening a later one, rather than directly. | | `postable` | boolean or null | no | Whether the period currently accepts postings for this entity and book, true when its status is 'open' or 'reopened'. This does not account for journal-type-specific locks. | | `postableReason` | string or null | no | Why the period does not accept postings, for example 'status is closed'. Empty when it is postable. | | `lastModifiedDate` | string (date-time) | no | The date and time this status row last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. It changes when the period is opened, closed or reopened for this entity and book. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Get Accounting Period Status `GET /accounting/period-status/{id}` Group: Accounting › Accounting Period Status · Operation ID: `getAccountingPeriodStatus` Returns the status of one accounting period for one entity and book, including whether it accepts postings and the period-close checklist. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getAccountingPeriodStatus) ## 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`. | | `id` | path | string | yes | The unique identifier for the entity-book period status row to return. This is the id of a GET accounting/period-status row, not the shared accountingPeriodId. | ## Response `200` Success Under `data` (getAccountingPeriodStatusOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for this entity-book period status row. This identifies the (entity, book, period) combination, not the shared accounting period. | | `accountingPeriodId` | string | no | The identifier of the underlying accounting period, shared by every entity that uses this fiscal calendar. See id on GET accounting/accounting-periods/:id. | | `entityId` | string | no | The identifier of the legal entity this status belongs to. See id on GET base/entities. Absent only when the entity-book link is broken. | | `entityName` | string | no | The name of the legal entity this status belongs to. | | `bookId` | string | no | The identifier of the accounting book this status belongs to. A period is opened and closed per book. See id on GET accounting/books/:id. | | `bookName` | string | no | The name of the accounting book this status belongs to. | | `periodName` | string | no | The display name of the period as its fiscal calendar defines it, for example 'Jan 2026'. The format varies by calendar, so do not parse it. Match on dates instead. | | `fiscalYear` | string | no | The fiscal year label this period belongs to, for example 'FY2024'. Set by the calendar, so it need not match the calendar year of startDate. | | `fiscalQuarter` | string | no | The fiscal quarter label this period belongs to, for example 'Q1 2026'. | | `startDate` | string (date) | no | The first day of the period, inclusive. Format YYYY-MM-DD. Absent only when the underlying period was deleted. | | `endDate` | string (date) | no | The last day of the period, inclusive. Format YYYY-MM-DD. | | `status` | string | yes | The status this entity and book have put the period in. Possible values: 'created' (generated, not yet opened), 'open', 'closed', 'reopened'. Only 'open' and 'reopened' accept postings. | | `postable` | boolean | yes | Whether the period currently accepts postings for this entity and book, true when its status is 'open' or 'reopened'. This does not account for journal-type-specific locks. | | `postableReason` | string | yes | Why the period does not accept postings, for example 'status is closed'. Empty when it is postable. | | `reopeningReason` | string | no | The free-text reason recorded when the period was reopened. Set only when status is 'reopened'. | | `reopenedByCascade` | boolean | no | Whether this period was reopened as a side effect of reopening a later one, rather than directly. | | `lastModifiedDate` | string (date-time) | yes | The date and time this status row last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. It changes when the period is opened, closed or reopened for this entity and book. | | `closeTasks` | array of object | no | The period-close checklist for this entity and book, one entry per task. An empty array when the row has no tasks yet. Check isExecutable before running a task with POST accounting/period-status/:id/execute-task. | | `closeTasks[].taskCode` | string | yes | The code identifying the task, and the value to pass as task to POST accounting/period-status/:id/execute-task. The forward tasks, for example 'open', 'close', 'reopen', 'lockAP', 'lockAR', 'lockRevenue', 'lockJournals', 'runRevaluation', 'calculateConsolidationRates'. The task that undoes one is in reverseTaskCode. | | `closeTasks[].reverseTaskCode` | string | no | The code of the task that undoes this one, for example 'unlockAP' for 'lockAP'. Empty when the task cannot be reversed. | | `closeTasks[].isExecuted` | boolean | yes | Whether this task has already run for this entity-book period. | | `closeTasks[].isExecutable` | boolean | yes | Whether this task can run now. It is false for a task already done and for one whose preconditions are unmet. notExecutableReason says which. | | `closeTasks[].notExecutableReason` | string | no | Why the task cannot run now. Present only when isExecutable is false and the engine gave a reason. | | `closeTasks[].isReversible` | boolean | yes | Whether the task that undoes this one can run now. | | `closeTasks[].notReversibleReason` | string | no | Why the reversal cannot run now. Present only when isReversible is false and the engine gave a reason. | | `closeTasks[].notApplicable` | boolean | no | Whether this task does not apply to this entity-book period at all, for example a consolidation task on an entity that does not consolidate. Such a task is neither executed nor executable and does not block the close. | | `closeTasks[].message` | string | no | A human-readable, translated description of the task's state: the applied message when it has run, the not-applied message when it has not, or the not-applicable message when it does not apply. | | `closeTasks[].lastModifiedDate` | string (date-time) | no | The date and time this task's state last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Absent on a task that has never run. | | `closeTasks[].lastModifiedBy` | string | no | The identifier of the user who last changed this task's state. Absent on a task that has never run. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Execute Accounting Period Task `POST /accounting/period-status/{id}/execute-task` Group: Accounting › Accounting Period Status · Operation ID: `executeAccountingPeriodTask` Runs one period-close task against one entity-book period status: opening, closing or reopening it, or locking or unlocking AP, AR, journals or revenue. Read closeTasks on GET accounting/period-status/:id and check isExecutable first. Returns no content. [Open in the interactive reference](https://docs.api.lensing.ai/?op=executeAccountingPeriodTask) ## 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. | | `id` | path | string | yes | The unique identifier for the entity-book period status row to run the task against. This is the id of a GET accounting/period-status row, not the shared accountingPeriodId. | ## Request body Required, `application/json`: executeAccountingPeriodTaskInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `task` | string | yes | The close task to run. Possible values: 'open', 'close', 'reopen', 'lockAR', 'unlockAR', 'lockAP', 'unlockAP', 'lockJournals', 'unlockJournals', 'lockRevenue', 'unlockRevenue', 'runRevaluation', 'calculateConsolidationRates'. These are the taskCode values on closeTasks. An unrecognized code fails with UNKNOWN_TASK. A recognized code that cannot run now fails with TASK_NOT_EXECUTABLE. | | `reopeningReason` | string | no | A free-text reason recorded against the period. Used only by the 'reopen' task, which stores it as the period's reopeningReason and shows it on both read routes. Other tasks ignore it. Supply it when reopening a closed period, since it is the only record of why the close was undone. | Example: ```json { "task": "", "reopeningReason": "" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `UNKNOWN_TASK` | 400 | | `TASK_NOT_EXECUTABLE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, UNKNOWN_TASK, TASK_NOT_EXECUTABLE - `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 --- # Accounting Periods `GET /accounting/accounting-periods` Group: Accounting › Accounting Periods · Operation ID: `accountingPeriods` Returns a list of accounting periods, one per (entity, book, period). To read one with its close checklist, use GET accounting/accounting-periods/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=accountingPeriods) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `accountingPeriodId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `bookId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `fiscalCalendarId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `periodName` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `fiscalYear` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `fiscalQuarter` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `startDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `endDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `reopenedByCascade` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `durationDays` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `hasTransactions` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `postable` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, periodName, fiscalYear, fiscalQuarter, startDate, endDate, status, durationDays, postable, lastModifiedDate - Selectable: id, accountingPeriodId, entityId, entityName, bookId, bookName, fiscalCalendarId, fiscalCalendarName, periodName, fiscalYear, fiscalQuarter, startDate, endDate, status, reopeningReason, reopenedByCascade, durationDays, hasTransactions, postable, postableReason, lastModifiedDate ## Response `200` Success Under `data`, an array of accountingPeriodsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for this entity-book accounting period row. This identifies the (entity, book, period) combination, not the shared accounting period. | | `accountingPeriodId` | string or null | no | The identifier of the underlying accounting period, shared by every entity that uses this fiscal calendar. See id on GET accounting/accounting-periods/:id. | | `entityId` | string or null | no | The identifier of the legal entity this status belongs to. See id on GET base/entities. Absent only when the entity-book link is broken. | | `entityName` | string or null | no | The name of the legal entity this status belongs to. | | `bookId` | string or null | no | The identifier of the accounting book this status belongs to. A period is opened and closed per book. See id on GET accounting/books/:id. | | `bookName` | string or null | no | The name of the accounting book this status belongs to. | | `fiscalCalendarId` | string or null | no | The identifier of the fiscal calendar this period belongs to. See id on GET accounting/fiscal-calendars/:id. Absent when the period was deleted from under its status row. | | `fiscalCalendarName` | string or null | no | The name of the fiscal calendar this period belongs to. | | `periodName` | string or null | no | The display name of the period as its fiscal calendar defines it, for example 'Jan 2026'. The format varies by calendar, so do not parse it. Match on dates instead. | | `fiscalYear` | string or null | no | The fiscal year label this period belongs to, for example 'FY2024'. Set by the calendar, so it need not match the calendar year of startDate. | | `fiscalQuarter` | string or null | no | The fiscal quarter label this period belongs to, for example 'Q1 2026'. | | `startDate` | string (date) or null | no | The first day of the period, inclusive. Format YYYY-MM-DD. Absent only when the underlying period was deleted. | | `endDate` | string (date) or null | no | The last day of the period, inclusive. Format YYYY-MM-DD. | | `status` | string | no | The status this entity and book have put the period in. Possible values: 'created' (generated, not yet opened), 'open', 'closed', 'reopened'. Only 'open' and 'reopened' accept postings. | | `reopeningReason` | string or null | no | The free-text reason recorded when the period was reopened. Set only when status is 'reopened'. | | `reopenedByCascade` | boolean or null | no | Whether this period was reopened as a side effect of reopening a later one, rather than directly. | | `durationDays` | number or null | no | The number of days the period spans, both bounds inclusive. Absent when startDate or endDate is. | | `hasTransactions` | boolean or null | no | Whether at least one journal entry line has been posted into the underlying period, by any entity. | | `postable` | boolean or null | no | Whether the period currently accepts postings for this entity and book, true when its status is 'open' or 'reopened'. This does not account for journal-type-specific locks. | | `postableReason` | string or null | no | Why the period does not accept postings, for example 'status is closed'. Empty when it is postable. | | `lastModifiedDate` | string (date-time) | no | The date and time this status row last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. It changes when the period is opened, closed or reopened for this entity and book. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Get Accounting Period `GET /accounting/accounting-periods/{id}` Group: Accounting › Accounting Periods · Operation ID: `getAccountingPeriod` Returns the accounting period with the given identifier, as it stands for one entity and book, including whether it accepts postings and the period-close checklist. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getAccountingPeriod) ## 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`. | | `id` | path | string | yes | The unique identifier for the entity-book accounting period row to return. This is the id of a GET accounting/accounting-periods row, not the shared accountingPeriodId. | ## Response `200` Success Under `data` (getAccountingPeriodOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for this entity-book accounting period row. This identifies the (entity, book, period) combination, not the shared accounting period. | | `accountingPeriodId` | string | no | The identifier of the underlying accounting period, shared by every entity that uses this fiscal calendar. See id on GET accounting/accounting-periods/:id. | | `entityId` | string | no | The identifier of the legal entity this status belongs to. See id on GET base/entities. Absent only when the entity-book link is broken. | | `entityName` | string | no | The name of the legal entity this status belongs to. | | `bookId` | string | no | The identifier of the accounting book this status belongs to. A period is opened and closed per book. See id on GET accounting/books/:id. | | `bookName` | string | no | The name of the accounting book this status belongs to. | | `fiscalCalendarId` | string | no | The identifier of the fiscal calendar this period belongs to. See id on GET accounting/fiscal-calendars/:id. Absent when the period was deleted from under its status row. | | `fiscalCalendarName` | string | no | The name of the fiscal calendar this period belongs to. | | `periodName` | string | no | The display name of the period as its fiscal calendar defines it, for example 'Jan 2026'. The format varies by calendar, so do not parse it. Match on dates instead. | | `fiscalYear` | string | no | The fiscal year label this period belongs to, for example 'FY2024'. Set by the calendar, so it need not match the calendar year of startDate. | | `fiscalQuarter` | string | no | The fiscal quarter label this period belongs to, for example 'Q1 2026'. | | `startDate` | string (date) | no | The first day of the period, inclusive. Format YYYY-MM-DD. Absent only when the underlying period was deleted. | | `endDate` | string (date) | no | The last day of the period, inclusive. Format YYYY-MM-DD. | | `durationDays` | number | no | The number of days the period spans, both bounds inclusive. Absent when startDate or endDate is. | | `status` | string | yes | The status this entity and book have put the period in. Possible values: 'created' (generated, not yet opened), 'open', 'closed', 'reopened'. Only 'open' and 'reopened' accept postings. | | `postable` | boolean | yes | Whether the period currently accepts postings for this entity and book, true when its status is 'open' or 'reopened'. This does not account for journal-type-specific locks. | | `postableReason` | string | yes | Why the period does not accept postings, for example 'status is closed'. Empty when it is postable. | | `reopeningReason` | string | no | The free-text reason recorded when the period was reopened. Set only when status is 'reopened'. | | `reopenedByCascade` | boolean | no | Whether this period was reopened as a side effect of reopening a later one, rather than directly. | | `hasTransactions` | boolean | no | Whether at least one journal entry line has been posted into the underlying period, by any entity. | | `lastModifiedDate` | string (date-time) | yes | The date and time this status row last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. It changes when the period is opened, closed or reopened for this entity and book. | | `closeTasks` | array of object | no | The period-close checklist for this entity and book, one entry per task. An empty array when the row has no tasks yet. Check isExecutable before running a task with POST accounting/period-status/:id/execute-task. | | `closeTasks[].taskCode` | string | yes | The code identifying the task, and the value to pass as task to POST accounting/period-status/:id/execute-task. The forward tasks, for example 'open', 'close', 'reopen', 'lockAP', 'lockAR', 'lockRevenue', 'lockJournals', 'runRevaluation', 'calculateConsolidationRates'. The task that undoes one is in reverseTaskCode. | | `closeTasks[].reverseTaskCode` | string | no | The code of the task that undoes this one, for example 'unlockAP' for 'lockAP'. Empty when the task cannot be reversed. | | `closeTasks[].isExecuted` | boolean | yes | Whether this task has already run for this entity-book period. | | `closeTasks[].isExecutable` | boolean | yes | Whether this task can run now. It is false for a task already done and for one whose preconditions are unmet. notExecutableReason says which. | | `closeTasks[].notExecutableReason` | string | no | Why the task cannot run now. Present only when isExecutable is false and the engine gave a reason. | | `closeTasks[].isReversible` | boolean | yes | Whether the task that undoes this one can run now. | | `closeTasks[].notReversibleReason` | string | no | Why the reversal cannot run now. Present only when isReversible is false and the engine gave a reason. | | `closeTasks[].notApplicable` | boolean | no | Whether this task does not apply to this entity-book period at all, for example a consolidation task on an entity that does not consolidate. Such a task is neither executed nor executable and does not block the close. | | `closeTasks[].message` | string | no | A human-readable, translated description of the task's state: the applied message when it has run, the not-applied message when it has not, or the not-applicable message when it does not apply. | | `closeTasks[].lastModifiedDate` | string (date-time) | no | The date and time this task's state last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Absent on a task that has never run. | | `closeTasks[].lastModifiedBy` | string | no | The identifier of the user who last changed this task's state. Absent on a task that has never run. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Accounts `GET /accounting/accounts` Group: Accounting › Accounts · Operation ID: `accounts` Returns a list of accounts. To match on name or by external identifier, use POST accounting/accounts/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=accounts) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `accountNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `accountType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `accountSubType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `accountStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `chartOfAccountsId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `parentAccountId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `groupingAccount` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `revaluation` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `payOnBehalf` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `elimination` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `exchangeRateType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, accountNumber, accountType, accountSubType, accountStatus, chartOfAccountsId, groupingAccount, entityId, currency, lastModifiedDate - Selectable: id, accountNumber, accountName, accountType, accountSubType, accountStatus, description, chartOfAccountsId, parentAccountId, groupingAccount, entityId, currency, revaluation, payOnBehalf, elimination, exchangeRateType, lastModifiedDate ## Response `200` Success Under `data`, an array of accountsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the account. | | `accountNumber` | string | no | The account number, unique within its chart of accounts. | | `accountName` | string | no | The display name of the account. | | `accountType` | string | no | The top-level classification of the account. Possible values: 'asset', 'liability', 'equity', 'income', 'costOfGoodsSold', 'expense', 'otherIncome', 'otherExpense'. | | `accountSubType` | string or null | no | A finer classification within accountType. Possible values: 'bank', 'accountsReceivable', 'fixedAsset', 'cip', 'prepaidExpenses', 'deferredExpenses', 'otherCurrentAsset', 'otherAsset', 'unbilledReceivable', 'longTermAsset', 'accountsPayable', 'creditCard', 'deferredRevenue', 'otherCurrentLiability', 'longTermLiability', 'equity', 'income', 'costOfGoodsSold', 'expense', 'otherIncome', 'otherExpense', 'otherComprehensiveIncome'. Absent when none is assigned. | | `accountStatus` | string or null | no | The lifecycle status of the account. Possible values: 'enabled' (available on transactions), 'disabled' (not available for new transactions), 'draft' (created but not yet reviewed), 'draftErrors' (a draft with validation errors to resolve before it can be enabled). | | `description` | string or null | no | A free-form description of the account. Absent when none was set. | | `chartOfAccountsId` | string or null | no | The identifier of the chart of accounts this account belongs to. An account belongs to exactly one. See id on GET accounting/chart-of-accounts/:id. | | `parentAccountId` | string or null | no | The identifier of the parent account this account rolls up to. Absent for a top-level account. See id on GET accounting/accounts/:id. | | `groupingAccount` | boolean | no | Whether this is a grouping (rollup) account. Balances from child accounts aggregate here, and journal entries cannot be posted to it directly. | | `entityId` | string or null | no | The identifier of the legal entity this account is denominated for. Set only on bank and credit-card accounts. See id on GET base/entities. | | `currency` | string or null | no | The account's currency. Format ISO 4217, for example 'USD'. Set only on accounts linked to a cash account, alongside entityId. | | `revaluation` | boolean or null | no | Whether balances on this account are revalued for exchange-rate changes. | | `payOnBehalf` | boolean or null | no | Whether this account takes part in pay-on-behalf accounting. | | `elimination` | boolean or null | no | Whether this account is eliminated during consolidation. | | `exchangeRateType` | string or null | no | Which rate translates this account's amounts on revaluation. Possible values: 'historical', 'average', 'current'. Absent on grouping accounts. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the account record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the account with the given identifier. `GET /accounting/accounts/{id}` Group: Accounting › Accounts · Operation ID: `getAccount` [Open in the interactive reference](https://docs.api.lensing.ai/?op=getAccount) ## 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`. | | `id` | path | string | yes | The unique identifier for the account to return. | ## Response `200` Success Under `data` (getAccountOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the account. | | `accountNumber` | string | yes | The account number, unique within its chart of accounts. | | `accountName` | string | yes | The display name of the account. | | `accountType` | string | yes | The top-level classification of the account. Possible values: 'asset', 'liability', 'equity', 'income', 'costOfGoodsSold', 'expense', 'otherIncome', 'otherExpense'. | | `accountSubType` | string | no | A finer classification within accountType. Possible values: 'bank', 'accountsReceivable', 'fixedAsset', 'cip', 'prepaidExpenses', 'deferredExpenses', 'otherCurrentAsset', 'otherAsset', 'unbilledReceivable', 'longTermAsset', 'accountsPayable', 'creditCard', 'deferredRevenue', 'otherCurrentLiability', 'longTermLiability', 'equity', 'income', 'costOfGoodsSold', 'expense', 'otherIncome', 'otherExpense', 'otherComprehensiveIncome'. Absent when none is assigned. | | `accountStatus` | string | no | The lifecycle status of the account. Possible values: 'enabled' (available on transactions), 'disabled' (not available for new transactions), 'draft' (created but not yet reviewed), 'draftErrors' (a draft with validation errors to resolve before it can be enabled). | | `description` | string | no | A free-form description of the account. Absent when none was set. | | `chartOfAccountsId` | string | yes | The identifier of the chart of accounts this account belongs to. An account belongs to exactly one. See id on GET accounting/chart-of-accounts/:id. | | `parentAccountId` | string | no | The identifier of the parent account this account rolls up to. Absent for a top-level account. See id on GET accounting/accounts/:id. | | `groupingAccount` | boolean | yes | Whether this is a grouping (rollup) account. Balances from child accounts aggregate here, and journal entries cannot be posted to it directly. | | `entityId` | string | no | The identifier of the legal entity this account is denominated for. Set only on bank and credit-card accounts. See id on GET base/entities. | | `currency` | string | no | The account's currency. Format ISO 4217, for example 'USD'. Set only on accounts linked to a cash account, alongside entityId. | | `revaluation` | boolean | no | Whether balances on this account are revalued for exchange-rate changes. | | `payOnBehalf` | boolean | no | Whether this account takes part in pay-on-behalf accounting. | | `elimination` | boolean | no | Whether this account is eliminated during consolidation. | | `exchangeRateType` | string | no | Which rate translates this account's amounts on revaluation. Possible values: 'historical', 'average', 'current'. Absent on grouping accounts. | | `lastModifiedDate` | string (date-time) | yes | The date and time the account record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `externalIdentifiers` | array of object | no | External identifiers owned by other systems, at most one per fieldName. An empty array when the account has none. | | `externalIdentifiers[].fieldName` | string | yes | The configured key of the external identifier, a camelCase slug such as 'netsuiteAccountId'. It is the fieldName of an existing account external-identifier configuration. | | `externalIdentifiers[].value` | string | yes | The identifier as the source system holds it. | | `externalIdentifiers[].externalUrl` | string | no | An absolute http(s) link to the object in the source system. Absent when none is stored. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Search Account `POST /accounting/accounts/search` Group: Accounting › Accounts · Operation ID: `searchAccount` Returns the accounts that match the given filters, ordered by account number. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchAccount) ## 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`. | ## Request body Required, `application/json`: searchAccountInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. An account must match every filter you send. Omit to return all accounts. | | `filters.id` | string | no | Limits results to the account with this identifier. The match is exact. | | `filters.accountNumber` | string | no | Limits results to the account with this number. Pair it with chartOfAccountsId, since the number is unique only within a chart of accounts. | | `filters.accountName` | string | no | Limits results to accounts whose name contains this text. The match is case-insensitive. | | `filters.chartOfAccountsId` | string | no | Limits results to accounts in this chart of accounts, by its identifier. An identifier that matches none returns an empty page. | | `filters.accountType` | string | no | Limits results to accounts of this type. Possible values: 'asset', 'liability', 'equity', 'income', 'costOfGoodsSold', 'expense', 'otherIncome', 'otherExpense'. Any other value matches nothing. | | `filters.accountSubType` | string | no | Limits results to accounts of this sub-type. Possible values: 'bank', 'accountsReceivable', 'fixedAsset', 'cip', 'prepaidExpenses', 'deferredExpenses', 'otherCurrentAsset', 'otherAsset', 'unbilledReceivable', 'longTermAsset', 'accountsPayable', 'creditCard', 'deferredRevenue', 'otherCurrentLiability', 'longTermLiability', 'equity', 'income', 'costOfGoodsSold', 'expense', 'otherIncome', 'otherExpense', 'otherComprehensiveIncome'. Any other value matches nothing. | | `filters.accountStatus` | string | no | Limits results to accounts with this status. Possible values: 'enabled', 'disabled', 'draft', 'draftErrors'. Any other value matches nothing. | | `filters.entityId` | string | no | Limits results to accounts denominated for this legal entity, by its identifier. Only bank and credit-card accounts name an entity, so no other sub-type matches. An identifier that matches none returns an empty page. | | `filters.groupingAccount` | boolean | no | True returns only grouping accounts, false only posting accounts. Omit for both. | | `filters.externalIdentifier` | object | no | Limits results to accounts carrying this external identifier. Both fieldName and value are required. Several accounts may share a value, so this returns the items array, not a single record. | | `filters.externalIdentifier.fieldName` | string | yes | The configured key of the external identifier, a camelCase slug such as 'netsuiteAccountId'. It must be the fieldName of an existing configuration, or the call fails with UNKNOWN_EXTERNAL_IDENTIFIER. | | `filters.externalIdentifier.value` | string | yes | The identifier as the source system holds it. The match is exact and case-sensitive. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of accounts per page, between 1 and 200. Defaults to 50. A value outside that range fails with INVALID_PAGE_SIZE rather than being adjusted. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "accountNumber": "", "accountName": "", "chartOfAccountsId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "accountType": "", "accountSubType": "", "accountStatus": "", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "groupingAccount": true, "externalIdentifier": { "fieldName": "", "value": "" } }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchAccountOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The accounts that match, ordered by account number. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the account. | | `items[].accountNumber` | string | yes | The account number, unique within its chart of accounts. | | `items[].accountName` | string | yes | The display name of the account. | | `items[].accountType` | string | yes | The top-level classification of the account. Possible values: 'asset', 'liability', 'equity', 'income', 'costOfGoodsSold', 'expense', 'otherIncome', 'otherExpense'. | | `items[].accountSubType` | string | no | A finer classification within accountType. Possible values: 'bank', 'accountsReceivable', 'fixedAsset', 'cip', 'prepaidExpenses', 'deferredExpenses', 'otherCurrentAsset', 'otherAsset', 'unbilledReceivable', 'longTermAsset', 'accountsPayable', 'creditCard', 'deferredRevenue', 'otherCurrentLiability', 'longTermLiability', 'equity', 'income', 'costOfGoodsSold', 'expense', 'otherIncome', 'otherExpense', 'otherComprehensiveIncome'. Absent when none is assigned. | | `items[].accountStatus` | string | no | The lifecycle status of the account. Possible values: 'enabled' (available on transactions), 'disabled' (not available for new transactions), 'draft' (created but not yet reviewed), 'draftErrors' (a draft with validation errors to resolve before it can be enabled). | | `items[].description` | string | no | A free-form description of the account. Absent when none was set. | | `items[].chartOfAccountsId` | string | yes | The identifier of the chart of accounts this account belongs to. An account belongs to exactly one. See id on GET accounting/chart-of-accounts/:id. | | `items[].parentAccountId` | string | no | The identifier of the parent account this account rolls up to. Absent for a top-level account. See id on GET accounting/accounts/:id. | | `items[].groupingAccount` | boolean | yes | Whether this is a grouping (rollup) account. Balances from child accounts aggregate here, and journal entries cannot be posted to it directly. | | `items[].entityId` | string | no | The identifier of the legal entity this account is denominated for. Set only on bank and credit-card accounts. See id on GET base/entities. | | `items[].currency` | string | no | The account's currency. Format ISO 4217, for example 'USD'. Set only on accounts linked to a cash account, alongside entityId. | | `items[].revaluation` | boolean | no | Whether balances on this account are revalued for exchange-rate changes. | | `items[].payOnBehalf` | boolean | no | Whether this account takes part in pay-on-behalf accounting. | | `items[].elimination` | boolean | no | Whether this account is eliminated during consolidation. | | `items[].exchangeRateType` | string | no | Which rate translates this account's amounts on revaluation. Possible values: 'historical', 'average', 'current'. Absent on grouping accounts. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the account record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].externalIdentifiers` | array of object | no | External identifiers owned by other systems, at most one per fieldName. An empty array when the account has none. | | `items[].externalIdentifiers[].fieldName` | string | yes | The configured key of the external identifier, a camelCase slug such as 'netsuiteAccountId'. It is the fieldName of an existing account external-identifier configuration. | | `items[].externalIdentifiers[].value` | string | yes | The identifier as the source system holds it. | | `items[].externalIdentifiers[].externalUrl` | string | no | An absolute http(s) link to the object in the source system. Absent when none is stored. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Errors | Code | Status | | --- | --- | | `UNKNOWN_EXTERNAL_IDENTIFIER` | 400 | | `INVALID_PAGE_SIZE` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: UNKNOWN_EXTERNAL_IDENTIFIER, INVALID_PAGE_SIZE - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Amortization Schedule Lines `GET /accounting/amortization-schedule-lines` Group: Accounting › Amortization Schedule Lines · Operation ID: `amortizationScheduleLines` Returns a list of amortization schedule instalments, one line per schedule per period. Filter outstanding=true, by entity and book, to find what a period close still owes. [Open in the interactive reference](https://docs.api.lensing.ai/?op=amortizationScheduleLines) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `amortizationScheduleHeaderId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `bookId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `scheduledPeriodId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `scheduledPeriodStartDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `scheduledPeriodEndDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `transactionCurrency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `amortizationMethod` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isJournalEntryPosted` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isConfirmedZero` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `outstanding` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, scheduledPeriodStartDate, scheduledPeriodEndDate, amount, amortizationStartDate, amortizationEndDate, lastModifiedDate - Selectable: id, amortizationScheduleHeaderId, entityId, entityName, bookId, bookName, scheduledPeriodId, scheduledPeriodName, scheduledPeriodStartDate, scheduledPeriodEndDate, amount, transactionCurrency, amortizationMethod, amortizationStartDate, amortizationEndDate, isJournalEntryPosted, isConfirmedZero, outstanding, lastModifiedDate ## Response `200` Success Under `data`, an array of amortizationScheduleLinesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for this amortization schedule line. | | `amortizationScheduleHeaderId` | string or null | no | The identifier of the amortization schedule this line belongs to. Several lines, one per period, share it. There is no endpoint for the schedule itself. | | `entityId` | string or null | no | The identifier of the legal entity this instalment belongs to. See id on GET base/entities. It is joined in through the schedule's source journal entry line, so it is absent when that link is broken, and such a row does not match an entityId filter even though it is still returned. | | `entityName` | string or null | no | The name of the legal entity this instalment belongs to. | | `bookId` | string or null | no | The identifier of the accounting book this schedule posts into. See id on GET accounting/books/:id. | | `bookName` | string or null | no | The name of the accounting book this schedule posts into. | | `scheduledPeriodId` | string or null | no | The identifier of the accounting period this instalment is scheduled into. See accountingPeriodId on GET accounting/accounting-periods. To check whether a close is blocked, bound by scheduledPeriodEndDate instead, since filtering on this misses earlier unposted instalments. | | `scheduledPeriodName` | string or null | no | The display name of the scheduled period as its fiscal calendar defines it, for example 'Aug 2026'. The format varies by calendar, so do not parse it. Match on dates instead. | | `scheduledPeriodStartDate` | string (date) or null | no | The first day of the scheduled period, inclusive. Format YYYY-MM-DD. | | `scheduledPeriodEndDate` | string (date) or null | no | The last day of the scheduled period, inclusive. Format YYYY-MM-DD. Use this bound for a close check: it catches every instalment due on or before the period being closed. | | `amount` | string (decimal) or null | no | The amount of this instalment, in the currency given by transactionCurrency. Read the two together. | | `transactionCurrency` | string or null | no | The currency that denominates amount, taken from the schedule's source journal entry line. Format ISO 4217, for example 'USD'. Absent on a row whose header or source document was deleted. A query that does not filter by book can return rows in several currencies. | | `amortizationMethod` | string or null | no | How the schedule spreads its total, from the header. Possible values: 'straightLine', 'straightLineProRateFirstLast', 'exactDays', 'pointInTime', 'usageBased'. | | `amortizationStartDate` | string (date) or null | no | The first day the schedule amortizes over, from the header. Format YYYY-MM-DD. This is the schedule's start, not this instalment's period. | | `amortizationEndDate` | string (date) or null | no | The last day the schedule amortizes over, from the header. Format YYYY-MM-DD. | | `isJournalEntryPosted` | boolean or null | no | Whether this instalment's journal entry has been posted. Prefer filtering on outstanding, which combines this with isConfirmedZero. | | `isConfirmedZero` | boolean or null | no | Whether this instalment was confirmed as a zero and so needs no posting. It can be unset, which means it is not confirmed zero. Do not negate it directly. Filter on outstanding instead. | | `outstanding` | boolean or null | no | Whether this instalment still has to happen, true when it is neither posted nor a confirmed zero. This is the field a period-close check filters on. | | `lastModifiedDate` | string (date-time) or null | no | The date and time this schedule line last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. It tracks the line itself, so a change to the header or the joined entity, book or period does not advance it. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of accounting books. `GET /accounting/books` Group: Accounting › Books · Operation ID: `books` [Open in the interactive reference](https://docs.api.lensing.ai/?op=books) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `type` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `accountingStandard` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `consolidation` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `baseBookId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, name, type, accountingStandard, consolidation, lastModifiedDate - Selectable: id, name, type, accountingStandard, consolidation, description, baseBookId, lastModifiedDate ## Response `200` Success Under `data`, an array of booksRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the accounting book. | | `name` | string | no | The display name of the accounting book. | | `type` | string | no | The kind of accounting book. Possible values: 'full', 'adjustment'. An adjustment book adjusts the full book named in baseBookId. | | `accountingStandard` | string or null | no | The accounting standard the book follows, for example 'IFRS' or 'US GAAP'. Absent when none is set. | | `consolidation` | boolean | no | Whether the book consolidates other books or entities. | | `description` | string or null | no | A free-form description of the accounting book. Absent when none is set. | | `baseBookId` | string or null | no | The full book this adjustment book adjusts. Absent for a full book. See id on GET accounting/books/:id. | | `lastModifiedDate` | string (date-time) | no | The date and time the accounting book record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # List Book Entity Assignments `GET /accounting/books/{bookId}/entity-assignments` Group: Accounting › Books · Operation ID: `listBookEntityAssignments` Returns the legal entities that post to the given accounting book, each with its chart of accounts, fiscal calendar and currency. [Open in the interactive reference](https://docs.api.lensing.ai/?op=listBookEntityAssignments) ## 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`. | | `bookId` | path | string | yes | The unique identifier for the accounting book whose assignments to return. | | `page` | query | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `pageSize` | query | string | no | The number of assignments per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | ## Response `200` Success Under `data` (listBookEntityAssignmentsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The book's entity assignments, one per legal entity that posts to it. An empty array when the book has none. | | `items[].id` | string | yes | The unique identifier for the entity assignment. | | `items[].bookId` | string | yes | The accounting book this assignment belongs to, the same book named on the path. See id on GET accounting/books/:id. | | `items[].bookName` | string | no | The display name of the accounting book. | | `items[].entityId` | string | yes | The legal entity assigned to the book. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the assigned legal entity. | | `items[].chartOfAccountsId` | string | no | The chart of accounts used for this entity on this book. Absent when none is assigned. See id on GET accounting/chart-of-accounts/:id. | | `items[].chartOfAccountsName` | string | no | The name of the chart of accounts. Absent when none is assigned. | | `items[].fiscalCalendarId` | string | no | The fiscal calendar used for this entity on this book. Absent when none is assigned. See id on GET accounting/fiscal-calendars/:id. | | `items[].fiscalCalendarName` | string | no | The name of the fiscal calendar. Absent when none is assigned. | | `items[].currency` | string | no | The currency for this entity on this book. Format ISO 4217, for example 'USD'. Absent when none is set. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the entity assignment last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as the page query parameter. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more assignments remain beyond this page. | ## Errors | Code | Status | | --- | --- | | `BOOK_NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: BOOK_NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the accounting book with the given identifier. `GET /accounting/books/{id}` Group: Accounting › Books · Operation ID: `getBook` [Open in the interactive reference](https://docs.api.lensing.ai/?op=getBook) ## 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`. | | `id` | path | string | yes | The unique identifier for the accounting book to return. | ## Response `200` Success Under `data` (getBookOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the accounting book. | | `name` | string | yes | The display name of the accounting book. | | `type` | string | yes | The kind of accounting book. Possible values: 'full', 'adjustment'. An adjustment book adjusts the full book named in baseBookId. | | `accountingStandard` | string | no | The accounting standard the book follows, for example 'IFRS' or 'US GAAP'. Absent when none is set. | | `consolidation` | boolean | yes | Whether the book consolidates other books or entities. | | `description` | string | no | A free-form description of the accounting book. Absent when none is set. | | `baseBookId` | string | no | The full book this adjustment book adjusts. Absent for a full book. See id on GET accounting/books/:id. | | `lastModifiedDate` | string (date-time) | yes | The date and time the accounting book record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Charts Of Accounts `GET /accounting/chart-of-accounts` Group: Accounting › Charts of Accounts · Operation ID: `chartsOfAccounts` Returns a list of charts of accounts, ordered by name. To match on name or description, use POST accounting/chart-of-accounts/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=chartsOfAccounts) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `accountNumbering` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `accountNumberingDigits` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, name, accountNumbering, accountNumberingDigits, lastModifiedDate - Selectable: id, name, accountNumbering, accountNumberingDigits, description, lastModifiedDate ## Response `200` Success Under `data`, an array of chartsOfAccountsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the chart of accounts. | | `name` | string | no | The display name of the chart of accounts. | | `accountNumbering` | string | no | How account numbers are assigned within this chart of accounts. Possible values: 'hierarchical' (numbers follow the account's position in the hierarchy), 'manual' (numbers are assigned by hand). | | `accountNumberingDigits` | string or null | no | The total width of an account number, which caps how deep the hierarchy can go. Possible values: 'sixDigits' (up to 4 levels), 'sevenDigits' (5), 'eightDigits' (6), 'nineDigits' (7), 'tenDigits' (8). Absent when no width was set. A width and the numbering strategy are independent, so check this field directly rather than inferring it from accountNumbering. | | `description` | string or null | no | A free-form description of the chart of accounts. Absent when none was set. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the chart-of-accounts record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the chart of accounts with the given identifier. `GET /accounting/chart-of-accounts/{id}` Group: Accounting › Charts of Accounts · Operation ID: `getChartOfAccounts` [Open in the interactive reference](https://docs.api.lensing.ai/?op=getChartOfAccounts) ## 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`. | | `id` | path | string | yes | The unique identifier for the chart of accounts to return. | ## Response `200` Success Under `data` (getChartOfAccountsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the chart of accounts. | | `name` | string | yes | The display name of the chart of accounts. | | `accountNumbering` | string | yes | How account numbers are assigned within this chart of accounts. Possible values: 'hierarchical' (numbers follow the account's position in the hierarchy), 'manual' (numbers are assigned by hand). | | `accountNumberingDigits` | string | no | The total width of an account number, which caps how deep the hierarchy can go. Possible values: 'sixDigits' (up to 4 levels), 'sevenDigits' (5), 'eightDigits' (6), 'nineDigits' (7), 'tenDigits' (8). Absent when no width was set. A width and the numbering strategy are independent, so check this field directly rather than inferring it from accountNumbering. | | `description` | string | no | A free-form description of the chart of accounts. Absent when none was set. | | `lastModifiedDate` | string (date-time) | yes | The date and time the chart-of-accounts record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Search Chart Of Accounts `POST /accounting/chart-of-accounts/search` Group: Accounting › Charts of Accounts · Operation ID: `searchChartOfAccounts` Returns the charts of accounts that match the given filters, ordered by name. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchChartOfAccounts) ## 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`. | ## Request body Required, `application/json`: searchChartOfAccountsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A chart of accounts must match every filter you send. Omit to return all charts of accounts. | | `filters.id` | string | no | Limits results to the chart of accounts with this identifier. The match is exact. A value that is not a valid identifier matches nothing. | | `filters.name` | string | no | Limits results to charts of accounts whose name contains this text. The match is case-insensitive, and '%' and '_' match themselves rather than acting as wildcards. An empty or blank value matches nothing. | | `filters.description` | string | no | Limits results to charts of accounts whose description contains this text. The match is case-insensitive. | | `filters.accountNumbering` | string | no | Limits results to charts of accounts with this numbering strategy. Possible values: 'hierarchical', 'manual'. Any other value matches nothing. | | `filters.accountNumberingDigits` | string | no | Limits results to charts of accounts with this account-number width. Possible values: 'sixDigits', 'sevenDigits', 'eightDigits', 'nineDigits', 'tenDigits'. Any other value matches nothing. A width and the numbering strategy are independent. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of charts of accounts per page, between 1 and 200. Defaults to 50. A value outside that range fails with INVALID_PAGE_SIZE rather than being adjusted. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "", "description": "", "accountNumbering": "", "accountNumberingDigits": "" }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchChartOfAccountsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The charts of accounts that match, ordered by name. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the chart of accounts. | | `items[].name` | string | yes | The display name of the chart of accounts. | | `items[].accountNumbering` | string | yes | How account numbers are assigned within this chart of accounts. Possible values: 'hierarchical' (numbers follow the account's position in the hierarchy), 'manual' (numbers are assigned by hand). | | `items[].accountNumberingDigits` | string | no | The total width of an account number, which caps how deep the hierarchy can go. Possible values: 'sixDigits' (up to 4 levels), 'sevenDigits' (5), 'eightDigits' (6), 'nineDigits' (7), 'tenDigits' (8). Absent when no width was set. A width and the numbering strategy are independent, so check this field directly rather than inferring it from accountNumbering. | | `items[].description` | string | no | A free-form description of the chart of accounts. Absent when none was set. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the chart-of-accounts record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Errors | Code | Status | | --- | --- | | `INVALID_PAGE_SIZE` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_PAGE_SIZE - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Credit Memos `GET /accounting/credit-memos` Group: Accounting › Credit Memos · Operation ID: `creditMemos` Returns a list of credit memos. To match by credit memo number, customer name or number, entity name, description or external identifier, use POST accounting/credit-memos/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=creditMemos) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `creditMemoDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `creditMemoStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `creditMemoType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `manualCreditMemoType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `customerId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `invoiceId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, creditMemoDate, creditMemoStatus, creditMemoType, manualCreditMemoType, currency, customerId, entityId, invoiceId, subtotal, totalTax, totalDutiesAndCustoms, totalAmountCredited, appliedAmount, unappliedAmount, lastModifiedDate - Selectable: id, creditMemoNumber, creditMemoDate, creditMemoStatus, creditMemoType, manualCreditMemoType, currency, description, externalCreditMemoId, customerId, customerName, customerNumber, entityId, entityName, invoiceId, invoiceNumber, subtotal, totalTax, totalDutiesAndCustoms, totalAmountCredited, appliedAmount, unappliedAmount, lastModifiedDate ## Response `200` Success Under `data`, an array of creditMemosRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the credit memo. | | `creditMemoNumber` | string or null | no | The credit memo number, for example 'CM-1'. Absent while none has been assigned. | | `creditMemoDate` | string (date) | no | The date the credit memo was issued. Format YYYY-MM-DD. | | `creditMemoStatus` | string or null | no | The application status of the credit memo. Possible values: 'unapplied', 'partiallyApplied', 'appliedInFull'. | | `creditMemoType` | string or null | no | How the credit memo was created. Possible values: 'manual', 'creditRebill', 'earlyRenewal', 'lateFees', 'cancellation', 'inventoryReturn'. Only 'manual' credit memos are created through this API. The rest come from internal workflows. | | `manualCreditMemoType` | string or null | no | The reason for a manual credit memo, as a tenant-configured key, for example 'writeOff' or 'customerSatisfaction'. Absent when creditMemoType is not 'manual'. | | `currency` | string | no | The currency of the credit memo. Every amount on the record, its lines and its application rows is in this currency. Format ISO 4217, for example 'USD'. | | `description` | string or null | no | A free-form description of the credit memo. | | `externalCreditMemoId` | string or null | no | The identifier of this credit memo in an external source system. For identifiers owned by your other systems, use externalIdentifiers below. | | `customerId` | string or null | no | The customer the credit memo is issued to. See id on GET accounting/customers/:id. | | `customerName` | string or null | no | The name of the customer the credit memo is issued to. | | `customerNumber` | string or null | no | The customer number of the customer the credit memo is issued to. | | `entityId` | string or null | no | The legal entity that issued the credit memo. See id on GET base/entities. | | `entityName` | string or null | no | The name of the legal entity that issued the credit memo. | | `invoiceId` | string or null | no | The invoice the credit memo was created from. Absent when it was not created from an invoice. See id on GET accounting/invoices/:id. | | `invoiceNumber` | string or null | no | The number of the invoice the credit memo was created from. Present whenever invoiceId is. | | `subtotal` | string (decimal) or null | no | The subtotal before tax and duties, in the credit memo currency. Absent when no subtotal has been computed. | | `totalTax` | string (decimal) or null | no | The total tax across all lines, in the credit memo currency. | | `totalDutiesAndCustoms` | string (decimal) or null | no | The total duties and customs across all lines, in the credit memo currency. | | `totalAmountCredited` | string (decimal) or null | no | The total credited, including tax and duties, in the credit memo currency. | | `appliedAmount` | string (decimal) or null | no | The amount applied to invoices and debit memos so far, in the credit memo currency. This is the net of the application rows below. | | `unappliedAmount` | string (decimal) or null | no | The amount still available to apply, in the credit memo currency. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the credit memo last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Credit Memo `POST /accounting/credit-memos` Group: Accounting › Credit Memos · Operation ID: `createCreditMemo` Creates a credit memo. Every credit memo created here is a manual credit memo. The other types come from internal workflows. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createCreditMemo) ## 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`: createCreditMemoInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `customerId` | string | yes | The customer the credit memo is issued to. See id on GET accounting/customers/:id. | | `entityId` | string | yes | The legal entity issuing the credit memo. See id on GET base/entities. | | `currency` | string | yes | The currency of the credit memo. Every amount on the credit memo is in this currency. Format ISO 4217, for example 'USD'. | | `creditMemoDate` | string (date) | yes | The date the credit memo is issued. Use format YYYY-MM-DD. | | `manualCreditMemoType` | string | no | The reason for the credit memo, as a tenant-configured key, for example 'writeOff' or 'customerSatisfaction'. Although the field is optional in the schema, a credit memo cannot be created without it. | | `description` | string | no | A free-form description of the credit memo. | | `externalCreditMemoId` | string | no | The identifier of this credit memo in an external source system. | | `lines` | array of object | yes | The lines of the credit memo. At least one is required, even when the request also has applications. Each line has a description and a total price. Tax is added internally. | | `lines[].description` | string | yes | The line description, as shown to the customer. | | `lines[].totalPrice` | string (decimal) | yes | The line total before tax, in the credit memo currency. | | `applications` | array of object | no | Initial applications, each applying the credit memo to an invoice when it is created. | | `applications[].invoiceId` | string | yes | The invoice this application targets. See id on GET accounting/invoices/:id. | | `applications[].appliedAmount` | string (decimal) | yes | The amount to apply, in the credit memo currency. Must be positive. | | `applications[].appliedDate` | string (date) | yes | The date the application is recorded. Use format YYYY-MM-DD. | Example: ```json { "customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "currency": "", "creditMemoDate": "2026-01-15", "manualCreditMemoType": "", "description": "", "externalCreditMemoId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "lines": [ { "description": "", "totalPrice": "19.99" } ], "applications": [ { "invoiceId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "appliedAmount": "19.99", "appliedDate": "2026-01-15" } ] } ``` ## Response `200` Success Under `data` (createCreditMemoOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created credit memo. Read the full record with GET accounting/credit-memos/: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 | | --- | --- | | `CUSTOMER_NOT_FOUND` | 400 | | `ENTITY_NOT_FOUND` | 400 | | `INVOICE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: CUSTOMER_NOT_FOUND, ENTITY_NOT_FOUND, INVOICE_NOT_FOUND, 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 --- # Get Credit Memo `GET /accounting/credit-memos/{id}` Group: Accounting › Credit Memos · Operation ID: `getCreditMemo` Returns the credit memo with the given identifier. Use the credit memo's id, not its creditMemoNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getCreditMemo) ## 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`. | | `id` | path | string | yes | The unique identifier for the credit memo to return. | ## Response `200` Success Under `data` (getCreditMemoOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the credit memo. | | `creditMemoNumber` | string | no | The credit memo number, for example 'CM-1'. Absent while none has been assigned. | | `creditMemoDate` | string (date) | yes | The date the credit memo was issued. Format YYYY-MM-DD. | | `creditMemoStatus` | string | no | The application status of the credit memo. Possible values: 'unapplied', 'partiallyApplied', 'appliedInFull'. | | `creditMemoType` | string | no | How the credit memo was created. Possible values: 'manual', 'creditRebill', 'earlyRenewal', 'lateFees', 'cancellation', 'inventoryReturn'. Only 'manual' credit memos are created through this API. The rest come from internal workflows. | | `manualCreditMemoType` | string | no | The reason for a manual credit memo, as a tenant-configured key, for example 'writeOff' or 'customerSatisfaction'. Absent when creditMemoType is not 'manual'. | | `currency` | string | yes | The currency of the credit memo. Every amount on the record, its lines and its application rows is in this currency. Format ISO 4217, for example 'USD'. | | `description` | string | no | A free-form description of the credit memo. | | `externalCreditMemoId` | string | no | The identifier of this credit memo in an external source system. For identifiers owned by your other systems, use externalIdentifiers below. | | `customerId` | string | yes | The customer the credit memo is issued to. See id on GET accounting/customers/:id. | | `customerName` | string | no | The name of the customer the credit memo is issued to. | | `customerNumber` | string | no | The customer number of the customer the credit memo is issued to. | | `entityId` | string | yes | The legal entity that issued the credit memo. See id on GET base/entities. | | `entityName` | string | no | The name of the legal entity that issued the credit memo. | | `invoiceId` | string | no | The invoice the credit memo was created from. Absent when it was not created from an invoice. See id on GET accounting/invoices/:id. | | `invoiceNumber` | string | no | The number of the invoice the credit memo was created from. Present whenever invoiceId is. | | `subtotal` | string (decimal) | no | The subtotal before tax and duties, in the credit memo currency. Absent when no subtotal has been computed. | | `totalTax` | string (decimal) | no | The total tax across all lines, in the credit memo currency. | | `totalDutiesAndCustoms` | string (decimal) | no | The total duties and customs across all lines, in the credit memo currency. | | `totalAmountCredited` | string (decimal) | no | The total credited, including tax and duties, in the credit memo currency. | | `appliedAmount` | string (decimal) | no | The amount applied to invoices and debit memos so far, in the credit memo currency. This is the net of the application rows below. | | `unappliedAmount` | string (decimal) | no | The amount still available to apply, in the credit memo currency. | | `lastModifiedDate` | string (date-time) | yes | The date and time the credit memo last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | no | The lines of the credit memo, each crediting one product or free-text item. An empty array when the credit memo has no lines. Every amount is in the credit memo currency. | | `lines[].id` | string | yes | The unique identifier for the credit memo line. | | `lines[].creditMemoLineNumber` | string | no | The line number within the credit memo. | | `lines[].description` | string | no | The line description, as shown to the customer. | | `lines[].productId` | string | no | The product credited by the line. Absent on free-text lines. See id on GET accounting/products/:id. | | `lines[].productCode` | string | no | The product code as it stood when the line was created. It does not follow later changes to the product. | | `lines[].productDescription` | string | no | The product description as it stood when the line was created. It does not follow later changes to the product. | | `lines[].quantity` | string (decimal) | no | The quantity credited. | | `lines[].unitPrice` | string (decimal) | no | The price per unit, in the credit memo currency. | | `lines[].totalPrice` | string (decimal) | no | The line total before tax and duties, in the credit memo currency. | | `lines[].taxAmount` | string (decimal) | no | The tax on the line, in the credit memo currency. | | `lines[].dutiesAndCustomsAmount` | string (decimal) | no | The duties and customs on the line, in the credit memo currency. | | `lines[].amountCredited` | string (decimal) | no | The total credited for the line, in the credit memo currency: totalPrice + taxAmount + dutiesAndCustomsAmount. | | `lines[].taxCode` | string | no | The tax code applied to the line. | | `lines[].taxDate` | string (date) | no | The date the line's tax was determined. Format YYYY-MM-DD. | | `lines[].serviceStartDate` | string (date) | no | The first day of the service period the line covers. Format YYYY-MM-DD. | | `lines[].serviceEndDate` | string (date) | no | The last day of the service period the line covers, inclusive. Format YYYY-MM-DD. | | `applications` | array of object | no | The application rows, each recording one allocation of the credit memo against an invoice or debit memo. Applying and unapplying both add a row, told apart by actionType and the sign of appliedAmount, so this is an append-only history rather than the current allocation. An empty array when the credit memo has never been applied. | | `applications[].id` | string | yes | The unique identifier for the application row. | | `applications[].appliedAmount` | string (decimal) | no | The signed amount moved by this row, in the row's currency: positive when applying, negative when unapplying. Sum the rows to reconcile against the header appliedAmount. | | `applications[].appliedDate` | string (date) | yes | The date the allocation was recorded. Format YYYY-MM-DD. | | `applications[].actionType` | string | no | What this row did. Possible values: 'applied', 'unapplied'. | | `applications[].type` | string | no | The kind of document the credit memo was allocated against. Possible values: 'invoice', 'debitMemo'. Absent when the stored target kind is not one this version recognizes. | | `applications[].targetId` | string | no | The target document: an invoice when type is 'invoice', a debit memo when type is 'debitMemo'. Absent when the target no longer exists or its kind is unrecognized. See id on GET accounting/invoices/:id and GET accounting/debit-memos/:id. | | `applications[].targetNumber` | string | no | The number of the target document. | | `applications[].currency` | string | yes | The currency of this row's appliedAmount. Matches the credit memo currency. Format ISO 4217, for example 'USD'. | | `externalIdentifiers` | array of object | no | The credit memo's identifiers in other systems, one per configured field name. An empty array or absent when the credit memo has none. | | `externalIdentifiers[].fieldName` | string | yes | The configured field name of the identifier, for example 'netsuiteCreditMemoId'. | | `externalIdentifiers[].value` | string | yes | The identifier in the other system. | | `externalIdentifiers[].externalUrl` | string | no | A link to the record in the other system. Absent when none is stored. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Credit Memo `PATCH /accounting/credit-memos/{id}` Group: Accounting › Credit Memos · Operation ID: `updateCreditMemo` Updates the credit memo with the given identifier. Only the fields in the request body change. Use the credit memo's id, not its creditMemoNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateCreditMemo) ## 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. | | `id` | path | string | yes | The unique identifier for the credit memo to update. | ## Request body Required, `application/json`: updateCreditMemoInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `creditMemoDate` | string (date) | no | The new issue date. Use format YYYY-MM-DD. | | `description` | string | no | The new description. | | `externalCreditMemoId` | string | no | The new identifier of this credit memo in an external source system. | | `updateLines` | array of object | no | The line edits, keyed by line id. Only the description can be edited. Lines cannot be added or removed here. | | `updateLines[].id` | string | yes | The unique identifier for the credit memo line to edit, as returned by GET accounting/credit-memos/:id. | | `updateLines[].description` | string | no | The new line description, as shown to the customer. On a manual credit memo it cannot be cleared, because a description is required there. | Example: ```json { "creditMemoDate": "2026-01-15", "description": "", "externalCreditMemoId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "updateLines": [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "description": "" } ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `LINE_NOT_FOUND` | 400 | | `LINE_DESCRIPTION_REQUIRED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, LINE_NOT_FOUND, LINE_DESCRIPTION_REQUIRED - `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 --- # Applies a credit memo to one or more invoices. `POST /accounting/credit-memos/{id}/apply` Group: Accounting › Credit Memos · Operation ID: `applyCreditMemo` [Open in the interactive reference](https://docs.api.lensing.ai/?op=applyCreditMemo) ## 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. | | `id` | path | string | yes | The unique identifier for the credit memo to apply. | ## Request body Required, `application/json`: applyCreditMemoInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `applications` | array of object | yes | The allocations to record. At least one is required. Several rows may target the same invoice, and are summed against that invoice's amount due. | | `applications[].invoiceId` | string | yes | The invoice to apply against. See id on GET accounting/invoices/:id. | | `applications[].appliedAmount` | string (decimal) | yes | The amount to apply, in the credit memo currency. Must be positive. The credit memo currency governs. There is no per-row currency. | | `applications[].appliedDate` | string (date) | yes | The date the allocation is recorded. Use format YYYY-MM-DD. It must fall in an open accounting period. | Example: ```json { "applications": [ { "invoiceId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "appliedAmount": "19.99", "appliedDate": "2026-01-15" } ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `INVOICE_NOT_FOUND` | 400 | | `NO_APPLICATIONS` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, INVOICE_NOT_FOUND, NO_APPLICATIONS - `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 --- # Reverses a credit memo's allocation against one or more invoices. `POST /accounting/credit-memos/{id}/unapply` Group: Accounting › Credit Memos · Operation ID: `unapplyCreditMemo` [Open in the interactive reference](https://docs.api.lensing.ai/?op=unapplyCreditMemo) ## 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. | | `id` | path | string | yes | The unique identifier for the credit memo to unapply. | ## Request body Required, `application/json`: unapplyCreditMemoInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `applications` | array of object | yes | The reversals to record. At least one is required. Several rows may target the same invoice, and are summed against what is currently applied to it. | | `applications[].invoiceId` | string | yes | The invoice to release the credit from. See id on GET accounting/invoices/:id. | | `applications[].appliedAmount` | string (decimal) | yes | The amount to reverse, in the credit memo currency. Must be positive. It is stored as a negative row and may not exceed what is currently applied to this invoice. | | `applications[].appliedDate` | string (date) | yes | The date the reversal is recorded. Use format YYYY-MM-DD. It must fall in an open accounting period. | Example: ```json { "applications": [ { "invoiceId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "appliedAmount": "19.99", "appliedDate": "2026-01-15" } ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `INVOICE_NOT_FOUND` | 400 | | `NO_APPLICATIONS` | 400 | | `UNAPPLY_EXCEEDS_APPLIED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, INVOICE_NOT_FOUND, NO_APPLICATIONS, UNAPPLY_EXCEEDS_APPLIED - `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 --- # Returns the credit memos that match the given filters. `POST /accounting/credit-memos/search` Group: Accounting › Credit Memos · Operation ID: `searchCreditMemo` [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchCreditMemo) ## 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`. | ## Request body Required, `application/json`: searchCreditMemoInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A credit memo must match every filter you send. Omit to return all credit memos. | | `filters.id` | string | no | Limits results to the credit memo with this id. | | `filters.creditMemoNumber` | string | no | Limits results to the credit memo with this number, for example 'CM-1'. The match is exact. | | `filters.customerId` | string | no | Limits results to credit memos issued to this customer. See id on GET accounting/customers/:id. | | `filters.customerName` | string | no | Limits results to credit memos whose customer name contains this text. The match is case-insensitive. | | `filters.customerNumber` | string | no | Limits results to credit memos with this customer number. The match is exact. | | `filters.entityId` | string | no | Limits results to credit memos issued by this legal entity. See id on GET base/entities. | | `filters.entityName` | string | no | Limits results to credit memos whose entity name contains this text. The match is case-insensitive. | | `filters.creditMemoStatus` | array of string | no | Limits results to credit memos in any of these application statuses. Possible values: 'unapplied', 'partiallyApplied', 'appliedInFull'. Any other value matches no credit memo. An empty array does not constrain. | | `filters.creditMemoType` | array of string | no | Limits results to credit memos of any of these types. Possible values: 'manual', 'creditRebill', 'earlyRenewal', 'lateFees', 'cancellation', 'inventoryReturn'. Any other value matches no credit memo. An empty array does not constrain. | | `filters.description` | string | no | Limits results to credit memos whose description contains this text. The match is case-insensitive. | | `filters.externalCreditMemoId` | string | no | Limits results to the credit memo with this external source system identifier. The match is exact. | | `filters.invoiceNumber` | string | no | Limits results to credit memos created from the invoice with this number. The match is exact. | | `filters.creditMemoDate` | object | no | Limits results to credit memos issued between the given bounds. Omit either bound to leave that side open. | | `filters.creditMemoDate.start` | string (date) | no | The inclusive lower bound. Format YYYY-MM-DD. | | `filters.creditMemoDate.end` | string (date) | no | The inclusive upper bound. Format YYYY-MM-DD. | | `filters.lastModifiedDate` | object | no | Limits results by when the credit memo last changed, the window to poll for an incremental sync. Omit either bound to leave that side open. | | `filters.lastModifiedDate.start` | string | no | The inclusive lower bound, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `filters.lastModifiedDate.end` | string | no | The inclusive upper bound, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `filters.externalIdentifier` | object | no | Limits results to credit memos with this external identifier. Both fieldName and value are required. Several credit memos may share a value, so this returns the items array, not a single record. | | `filters.externalIdentifier.fieldName` | string | yes | The configured field name of the identifier, for example 'netsuiteCreditMemoId'. It must be the fieldName of an existing credit-memo external-identifier configuration, or the call fails with UNKNOWN_EXTERNAL_IDENTIFIER. | | `filters.externalIdentifier.value` | string | yes | The identifier value to match, exactly as the other system stores it. | | `page` | object | no | The pagination controls. Omit on the first request. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of credit memos per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "creditMemoNumber": "", "customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "customerName": "", "customerNumber": "", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityName": "", "creditMemoStatus": [ "" ], "creditMemoType": [ "" ], "description": "", "externalCreditMemoId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "invoiceNumber": "", "creditMemoDate": { "start": "2026-01-15", "end": "2026-01-15" }, "lastModifiedDate": { "start": "", "end": "" }, "externalIdentifier": { "fieldName": "", "value": "" } }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchCreditMemoOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The matching credit memos for this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the credit memo. | | `items[].creditMemoNumber` | string | no | The credit memo number, for example 'CM-1'. Absent while none has been assigned. | | `items[].creditMemoDate` | string (date) | yes | The date the credit memo was issued. Format YYYY-MM-DD. | | `items[].creditMemoStatus` | string | no | The application status of the credit memo. Possible values: 'unapplied', 'partiallyApplied', 'appliedInFull'. | | `items[].creditMemoType` | string | no | How the credit memo was created. Possible values: 'manual', 'creditRebill', 'earlyRenewal', 'lateFees', 'cancellation', 'inventoryReturn'. Only 'manual' credit memos are created through this API. The rest come from internal workflows. | | `items[].manualCreditMemoType` | string | no | The reason for a manual credit memo, as a tenant-configured key, for example 'writeOff' or 'customerSatisfaction'. Absent when creditMemoType is not 'manual'. | | `items[].currency` | string | yes | The currency of the credit memo. Every amount on the record, its lines and its application rows is in this currency. Format ISO 4217, for example 'USD'. | | `items[].description` | string | no | A free-form description of the credit memo. | | `items[].externalCreditMemoId` | string | no | The identifier of this credit memo in an external source system. For identifiers owned by your other systems, use externalIdentifiers below. | | `items[].customerId` | string | yes | The customer the credit memo is issued to. See id on GET accounting/customers/:id. | | `items[].customerName` | string | no | The name of the customer the credit memo is issued to. | | `items[].customerNumber` | string | no | The customer number of the customer the credit memo is issued to. | | `items[].entityId` | string | yes | The legal entity that issued the credit memo. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the legal entity that issued the credit memo. | | `items[].invoiceId` | string | no | The invoice the credit memo was created from. Absent when it was not created from an invoice. See id on GET accounting/invoices/:id. | | `items[].invoiceNumber` | string | no | The number of the invoice the credit memo was created from. Present whenever invoiceId is. | | `items[].subtotal` | string (decimal) | no | The subtotal before tax and duties, in the credit memo currency. Absent when no subtotal has been computed. | | `items[].totalTax` | string (decimal) | no | The total tax across all lines, in the credit memo currency. | | `items[].totalDutiesAndCustoms` | string (decimal) | no | The total duties and customs across all lines, in the credit memo currency. | | `items[].totalAmountCredited` | string (decimal) | no | The total credited, including tax and duties, in the credit memo currency. | | `items[].appliedAmount` | string (decimal) | no | The amount applied to invoices and debit memos so far, in the credit memo currency. This is the net of the application rows below. | | `items[].unappliedAmount` | string (decimal) | no | The amount still available to apply, in the credit memo currency. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the credit memo last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | no | The lines of the credit memo, each crediting one product or free-text item. An empty array when the credit memo has no lines. Every amount is in the credit memo currency. | | `items[].lines[].id` | string | yes | The unique identifier for the credit memo line. | | `items[].lines[].creditMemoLineNumber` | string | no | The line number within the credit memo. | | `items[].lines[].description` | string | no | The line description, as shown to the customer. | | `items[].lines[].productId` | string | no | The product credited by the line. Absent on free-text lines. See id on GET accounting/products/:id. | | `items[].lines[].productCode` | string | no | The product code as it stood when the line was created. It does not follow later changes to the product. | | `items[].lines[].productDescription` | string | no | The product description as it stood when the line was created. It does not follow later changes to the product. | | `items[].lines[].quantity` | string (decimal) | no | The quantity credited. | | `items[].lines[].unitPrice` | string (decimal) | no | The price per unit, in the credit memo currency. | | `items[].lines[].totalPrice` | string (decimal) | no | The line total before tax and duties, in the credit memo currency. | | `items[].lines[].taxAmount` | string (decimal) | no | The tax on the line, in the credit memo currency. | | `items[].lines[].dutiesAndCustomsAmount` | string (decimal) | no | The duties and customs on the line, in the credit memo currency. | | `items[].lines[].amountCredited` | string (decimal) | no | The total credited for the line, in the credit memo currency: totalPrice + taxAmount + dutiesAndCustomsAmount. | | `items[].lines[].taxCode` | string | no | The tax code applied to the line. | | `items[].lines[].taxDate` | string (date) | no | The date the line's tax was determined. Format YYYY-MM-DD. | | `items[].lines[].serviceStartDate` | string (date) | no | The first day of the service period the line covers. Format YYYY-MM-DD. | | `items[].lines[].serviceEndDate` | string (date) | no | The last day of the service period the line covers, inclusive. Format YYYY-MM-DD. | | `items[].applications` | array of object | no | The application rows, each recording one allocation of the credit memo against an invoice or debit memo. Applying and unapplying both add a row, told apart by actionType and the sign of appliedAmount, so this is an append-only history rather than the current allocation. An empty array when the credit memo has never been applied. | | `items[].applications[].id` | string | yes | The unique identifier for the application row. | | `items[].applications[].appliedAmount` | string (decimal) | no | The signed amount moved by this row, in the row's currency: positive when applying, negative when unapplying. Sum the rows to reconcile against the header appliedAmount. | | `items[].applications[].appliedDate` | string (date) | yes | The date the allocation was recorded. Format YYYY-MM-DD. | | `items[].applications[].actionType` | string | no | What this row did. Possible values: 'applied', 'unapplied'. | | `items[].applications[].type` | string | no | The kind of document the credit memo was allocated against. Possible values: 'invoice', 'debitMemo'. Absent when the stored target kind is not one this version recognizes. | | `items[].applications[].targetId` | string | no | The target document: an invoice when type is 'invoice', a debit memo when type is 'debitMemo'. Absent when the target no longer exists or its kind is unrecognized. See id on GET accounting/invoices/:id and GET accounting/debit-memos/:id. | | `items[].applications[].targetNumber` | string | no | The number of the target document. | | `items[].applications[].currency` | string | yes | The currency of this row's appliedAmount. Matches the credit memo currency. Format ISO 4217, for example 'USD'. | | `items[].externalIdentifiers` | array of object | no | The credit memo's identifiers in other systems, one per configured field name. An empty array or absent when the credit memo has none. | | `items[].externalIdentifiers[].fieldName` | string | yes | The configured field name of the identifier, for example 'netsuiteCreditMemoId'. | | `items[].externalIdentifiers[].value` | string | yes | The identifier in the other system. | | `items[].externalIdentifiers[].externalUrl` | string | no | A link to the record in the other system. Absent when none is stored. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Errors | Code | Status | | --- | --- | | `UNKNOWN_EXTERNAL_IDENTIFIER` | 400 | | `INVALID_TIMESTAMP` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: UNKNOWN_EXTERNAL_IDENTIFIER, INVALID_TIMESTAMP - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Customers `GET /accounting/customers` Group: Accounting › Customers · Operation ID: `customers` List customers with structured filters. GET data-set exposing filter[field][op]= (eq/ne/gt/gte/lt/lte/in/notIn/between/like), sort=-lastModifiedDate,customerName, sparse fields=, and page[size]/page[cursor] cursor pagination. Returns the collection header of each customer; the full record, with addresses and external identifiers, comes from GET accounting/customers/:id or POST accounting/customers/search. Name and e-mail matching belong in search. Every request must carry the Api-Version header. [Open in the interactive reference](https://docs.api.lensing.ai/?op=customers) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `customerType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `businessType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `customerSegment` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `defaultCurrency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `taxExempt` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentTermLabel` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, customerName, customerType, businessType, customerSegment, defaultCurrency, taxExempt, paymentTermLabel, exemptionNumber, invoiceEmail, lastModifiedDate - Selectable: id, customerNumber, customerName, customerType, businessType, customerSegment, entityName, defaultCurrency, taxExempt, paymentTermLabel, exemptionNumber, invoiceEmail, lastModifiedDate ## Response `200` Success Under `data`, an array of customersRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | Stable, platform-assigned id (a UUID) of the customer — the public identifier used by GET accounting/customers/:id. Filter e.g. filter[id][in]=,. | | `customerNumber` | string or null | no | System-generated customer number, unique per tenant. Absent until one has been assigned. Returned but NOT filterable or orderable — a business document identifier is sensitive content and must not travel in a URL; match on it through POST accounting/customers/search. | | `customerName` | string | no | Customer display name, as it appears on invoices and official documents. Orderable but NOT filterable by design — name matching belongs in POST accounting/customers/search, so it never travels in a URL. | | `customerType` | string | no | Customer type. One of 'default', 'partner', 'endCustomer'. Filter target: filter[customerType][eq]=partner. | | `businessType` | string | no | Whether the customer is a registered company or an individual. One of 'company', 'individual'. Filter target: filter[businessType][eq]=company. | | `customerSegment` | string or null | no | Market segment classification. One of 'SMB', 'MidMarket', 'Enterprise'. Filter target: filter[customerSegment][in]=MidMarket,Enterprise. | | `entityName` | string or null | no | Name of the legal entity that owns this customer, as denormalised onto the customer record. Returned but NOT filterable or orderable — it identifies the counterparty's owning entity and must not travel in a URL; match on it through POST accounting/customers/search, which filters on the entity's id. | | `defaultCurrency` | string or null | no | Default transaction currency as a 3-letter ISO 4217 code, e.g. USD, EUR. Filter target: filter[defaultCurrency][in]=USD,EUR. | | `taxExempt` | boolean or null | no | True when the customer is exempt from tax on all transactions. Filter target: filter[taxExempt][eq]=true. | | `paymentTermLabel` | string or null | no | Label of the payment term applied to this customer's invoices, denormalised from the linked payment term. Filter target: filter[paymentTermLabel][eq]=Net%2030. | | `exemptionNumber` | string or null | no | Government-issued tax-exemption certificate number, when the customer is exempt. Orderable but NOT filterable — a certificate number must not travel in a URL; match on it through POST accounting/customers/search. | | `invoiceEmail` | string or null | no | Email address that receives invoices and billing statements. Orderable but NOT filterable — it is personal data and must not travel in a URL; match on it through POST accounting/customers/search. | | `lastModifiedDate` | string (date-time) or null | no | Instant the customer record last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Poll on this to sync incrementally rather than re-reading the whole collection. Absent from this data-set because the engine cannot yet bridge DateTime to a data-set field type; use POST accounting/customers/search instead. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create a customer `POST /accounting/customers` Group: Accounting › Customers · Operation ID: `createCustomer` Creates one customer under the given legal entity and returns only its id (a UUID) — the identifier every other customer route is keyed by. Read the created record back with GET accounting/customers/:id. `name`, `entityId` are required; `entityId` names an existing legal entity and is not itself updatable afterwards — create a new customer under the right entity instead of moving one. `customerType` defaults to 'default' when omitted; the combination of (name, entityId, customerType) must be unique, and a collision fails the call with DUPLICATE_CUSTOMER, naming the existing customer's id. customerType 'endCustomer' is not supported by this endpoint today: linking the required partner business record has no equivalent field anywhere in this API's read contract, so a request naming it fails with END_CUSTOMER_NOT_SUPPORTED rather than guessing at that shape. `addresses` carries the customer's addresses, each naming its own type ('billTo', 'soldTo'); mark at most one entry per type with `isDefault`, and when a type has none marked its first entry becomes that type's default. `externalIdentifiers` accepts the customer's initial set, each fieldName matching an active CustomerExternalIdentifierConfiguration (call enumValues/list-style discovery, or POST accounting/customers/search's externalIdentifier filter, to see one already in use). The call runs in a transaction: a failure writes nothing. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createCustomer) ## 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`: createCustomerInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | Customer display name, as it will appear on invoices and official documents. | | `entityId` | string | yes | Id of the legal entity that owns this customer (a UUID). Not updatable afterwards. | | `customerType` | string | no | Customer type. One of 'default', 'partner', 'endCustomer'. Defaults to 'default' when omitted. 'endCustomer' is not yet supported by this endpoint (see END_CUSTOMER_NOT_SUPPORTED). | | `partnerType` | string | no | Partner type, relevant when customerType is 'partner'. One of 'reseller', 'thirdPartyPayer'. | | `businessType` | string | no | Whether the customer is a registered company or an individual. One of 'company', 'individual'. Defaults to 'company' when omitted. | | `invoiceEmail` | string | no | Email address that receives invoices and billing statements. | | `mainEmail` | string | no | Primary business contact email for general correspondence. | | `taxExempt` | boolean | no | Whether the customer is exempt from tax on all transactions. Defaults to false when omitted. | | `defaultCurrency` | string | no | Default transaction currency as a 3-letter ISO 4217 code (e.g. USD, EUR, GBP). Defaults to the owning legal entity's own currency when omitted. | | `segment` | string | no | Market segment classification. One of 'SMB', 'MidMarket', 'Enterprise'. Ignored when businessType is 'individual'. | | `exemptionNumber` | string | no | Government-issued tax-exemption certificate number. Only storable when taxExempt is true; supplying one otherwise fails with EXEMPTION_NUMBER_REQUIRES_TAX_EXEMPT (taxExempt defaults to false). | | `paymentTermCode` | string | no | Payment-term code applied to this customer's invoices. Fails with UNKNOWN_REFERENCE when no such payment term exists. | | `parentId` | string | no | Customer UUID of this customer's parent in the business-partner hierarchy. Fails with UNKNOWN_REFERENCE when no such customer exists. | | `partnerId` | string | no | Customer UUID of the partner this customer buys through. Fails with UNKNOWN_REFERENCE when no such customer exists. | | `salesChannel` | string | no | Channel the customer was won through. A SalesChannel member. | | `additionalContactEmail` | string | no | Further contact addresses, beyond the invoice and main contacts. | | `mainContactName` | string | no | Name of the customer's main contact person. | | `preferredLanguage` | string | no | Language the customer prefers correspondence in. A PreferredLanguage member. | | `crmCustomerId` | string | no | The customer's identifier in the CRM system it was sourced from. | | `businessIdentificationNumber` | string | no | Government-issued business registration number. | | `invoiceConsolidation` | string | no | How invoices are consolidated for this customer. An InvoiceConsolidation member. | | `invoiceDeliveryMode` | string | no | How invoices reach the customer. An InvoiceDeliveryMode member. | | `isExemptFromCreditCardFee` | boolean | no | Whether the customer is exempt from credit-card surcharges. | | `billableTravelExpenseInvoiceDetail` | string | no | Whether billable travel and expense items appear itemised or consolidated. An InvoiceDetail member. | | `status` | string | no | Lifecycle status of the customer record. A CustomerStatus member. | | `taxExemptCode` | string | no | Code behind the tax exemption, as the authority categorises it. Only meaningful when taxExempt is true. | | `taxRegistrations` | array of object | no | Tax registrations to record for this customer. Upserted by (country, type): an entry matching an existing registration updates it, anything else is added; registrations left out are untouched. | | `taxRegistrations[].country` | string | yes | ISO 3166-1 country code the registration applies in. | | `taxRegistrations[].taxNumber` | string | yes | Official tax identification number issued by the tax authority. | | `taxRegistrations[].type` | string | yes | Kind of tax identification number. One of 'tax', 'salesTax', 'VAT', 'GST', 'EIN', 'TIN'. | | `taxRegistrations[].effectiveDate` | string (date) | no | Date the registration becomes valid for tax calculations (YYYY-MM-DD). | | `taxRegistrations[].isActive` | boolean | no | Whether the registration is used for tax calculations. Defaults to true. | | `payment` | object | no | Payment-provider link for this customer. `externalPaymentMethodId` requires an `externalCustomerId`, either in the same request or already stored. | | `payment.provider` | string | no | Payment provider name, e.g. 'Stripe'. Required when any other payment field is supplied. | | `payment.externalCustomerId` | string | no | The customer's identifier in the payment provider's system, e.g. a Stripe customer id. | | `payment.externalPaymentMethodId` | string | no | The customer's default payment-method identifier in the provider's system, e.g. a Stripe payment method id. | | `addresses` | array of object | no | The customer's addresses, each naming its own type. Mark at most one entry per type with isDefault; when a type has none marked, its first entry becomes that type's default. | | `addresses[].type` | string | yes | Which address this is. One of 'billTo', 'soldTo'. | | `addresses[].attention` | string | no | Person or department correspondence at this address is directed to. | | `addresses[].isDefault` | boolean | no | Marks this entry as its type's default. At most one per type; when a type has none marked, its first entry becomes the default. | | `addresses[].line1` | string | yes | Street address line 1: number, street name, or PO box. | | `addresses[].line2` | string | no | Apartment, suite, unit, building, floor, etc. | | `addresses[].city` | string | yes | City or locality. | | `addresses[].stateProvince` | string | no | State, province, or region. | | `addresses[].country` | string | yes | Country, as a name or ISO 3166-1 code. | | `addresses[].zipCode` | string | yes | Postal or ZIP code. | | `externalIdentifiers` | array of object | no | External identifiers owned by other systems. At most one entry per fieldName; each fieldName must match an active CustomerExternalIdentifierConfiguration or the call fails with UNKNOWN_EXTERNAL_IDENTIFIER. | | `externalIdentifiers[].fieldName` | string | yes | Configured key of the external identifier, e.g. 'netsuiteSoNumber'. | | `externalIdentifiers[].value` | string | yes | The identifier as the source system holds it. | | `externalIdentifiers[].externalUrl` | string | no | Absolute http(s) link to the object in the originating system. | Example: ```json { "name": "Example Robotics Inc", "entityId": "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "customerType": "default", "partnerType": "", "businessType": "company", "invoiceEmail": "billing@example-robotics.com", "mainEmail": "", "taxExempt": true, "defaultCurrency": "USD", "segment": "", "exemptionNumber": "", "paymentTermCode": "", "parentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "partnerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "salesChannel": "", "additionalContactEmail": "", "mainContactName": "", "preferredLanguage": "", "crmCustomerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "businessIdentificationNumber": "", "invoiceConsolidation": "", "invoiceDeliveryMode": "", "isExemptFromCreditCardFee": true, "billableTravelExpenseInvoiceDetail": "", "status": "", "taxExemptCode": "", "taxRegistrations": [ { "country": "", "taxNumber": "", "type": "", "effectiveDate": "2026-01-15", "isActive": true } ], "payment": { "provider": "", "externalCustomerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "externalPaymentMethodId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }, "addresses": [ { "type": "billTo", "line1": "500 Market St", "city": "San Francisco", "stateProvince": "CA", "country": "US", "zipCode": "94105", "isDefault": true } ], "externalIdentifiers": [ { "fieldName": "", "value": "", "externalUrl": "" } ] } ``` ## Response `200` Success Under `data` (createCustomerOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Platform id (a UUID) assigned to the newly created customer. Read the full record with GET accounting/customers/: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 | | --- | --- | | `INVALID_ENTITY` | 400 | | `INVALID_FIELD_VALUE` | 400 | | `MULTIPLE_DEFAULT_ADDRESSES` | 400 | | `DUPLICATE_CUSTOMER` | 400 | | `END_CUSTOMER_NOT_SUPPORTED` | 400 | | `UNKNOWN_EXTERNAL_IDENTIFIER` | 400 | | `DUPLICATE_EXTERNAL_IDENTIFIER` | 400 | | `UNKNOWN_REFERENCE` | 400 | | `UNKNOWN_PAYMENT_PROVIDER` | 400 | | `PAYMENT_METHOD_REQUIRES_CUSTOMER_ID` | 400 | | `EXEMPTION_NUMBER_REQUIRES_TAX_EXEMPT` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_ENTITY, INVALID_FIELD_VALUE, MULTIPLE_DEFAULT_ADDRESSES, DUPLICATE_CUSTOMER, END_CUSTOMER_NOT_SUPPORTED, UNKNOWN_EXTERNAL_IDENTIFIER, DUPLICATE_EXTERNAL_IDENTIFIER, UNKNOWN_REFERENCE, UNKNOWN_PAYMENT_PROVIDER, PAYMENT_METHOD_REQUIRES_CUSTOMER_ID, EXEMPTION_NUMBER_REQUIRES_TAX_EXEMPT - `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 --- # Get Customer `GET /accounting/customers/{id}` Group: Accounting › Customers · Operation ID: `getCustomer` Fetch a single customer by its id (a UUID), bound from the :id path segment. Returns the full customer record, including its default bill-to / sold-to addresses and external identifiers — the same item schema as POST accounting/customers/search. Read-only. Raises NOT_FOUND when no customer has that id (should map to 404). Every request must carry the Api-Version header. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getCustomer) ## 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`. | | `id` | path | string | yes | Id of the customer to fetch (a UUID); bound from the :id path segment. | ## Response `200` Success Under `data` (getCustomerOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Id of the customer (a UUID) — the public identifier used by this route. | | `name` | string | yes | Customer display name, as it appears on invoices and official documents. | | `customerNumber` | string | yes | System-generated customer number, unique per tenant. Absent until one has been assigned. | | `customerType` | string | no | Customer type. One of 'default', 'partner', 'endCustomer'. | | `partnerType` | string | no | Partner type, set when customerType is 'partner'. One of 'reseller', 'thirdPartyPayer'. | | `businessType` | string | no | Whether the customer is a registered company or an individual. One of 'company', 'individual'. | | `entityId` | string | yes | Id of the legal entity that owns this customer (a UUID). A customer belongs to exactly one entity. | | `entityName` | string | no | Name of the owning legal entity. | | `lastModifiedDate` | string (date-time) | yes | Instant the customer record last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Poll on this to sync incrementally rather than re-reading the whole collection. | | `invoiceEmail` | string | no | Email address that receives invoices and billing statements. | | `mainEmail` | string | no | Primary business contact email for general correspondence. | | `taxExempt` | boolean | no | True when the customer is exempt from tax on all transactions. | | `defaultCurrency` | string | no | Default transaction currency as a 3-letter ISO 4217 code (e.g. USD, EUR, GBP), when set. | | `segment` | string | no | Market segment classification. One of 'SMB', 'MidMarket', 'Enterprise'. | | `paymentTermLabel` | string | no | Label of the payment term applied to this customer's invoices, denormalised from the linked payment term. | | `exemptionNumber` | string | no | Government-issued tax-exemption certificate number, when the customer is exempt. | | `addresses` | array of object | no | The customer's default addresses, at most one per type: the default bill-to (where invoices are sent) first, then the default sold-to (used for tax determination). A type with no address flagged default contributes no entry, so this is empty when the customer has neither. Each entry names its own type, so read the list rather than assuming positions. | | `addresses[].type` | string | yes | Which address this is. One of 'billTo', 'soldTo'. | | `addresses[].isDefault` | boolean | yes | Whether this is the customer's default address of that type. Always true today — only default addresses are returned — and carried so each entry is self-describing. | | `addresses[].addressId` | string | no | Id of the address record (a UUID). | | `addresses[].attention` | string | no | Person or department correspondence at this address is directed to. | | `addresses[].line1` | string | no | Street address line 1: number, street name, or PO box. | | `addresses[].line2` | string | no | Apartment, suite, unit, building, floor, etc. | | `addresses[].city` | string | no | City or locality. | | `addresses[].stateProvince` | string | no | State, province, or region. | | `addresses[].country` | string | no | Country, as a name or ISO 3166-1 code. | | `addresses[].zipCode` | string | no | Postal or ZIP code. | | `taxExemptCode` | string | no | Code behind the tax exemption, as the authority categorises it. | | `salesChannel` | string | no | Channel the customer was won through. A SalesChannel member. | | `additionalContactEmail` | string | no | Further contact addresses, beyond the invoice and main contacts. | | `mainContactName` | string | no | Name of the customer's main contact person. | | `preferredLanguage` | string | no | Language the customer prefers correspondence in. A PreferredLanguage member. | | `crmCustomerId` | string | no | The customer's identifier in the CRM system it was sourced from. | | `businessIdentificationNumber` | string | no | Government-issued business registration number. | | `invoiceConsolidation` | string | no | How invoices are consolidated for this customer. An InvoiceConsolidation member. | | `invoiceDeliveryMode` | string | no | How invoices reach the customer. An InvoiceDeliveryMode member. | | `isExemptFromCreditCardFee` | boolean | no | Whether the customer is exempt from credit-card surcharges. | | `billableTravelExpenseInvoiceDetail` | string | no | Whether billable travel and expense items appear itemised or consolidated. An InvoiceDetail member. | | `status` | string | no | Lifecycle status of the customer record. A CustomerStatus member. | | `paymentTerm` | string | no | Code of the payment term applied to this customer's invoices; `paymentTermLabel` carries its display name. | | `taxRegistrations` | array of object | no | Tax registrations on file for this customer, across jurisdictions and tax types. Empty when the customer has none. | | `taxRegistrations[].country` | string | yes | ISO 3166-1 country code the registration applies in. | | `taxRegistrations[].taxNumber` | string | yes | Official tax identification number issued by the tax authority. | | `taxRegistrations[].type` | string | yes | Kind of tax identification number. One of 'tax', 'salesTax', 'VAT', 'GST', 'EIN', 'TIN'. | | `taxRegistrations[].effectiveDate` | string | no | Date the registration becomes valid for tax calculations (YYYY-MM-DD). | | `taxRegistrations[].isActive` | boolean | no | Whether the registration is currently used for tax calculations. | | `taxRegistrations[].note` | string | no | Free-text internal note recorded against the registration. | | `taxRegistrations[].validationStatus` | string | no | Validation state assigned by the tax engine, not by the caller. | | `taxRegistrations[].validationMessages` | string | no | Detail behind the validation state, when the engine recorded any. | | `payment` | object | no | The customer's payment-provider link, when one is registered: the provider and the customer / default-payment-method identifiers in that provider's system. Absent when the customer has none. | | `payment.provider` | string | no | Payment provider name, e.g. 'Stripe'. | | `payment.externalCustomerId` | string | no | The customer's identifier in the payment provider's system. | | `payment.externalPaymentMethodId` | string | no | The customer's default payment-method identifier in the provider's system. | | `externalIdentifiers` | array of object | no | External identifiers owned by other systems. At most one entry per fieldName. Absent when the customer has none; an omitted field and [] are equivalent. | | `externalIdentifiers[].fieldName` | string | yes | Configured key of the external identifier — a stable camelCase slug such as 'netsuiteSoNumber', not the human-readable label. Must match an active CustomerExternalIdentifierConfiguration. | | `externalIdentifiers[].value` | string | yes | The identifier as the source system holds it. | | `externalIdentifiers[].externalUrl` | string | no | Absolute http(s) link to the object in the originating system, when one is stored. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Partially update a customer `PATCH /accounting/customers/{id}` Group: Accounting › Customers · Operation ID: `updateCustomer` Writes only the fields the request body carries. Everything omitted is left exactly as it was, so a caller never has to read the customer first in order to change one field. The response is empty; read the updated record with GET accounting/customers/:id. Omission and clearing are two different requests on the nullable fields (partnerType, invoiceEmail, mainEmail, segment, exemptionNumber): pass the key with a null value to clear it, or leave it out entirely to keep the current value. A cleared field is simply absent from the next read, the same as a customer that never had one set. `name`, `customerType`, `businessType`, `taxExempt` and `defaultCurrency` have no equivalent clear — they are required on the underlying record. `entityId` is not part of this request: the owning legal entity is fixed at creation and cannot move — create a new customer under the right entity instead. Changing `name` and/or `customerType` is still checked against the (name, entityId, customerType) uniqueness the customer directory enforces; a collision fails with DUPLICATE_CUSTOMER. customerType 'endCustomer' is rejected with END_CUSTOMER_NOT_SUPPORTED for the same reason CustomerCreateEndpointV1 rejects it — no field in this API's contract can carry the required partner link. Addresses can be written two ways, and the request may use one or the other, not both. `addresses` replaces the whole set: send the complete list you want stored, or an empty array to remove them all — every address is recreated, so the addressIds change. `createAddresses`, `updateAddresses` and `deleteAddresses` instead edit the set in place, leaving anything they do not name untouched and keeping its addressId; `updateAddresses` and `deleteAddresses` identify an address by the addressId the read endpoints return, and fail with ADDRESS_NOT_FOUND when it is not on this customer. A deleted address is only detached from the customer — the record survives, since an invoice may still point at it. Each type keeps exactly one default throughout. `externalIdentifiers` follow the full create/update/delete split: `createExternalIdentifiers` adds a fieldName not already present, `updateExternalIdentifiers` rewrites one that is, and `deleteExternalIdentifiers` removes by fieldName; referencing a fieldName in the wrong bucket fails with EXTERNAL_IDENTIFIER_NOT_FOUND or DUPLICATE_EXTERNAL_IDENTIFIER. An unknown id fails with NOT_FOUND. The call runs in a transaction, so a failure leaves the customer untouched. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateCustomer) ## 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. | | `id` | path | string | yes | Platform id (a UUID) of the customer to update; bound from the :id path segment. | ## Request body Required, `application/json`: updateCustomerInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | New customer display name. Omit to leave unchanged. | | `customerType` | string | no | New customer type. One of 'default', 'partner', 'endCustomer' (the last rejected — see END_CUSTOMER_NOT_SUPPORTED). Cannot be changed once the customer appears on a sales order. Omit to leave unchanged. | | `partnerType` | string or null | no | New partner type, relevant when customerType is 'partner'. One of 'reseller', 'thirdPartyPayer'. Omit to leave unchanged; pass null to clear. | | `businessType` | string | no | New business type. One of 'company', 'individual'. Omit to leave unchanged. | | `invoiceEmail` | string or null | no | New invoice email. Omit to leave unchanged; pass null to clear. | | `mainEmail` | string or null | no | New main contact email. Omit to leave unchanged; pass null to clear. | | `taxExempt` | boolean | no | New tax-exempt flag. Omit to leave unchanged. Setting this to false also clears exemptionNumber. | | `defaultCurrency` | string | no | New default currency, as a 3-letter ISO 4217 code. Omit to leave unchanged. Cannot be cleared — every customer carries a currency. | | `segment` | string or null | no | New market segment. One of 'SMB', 'MidMarket', 'Enterprise'. Ignored when the customer's effective businessType is 'individual'. Omit to leave unchanged; pass null to clear. | | `exemptionNumber` | string or null | no | New tax-exemption certificate number. Only storable while the customer is tax exempt: supplying one when taxExempt is, or becomes, false fails with EXEMPTION_NUMBER_REQUIRES_TAX_EXEMPT. Omit to leave unchanged; pass null to clear. | | `paymentTermCode` | string | no | Payment-term code applied to this customer's invoices. Fails with UNKNOWN_REFERENCE when no such payment term exists. | | `parentId` | string | no | Customer UUID of this customer's parent in the business-partner hierarchy. Fails with UNKNOWN_REFERENCE when no such customer exists. | | `partnerId` | string | no | Customer UUID of the partner this customer buys through. Fails with UNKNOWN_REFERENCE when no such customer exists. | | `salesChannel` | string or null | no | Channel the customer was won through. A SalesChannel member. Omit to leave unchanged; pass null to clear. | | `additionalContactEmail` | string or null | no | Further contact addresses, beyond the invoice and main contacts. Omit to leave unchanged; pass null to clear. | | `mainContactName` | string or null | no | Name of the customer's main contact person. Omit to leave unchanged; pass null to clear. | | `preferredLanguage` | string or null | no | Language the customer prefers correspondence in. A PreferredLanguage member. Omit to leave unchanged; pass null to clear. | | `crmCustomerId` | string or null | no | The customer's identifier in the CRM system it was sourced from. Omit to leave unchanged; pass null to clear. | | `businessIdentificationNumber` | string or null | no | Government-issued business registration number. Omit to leave unchanged; pass null to clear. | | `invoiceConsolidation` | string or null | no | How invoices are consolidated for this customer. An InvoiceConsolidation member. Omit to leave unchanged; pass null to clear. | | `invoiceDeliveryMode` | string or null | no | How invoices reach the customer. An InvoiceDeliveryMode member. Omit to leave unchanged; pass null to clear. | | `isExemptFromCreditCardFee` | boolean | no | Whether the customer is exempt from credit-card surcharges. | | `billableTravelExpenseInvoiceDetail` | string | no | Whether billable travel and expense items appear itemised or consolidated. An InvoiceDetail member. | | `status` | string | no | Lifecycle status of the customer record. A CustomerStatus member. | | `taxExemptCode` | string or null | no | Code behind the tax exemption. Omit to leave unchanged; pass null to clear. | | `taxRegistrations` | array of object | no | Tax registrations to record for this customer. Upserted by (country, type): an entry matching an existing registration updates it, anything else is added; registrations left out are untouched. | | `taxRegistrations[].country` | string | yes | ISO 3166-1 country code the registration applies in. | | `taxRegistrations[].taxNumber` | string | yes | Official tax identification number issued by the tax authority. | | `taxRegistrations[].type` | string | yes | Kind of tax identification number. One of 'tax', 'salesTax', 'VAT', 'GST', 'EIN', 'TIN'. | | `taxRegistrations[].effectiveDate` | string (date) | no | Date the registration becomes valid for tax calculations (YYYY-MM-DD). | | `taxRegistrations[].isActive` | boolean | no | Whether the registration is used for tax calculations. Defaults to true. | | `payment` | object | no | Payment-provider link for this customer. `externalPaymentMethodId` requires an `externalCustomerId`, either in the same request or already stored. | | `payment.provider` | string | no | Payment provider name, e.g. 'Stripe'. Required when any other payment field is supplied. | | `payment.externalCustomerId` | string | no | The customer's identifier in the payment provider's system, e.g. a Stripe customer id. | | `payment.externalPaymentMethodId` | string | no | The customer's default payment-method identifier in the provider's system, e.g. a Stripe payment method id. | | `addresses` | array of object | no | The customer's addresses. The array REPLACES the whole address set across both types: omit the field to leave every address unchanged, or send an empty array to remove them all. | | `addresses[].type` | string | yes | Which address this is. One of 'billTo', 'soldTo'. | | `addresses[].attention` | string | no | Person or department correspondence at this address is directed to. | | `addresses[].isDefault` | boolean | no | Marks this entry as its type's default. At most one per type; when a type has none marked, its first entry becomes the default. | | `addresses[].line1` | string | yes | Street address line 1: number, street name, or PO box. | | `addresses[].line2` | string | no | Apartment, suite, unit, building, floor, etc. | | `addresses[].city` | string | yes | City or locality. | | `addresses[].stateProvince` | string | no | State, province, or region. | | `addresses[].country` | string | yes | Country, as a name or ISO 3166-1 code. | | `addresses[].zipCode` | string | yes | Postal or ZIP code. | | `createAddresses` | array of object | no | Addresses to add, on top of the ones the customer already has. Cannot be combined with `addresses`. | | `createAddresses[].type` | string | yes | Which address this is. One of 'billTo', 'soldTo'. | | `createAddresses[].attention` | string | no | Person or department correspondence at this address is directed to. | | `createAddresses[].isDefault` | boolean | no | Marks this entry as its type's default, demoting the previous holder. At most one per type per request. | | `createAddresses[].line1` | string | yes | Street address line 1: number, street name, or PO box. | | `createAddresses[].line2` | string | no | Apartment, suite, unit, building, floor, etc. | | `createAddresses[].city` | string | yes | City or locality. | | `createAddresses[].stateProvince` | string | no | State, province, or region. | | `createAddresses[].country` | string | yes | Country, as a name or ISO 3166-1 code. | | `createAddresses[].zipCode` | string | yes | Postal or ZIP code. | | `updateAddresses` | array of object | no | Addresses to rewrite, each identified by the addressId the read endpoints return. Every field but addressId is optional: an omitted one keeps its stored value. Rejected with ADDRESS_NOT_FOUND if the addressId is not on this customer. Cannot be combined with `addresses`. | | `updateAddresses[].addressId` | string | yes | Id of the address record to rewrite (a UUID), as returned by GET accounting/customers/:id. | | `updateAddresses[].type` | string | no | Moves the address to a different set. One of 'billTo', 'soldTo'. | | `updateAddresses[].attention` | string | no | Person or department correspondence at this address is directed to. | | `updateAddresses[].isDefault` | boolean | no | Marks this entry as its type's default, demoting the previous holder. | | `updateAddresses[].line1` | string | no | Street address line 1: number, street name, or PO box. | | `updateAddresses[].line2` | string | no | Apartment, suite, unit, building, floor, etc. | | `updateAddresses[].city` | string | no | City or locality. | | `updateAddresses[].stateProvince` | string | no | State, province, or region. | | `updateAddresses[].country` | string | no | Country, as a name or ISO 3166-1 code. | | `updateAddresses[].zipCode` | string | no | Postal or ZIP code. | | `deleteAddresses` | array of string | no | addressIds of addresses to remove from this customer. The address record itself is kept, since an invoice may still reference it; only the customer's link to it goes. Rejected with ADDRESS_NOT_FOUND if the addressId is not on this customer. Cannot be combined with `addresses`. | | `createExternalIdentifiers` | array of object | no | External identifiers to add. Rejected with DUPLICATE_EXTERNAL_IDENTIFIER if the fieldName is already present on the record. | | `createExternalIdentifiers[].fieldName` | string | yes | | | `createExternalIdentifiers[].value` | string | yes | | | `createExternalIdentifiers[].externalUrl` | string | no | | | `updateExternalIdentifiers` | array of object | no | External identifiers to rewrite, matched by fieldName. Rejected with EXTERNAL_IDENTIFIER_NOT_FOUND if the fieldName is not already present. | | `updateExternalIdentifiers[].fieldName` | string | yes | | | `updateExternalIdentifiers[].value` | string | yes | | | `updateExternalIdentifiers[].externalUrl` | string | no | | | `deleteExternalIdentifiers` | array of string | no | fieldNames of external identifiers to remove. Rejected with EXTERNAL_IDENTIFIER_NOT_FOUND if a name is not present. | Example: ```json { "name": "", "customerType": "", "partnerType": "", "businessType": "", "invoiceEmail": "ap@example-robotics.com", "mainEmail": "", "taxExempt": true, "defaultCurrency": "", "segment": "MidMarket", "exemptionNumber": "", "paymentTermCode": "", "parentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "partnerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "salesChannel": "", "additionalContactEmail": "", "mainContactName": "", "preferredLanguage": "", "crmCustomerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "businessIdentificationNumber": "", "invoiceConsolidation": "", "invoiceDeliveryMode": "", "isExemptFromCreditCardFee": true, "billableTravelExpenseInvoiceDetail": "", "status": "", "taxExemptCode": "", "taxRegistrations": [ { "country": "", "taxNumber": "", "type": "", "effectiveDate": "2026-01-15", "isActive": true } ], "payment": { "provider": "", "externalCustomerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "externalPaymentMethodId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }, "addresses": [ { "type": "", "attention": "", "isDefault": true, "line1": "", "line2": "", "city": "", "stateProvince": "", "country": "", "zipCode": "" } ], "createAddresses": [ { "type": "", "attention": "", "isDefault": true, "line1": "", "line2": "", "city": "", "stateProvince": "", "country": "", "zipCode": "" } ], "updateAddresses": [ { "addressId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "type": "", "attention": "", "isDefault": true, "line1": "", "line2": "", "city": "", "stateProvince": "", "country": "", "zipCode": "" } ], "deleteAddresses": [ "" ], "createExternalIdentifiers": [ { "fieldName": "", "value": "", "externalUrl": "" } ], "updateExternalIdentifiers": [ { "fieldName": "", "value": "", "externalUrl": "" } ], "deleteExternalIdentifiers": [ "" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `INVALID_FIELD_VALUE` | 400 | | `MULTIPLE_DEFAULT_ADDRESSES` | 400 | | `DUPLICATE_CUSTOMER` | 400 | | `END_CUSTOMER_NOT_SUPPORTED` | 400 | | `UNKNOWN_EXTERNAL_IDENTIFIER` | 400 | | `DUPLICATE_EXTERNAL_IDENTIFIER` | 400 | | `EXTERNAL_IDENTIFIER_NOT_FOUND` | 400 | | `ADDRESS_NOT_FOUND` | 400 | | `CUSTOMER_TYPE_CHANGE_NOT_ALLOWED` | 400 | | `UNKNOWN_REFERENCE` | 400 | | `UNKNOWN_PAYMENT_PROVIDER` | 400 | | `PAYMENT_METHOD_REQUIRES_CUSTOMER_ID` | 400 | | `EXEMPTION_NUMBER_REQUIRES_TAX_EXEMPT` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, INVALID_FIELD_VALUE, MULTIPLE_DEFAULT_ADDRESSES, DUPLICATE_CUSTOMER, END_CUSTOMER_NOT_SUPPORTED, UNKNOWN_EXTERNAL_IDENTIFIER, DUPLICATE_EXTERNAL_IDENTIFIER, EXTERNAL_IDENTIFIER_NOT_FOUND, ADDRESS_NOT_FOUND, CUSTOMER_TYPE_CHANGE_NOT_ALLOWED, UNKNOWN_REFERENCE, UNKNOWN_PAYMENT_PROVIDER, PAYMENT_METHOD_REQUIRES_CUSTOMER_ID, EXEMPTION_NUMBER_REQUIRES_TAX_EXEMPT - `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 --- # Search Customer `POST /accounting/customers/search` Group: Accounting › Customers · Operation ID: `searchCustomer` Search customers by id, customer number, name, legal entity or customer type, and page through the matches. POST so the filters — `name` above all — travel in the JSON body rather than the URL. Read-only. Returns the full customer record for each match, including bill-to / sold-to addresses and external identifiers; matching nothing is an empty items array with HTTP 200, not a 404. Every request must carry the Api-Version header. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchCustomer) ## 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`. | ## Request body Required, `application/json`: searchCustomerInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | Search filters, nested as one object so they map onto the POST body. All are optional and are ANDed; omitting every one of them matches all customers. Omit the whole object to do exactly that. | | `filters.id` | string | no | Exact match on a customer's id (a UUID). This is how a single known customer is fetched. | | `filters.customerNumber` | string | no | Exact match on the system-generated customer number, e.g. CUST-001234. | | `filters.name` | string | no | Case-insensitive SUBSTRING match on the customer display name. In the body precisely because a name must never travel in a URL. | | `filters.entityId` | string | no | Restrict to customers owned by this legal entity, given as the entity id (a UUID). An id that matches no entity yields an empty page, not an error. | | `filters.customerType` | string | no | Restrict to customers of this type. One of 'default', 'partner', 'endCustomer'; any other value yields an empty page. | | `filters.externalIdentifier` | object | no | Match one identifier owned by another system. Both members are required: a fieldName on its own would match every value stored under that key. Not unique across customers — several may share one value — so this returns the ordinary items array, not a single record. | | `filters.externalIdentifier.fieldName` | string | yes | Configured key of the external identifier — a stable camelCase slug such as 'netsuiteSoNumber', not the human-readable label. Must match an active CustomerExternalIdentifierConfiguration or the call fails with UNKNOWN_EXTERNAL_IDENTIFIER. | | `filters.externalIdentifier.value` | string | yes | The identifier as the source system holds it. Matched exactly, case-sensitively. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | Opaque forward cursor from a previous response's pagination.nextCursor; omit for the first page. Do not construct manually. | | `page.size` | number | no | Maximum number of customers to return. Defaults to 50, capped at 200; the server may return fewer, but never more. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "customerNumber": "", "name": "", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "customerType": "", "externalIdentifier": { "fieldName": "", "value": "" } }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchCustomerOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | Matching customers for this page, newest first. Empty when nothing matches. | | `items[].id` | string | yes | Id of the customer (a UUID) — the public identifier of this record. | | `items[].name` | string | yes | Customer display name, as it appears on invoices and official documents. | | `items[].customerNumber` | string | yes | System-generated customer number, unique per tenant. Absent until one has been assigned. | | `items[].customerType` | string | no | Customer type. One of 'default', 'partner', 'endCustomer'. | | `items[].partnerType` | string | no | Partner type, set when customerType is 'partner'. One of 'reseller', 'thirdPartyPayer'. | | `items[].businessType` | string | no | Whether the customer is a registered company or an individual. One of 'company', 'individual'. | | `items[].entityId` | string | yes | Id of the legal entity that owns this customer (a UUID). A customer belongs to exactly one entity. | | `items[].entityName` | string | no | Name of the owning legal entity. | | `items[].lastModifiedDate` | string (date-time) | yes | Instant the customer record last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Poll on this to sync incrementally rather than re-reading the whole collection. | | `items[].invoiceEmail` | string | no | Email address that receives invoices and billing statements. | | `items[].mainEmail` | string | no | Primary business contact email for general correspondence. | | `items[].taxExempt` | boolean | no | True when the customer is exempt from tax on all transactions. | | `items[].defaultCurrency` | string | no | Default transaction currency as a 3-letter ISO 4217 code (e.g. USD, EUR, GBP), when set. | | `items[].segment` | string | no | Market segment classification. One of 'SMB', 'MidMarket', 'Enterprise'. | | `items[].paymentTermLabel` | string | no | Label of the payment term applied to this customer's invoices, denormalised from the linked payment term. | | `items[].exemptionNumber` | string | no | Government-issued tax-exemption certificate number, when the customer is exempt. | | `items[].addresses` | array of object | no | The customer's addresses: bill-to (where invoices are sent) then sold-to (used for tax determination). Every address of a type is returned, not only the default one, so a type may contribute several entries. Each entry names its own type and whether it is that type's default, so read the list rather than assuming positions. | | `items[].addresses[].type` | string | yes | Which address this is. One of 'billTo', 'soldTo'. | | `items[].addresses[].isDefault` | boolean | yes | Whether this is the customer's default address of that type. | | `items[].addresses[].addressId` | string | no | Id of the address record (a UUID). | | `items[].addresses[].attention` | string | no | Person or department correspondence at this address is directed to. | | `items[].addresses[].line1` | string | no | Street address line 1: number, street name, or PO box. | | `items[].addresses[].line2` | string | no | Apartment, suite, unit, building, floor, etc. | | `items[].addresses[].city` | string | no | City or locality. | | `items[].addresses[].stateProvince` | string | no | State, province, or region. | | `items[].addresses[].country` | string | no | Country, as a name or ISO 3166-1 code. | | `items[].addresses[].zipCode` | string | no | Postal or ZIP code. | | `items[].taxExemptCode` | string | no | Code behind the tax exemption, as the authority categorises it. | | `items[].salesChannel` | string | no | Channel the customer was won through. A SalesChannel member. | | `items[].additionalContactEmail` | string | no | Further contact addresses, beyond the invoice and main contacts. | | `items[].mainContactName` | string | no | Name of the customer's main contact person. | | `items[].preferredLanguage` | string | no | Language the customer prefers correspondence in. A PreferredLanguage member. | | `items[].crmCustomerId` | string | no | The customer's identifier in the CRM system it was sourced from. | | `items[].businessIdentificationNumber` | string | no | Government-issued business registration number. | | `items[].invoiceConsolidation` | string | no | How invoices are consolidated for this customer. An InvoiceConsolidation member. | | `items[].invoiceDeliveryMode` | string | no | How invoices reach the customer. An InvoiceDeliveryMode member. | | `items[].isExemptFromCreditCardFee` | boolean | no | Whether the customer is exempt from credit-card surcharges. | | `items[].billableTravelExpenseInvoiceDetail` | string | no | Whether billable travel and expense items appear itemised or consolidated. An InvoiceDetail member. | | `items[].status` | string | no | Lifecycle status of the customer record. A CustomerStatus member. | | `items[].paymentTerm` | string | no | Code of the payment term applied to this customer's invoices; `paymentTermLabel` carries its display name. | | `items[].taxRegistrations` | array of object | no | Tax registrations on file for this customer, across jurisdictions and tax types. Empty when the customer has none. | | `items[].taxRegistrations[].country` | string | yes | ISO 3166-1 country code the registration applies in. | | `items[].taxRegistrations[].taxNumber` | string | yes | Official tax identification number issued by the tax authority. | | `items[].taxRegistrations[].type` | string | yes | Kind of tax identification number. One of 'tax', 'salesTax', 'VAT', 'GST', 'EIN', 'TIN'. | | `items[].taxRegistrations[].effectiveDate` | string | no | Date the registration becomes valid for tax calculations (YYYY-MM-DD). | | `items[].taxRegistrations[].isActive` | boolean | no | Whether the registration is currently used for tax calculations. | | `items[].taxRegistrations[].note` | string | no | Free-text internal note recorded against the registration. | | `items[].taxRegistrations[].validationStatus` | string | no | Validation state assigned by the tax engine, not by the caller. | | `items[].taxRegistrations[].validationMessages` | string | no | Detail behind the validation state, when the engine recorded any. | | `items[].payment` | object | no | The customer's payment-provider link, when one is registered: the provider and the customer / default-payment-method identifiers in that provider's system. Absent when the customer has none. | | `items[].payment.provider` | string | no | Payment provider name, e.g. 'Stripe'. | | `items[].payment.externalCustomerId` | string | no | The customer's identifier in the payment provider's system. | | `items[].payment.externalPaymentMethodId` | string | no | The customer's default payment-method identifier in the provider's system. | | `items[].externalIdentifiers` | array of object | no | External identifiers owned by other systems. At most one entry per fieldName. Absent when the customer has none; an omitted field and [] are equivalent. | | `items[].externalIdentifiers[].fieldName` | string | yes | Configured key of the external identifier — a stable camelCase slug such as 'netsuiteSoNumber', not the human-readable label. Must match an active CustomerExternalIdentifierConfiguration. | | `items[].externalIdentifiers[].value` | string | yes | The identifier as the source system holds it. | | `items[].externalIdentifiers[].externalUrl` | string | no | Absolute http(s) link to the object in the originating system, when one is stored. | | `pagination` | object | yes | Pagination cursor for this page. | | `pagination.nextCursor` | string | no | Opaque cursor to pass as page.cursor for the next page; absent on the last page (mirrors the list/v1 data-set pagination.nextCursor). | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page (mirrors the list/v1 data-set pagination.hasMore). | ## Errors | Code | Status | | --- | --- | | `UNKNOWN_EXTERNAL_IDENTIFIER` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: UNKNOWN_EXTERNAL_IDENTIFIER - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Debit Memos `GET /accounting/debit-memos` Group: Accounting › Debit Memos · Operation ID: `debitMemos` Returns a list of debit memos. To filter by document number, counterparty name, purchase order, customer message or any amount, use POST accounting/debit-memos/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=debitMemos) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `creditMemoId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `debitMemoDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `dueDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `taxDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `approvalStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `debitMemoStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `debitMemoType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `customerId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentTerm` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isMigrated` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `debitGlAccountId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `billToAddressId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `soldToAddressId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, creditMemoId, customerId, entityId, debitMemoDate, dueDate, taxDate, approvalStatus, debitMemoStatus, debitMemoType, currency, paymentTerm, isMigrated, lastModifiedDate - Selectable: id, debitMemoNumber, creditMemoId, creditMemoNumber, debitMemoDate, dueDate, taxDate, approvalStatus, debitMemoStatus, debitMemoType, currency, customerId, customerName, customerNumber, entityId, entityName, paymentTerm, purchaseOrder, customerMessage, externalDebitMemoId, isMigrated, debitGlAccountId, billToAddressId, soldToAddressId, subtotal, totalAmount, totalTax, totalAmountDebited, lineDiscounts, unappliedCreditAmount, lastModifiedDate ## Response `200` Success Under `data`, an array of debitMemosRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the debit memo. | | `debitMemoNumber` | string or null | no | The debit memo number, for example 'DM-1'. Absent while none has been assigned. | | `creditMemoId` | string or null | no | The source credit memo this debit memo settles. Absent on a standalone debit memo. See id on GET accounting/credit-memos/:id. | | `creditMemoNumber` | string or null | no | The number of the source credit memo. Present whenever creditMemoId is. | | `debitMemoDate` | string (date) | no | The date the debit memo was issued. Format YYYY-MM-DD. | | `dueDate` | string (date) or null | no | The date the debit memo falls due. Format YYYY-MM-DD. Computed from debitMemoDate plus the payment term's days. | | `taxDate` | string (date) or null | no | The date the debit memo's tax was determined on. Format YYYY-MM-DD. Defaults to debitMemoDate. | | `approvalStatus` | string or null | no | The approval status of the debit memo. Possible values: 'draft', 'pendingApproval', 'revise', 'approved', 'rejected'. Only an approved debit memo has financial effect. | | `debitMemoStatus` | string or null | no | The payment and credit coverage status. Possible values: 'unpaid', 'isOverdue', 'partiallyPaid', 'paidInFull', 'creditedInFull', 'disputed'. Absent before approval. | | `debitMemoType` | string | no | The reason key for the charge, for example 'orderEntryError'. The accepted keys are configured per tenant. | | `currency` | string | no | The currency of the debit memo. Format ISO 4217, for example 'USD'. It governs every amount on the record. | | `customerId` | string or null | no | The customer the debit memo is issued to. See id on GET accounting/customers/:id. | | `customerName` | string or null | no | The display name of the customer. | | `customerNumber` | string or null | no | The customer number. | | `entityId` | string or null | no | The legal entity that issued the debit memo. See id on GET base/entities. | | `entityName` | string or null | no | The name of the issuing legal entity. | | `paymentTerm` | string | no | The payment-term key applied to the debit memo. See code on GET accounting/payment-terms. | | `purchaseOrder` | string or null | no | The customer's own purchase-order reference. | | `customerMessage` | string or null | no | A free-text message shown to the customer on the document. | | `externalDebitMemoId` | string or null | no | The identifier of this debit memo in an external source system. | | `isMigrated` | boolean or null | no | Whether the debit memo was loaded from a previous system. When true, it is excluded from dunning and its journal entries are auto-reversed. | | `debitGlAccountId` | string or null | no | The header debit GL account the journal entry posts to, an Asset-type account. See id on GET accounting/accounts/:id. | | `billToAddressId` | string or null | no | The bill-to address on the document. There is no endpoint for it. | | `soldToAddressId` | string or null | no | The sold-to address on the document. There is no endpoint for it. | | `subtotal` | string (decimal) or null | no | The sum of the lines' pre-discount totals, in the debit memo currency. | | `totalAmount` | string (decimal) or null | no | The total including tax, in the debit memo currency. | | `totalTax` | string (decimal) or null | no | The total tax across all lines, in the debit memo currency. | | `totalAmountDebited` | string (decimal) or null | no | The gross amount debited, in the debit memo currency. | | `lineDiscounts` | string (decimal) or null | no | The sum of the prorated discount amounts across all lines, in the debit memo currency. | | `unappliedCreditAmount` | string (decimal) or null | no | The credit still unapplied on the source credit memo, in the debit memo currency. Absent on a standalone debit memo. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the debit memo record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Debit Memo `POST /accounting/debit-memos` Group: Accounting › Debit Memos · Operation ID: `createDebitMemo` Creates a debit memo to settle a source credit memo. The customer, legal entity and currency come from that credit memo, not the request. customerId, entityId and currency are not accepted as input. They come from the source credit memo, so a debit memo can never disagree with the credit it settles. debitMemoType, billToAddressId and soldToAddressId fall back to the credit memo's own values, and when neither the request nor the credit memo supplies one the call fails with MISSING_REQUIRED_FIELD. The debit memo is approved on create and its journal entry posts as part of the call, so approval is not a per-request choice. A standalone debit memo, one with no source credit memo, is not supported. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createDebitMemo) ## 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`: createDebitMemoInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `creditMemoId` | string | yes | The source credit memo this debit memo settles. Required: a standalone debit memo cannot be created over this API. The customer, legal entity and currency come from this credit memo, not the request. See id on GET accounting/credit-memos/:id. | | `debitMemoDate` | string (date) | yes | The date the debit memo is issued. Use format YYYY-MM-DD. Must fall in an open accounting period unless isMigrated is true. | | `paymentTerm` | string | yes | The payment-term key to apply. When dueDate is omitted, it is computed from debitMemoDate plus this term's days. See code on GET accounting/payment-terms. | | `debitMemoType` | string | no | The reason key for the charge, for example 'orderEntryError'. Omit to inherit the source credit memo's reason key. When neither supplies one the call fails with MISSING_REQUIRED_FIELD. The accepted keys are configured per tenant. | | `dueDate` | string (date) | no | The date the debit memo falls due. Use format YYYY-MM-DD. Omit to compute it from debitMemoDate plus the payment term's days. | | `taxDate` | string (date) | no | The date to determine tax on. Use format YYYY-MM-DD. Omit to use debitMemoDate. | | `billToAddressId` | string | no | The bill-to address for the document. Omit to inherit the source credit memo's. When neither supplies one the call fails with MISSING_REQUIRED_FIELD. There is no endpoint for it. | | `soldToAddressId` | string | no | The sold-to address for the document. Omit to inherit the source credit memo's. When neither supplies one the call fails with MISSING_REQUIRED_FIELD. There is no endpoint for it. | | `purchaseOrder` | string | no | The customer's own purchase-order reference to record on the document. | | `customerMessage` | string | no | A free-text message to show the customer on the document. | | `debitGlAccountId` | string | no | The header debit GL account the journal entry posts to, an Asset-type account. Omit to use the entity's AR offset. See id on GET accounting/accounts/:id. | | `isMigrated` | boolean | no | Whether the debit memo was loaded from a previous system. When true, it is excluded from dunning, its journal entries are auto-reversed, and the open-accounting-period check on debitMemoDate is skipped. Defaults to false. | | `lines` | array of object | yes | The lines of the debit memo. At least one is required, or the call fails with NO_LINES. Every amount is in the source credit memo's currency. | | `lines[].creditMemoLineId` | string | yes | The source credit memo line this line debits. Must be a line of the credit memo named by creditMemoId, or the call fails with CREDIT_MEMO_LINE_NOT_FOUND. The debit memo line inherits its tax-exclusion state from that source line. See lines[].id on GET accounting/credit-memos/:id. | | `lines[].totalPrice` | string (decimal) | yes | The line total excluding tax, in the source credit memo's currency. Must be greater than 0. | | `lines[].description` | string | no | A free-text line description, shown to the customer. | | `lines[].productId` | string | no | The product the line refers to. Omit on a free-text line. See id on GET accounting/products/:id. | | `lines[].quantity` | string (decimal) | no | The quantity debited. A count, not an amount, so it has no currency. | | `lines[].unitOfMeasure` | string | no | The unit-of-measure key, for example 'each'. An unrecognized value fails with INVALID_ENUM_VALUE. | | `lines[].unitPrice` | string (decimal) | no | The price per unit, in the source credit memo's currency. | | `lines[].adjustedUnitPrice` | string (decimal) | no | The price per unit after adjustment, in the source credit memo's currency. | | `lines[].taxAmount` | string (decimal) | no | The tax on the line, in the source credit memo's currency. Omit to have the tax engine determine it. | | `lines[].taxCode` | string | no | The tax code to apply to the line. | | `lines[].taxDate` | string (date) | no | The date to determine the line's tax on. Use format YYYY-MM-DD. | | `lines[].taxMode` | string, one of `"taxInclusive"`, `"taxExclusive"` | no | How the line's prices are quoted for tax. | | `lines[].taxExemptCode` | string | no | The customer-level tax-exempt code to apply to the line. | | `lines[].creditGlAccountId` | string | no | The line's credit GL account, the credit side of the journal entry. See id on GET accounting/accounts/:id. | | `lines[].serviceStartDate` | string (date) | no | The first day of the service period the line covers. Use format YYYY-MM-DD. | | `lines[].serviceEndDate` | string (date) | no | The last day of the service period the line covers, inclusive. Use format YYYY-MM-DD. | Example: ```json { "creditMemoId": "5a0f3e6d-81b7-4c8e-9b21-9c2a6a107d34", "debitMemoDate": "2026-08-01", "paymentTerm": "net30", "debitMemoType": "orderEntryError", "dueDate": "2026-01-15", "taxDate": "2026-01-15", "billToAddressId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "soldToAddressId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "purchaseOrder": "PO-88213", "customerMessage": "", "debitGlAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "isMigrated": true, "lines": [ { "creditMemoLineId": "7d34c8e9-b21f-4a5c-9f3e-6d81b7d0aec3", "description": "Re-debit of over-credited licence fee", "quantity": "2", "unitOfMeasure": "license", "unitPrice": "250.00", "totalPrice": "500.00" } ] } ``` ## Response `200` Success Under `data` (createDebitMemoOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created debit memo. | | `debitMemoNumber` | string | no | The debit memo number, for example 'DM-1'. Absent while none has been assigned. | | `creditMemoId` | string | no | The source credit memo this debit memo settles. See id on GET accounting/credit-memos/:id. | | `creditMemoNumber` | string | no | The number of the source credit memo. Present whenever creditMemoId is. | | `debitMemoDate` | string (date) | yes | The date the debit memo was issued. Format YYYY-MM-DD. | | `dueDate` | string (date) | no | The date the debit memo falls due. Format YYYY-MM-DD. As supplied, or computed from debitMemoDate plus the payment term's days. | | `taxDate` | string (date) | no | The date the debit memo's tax was determined on. Format YYYY-MM-DD. Defaults to debitMemoDate. | | `approvalStatus` | string | no | The approval status of the new debit memo. Always 'approved'. | | `debitMemoStatus` | string | no | The payment and credit coverage status. Possible values: 'unpaid', 'isOverdue', 'partiallyPaid', 'paidInFull', 'creditedInFull', 'disputed'. Absent before approval. | | `debitMemoType` | string | yes | The reason key for the charge, as supplied or inherited from the source credit memo, for example 'orderEntryError'. | | `currency` | string | yes | The currency of the debit memo, taken from the source credit memo. Format ISO 4217, for example 'USD'. It governs every amount on the record. | | `customerId` | string | yes | The customer the debit memo is issued to, taken from the source credit memo. See id on GET accounting/customers/:id. | | `customerName` | string | no | The display name of the customer. | | `customerNumber` | string | no | The customer number. | | `entityId` | string | yes | The legal entity that issued the debit memo, taken from the source credit memo. See id on GET base/entities. | | `entityName` | string | no | The name of the issuing legal entity. | | `paymentTerm` | string | yes | The payment-term key applied to the debit memo. See code on GET accounting/payment-terms. | | `purchaseOrder` | string | no | The customer's own purchase-order reference. | | `customerMessage` | string | no | A free-text message shown to the customer on the document. | | `externalDebitMemoId` | string | no | The identifier of this debit memo in an external source system. | | `isMigrated` | boolean | no | Whether the debit memo was loaded from a previous system. When true, it is excluded from dunning and its journal entries are auto-reversed. | | `debitGlAccountId` | string | no | The header debit GL account the journal entry posts to, as supplied or defaulted to the entity's AR offset. See id on GET accounting/accounts/:id. | | `billToAddressId` | string | yes | The bill-to address on the document. There is no endpoint for it. | | `soldToAddressId` | string | yes | The sold-to address on the document. There is no endpoint for it. | | `subtotal` | string (decimal) | no | The sum of the lines' pre-discount totals, in the debit memo currency. | | `totalAmount` | string (decimal) | no | The total including tax, in the debit memo currency. | | `totalTax` | string (decimal) | no | The total tax across all lines, in the debit memo currency. | | `totalAmountDebited` | string (decimal) | no | The gross amount debited, in the debit memo currency. | | `lineDiscounts` | string (decimal) | no | The sum of the prorated discount amounts across all lines, in the debit memo currency. | | `unappliedCreditAmount` | string (decimal) | no | The credit still unapplied on the source credit memo, in the debit memo currency. Absent on a standalone debit memo. | | `lastModifiedDate` | string (date-time) | yes | The date and time the debit memo record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | no | The lines of the created debit memo, each with its assigned id and line number. | | `lines[].id` | string | yes | The unique identifier for the debit memo line. | | `lines[].lineNumber` | string | no | The sequential line number within the debit memo. | | `lines[].description` | string | no | A free-text line description, shown to the customer. | | `lines[].productId` | string | no | The debited product. Absent on a free-text line. See id on GET accounting/products/:id. | | `lines[].creditMemoLineId` | string | no | The source credit memo line this line debits. See lines[].id on GET accounting/credit-memos/:id. | | `lines[].quantity` | string (decimal) | no | The quantity debited. A count, not an amount, so it has no currency. | | `lines[].unitOfMeasure` | string | no | The unit-of-measure key, for example 'each'. | | `lines[].unitPrice` | string (decimal) | no | The price per unit, in the debit memo currency. | | `lines[].adjustedUnitPrice` | string (decimal) | no | The price per unit after adjustment, in the debit memo currency. | | `lines[].listPrice` | string (decimal) | no | The list price per unit as it stood on the source credit memo line, in the debit memo currency. | | `lines[].listPriceTotal` | string (decimal) | no | The pre-discount line total, in the debit memo currency. | | `lines[].discountAmount` | string (decimal) | no | The discount from the source credit memo line, in the debit memo currency. | | `lines[].discountPercentage` | string (decimal) | no | The discount as a percentage of the list price. A percentage, not an amount, so it has no currency. | | `lines[].totalPrice` | string (decimal) | no | The line total excluding tax, in the debit memo currency. | | `lines[].amountDebited` | string (decimal) | no | The amount debited for the line, in the debit memo currency. | | `lines[].totalAmountDue` | string (decimal) | no | The amount of the source credit memo line still available to debit, in the debit memo currency. | | `lines[].taxAmount` | string (decimal) | no | The tax on the line, in the debit memo currency. | | `lines[].taxCode` | string | no | The tax code applied to the line. | | `lines[].taxDate` | string (date) | no | The date the line's tax was determined on. Format YYYY-MM-DD. | | `lines[].taxMode` | string | no | How the line's prices are quoted for tax. Possible values: 'TaxInclusive', 'TaxExclusive'. | | `lines[].taxExemptCode` | string | no | The customer-level tax-exempt code applied to the line. | | `lines[].creditGlAccountId` | string | no | The line's credit GL account, the credit side of the journal entry. See id on GET accounting/accounts/:id. | | `lines[].serviceStartDate` | string (date) | no | The first day of the service period the line covers. Format YYYY-MM-DD. | | `lines[].serviceEndDate` | string (date) | no | The last day of the service period the line covers, inclusive. Format YYYY-MM-DD. | ## 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 | | --- | --- | | `CREDIT_MEMO_NOT_FOUND` | 400 | | `CREDIT_MEMO_LINE_NOT_FOUND` | 400 | | `PRODUCT_NOT_FOUND` | 400 | | `ACCOUNT_NOT_FOUND` | 400 | | `ADDRESS_NOT_FOUND` | 400 | | `INVALID_AMOUNT` | 400 | | `INVALID_ENUM_VALUE` | 400 | | `NO_LINES` | 400 | | `MISSING_REQUIRED_FIELD` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: CREDIT_MEMO_NOT_FOUND, CREDIT_MEMO_LINE_NOT_FOUND, PRODUCT_NOT_FOUND, ACCOUNT_NOT_FOUND, ADDRESS_NOT_FOUND, INVALID_AMOUNT, INVALID_ENUM_VALUE, NO_LINES, MISSING_REQUIRED_FIELD - `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 --- # Get Debit Memo `GET /accounting/debit-memos/{id}` Group: Accounting › Debit Memos · Operation ID: `getDebitMemo` Returns the debit memo with the given identifier, including its lines. Use the debit memo's id, not its debitMemoNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getDebitMemo) ## 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`. | | `id` | path | string | yes | The unique identifier for the debit memo to return. | ## Response `200` Success Under `data` (getDebitMemoOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the debit memo. | | `debitMemoNumber` | string | no | The debit memo number, for example 'DM-1'. Absent while none has been assigned. | | `creditMemoId` | string | no | The source credit memo this debit memo settles. Absent on a standalone debit memo. See id on GET accounting/credit-memos/:id. | | `creditMemoNumber` | string | no | The number of the source credit memo. Present whenever creditMemoId is. | | `debitMemoDate` | string (date) | yes | The date the debit memo was issued. Format YYYY-MM-DD. | | `dueDate` | string (date) | no | The date the debit memo falls due. Format YYYY-MM-DD. Computed from debitMemoDate plus the payment term's days. | | `taxDate` | string (date) | no | The date the debit memo's tax was determined on. Format YYYY-MM-DD. Defaults to debitMemoDate. | | `approvalStatus` | string | no | The approval status of the debit memo. Possible values: 'draft', 'pendingApproval', 'revise', 'approved', 'rejected'. Only an approved debit memo has financial effect. | | `debitMemoStatus` | string | no | The payment and credit coverage status. Possible values: 'unpaid', 'isOverdue', 'partiallyPaid', 'paidInFull', 'creditedInFull', 'disputed'. Absent before approval. | | `debitMemoType` | string | yes | The reason key for the charge, for example 'orderEntryError'. The accepted keys are configured per tenant. | | `currency` | string | yes | The currency of the debit memo. Format ISO 4217, for example 'USD'. It governs every amount on the record. | | `customerId` | string | yes | The customer the debit memo is issued to. See id on GET accounting/customers/:id. | | `customerName` | string | no | The display name of the customer. | | `customerNumber` | string | no | The customer number. | | `entityId` | string | yes | The legal entity that issued the debit memo. See id on GET base/entities. | | `entityName` | string | no | The name of the issuing legal entity. | | `paymentTerm` | string | yes | The payment-term key applied to the debit memo. See code on GET accounting/payment-terms. | | `purchaseOrder` | string | no | The customer's own purchase-order reference. | | `customerMessage` | string | no | A free-text message shown to the customer on the document. | | `externalDebitMemoId` | string | no | The identifier of this debit memo in an external source system. | | `isMigrated` | boolean | no | Whether the debit memo was loaded from a previous system. When true, it is excluded from dunning and its journal entries are auto-reversed. | | `debitGlAccountId` | string | no | The header debit GL account the journal entry posts to, an Asset-type account. See id on GET accounting/accounts/:id. | | `billToAddressId` | string | yes | The bill-to address on the document. There is no endpoint for it. | | `soldToAddressId` | string | yes | The sold-to address on the document. There is no endpoint for it. | | `subtotal` | string (decimal) | no | The sum of the lines' pre-discount totals, in the debit memo currency. | | `totalAmount` | string (decimal) | no | The total including tax, in the debit memo currency. | | `totalTax` | string (decimal) | no | The total tax across all lines, in the debit memo currency. | | `totalAmountDebited` | string (decimal) | no | The gross amount debited, in the debit memo currency. | | `lineDiscounts` | string (decimal) | no | The sum of the prorated discount amounts across all lines, in the debit memo currency. | | `unappliedCreditAmount` | string (decimal) | no | The credit still unapplied on the source credit memo, in the debit memo currency. Absent on a standalone debit memo. | | `lastModifiedDate` | string (date-time) | yes | The date and time the debit memo record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | no | The lines of the debit memo, each debiting one line of the source credit memo. An empty array when the debit memo has no lines. All line amounts are in the debit memo currency. | | `lines[].id` | string | yes | The unique identifier for the debit memo line. | | `lines[].lineNumber` | string | no | The sequential line number within the debit memo. | | `lines[].description` | string | no | A free-text line description, shown to the customer. | | `lines[].productId` | string | no | The debited product. Absent on a free-text line. See id on GET accounting/products/:id. | | `lines[].creditMemoLineId` | string | no | The source credit memo line this line debits. See lines[].id on GET accounting/credit-memos/:id. | | `lines[].quantity` | string (decimal) | no | The quantity debited. A count, not an amount, so it has no currency. | | `lines[].unitOfMeasure` | string | no | The unit-of-measure key, for example 'each'. | | `lines[].unitPrice` | string (decimal) | no | The price per unit, in the debit memo currency. | | `lines[].adjustedUnitPrice` | string (decimal) | no | The price per unit after adjustment, in the debit memo currency. | | `lines[].listPrice` | string (decimal) | no | The list price per unit as it stood on the source credit memo line, in the debit memo currency. | | `lines[].listPriceTotal` | string (decimal) | no | The pre-discount line total, in the debit memo currency. | | `lines[].discountAmount` | string (decimal) | no | The discount from the source credit memo line, in the debit memo currency. | | `lines[].discountPercentage` | string (decimal) | no | The discount as a percentage of the list price. A percentage, not an amount, so it has no currency. | | `lines[].totalPrice` | string (decimal) | no | The line total excluding tax, in the debit memo currency. | | `lines[].amountDebited` | string (decimal) | no | The amount debited for the line, in the debit memo currency. | | `lines[].totalAmountDue` | string (decimal) | no | The amount of the source credit memo line still available to debit, in the debit memo currency. | | `lines[].taxAmount` | string (decimal) | no | The tax on the line, in the debit memo currency. | | `lines[].taxCode` | string | no | The tax code applied to the line. | | `lines[].taxDate` | string (date) | no | The date the line's tax was determined on. Format YYYY-MM-DD. | | `lines[].taxMode` | string | no | How the line's prices are quoted for tax. Possible values: 'TaxInclusive', 'TaxExclusive'. | | `lines[].taxExemptCode` | string | no | The customer-level tax-exempt code applied to the line. | | `lines[].creditGlAccountId` | string | no | The line's credit GL account, the credit side of the journal entry. See id on GET accounting/accounts/:id. | | `lines[].serviceStartDate` | string (date) | no | The first day of the service period the line covers. Format YYYY-MM-DD. | | `lines[].serviceEndDate` | string (date) | no | The last day of the service period the line covers, inclusive. Format YYYY-MM-DD. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Debit Memo `PATCH /accounting/debit-memos/{id}` Group: Accounting › Debit Memos · Operation ID: `updateDebitMemo` Updates the customer message, purchase-order reference or payment term of the debit memo. Only the fields in the request body change. Changing the payment term does not recompute the due date. Use the debit memo's id, not its debitMemoNumber. Everything omitted from the request body is left as it was. Only customerMessage, purchaseOrder and paymentTerm can change. A debit memo is approved on create and has already posted its journal entry, so nothing that would move the posting or the aging can change: not the dates, amounts, lines, GL account or addresses. Changing paymentTerm does not recompute dueDate. The response is empty. Read the updated record with GET accounting/debit-memos/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateDebitMemo) ## 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. | | `id` | path | string | yes | The unique identifier for the debit memo to update. | ## Request body Required, `application/json`: updateDebitMemoInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `customerMessage` | string | no | A new free-text message to show the customer on the document. Pass an empty string to clear it. | | `purchaseOrder` | string | no | A new purchase-order reference from the customer. Pass an empty string to clear it. | | `paymentTerm` | string | no | A new payment-term key to record on the document. This does not recompute dueDate. See code on GET accounting/payment-terms. | Example: ```json { "customerMessage": "Revised: covers the re-debited licence fee only.", "purchaseOrder": "PO-88214", "paymentTerm": "" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Search Debit Memo `POST /accounting/debit-memos/search` Group: Accounting › Debit Memos · Operation ID: `searchDebitMemo` Returns the debit memos that match the given filters, each in full including its lines. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchDebitMemo) ## 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`. | ## Request body Required, `application/json`: searchDebitMemoInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A debit memo must match every filter you send. Omit to return all debit memos. | | `filters.id` | string | no | Limits results to the debit memo with this identifier. | | `filters.debitMemoNumber` | string | no | Limits results to the debit memo with this number, for example 'DM-1'. The match is exact. | | `filters.creditMemoId` | string | no | Limits results to debit memos settling this credit memo. See id on GET accounting/credit-memos/:id. | | `filters.creditMemoNumber` | string | no | Limits results to debit memos settling the credit memo with this number. The match is exact. | | `filters.customerId` | string | no | Limits results to debit memos issued to this customer. See id on GET accounting/customers/:id. | | `filters.customerName` | string | no | Limits results to debit memos whose customer name contains this text. The match is case-insensitive. | | `filters.customerNumber` | string | no | Limits results to debit memos with this customer number. The match is exact. | | `filters.entityId` | string | no | Limits results to debit memos issued by this legal entity. See id on GET base/entities. | | `filters.entityName` | string | no | Limits results to debit memos whose legal entity name contains this text. The match is case-insensitive. | | `filters.approvalStatus` | array of string | no | Limits results to debit memos in any of these approval statuses. Possible values: 'draft', 'pendingApproval', 'revise', 'approved', 'rejected'. Any other value matches no debit memo. | | `filters.debitMemoStatus` | array of string | no | Limits results to debit memos in any of these coverage statuses. Possible values: 'unpaid', 'isOverdue', 'partiallyPaid', 'paidInFull', 'creditedInFull', 'disputed'. Any other value matches no debit memo. | | `filters.debitMemoType` | array of string | no | Limits results to debit memos carrying any of these reason keys, for example 'orderEntryError'. The accepted keys are configured per tenant. | | `filters.currency` | string | no | Limits results to debit memos in this currency. Format ISO 4217, for example 'USD'. The match is exact. | | `filters.paymentTerm` | string | no | Limits results to debit memos with this payment-term key. The match is exact. See code on GET accounting/payment-terms. | | `filters.purchaseOrder` | string | no | Limits results to debit memos whose purchase-order reference contains this text. The match is case-insensitive. | | `filters.customerMessage` | string | no | Limits results to debit memos whose customer message contains this text. The match is case-insensitive. | | `filters.externalDebitMemoId` | string | no | Limits results to the debit memo with this external system identifier. The match is exact. | | `filters.isMigrated` | boolean | no | Limits results by whether the debit memo was loaded from a previous system. Omit to return both. | | `filters.debitMemoDate` | object | no | Limits results to debit memos issued in this date range. Omit either bound to leave that side open. | | `filters.debitMemoDate.start` | string (date) | no | The inclusive lower bound. Format YYYY-MM-DD. | | `filters.debitMemoDate.end` | string (date) | no | The inclusive upper bound. Format YYYY-MM-DD. | | `filters.dueDate` | object | no | Limits results to debit memos falling due in this date range. Omit either bound to leave that side open. | | `filters.dueDate.start` | string (date) | no | The inclusive lower bound. Format YYYY-MM-DD. | | `filters.dueDate.end` | string (date) | no | The inclusive upper bound. Format YYYY-MM-DD. | | `filters.lastModifiedDate` | object | no | Limits results to debit memos last changed in this range. Omit either bound to leave that side open. | | `filters.lastModifiedDate.start` | string | no | The inclusive lower bound, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `filters.lastModifiedDate.end` | string | no | The inclusive upper bound, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of debit memos per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "debitMemoNumber": "", "creditMemoId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "creditMemoNumber": "", "customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "customerName": "", "customerNumber": "", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityName": "", "approvalStatus": [ "" ], "debitMemoStatus": [ "" ], "debitMemoType": [ "" ], "currency": "", "paymentTerm": "", "purchaseOrder": "", "customerMessage": "", "externalDebitMemoId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "isMigrated": true, "debitMemoDate": { "start": "2026-01-15", "end": "2026-01-15" }, "dueDate": { "start": "2026-01-15", "end": "2026-01-15" }, "lastModifiedDate": { "start": "", "end": "" } }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchDebitMemoOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The debit memos on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the debit memo. | | `items[].debitMemoNumber` | string | no | The debit memo number, for example 'DM-1'. Absent while none has been assigned. | | `items[].creditMemoId` | string | no | The source credit memo this debit memo settles. Absent on a standalone debit memo. See id on GET accounting/credit-memos/:id. | | `items[].creditMemoNumber` | string | no | The number of the source credit memo. Present whenever creditMemoId is. | | `items[].debitMemoDate` | string (date) | yes | The date the debit memo was issued. Format YYYY-MM-DD. | | `items[].dueDate` | string (date) | no | The date the debit memo falls due. Format YYYY-MM-DD. Computed from debitMemoDate plus the payment term's days. | | `items[].taxDate` | string (date) | no | The date the debit memo's tax was determined on. Format YYYY-MM-DD. Defaults to debitMemoDate. | | `items[].approvalStatus` | string | no | The approval status of the debit memo. Possible values: 'draft', 'pendingApproval', 'revise', 'approved', 'rejected'. Only an approved debit memo has financial effect. | | `items[].debitMemoStatus` | string | no | The payment and credit coverage status. Possible values: 'unpaid', 'isOverdue', 'partiallyPaid', 'paidInFull', 'creditedInFull', 'disputed'. Absent before approval. | | `items[].debitMemoType` | string | yes | The reason key for the charge, for example 'orderEntryError'. The accepted keys are configured per tenant. | | `items[].currency` | string | yes | The currency of the debit memo. Format ISO 4217, for example 'USD'. It governs every amount on the record. | | `items[].customerId` | string | yes | The customer the debit memo is issued to. See id on GET accounting/customers/:id. | | `items[].customerName` | string | no | The display name of the customer. | | `items[].customerNumber` | string | no | The customer number. | | `items[].entityId` | string | yes | The legal entity that issued the debit memo. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the issuing legal entity. | | `items[].paymentTerm` | string | yes | The payment-term key applied to the debit memo. See code on GET accounting/payment-terms. | | `items[].purchaseOrder` | string | no | The customer's own purchase-order reference. | | `items[].customerMessage` | string | no | A free-text message shown to the customer on the document. | | `items[].externalDebitMemoId` | string | no | The identifier of this debit memo in an external source system. | | `items[].isMigrated` | boolean | no | Whether the debit memo was loaded from a previous system. When true, it is excluded from dunning and its journal entries are auto-reversed. | | `items[].debitGlAccountId` | string | no | The header debit GL account the journal entry posts to, an Asset-type account. See id on GET accounting/accounts/:id. | | `items[].billToAddressId` | string | yes | The bill-to address on the document. There is no endpoint for it. | | `items[].soldToAddressId` | string | yes | The sold-to address on the document. There is no endpoint for it. | | `items[].subtotal` | string (decimal) | no | The sum of the lines' pre-discount totals, in the debit memo currency. | | `items[].totalAmount` | string (decimal) | no | The total including tax, in the debit memo currency. | | `items[].totalTax` | string (decimal) | no | The total tax across all lines, in the debit memo currency. | | `items[].totalAmountDebited` | string (decimal) | no | The gross amount debited, in the debit memo currency. | | `items[].lineDiscounts` | string (decimal) | no | The sum of the prorated discount amounts across all lines, in the debit memo currency. | | `items[].unappliedCreditAmount` | string (decimal) | no | The credit still unapplied on the source credit memo, in the debit memo currency. Absent on a standalone debit memo. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the debit memo record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | no | The lines of the debit memo, each debiting one line of the source credit memo. An empty array when the debit memo has no lines. All line amounts are in the debit memo currency. | | `items[].lines[].id` | string | yes | The unique identifier for the debit memo line. | | `items[].lines[].lineNumber` | string | no | The sequential line number within the debit memo. | | `items[].lines[].description` | string | no | A free-text line description, shown to the customer. | | `items[].lines[].productId` | string | no | The debited product. Absent on a free-text line. See id on GET accounting/products/:id. | | `items[].lines[].creditMemoLineId` | string | no | The source credit memo line this line debits. See lines[].id on GET accounting/credit-memos/:id. | | `items[].lines[].quantity` | string (decimal) | no | The quantity debited. A count, not an amount, so it has no currency. | | `items[].lines[].unitOfMeasure` | string | no | The unit-of-measure key, for example 'each'. | | `items[].lines[].unitPrice` | string (decimal) | no | The price per unit, in the debit memo currency. | | `items[].lines[].adjustedUnitPrice` | string (decimal) | no | The price per unit after adjustment, in the debit memo currency. | | `items[].lines[].listPrice` | string (decimal) | no | The list price per unit as it stood on the source credit memo line, in the debit memo currency. | | `items[].lines[].listPriceTotal` | string (decimal) | no | The pre-discount line total, in the debit memo currency. | | `items[].lines[].discountAmount` | string (decimal) | no | The discount from the source credit memo line, in the debit memo currency. | | `items[].lines[].discountPercentage` | string (decimal) | no | The discount as a percentage of the list price. A percentage, not an amount, so it has no currency. | | `items[].lines[].totalPrice` | string (decimal) | no | The line total excluding tax, in the debit memo currency. | | `items[].lines[].amountDebited` | string (decimal) | no | The amount debited for the line, in the debit memo currency. | | `items[].lines[].totalAmountDue` | string (decimal) | no | The amount of the source credit memo line still available to debit, in the debit memo currency. | | `items[].lines[].taxAmount` | string (decimal) | no | The tax on the line, in the debit memo currency. | | `items[].lines[].taxCode` | string | no | The tax code applied to the line. | | `items[].lines[].taxDate` | string (date) | no | The date the line's tax was determined on. Format YYYY-MM-DD. | | `items[].lines[].taxMode` | string | no | How the line's prices are quoted for tax. Possible values: 'TaxInclusive', 'TaxExclusive'. | | `items[].lines[].taxExemptCode` | string | no | The customer-level tax-exempt code applied to the line. | | `items[].lines[].creditGlAccountId` | string | no | The line's credit GL account, the credit side of the journal entry. See id on GET accounting/accounts/:id. | | `items[].lines[].serviceStartDate` | string (date) | no | The first day of the service period the line covers. Format YYYY-MM-DD. | | `items[].lines[].serviceEndDate` | string (date) | no | The last day of the service period the line covers, inclusive. Format YYYY-MM-DD. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Errors | Code | Status | | --- | --- | | `INVALID_TIMESTAMP` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_TIMESTAMP - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of expense categories. `GET /accounting/expense-categories` Group: Accounting › Expense Categories · Operation ID: `expenseCategories` [Open in the interactive reference](https://docs.api.lensing.ai/?op=expenseCategories) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `category` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `categoryType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `reimbursable` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `requireAttachment` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `requireAttendees` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `externalId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, category, categoryType, status, lastModifiedDate - Selectable: id, category, categoryType, reimbursable, status, requireAttachment, requireAttendees, externalId, lastModifiedDate ## Response `200` Success Under `data`, an array of expenseCategoriesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the expense category. | | `category` | string or null | no | The display name of the expense category, for example 'Meals'. Unique within the tenant. | | `categoryType` | string or null | no | The kind of expense. One of 'Pur', 'Dis' or 'PerDiem'. 'Dis' is distance traveled. | | `reimbursable` | boolean or null | no | Whether expenses in this category are reimbursed to the employee. | | `status` | string or null | no | The lifecycle status of the expense category. Possible values: 'active', 'inactive'. An inactive category stays readable so historical expense items still resolve, and is not offered for new ones. | | `requireAttachment` | boolean or null | no | Whether an expense in this category must have a receipt attachment. | | `requireAttendees` | boolean or null | no | Whether an expense in this category must list attendees. | | `externalId` | string or null | no | The identifier this category has in an external system. It is not unique. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the expense category last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates an expense category. The name must be unique across the tenant. `POST /accounting/expense-categories` Group: Accounting › Expense Categories · Operation ID: `createExpenseCategory` A reimbursable category must post to at least one account, and omitting accountIds fails with NO_ACCOUNT_SELECTED. At most one active distance category may exist for each reimbursable value, one reimbursable and one not, so a third fails with DUPLICATE_DISTANCE_CATEGORY. Every linked account must use an account subtype enabled for expense reports, and a violation is rejected without one of the error codes below. The response contains only the new category's id. Read the record with GET accounting/expense-categories/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createExpenseCategory) ## 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`: createExpenseCategoryInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `category` | string | yes | The name of the expense category, for example 'Client Entertainment'. Must be unique across the tenant. | | `categoryType` | string | no | The kind of expense. One of 'Pur', 'Dis' or 'PerDiem'. 'Dis' is distance traveled. Omit to leave it unset. | | `status` | string | no | The lifecycle status. Accepted values: 'active', 'inactive'. Defaults to 'active'. | | `reimbursable` | boolean | no | Whether expenses in this category are reimbursed to the employee. Defaults to true. A reimbursable category must have at least one accountId. | | `requireAttachment` | boolean | no | Whether an expense in this category must have a receipt attachment. | | `requireAttendees` | boolean | no | Whether an expense in this category must list attendees. | | `entityIds` | array of string | no | The legal entities this category is available in. This array replaces the whole set. Omit to leave it unchanged. Send an empty array to remove every entity. See id on GET base/entities. | | `accountIds` | array of string | no | The GL accounts this category posts to. This array replaces the whole set. Required unless the category is not reimbursable. See id on GET accounting/accounts/:id. | Example: ```json { "category": "Client Entertainment", "categoryType": "Pur", "status": "", "reimbursable": true, "requireAttachment": true, "requireAttendees": true, "entityIds": [ "" ], "accountIds": [ "7c1a9e40-2b53-4d68-9f01-3a5b7c9d1e24" ] } ``` ## Response `200` Success Under `data` (createExpenseCategoryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created expense category. | ## 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 | | --- | --- | | `DUPLICATE_CATEGORY` | 400 | | `DUPLICATE_DISTANCE_CATEGORY` | 400 | | `NO_ACCOUNT_SELECTED` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: DUPLICATE_CATEGORY, DUPLICATE_DISTANCE_CATEGORY, NO_ACCOUNT_SELECTED, REFERENCE_NOT_FOUND - `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 --- # Returns the expense category with the given identifier. `GET /accounting/expense-categories/{id}` Group: Accounting › Expense Categories · Operation ID: `getExpenseCategory` To look up a category by its external identifier, which is not unique, use POST accounting/expense-categories/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getExpenseCategory) ## 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`. | | `id` | path | string | yes | The unique identifier for the expense category to return. | ## Response `200` Success Under `data` (getExpenseCategoryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the expense category. | | `category` | string | yes | The display name of the expense category, for example 'Meals'. Unique within the tenant. | | `categoryType` | string | no | The kind of expense. One of 'Pur', 'Dis' or 'PerDiem'. 'Dis' is distance traveled. | | `reimbursable` | boolean | yes | Whether expenses in this category are reimbursed to the employee. | | `status` | string | no | The lifecycle status of the expense category. Possible values: 'active', 'inactive'. An inactive category stays readable so historical expense items still resolve, and is not offered for new ones. | | `requireAttachment` | boolean | no | Whether an expense in this category must have a receipt attachment. | | `requireAttendees` | boolean | no | Whether an expense in this category must list attendees. | | `externalId` | string | no | The identifier this category has in an external system. It is not unique. | | `lastModifiedDate` | string (date-time) | yes | The date and time the expense category last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Expense Category `PATCH /accounting/expense-categories/{id}` Group: Accounting › Expense Categories · Operation ID: `updateExpenseCategory` Updates the expense category with the given identifier. Only the fields in the request body change. Only the fields in the request body change. entityIds and accountIds each replace the whole set when sent. Omit one to leave it unchanged, or send an empty array to clear it. Renaming to a name another category already uses fails with DUPLICATE_CATEGORY. Moving a category to 'Dis' when an active distance category already exists for the same reimbursable value fails with DUPLICATE_DISTANCE_CATEGORY. To retire a category that is in use, set status to 'inactive' rather than deleting it. Removing the last prepaid account from accountIds discards draft amortization schedules on the affected expense items. The response is empty. Read the updated record with GET accounting/expense-categories/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateExpenseCategory) ## 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. | | `id` | path | string | yes | The unique identifier for the expense category to update. | ## Request body Required, `application/json`: updateExpenseCategoryInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `category` | string | no | The new name. Omit to leave it unchanged. It must stay unique across the tenant. The category's own current name is not a collision. | | `categoryType` | string | no | The new kind of expense. One of 'Pur', 'Dis' or 'PerDiem'. Omit to leave it unchanged. | | `status` | string | no | The new lifecycle status. Accepted values: 'active', 'inactive'. Setting 'inactive' retires a category that is in use without deleting it. | | `reimbursable` | boolean | no | Whether expenses in this category are reimbursed to the employee. Omit to leave it unchanged. | | `requireAttachment` | boolean | no | Whether an expense in this category must have a receipt attachment. Omit to leave it unchanged. | | `requireAttendees` | boolean | no | Whether an expense in this category must list attendees. Omit to leave it unchanged. | | `entityIds` | array of string | no | The legal entities this category is available in. This array replaces the whole set. Omit to leave it unchanged. Send an empty array to clear it. See id on GET base/entities. | | `accountIds` | array of string | no | The GL accounts this category posts to. This array replaces the whole set. Omit to leave it unchanged. See id on GET accounting/accounts/:id. | Example: ```json { "category": "", "categoryType": "", "status": "inactive", "reimbursable": true, "requireAttachment": true, "requireAttendees": true, "entityIds": [ "" ], "accountIds": [ "" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `DUPLICATE_CATEGORY` | 400 | | `DUPLICATE_DISTANCE_CATEGORY` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, DUPLICATE_CATEGORY, DUPLICATE_DISTANCE_CATEGORY, REFERENCE_NOT_FOUND - `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 --- # Deletes the expense category with the given identifier. `DELETE /accounting/expense-categories/{id}` Group: Accounting › Expense Categories · Operation ID: `deleteExpenseCategory` Deletes one expense category and its account links. While any expense item references it, the call fails with EXPENSE_CATEGORY_IN_USE, which reports how many items still point at it, drafts included. To keep historical expenses readable, deactivate the category instead with PATCH accounting/expense-categories/:id and status 'inactive'. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteExpenseCategory) ## 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. | | `id` | path | string | yes | The unique identifier for the expense category to delete. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `EXPENSE_CATEGORY_IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, EXPENSE_CATEGORY_IN_USE - `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 --- # Returns the expense categories that match the given filters. `POST /accounting/expense-categories/search` Group: Accounting › Expense Categories · Operation ID: `searchExpenseCategory` All filters are optional and are combined with AND. Omit every filter to match all expense categories. This endpoint accepts externalId, which is not unique, so a lookup on it can return several categories. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchExpenseCategory) ## 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`. | ## Request body Required, `application/json`: searchExpenseCategoryInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A category must match every filter you send. Omit to return all expense categories. | | `filters.id` | string | no | Limits results to the category with this identifier. | | `filters.category` | string | no | Limits results to the category with this exact name, for example 'Meals'. The name is unique within the tenant, so this returns at most one category. The match is case-sensitive. | | `filters.categoryType` | string | no | Limits results to categories of this kind. One of 'Pur', 'Dis' or 'PerDiem'. Any other value matches no category. | | `filters.status` | string | no | Limits results to categories in this lifecycle status. Accepted values: 'active', 'inactive'. Any other value matches no category. | | `filters.externalId` | string | no | Limits results to categories with this external identifier. It is not unique, so this can return several categories. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of expense categories per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "status": "active" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchExpenseCategoryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The expense categories on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the expense category. | | `items[].category` | string | yes | The display name of the expense category, for example 'Meals'. Unique within the tenant. | | `items[].categoryType` | string | no | The kind of expense. One of 'Pur', 'Dis' or 'PerDiem'. 'Dis' is distance traveled. | | `items[].reimbursable` | boolean | yes | Whether expenses in this category are reimbursed to the employee. | | `items[].status` | string | no | The lifecycle status of the expense category. Possible values: 'active', 'inactive'. An inactive category stays readable so historical expense items still resolve, and is not offered for new ones. | | `items[].requireAttachment` | boolean | no | Whether an expense in this category must have a receipt attachment. | | `items[].requireAttendees` | boolean | no | Whether an expense in this category must list attendees. | | `items[].externalId` | string | no | The identifier this category has in an external system. It is not unique. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the expense category last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Expense Reports `GET /accounting/expense-reports` Group: Accounting › Expense Reports · Operation ID: `expenseReports` Returns a list of expense reports. Each entry is the report header. Read one report with its expense items from GET accounting/expense-reports/:id or POST accounting/expense-reports/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=expenseReports) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `expenseNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `submittedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `reimbursementCurrency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `externalExpenseReportId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `origin` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, expenseNumber, status, paymentStatus, submittedDate, reimbursementCurrency, reimbursableTotal, openAmount, lastModifiedDate - Selectable: id, expenseNumber, status, paymentStatus, submittedDate, reimbursementCurrency, reimbursableTotal, reimbursableTotalNetAmount, amountPaid, openAmount, pendingAmount, containsBillableItem, pendingPayment, externalExpenseReportId, origin, employeeName, entityName, lastModifiedDate ## Response `200` Success Under `data`, an array of expenseReportsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the expense report. | | `expenseNumber` | string or null | no | The expense number. | | `status` | string or null | no | The approval status of the report. Values include 'draft', 'pendingApproval', 'approved', 'rejected' and 'paid'. | | `paymentStatus` | string or null | no | The payment status of the report. | | `submittedDate` | string (date) or null | no | The date the employee submitted the report. Format YYYY-MM-DD. | | `reimbursementCurrency` | string or null | no | The currency the report is reimbursed in. Format ISO 4217, for example 'USD'. Governs every header total below. | | `reimbursableTotal` | string (decimal) or null | no | The total reimbursable amount across the report's items, in reimbursementCurrency. | | `reimbursableTotalNetAmount` | string (decimal) or null | no | The total reimbursable amount excluding VAT, in reimbursementCurrency. | | `amountPaid` | string (decimal) or null | no | The amount already reimbursed to the employee, in reimbursementCurrency. | | `openAmount` | string (decimal) or null | no | The amount still owed to the employee, reimbursableTotal minus amountPaid, in reimbursementCurrency. | | `pendingAmount` | string (decimal) or null | no | The amount committed to an in-flight payment run but not yet paid, in reimbursementCurrency. | | `containsBillableItem` | boolean or null | no | Whether at least one item on the report is marked billable to a customer. | | `pendingPayment` | boolean or null | no | Whether a payment for this report is in flight. | | `externalExpenseReportId` | string or null | no | The identifier this report has in the upstream system that produced it. It is not unique. | | `origin` | string or null | no | The source system this report came from, as free-form text. | | `employeeName` | string or null | no | The name of the employee the report belongs to. | | `entityName` | string or null | no | The name of the legal entity the report is posted under. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the expense report last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates an expense report. `POST /accounting/expense-reports` Group: Accounting › Expense Reports · Operation ID: `createExpenseReport` The response contains only the new report's id. Read the record with GET accounting/expense-reports/:id if you need its stored state, where expenseNumber appears once assigned. The report is always created pendingApproval, so a status cannot be supplied here. Two currencies are in play: the report's reimbursementCurrency is what the employee is paid in, and each item's currency is what the expense was incurred in, and they need not match. Every amount is a decimal governed by the currency on the same object. reimbursable is derived, not supplied on the report: the report counts as reimbursable unless one of its items sets reimbursable to false. Every item inherits the report's employeeId, which cannot be set per item. A report with more items than your tenant runs in one call is refused with TOO_MANY_LINES, so split it across several requests. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createExpenseReport) ## 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`: createExpenseReportInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `employeeId` | string | yes | The employee the report belongs to. See id on GET people/:id. | | `entityId` | string | yes | The legal entity the report is posted under. See id on GET base/entities. | | `reimbursementCurrency` | string | yes | The currency the employee is reimbursed in. Format ISO 4217, for example 'USD'. Individual items may be in other currencies. | | `submittedDate` | string (date) | yes | The date the report is submitted. Use format YYYY-MM-DD. | | `departmentId` | string | no | The department the report is charged to. See id on GET departments/:id. | | `offsetAccountId` | string | no | The GL offset account for the reimbursement. See id on GET accounting/accounts/:id. | | `externalExpenseReportId` | string | no | The identifier this report has in the upstream system that produced it. It is not unique. | | `externalUrl` | string | no | A link to the report in the upstream system that produced it. | | `origin` | string | no | The source system this report came from, as free-form text. | | `markAsPaid` | boolean | no | Whether to mark the report paid on creation. Defaults to false. | | `skipJournalEntry` | boolean | no | Whether to skip journal-entry creation for this report. Defaults to false. | | `expenseItems` | array of object | yes | The expense items on the report, in the order they should appear. At least one is expected. | | `expenseItems[].amount` | string (decimal) | yes | The amount of the expense, as a decimal in this item's own currency. | | `expenseItems[].currency` | string | yes | The currency the expense was incurred in. Format ISO 4217, for example 'USD'. Governs this item's amount, and need not match the report's reimbursementCurrency. | | `expenseItems[].billDate` | string (date) | yes | The date on the receipt. Use format YYYY-MM-DD. | | `expenseItems[].merchant` | string | yes | The merchant the expense was paid to, as printed on the receipt. | | `expenseItems[].expenseType` | string | yes | The kind of expense. Values include 'Pur' (purchase), 'Dis' (distance traveled) and 'PerDiem'. | | `expenseItems[].description` | string | no | A description of the expense. | | `expenseItems[].categoryId` | string | no | The expense category this item is booked under. See id on GET accounting/expense-categories/:id. | | `expenseItems[].accountId` | string | no | The GL account this item posts to. See id on GET accounting/accounts/:id. Omit to let the category decide. | | `expenseItems[].reimbursable` | boolean | no | Whether this item is reimbursed to the employee. Defaults to true. | | `expenseItems[].externalExpenseItemId` | string | no | The identifier this item has in the upstream system that produced it. | | `expenseItems[].origin` | string | no | The source system this item came from, as free-form text. | | `expenseItems[].attendees` | string | no | Who attended, when the item's category requires it. Free text. | | `expenseItems[].attachments` | array of object | no | Receipts for this item, each naming an already-uploaded file by its id. Required when the category sets requireAttachment. On an existing item, omitting this leaves its attachments untouched, and an empty array clears them. | | `expenseItems[].attachments[].fileName` | string | yes | The display name of the attached file. | | `expenseItems[].attachments[].fileId` | string | yes | The identifier of the already-uploaded file. There is no endpoint for it. | Example: ```json { "employeeId": "5f1b8c02-7d34-4e56-9a78-1b2c3d4e5f60", "entityId": "8f2c1d40-5a6b-4c7d-9e80-1f2a3b4c5d6e", "reimbursementCurrency": "EUR", "submittedDate": "2026-08-27", "departmentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "offsetAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "externalExpenseReportId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "externalUrl": "", "origin": "", "markAsPaid": true, "skipJournalEntry": true, "expenseItems": [ { "amount": "48.90", "currency": "EUR", "billDate": "2026-08-20", "merchant": "Deutsche Bahn", "expenseType": "Pur" } ] } ``` ## Response `200` Success Under `data` (createExpenseReportOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created expense report. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `TOO_MANY_LINES` | 400 | | `CATEGORY_REQUIREMENT_UNMET` | 400 | | `CATEGORY_INACTIVE` | 400 | | `ITEM_NEEDS_CATEGORY_OR_ACCOUNT` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, TOO_MANY_LINES, CATEGORY_REQUIREMENT_UNMET, CATEGORY_INACTIVE, ITEM_NEEDS_CATEGORY_OR_ACCOUNT - `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 --- # Returns the expense report with the given identifier. `GET /accounting/expense-reports/{id}` Group: Accounting › Expense Reports · Operation ID: `getExpenseReport` An expense report is an employee's claim for reimbursement, with its header and its expense items. It shares its read with POST accounting/expense-reports/search, which returns the same record. The list at GET accounting/expense-reports is header-only, so the expense items come back here and on search. Every amount is a decimal: the header totals in reimbursementCurrency, and each item's amount in the item's own currency. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getExpenseReport) ## 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`. | | `id` | path | string | yes | The unique identifier for the expense report to return. | ## Response `200` Success Under `data` (getExpenseReportOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the expense report. | | `expenseNumber` | string | no | The expense number. Absent until one has been assigned. | | `status` | string | yes | The approval status of the report. Values include 'draft', 'pendingApproval', 'approved', 'rejected' and 'paid'. | | `paymentStatus` | string | no | The payment status of the report. Absent until a payment run has touched it. | | `submittedDate` | string (date) | no | The date the employee submitted the report for approval. Format YYYY-MM-DD. Absent while the report is still a draft. | | `reimbursementCurrency` | string | yes | The currency the report is reimbursed in. Format ISO 4217, for example 'USD'. Governs every header total below. | | `reimbursableTotal` | string (decimal) | no | The total reimbursable amount across the report's items, in reimbursementCurrency. | | `reimbursableTotalNetAmount` | string (decimal) | no | The total reimbursable amount excluding VAT, in reimbursementCurrency. | | `amountPaid` | string (decimal) | no | The amount already reimbursed to the employee, in reimbursementCurrency. | | `openAmount` | string (decimal) | no | The amount still owed to the employee, reimbursableTotal minus amountPaid, in reimbursementCurrency. | | `pendingAmount` | string (decimal) | no | The amount committed to an in-flight payment run but not yet paid, in reimbursementCurrency. | | `containsBillableItem` | boolean | no | Whether at least one item on the report is marked billable to a customer. | | `markAsPaid` | boolean | no | Whether the report was flagged as paid without going through a payment run. | | `pendingPayment` | boolean | no | Whether a payment for this report is in flight. | | `skipJournalEntry` | boolean | no | Whether the report is configured not to post a journal entry. | | `reviewMessage` | string | no | The reviewer's note left on approval or rejection. | | `externalUrl` | string | no | A link to the report in the upstream system that produced it. | | `externalExpenseReportId` | string | no | The identifier this report has in the upstream system that produced it. It is not unique. | | `origin` | string | no | The source system this report came from, as free-form text. | | `employeeId` | string | no | The employee the report belongs to. See id on GET people/:id. | | `employeeName` | string | no | The name of the employee the report belongs to. | | `entityId` | string | no | The legal entity the report is posted under. See id on GET base/entities. | | `entityName` | string | no | The name of the legal entity the report is posted under. | | `departmentId` | string | no | The department the report is charged to. See id on GET departments/:id. | | `offsetAccountId` | string | no | The GL offset account for the reimbursement. See id on GET accounting/accounts/:id. | | `lastModifiedDate` | string (date-time) | yes | The date and time the expense report last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `approvals` | array of object | no | The approval decisions on this report, oldest first. An empty array when no approval policy has matched the report. | | `approvals[].decision` | string | yes | What was decided. Values include 'approve' and 'reject'. | | `approvals[].employeeName` | string | no | The name of the employee who decided. | | `approvals[].message` | string | no | The reviewer's note, where one was given. | | `approvals[].decidedAt` | string (date-time) | yes | The date and time the decision was recorded, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `expenseItems` | array of object | no | The expense items on the report, in their order on it. An empty array when the report has none. | | `expenseItems[].id` | string | yes | The unique identifier for the expense item. | | `expenseItems[].itemNumber` | string | no | The expense item number. | | `expenseItems[].merchant` | string | no | The merchant the expense was incurred with. | | `expenseItems[].billDate` | string (date) | no | The date the expense was incurred. Format YYYY-MM-DD. | | `expenseItems[].expenseType` | string | no | The kind of expense. Values include 'Pur' (purchase), 'Dis' (distance traveled) and 'PerDiem'. | | `expenseItems[].description` | string | no | A description of the expense item. | | `expenseItems[].amount` | string (decimal) | no | The amount of the expense item, as a decimal in the item's own currency. | | `expenseItems[].currency` | string | no | The currency this item's amount is in. Format ISO 4217, for example 'USD'. May differ from the report's reimbursementCurrency. | | `expenseItems[].reimbursable` | boolean | no | Whether this item is reimbursable to the employee. | | `expenseItems[].categoryId` | string | no | The expense category the item is booked to. See id on GET accounting/expense-categories/:id. | | `expenseItems[].externalExpenseItemId` | string | no | The identifier this item has in the upstream system that produced it. | | `expenseItems[].attendees` | string | no | Who attended, when the item's category requires it. | | `expenseItems[].attachments` | array of object | no | The receipts on this item. | | `expenseItems[].attachments[].id` | string | yes | The unique identifier for the attachment record. | | `expenseItems[].attachments[].fileName` | string | no | The display name of the attached file. | | `expenseItems[].attachments[].fileId` | string | no | The identifier of the stored file. There is no endpoint for it. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Expense Report `PATCH /accounting/expense-reports/{id}` Group: Accounting › Expense Reports · Operation ID: `updateExpenseReport` Updates the expense report with the given identifier. Only the fields in the request body change. Only the fields in the request body change. Expense items change through three arrays, applied in the order delete, then update, then create, so one call can retire an item and add its replacement. updateExpenseItems and deleteExpenseItems address items by the id the report publishes, and an item id belonging to another report is refused with ITEM_NOT_FOUND. Omit all three to leave the items untouched. reimbursable stays derived: after the merge the report is reimbursable unless a surviving item sets it to false. The response is empty. Read the record with GET accounting/expense-reports/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateExpenseReport) ## 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. | | `id` | path | string | yes | The unique identifier for the expense report to update. | ## Request body Required, `application/json`: updateExpenseReportInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `employeeId` | string | no | The employee the report belongs to, given as an id. See id on GET people/:id. Omit to leave unchanged. | | `entityId` | string | no | The legal entity the report is posted under, given as an id. See id on GET base/entities. Omit to leave unchanged. | | `reimbursementCurrency` | string | no | The currency the employee is reimbursed in. Format ISO 4217, for example 'USD'. Omit to leave unchanged. | | `submittedDate` | string (date) | no | The date the report is submitted. Use format YYYY-MM-DD. Omit to leave unchanged. | | `departmentId` | string | no | The department the report is charged to, given as an id. See id on GET departments/:id. Omit to leave unchanged. | | `offsetAccountId` | string | no | The GL offset account for the reimbursement, given as an id. See id on GET accounting/accounts/:id. Omit to leave unchanged. | | `externalExpenseReportId` | string | no | The identifier this report has in the upstream system that produced it. Omit to leave unchanged. | | `externalUrl` | string | no | A link to the report in the upstream system that produced it. Omit to leave unchanged. | | `origin` | string | no | The source system this report came from, as free-form text. Omit to leave unchanged. | | `markAsPaid` | boolean | no | Whether the report is marked paid. Omit to leave unchanged. | | `createExpenseItems` | array of object | no | The expense items to add. Omit for no additions. | | `createExpenseItems[].amount` | string (decimal) | yes | The amount of the expense, as a decimal in this item's own currency. | | `createExpenseItems[].currency` | string | yes | The currency the expense was incurred in. Format ISO 4217, for example 'USD'. Governs this item's amount, and need not match the report's reimbursementCurrency. | | `createExpenseItems[].billDate` | string (date) | yes | The date on the receipt. Use format YYYY-MM-DD. | | `createExpenseItems[].merchant` | string | yes | The merchant the expense was paid to, as printed on the receipt. | | `createExpenseItems[].expenseType` | string | yes | The kind of expense. Values include 'Pur' (purchase), 'Dis' (distance traveled) and 'PerDiem'. | | `createExpenseItems[].description` | string | no | A description of the expense. | | `createExpenseItems[].categoryId` | string | no | The expense category this item is booked under. See id on GET accounting/expense-categories/:id. | | `createExpenseItems[].accountId` | string | no | The GL account this item posts to. See id on GET accounting/accounts/:id. Omit to let the category decide. | | `createExpenseItems[].reimbursable` | boolean | no | Whether this item is reimbursed to the employee. Defaults to true. | | `createExpenseItems[].externalExpenseItemId` | string | no | The identifier this item has in the upstream system that produced it. | | `createExpenseItems[].origin` | string | no | The source system this item came from, as free-form text. | | `createExpenseItems[].attendees` | string | no | Who attended, when the item's category requires it. Free text. | | `createExpenseItems[].attachments` | array of object | no | Receipts for this item, each naming an already-uploaded file by its id. Required when the category sets requireAttachment. On an existing item, omitting this leaves its attachments untouched, and an empty array clears them. | | `createExpenseItems[].attachments[].fileName` | string | yes | The display name of the attached file. | | `createExpenseItems[].attachments[].fileId` | string | yes | The identifier of the already-uploaded file. There is no endpoint for it. | | `updateExpenseItems` | array of object | no | The expense items to change, each addressed by the id the report publishes. Every field but the id is optional, and omitting one leaves it unchanged. Omit for no changes. | | `updateExpenseItems[].id` | string | yes | The unique identifier for the expense item to change. It must belong to this report. | | `updateExpenseItems[].amount` | string (decimal) | no | The amount of the expense, as a decimal in this item's own currency. | | `updateExpenseItems[].currency` | string | no | The currency the expense was incurred in. Format ISO 4217, for example 'USD'. Governs this item's amount, and need not match the report's reimbursementCurrency. | | `updateExpenseItems[].billDate` | string (date) | no | The date on the receipt. Use format YYYY-MM-DD. | | `updateExpenseItems[].merchant` | string | no | The merchant the expense was paid to, as printed on the receipt. | | `updateExpenseItems[].expenseType` | string | no | The kind of expense. Values include 'Pur' (purchase), 'Dis' (distance traveled) and 'PerDiem'. | | `updateExpenseItems[].description` | string | no | A description of the expense. | | `updateExpenseItems[].categoryId` | string | no | The expense category this item is booked under. See id on GET accounting/expense-categories/:id. | | `updateExpenseItems[].accountId` | string | no | The GL account this item posts to. See id on GET accounting/accounts/:id. Omit to let the category decide. | | `updateExpenseItems[].reimbursable` | boolean | no | Whether this item is reimbursed to the employee. Defaults to true. | | `updateExpenseItems[].externalExpenseItemId` | string | no | The identifier this item has in the upstream system that produced it. | | `updateExpenseItems[].origin` | string | no | The source system this item came from, as free-form text. | | `updateExpenseItems[].attendees` | string | no | Who attended, when the item's category requires it. Free text. | | `updateExpenseItems[].attachments` | array of object | no | Receipts for this item, each naming an already-uploaded file by its id. Required when the category sets requireAttachment. On an existing item, omitting this leaves its attachments untouched, and an empty array clears them. | | `updateExpenseItems[].attachments[].fileName` | string | yes | The display name of the attached file. | | `updateExpenseItems[].attachments[].fileId` | string | yes | The identifier of the already-uploaded file. There is no endpoint for it. | | `deleteExpenseItems` | array of string | no | The ids of expense items to remove, applied before the updates and creates. Each must belong to this report. Omit for no deletions. | Example: ```json { "employeeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "reimbursementCurrency": "", "submittedDate": "2026-01-15", "departmentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "offsetAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "externalExpenseReportId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "externalUrl": "", "origin": "", "markAsPaid": true, "createExpenseItems": [ { "amount": "12.50", "currency": "EUR", "billDate": "2026-08-21", "merchant": "Taxi", "expenseType": "Pur" } ], "updateExpenseItems": [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "amount": "19.99", "currency": "", "billDate": "2026-01-15", "merchant": "", "expenseType": "", "description": "", "categoryId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "reimbursable": true, "externalExpenseItemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "origin": "", "attendees": "", "attachments": [ { "fileName": "", "fileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ] } ], "deleteExpenseItems": [ "" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `ITEM_NOT_FOUND` | 400 | | `TOO_MANY_LINES` | 400 | | `CATEGORY_REQUIREMENT_UNMET` | 400 | | `CATEGORY_INACTIVE` | 400 | | `ITEM_NEEDS_CATEGORY_OR_ACCOUNT` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND, ITEM_NOT_FOUND, TOO_MANY_LINES, CATEGORY_REQUIREMENT_UNMET, CATEGORY_INACTIVE, ITEM_NEEDS_CATEGORY_OR_ACCOUNT - `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 --- # Deletes an expense report. `DELETE /accounting/expense-reports/{id}` Group: Accounting › Expense Reports · Operation ID: `deleteExpenseReport` The response is empty. Its expense items, their attachments and any business-trip details are deleted with it. Two states block the delete, both checked before anything is written and only for a submitted, approved report. A report whose accounting period is closed or locked gives POSTING_PERIOD_CLOSED, naming the period. A report with an expense item carrying a posted amortization schedule gives POSTED_AMORTIZATION. A draft or still-pending report is subject to neither. Deleting is permanent. A report that must leave an audit trail should be rejected instead, which is not part of this version. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteExpenseReport) ## 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. | | `id` | path | string | yes | The unique identifier for the expense report to delete. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `POSTING_PERIOD_CLOSED` | 400 | | `POSTED_AMORTIZATION` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, POSTING_PERIOD_CLOSED, POSTED_AMORTIZATION - `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 --- # Approves the expense report. `POST /accounting/expense-reports/{id}/approve` Group: Accounting › Expense Reports · Operation ID: `approveExpenseReport` Approves the expense report through your tenant's approval policy. With no approvers configured the report goes straight to approved, and with a multi-step policy it advances one step. The status after the operation is returned. A report that is already approved is refused with STATUS_CONFLICT. A refusal from the approval framework comes back as APPROVAL_FAILED, with its own message in reason. [Open in the interactive reference](https://docs.api.lensing.ai/?op=approveExpenseReport) ## 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. | | `id` | path | string | yes | The unique identifier for the expense report to approve. | ## Response `200` Success Under `data` (approveExpenseReportOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string | yes | The approval status after the operation. The approval workflow decides it, so it is read back. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `STATUS_CONFLICT` | 400 | | `APPROVAL_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, STATUS_CONFLICT, APPROVAL_FAILED - `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 --- # Cancels an expense report's submission. `POST /accounting/expense-reports/{id}/cancel-submission` Group: Accounting › Expense Reports · Operation ID: `cancelExpenseReportSubmission` Canceling a submission deletes the report: the record, its expense items and its attachments are removed. It is the submitter's way of taking a report back, and it is terminal. An approved report can be canceled only while its posting period is open and no amortization schedule on it has posted, otherwise POSTING_PERIOD_CLOSED or POSTED_AMORTIZATION. A report still awaiting approval has neither constraint. This endpoint deletes the report but not its expense items, their attachments or its business-trip details, so canceling leaves those rows orphaned. To remove a report and clean up its children, use DELETE accounting/expense-reports/:id instead. [Open in the interactive reference](https://docs.api.lensing.ai/?op=cancelExpenseReportSubmission) ## 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. | | `id` | path | string | yes | The unique identifier for the expense report whose submission to cancel. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `POSTING_PERIOD_CLOSED` | 400 | | `POSTED_AMORTIZATION` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, POSTING_PERIOD_CLOSED, POSTED_AMORTIZATION - `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 --- # Rejects the expense report. `POST /accounting/expense-reports/{id}/reject` Group: Accounting › Expense Reports · Operation ID: `rejectExpenseReport` Rejects the expense report, with a reviewMessage explaining why. The message is recorded against the decision and is what the submitter sees, so it is required. A report that is already rejected is refused with STATUS_CONFLICT. A refusal from the approval framework comes back as APPROVAL_FAILED, with its own message in reason. Rejecting is not deleting: the report stays, with its number and its audit trail. To withdraw a submission entirely, use POST accounting/expense-reports/:id/cancel-submission. [Open in the interactive reference](https://docs.api.lensing.ai/?op=rejectExpenseReport) ## 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. | | `id` | path | string | yes | The unique identifier for the expense report to reject. | ## Request body Required, `application/json`: rejectExpenseReportInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `reviewMessage` | string | yes | Why the report is being rejected. It is recorded against the decision and shown to the submitter. | Example: ```json { "reviewMessage": "Missing receipt for the hotel line" } ``` ## Response `200` Success Under `data` (rejectExpenseReportOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string | yes | The approval status after the operation. The approval workflow decides it, so it is read back. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `STATUS_CONFLICT` | 400 | | `APPROVAL_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, STATUS_CONFLICT, APPROVAL_FAILED - `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 --- # Search Expense Report `POST /accounting/expense-reports/search` Group: Accounting › Expense Reports · Operation ID: `searchExpenseReport` Returns the expense reports that match the given filters, each with its expense items. Searches expense reports and pages through the matches, returning the full record, header and expense items, for each. Matching nothing returns an empty items array, not an error. The expenseNumber and externalExpenseReportId filters are not unique, so a match on either can return several reports. The employee and legal-entity name filters are here rather than on the collection because a name must not appear in a URL. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchExpenseReport) ## 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`. | ## Request body Required, `application/json`: searchExpenseReportInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. An expense report must match every filter you send. Omit to return all expense reports. | | `filters.id` | string | no | Limits results to the expense report with this identifier. | | `filters.expenseNumber` | string | no | Limits results to reports with this expense number. It is not unique, so this can match several reports. | | `filters.employeeId` | string | no | Limits results to reports belonging to this employee, given as the employee id. See id on GET people/:id. An id that matches no employee yields an empty page, not an error. | | `filters.entityId` | string | no | Limits results to reports posted under this legal entity, given as the entity id. See id on GET base/entities. An id that matches no entity yields an empty page, not an error. | | `filters.employeeName` | string | no | Limits results to reports whose employee has this name. The match is exact and case-sensitive. | | `filters.entityName` | string | no | Limits results to reports whose legal entity has this name. The match is exact and case-sensitive. | | `filters.status` | string | no | Limits results to reports in this approval status. Values include 'draft', 'pendingApproval', 'approved', 'rejected' and 'paid'. Any other value matches no report. | | `filters.paymentStatus` | string | no | Limits results to reports in this payment status. Any other value matches no report. | | `filters.externalExpenseReportId` | string | no | Limits results to reports with this upstream identifier. It is not unique, so this can match several reports. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first request. | | `page.size` | number | no | The number of expense reports per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "status": "pendingApproval" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchExpenseReportOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The expense reports that match, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the expense report. | | `items[].expenseNumber` | string | no | The expense number. Absent until one has been assigned. | | `items[].status` | string | yes | The approval status of the report. Values include 'draft', 'pendingApproval', 'approved', 'rejected' and 'paid'. | | `items[].paymentStatus` | string | no | The payment status of the report. Absent until a payment run has touched it. | | `items[].submittedDate` | string (date) | no | The date the employee submitted the report for approval. Format YYYY-MM-DD. | | `items[].reimbursementCurrency` | string | yes | The currency the report is reimbursed in. Format ISO 4217, for example 'USD'. Governs every header total below. | | `items[].reimbursableTotal` | string (decimal) | no | The total reimbursable amount across the report's items, in reimbursementCurrency. | | `items[].reimbursableTotalNetAmount` | string (decimal) | no | The total reimbursable amount excluding VAT, in reimbursementCurrency. | | `items[].amountPaid` | string (decimal) | no | The amount already reimbursed to the employee, in reimbursementCurrency. | | `items[].openAmount` | string (decimal) | no | The amount still owed to the employee, reimbursableTotal minus amountPaid, in reimbursementCurrency. | | `items[].pendingAmount` | string (decimal) | no | The amount committed to an in-flight payment run but not yet paid, in reimbursementCurrency. | | `items[].containsBillableItem` | boolean | no | Whether at least one item on the report is marked billable to a customer. | | `items[].markAsPaid` | boolean | no | Whether the report was flagged as paid without going through a payment run. | | `items[].pendingPayment` | boolean | no | Whether a payment for this report is in flight. | | `items[].skipJournalEntry` | boolean | no | Whether the report is configured not to post a journal entry. | | `items[].reviewMessage` | string | no | The reviewer's note left on approval or rejection. | | `items[].externalUrl` | string | no | A link to the report in the upstream system that produced it. | | `items[].externalExpenseReportId` | string | no | The identifier this report has in the upstream system that produced it. It is not unique. | | `items[].origin` | string | no | The source system this report came from, as free-form text. | | `items[].employeeId` | string | no | The employee the report belongs to. See id on GET people/:id. | | `items[].employeeName` | string | no | The name of the employee the report belongs to. | | `items[].entityId` | string | no | The legal entity the report is posted under. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the legal entity the report is posted under. | | `items[].departmentId` | string | no | The department the report is charged to. See id on GET departments/:id. | | `items[].offsetAccountId` | string | no | The GL offset account for the reimbursement. See id on GET accounting/accounts/:id. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the expense report last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].expenseItems` | array of object | no | The expense items on the report, in their order on it. An empty array when the report has none. | | `items[].expenseItems[].id` | string | yes | The unique identifier for the expense item. | | `items[].expenseItems[].itemNumber` | string | no | The expense item number. | | `items[].expenseItems[].merchant` | string | no | The merchant the expense was incurred with. | | `items[].expenseItems[].billDate` | string (date) | no | The date the expense was incurred. Format YYYY-MM-DD. | | `items[].expenseItems[].expenseType` | string | no | The kind of expense. Values include 'Pur' (purchase), 'Dis' (distance traveled) and 'PerDiem'. | | `items[].expenseItems[].description` | string | no | A description of the expense item. | | `items[].expenseItems[].amount` | string (decimal) | no | The amount of the expense item, as a decimal in the item's own currency. | | `items[].expenseItems[].currency` | string | no | The currency this item's amount is in. Format ISO 4217, for example 'USD'. May differ from the report's reimbursementCurrency. | | `items[].expenseItems[].reimbursable` | boolean | no | Whether this item is reimbursable to the employee. | | `items[].expenseItems[].categoryId` | string | no | The expense category the item is booked to. See id on GET accounting/expense-categories/:id. | | `items[].expenseItems[].externalExpenseItemId` | string | no | The identifier this item has in the upstream system that produced it. | | `items[].expenseItems[].attendees` | string | no | Who attended, when the item's category requires it. | | `items[].expenseItems[].attachments` | array of object | no | The receipts on this item. | | `items[].expenseItems[].attachments[].id` | string | yes | The unique identifier for the attachment record. | | `items[].expenseItems[].attachments[].fileName` | string | no | The display name of the attached file. | | `items[].expenseItems[].attachments[].fileId` | string | no | The identifier of the stored file. There is no endpoint for it. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more reports remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Fiscal Calendars `GET /accounting/fiscal-calendars` Group: Accounting › Fiscal Calendars · Operation ID: `fiscalCalendars` Returns a list of fiscal calendars. To read one with its fiscal years and assigned entities, use GET accounting/fiscal-calendars/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=fiscalCalendars) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `periodFormat` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `fiscalYearsCount` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `anchorDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, calendarName, periodFormat, status, fiscalYearsCount, anchorDate, lastModifiedDate - Selectable: id, calendarName, description, periodFormat, status, fiscalYearsCount, anchorDate, lastModifiedDate ## Response `200` Success Under `data`, an array of fiscalCalendarsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the fiscal calendar. | | `calendarName` | string | no | The name of the fiscal calendar, unique within the tenant. | | `description` | string or null | no | A description of the fiscal calendar. Absent when none has been set. | | `periodFormat` | string | no | How the fiscal calendar divides a fiscal year into periods. Possible values: 'calendarMonths' (twelve monthly periods), '4-4-5', '4-5-4', '5-4-4' (retail quarters of thirteen weeks, split into periods of that many weeks). | | `status` | string or null | no | The lifecycle status of the fiscal calendar. Possible values: 'new', 'active', 'deactivated'. Absent when none has been set. | | `fiscalYearsCount` | number or null | no | The number of distinct fiscal years on this calendar. Absent until the first fiscal year exists. | | `anchorDate` | string (date) or null | no | The anchor reference date, set only for retail calendars (a '4-4-5', '4-5-4' or '5-4-4' periodFormat). It is the last day of the month before the first period starts. Format YYYY-MM-DD. Absent on a calendarMonths calendar. | | `lastModifiedDate` | string (date-time) | no | The date and time the fiscal calendar last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Get Fiscal Calendar `GET /accounting/fiscal-calendars/{id}` Group: Accounting › Fiscal Calendars · Operation ID: `getFiscalCalendar` Returns the fiscal calendar with the given identifier, including the fiscal years it spans and the legal entities it is assigned to. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getFiscalCalendar) ## 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`. | | `id` | path | string | yes | The unique identifier for the fiscal calendar to return. | ## Response `200` Success Under `data` (getFiscalCalendarOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the fiscal calendar. | | `calendarName` | string | yes | The name of the fiscal calendar, unique within the tenant. | | `description` | string | no | A description of the fiscal calendar. Absent when none has been set. | | `periodFormat` | string | yes | How the fiscal calendar divides a fiscal year into periods. Possible values: 'calendarMonths' (twelve monthly periods), '4-4-5', '4-5-4', '5-4-4' (retail quarters of thirteen weeks, split into periods of that many weeks). | | `status` | string | no | The lifecycle status of the fiscal calendar. Possible values: 'new', 'active', 'deactivated'. Absent when none has been set. | | `fiscalYearsCount` | number | no | The number of distinct fiscal years on this calendar. Absent until the first fiscal year exists. | | `fiscalYears` | array of string | no | The distinct fiscal years this calendar spans, taken from its accounting periods, in no guaranteed order. Each is a free-text label such as 'FY2024'. An empty array when the calendar has no fiscal years yet. | | `assignedEntities` | array of string | no | The names, not the identifiers, of the legal entities this calendar is assigned to, in no guaranteed order. An empty array when it is assigned to none. | | `anchorDate` | string (date) | no | The anchor reference date, set only for retail calendars (a '4-4-5', '4-5-4' or '5-4-4' periodFormat). It is the last day of the month before the first period starts. Format YYYY-MM-DD. Absent on a calendarMonths calendar. | | `lastModifiedDate` | string (date-time) | yes | The date and time the fiscal calendar last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Invoice Frequency Templates `GET /accounting/invoice-frequency-templates` Group: Accounting › Invoice Frequency Templates · Operation ID: `invoiceFrequencyTemplates` List invoice frequency templates with structured filters. GET data-set exposing filter[field][op]= (eq/ne/gt/gte/lt/lte/in/notIn/between/like), sort=-name,lastModifiedDate, sparse fields=, and page[size]/page[cursor] cursor pagination. Returns the catalogue of reusable billing schedules, each an RRULE recurrence rule a sales order product line can bill on instead of a fixed invoice frequency. Every request must carry the Api-Version header: a missing or empty header is rejected with 400, an identifier naming no version of the company with 400, and a route that exists but not in the requested version with 404. [Open in the interactive reference](https://docs.api.lensing.ai/?op=invoiceFrequencyTemplates) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `name` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isEvenInvoicing` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, name, isEvenInvoicing, lastModifiedDate - Selectable: id, name, rule, isEvenInvoicing, lastModifiedDate ## Response `200` Success Under `data`, an array of invoiceFrequencyTemplatesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | Stable, platform-assigned id (a UUID) of the invoice frequency template — the public identifier other accounting contracts reference a template by. | | `name` | string | no | Human-readable name of the template, e.g. 'Month end' or 'Twice monthly'. This is what a user picks from the invoice frequency dropdown on a sales order product line. Filter target: filter[name][eq]=Month end, or filter[name][in]=Month end,Twice monthly. Filterable because it is the node's only human-facing key — there is no machine code to filter instead, and no Search sibling to fall back on (R2.3.4c). | | `rule` | string | no | Recurrence rule of the template as an RFC 5545 RRULE string, e.g. 'FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=15,-1' for the 15th and the last day of every month. BYMONTHDAY=-1 denotes the last day of the month whatever its length, so it resolves to the 28th, 29th, 30th or 31st as applicable. Returned only: in neither filterableFields nor orderableFields, because it is a structured expression rather than a comparable scalar (R7.2) — filtering on what a rule MEANS needs the Search target. | | `isEvenInvoicing` | boolean or null | no | Whether the period amount is split evenly across the period's invoices (true) rather than prorated by day count (false). On a twice-monthly template billing 2000 per month, true invoices 1000 and 1000 regardless of how many days each half holds, while false apportions by days. Applies to recurring product lines; a usage line's amount is metered, so the flag has no effect there. Filter target: filter[isEvenInvoicing][eq]=true. | | `lastModifiedDate` | string (date-time) | no | Instant the invoice frequency template last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Filter and sort on this to sync incrementally: filter[lastModifiedDate][gte]=2026-08-01T00:00:00.000Z with sort=lastModifiedDate. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Invoice Runs `GET /accounting/invoice-runs` Group: Accounting › Invoice Runs · Operation ID: `invoiceRuns` Returns a list of invoice runs. 'status' and 'scope' differ in type from GET accounting/invoice-runs/:id, which the endpoint description explains. Lists invoice runs: what each was scoped to, when it ran and how it came out. Use it to audit billing activity or to find a run whose id you lost. Read a single run with GET accounting/invoice-runs/:id, which has the invoices and errors this list cannot. Two fields differ from what GET accounting/invoice-runs/:id publishes. 'status' here is the stored value, not the corrected one, so a run whose worker was killed or lost reads 'running' without end, where the single-run endpoint reports 'error' instead. Treat any row that has been 'running' far longer than a run should take as unsettled, and read it through the single-run endpoint. 'scope' here is the raw stored text, the billed line types joined by semicolons into one string such as 'all', where the single-run endpoint publishes an array. There is no 'customerId'. To find a customer's billing history, list invoices by customer instead. [Open in the interactive reference](https://docs.api.lensing.ai/?op=invoiceRuns) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `invoiceRunNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `trigger` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `salesOrderId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `startedAt` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `finishedAt` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: invoiceRunNumber, startedAt, finishedAt, lastModifiedDate - Selectable: id, invoiceRunNumber, status, invoicesCreated, trigger, scope, entityId, salesOrderId, startedAt, finishedAt, lastModifiedDate ## Response `200` Success Under `data`, an array of invoiceRunsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the invoice run. | | `invoiceRunNumber` | string or null | no | The invoice run number, for example 'ISR-1042'. | | `status` | string or null | no | The execution status of the run, as stored rather than corrected, so a run whose worker was killed or lost reads 'running' without end. GET accounting/invoice-runs/:id reports 'error' for such a run instead. Possible values: 'created', 'running', 'completed', 'error', 'completeAndError'. | | `invoicesCreated` | number or null | no | The number of invoices the run created. | | `trigger` | string or null | no | What started the run. Possible values: 'immediately', 'daily', 'monthly'. | | `scope` | string or null | no | The line types billed, as the raw stored text: the billed line types joined by semicolons into one string, such as 'all'. GET accounting/invoice-runs/:id publishes this as an array instead. | | `entityId` | string or null | no | The legal entity billed. See id on GET base/entities. | | `salesOrderId` | string or null | no | The sales order billed. Absent on a customer- or entity-scoped run. See id on GET accounting/sales-orders/:id. | | `startedAt` | string (date-time) or null | no | The date and time the run was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `finishedAt` | string (date-time) or null | no | The date and time the run finished, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Absent while the run is still working, and on a run whose worker stopped without recording one. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the invoice run record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Starts an invoice run for a sales order, customer or legal entity. `POST /accounting/invoice-runs` Group: Accounting › Invoice Runs · Operation ID: `startInvoiceRun` Starts an invoice run and returns its id. A run is the audited way to create invoices: it records how many it created, links each one back to itself, and records a row for every line that failed. Read it back with GET accounting/invoice-runs/:id. A run bills at one level. Name exactly one of 'salesOrderId', 'customerId' or 'entityId', widest last: 'entityId' covers every approved order in the entity, which is what the scheduled nightly run bills. The three nest, since an order belongs to one customer and a customer to one entity, so naming two adds nothing the narrower one did not already say and is refused with MULTIPLE_TARGETS_GIVEN. The two date inputs are not interchangeable. 'billingDates' names the exact planned dates to bill and belongs to a sales-order run. Omit it to bill every pending line on the order. 'billUpTo' is an upper bound, billing everything planned on or before it, and is required on a customer or entity run. Sending the wrong one for the scope is refused rather than reinterpreted. 'invoiceDate' is the date stamped on the invoices created, defaulting to today. It must fall in an open accounting period or the call is refused with ACCOUNTING_PERIOD_CLOSED. By default the run is asynchronous: the response has 'status' of 'running' and you poll. Set 'sync' to true to block until it settles and get the final status and count back directly, which suits a single sales order but not a customer-wide run that can take minutes. A run whose lines already failed in an earlier run comes back empty, because fresh runs hold those lines back. When that happens the response has 'unresolvedRunId': call POST accounting/invoice-runs/:id/rerun on that run instead of repeating this call. A retry while the run is still working is safe. While an asynchronous run is in flight, another call naming the same target, scope, dates and invoice date joins it rather than billing the same lines twice, and the response has 'deduplicated' set to true alongside that run's id. So a timed-out call can simply be retried. The guard lasts only as long as the run does: once a run has finished, asking to bill the same scope again starts a new run. It does not apply to a 'sync' call, so two concurrent identical 'sync' calls can each bill. Externalized sales orders are refused with SALES_ORDER_EXTERNALISED: their invoices include tax calculated by the source system, so they are imported through POST accounting/invoices/import rather than created by a run. [Open in the interactive reference](https://docs.api.lensing.ai/?op=startInvoiceRun) ## 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`: startInvoiceRunInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `salesOrderId` | string | no | The sales order to bill. Give exactly one of 'salesOrderId', 'customerId' or 'entityId'. Alone, it bills every pending line on the order. See id on GET accounting/sales-orders/:id. | | `customerId` | string | no | The customer to bill, across all their sales orders. Give exactly one of the three targets. Requires 'billUpTo'. See id on GET accounting/customers/:id. | | `entityId` | string | no | The legal entity to bill, across every approved order in it. This is the scope the scheduled nightly run uses, and the broadest this API offers. Give exactly one of the three targets. Requires 'billUpTo'. See id on GET base/entities. | | `billingDates` | array of string (date) | no | Sales-order runs only. The exact planned dates to bill. Use format YYYY-MM-DD. Bills only instalments that fall on them. Omit to bill every pending line on the order. Rejected on a customer or entity run, which bounds by date instead. | | `billUpTo` | string (date) | no | Customer and entity runs only. Bills everything planned on or before this date, an upper bound rather than an exact match. Use format YYYY-MM-DD. Required on both. Rejected on a sales-order run, which names exact dates instead. | | `invoiceDate` | string (date) | no | The date stamped on the invoices created. Use format YYYY-MM-DD. Defaults to today. Must fall in an open accounting period. | | `scope` | array of string, one of `"all"`, `"oneTimeAndRecurring"`, `"usageInArrears"`, `"billableProducts"` | no | The line types to bill. Defaults to all line types. 'oneTimeAndRecurring' covers one-time, recurring and commit lines. 'billableProducts' covers billable time, travel and expense, and pass-through. | | `sync` | boolean | no | Whether to block until the run has settled and report its final status and count. Defaults to false, which returns immediately with 'status' of 'running' for you to poll. Use it for one sales order, not for a customer-wide run. | Example: ```json { "salesOrderId": "8f14e45f-ceea-467a-9f6b-2c1d0e5a7b39", "customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "billingDates": [ "2026-01-15" ], "billUpTo": "2026-01-15", "invoiceDate": "2026-09-30", "scope": [ "all" ], "sync": true } ``` ## Response `200` Success Under `data` (startInvoiceRunOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The invoice run that was started. | | `data.id` | string | yes | The unique identifier for the created invoice run. | | `data.invoiceRunNumber` | string | yes | The invoice run number, for example 'ISR-1042'. | | `data.status` | string | yes | The execution status of the run. On an async call it is 'created' or 'running'. With 'sync' set to true it is settled: 'completed', 'error', or 'completeAndError' when some invoices were created and some lines failed. | | `data.invoicesCreated` | number | yes | The number of invoices the run has created so far. Meaningful once the run has settled. 0 on an async call that has not finished. | | `data.unresolvedRunId` | string | no | The unique identifier for an earlier failed run, present only when this run could produce nothing because that run's failed lines are held back. Rerun that one rather than repeating this call. | | `data.deduplicated` | boolean | no | True when an identical run was already in flight and this call joined it instead of starting a second. The id above is that run's. Absent when this call started the run. | ## 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 | | --- | --- | | `NO_TARGET_GIVEN` | 400 | | `MULTIPLE_TARGETS_GIVEN` | 400 | | `SALES_ORDER_NOT_FOUND` | 400 | | `CUSTOMER_NOT_FOUND` | 400 | | `ENTITY_NOT_FOUND` | 400 | | `SALES_ORDER_EXTERNALISED` | 400 | | `SALES_ORDER_NOT_APPROVED` | 400 | | `BILL_UP_TO_NOT_APPLICABLE` | 400 | | `BILLING_DATES_NOT_APPLICABLE` | 400 | | `BILL_UP_TO_REQUIRED` | 400 | | `ACCOUNTING_PERIOD_CLOSED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NO_TARGET_GIVEN, MULTIPLE_TARGETS_GIVEN, SALES_ORDER_NOT_FOUND, CUSTOMER_NOT_FOUND, ENTITY_NOT_FOUND, SALES_ORDER_EXTERNALISED, SALES_ORDER_NOT_APPROVED, BILL_UP_TO_NOT_APPLICABLE, BILLING_DATES_NOT_APPLICABLE, BILL_UP_TO_REQUIRED, ACCOUNTING_PERIOD_CLOSED - `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 --- # Returns one invoice run and its outcome. `GET /accounting/invoice-runs/{id}` Group: Accounting › Invoice Runs · Operation ID: `getInvoiceRun` Reads one invoice run: what it was scoped to, where it got to, the invoices it created and the lines that failed. This is the poll half of POST accounting/invoice-runs. Branch on 'status'. 'created' and 'running' mean keep polling. 'completed' means every selected line was billed. 'error' means nothing was billed and 'errors' says why. 'completeAndError' means some invoices were created and some lines failed, so check 'status' rather than assuming success once 'invoicesCreated' is non-zero. 'status' is corrected against the worker running the job. The stored status settles only when the run records a finish time, which a killed, canceled or lost worker never does, so such a run would otherwise read 'running' without end. Where that has happened this endpoint reports 'error', so a polling loop always ends. No total amount is published for the run. Each entry in 'invoices' has its own 'totalAmount' and 'currency' instead, because a run can create invoices in several currencies and one summed figure would add unlike units. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getInvoiceRun) ## 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`. | | `id` | path | string | yes | The unique identifier for the invoice run to return. | ## Response `200` Success Under `data` (getInvoiceRunOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The requested invoice run. | | `data.id` | string | yes | The unique identifier for the invoice run. | | `data.invoiceRunNumber` | string | no | The invoice run number, for example 'ISR-1042'. Absent while none has been assigned. | | `data.status` | string | yes | The execution status of the run. Possible values: 'created', 'running', 'completed', 'error', 'completeAndError'. Keep polling on 'created' and 'running'. 'completeAndError' means some invoices were created and some lines failed. | | `data.invoicesCreated` | number | yes | The number of invoices the run has created. | | `data.startedAt` | string | no | The date and time the run was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `data.finishedAt` | string | no | The date and time the run finished, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Absent while the run is still working, and on a run whose worker stopped without recording one. | | `data.lastModifiedDate` | string | no | The date and time the invoice run record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `data.trigger` | string | no | What started the run. Possible values: 'immediately', 'daily', 'monthly'. 'immediately' is an ad-hoc run. 'daily' and 'monthly' are scheduled. | | `data.scope` | array of string, one of `"all"`, `"oneTimeAndRecurring"`, `"usageInArrears"`, `"billableProducts"` | yes | The line types the run billed. | | `data.entityId` | string | no | The legal entity billed. See id on GET base/entities. | | `data.salesOrderId` | string | no | The sales order billed. Present on a sales-order-scoped run. See id on GET accounting/sales-orders/:id. | | `data.customerId` | string | no | The customer billed. Absent on a run that created no invoice and recorded no error. See id on GET accounting/customers/:id. | | `data.progress` | object | no | The run's own progress reporting while it works. It is advisory: absent before the run starts reporting, and never a substitute for 'status'. | | `data.progress.title` | string | no | The phase the run is in, for example preparing data or creating invoices. | | `data.progress.message` | string | no | Further detail on the current phase, when the run supplies any. | | `data.progress.progress` | number | no | The number of items processed so far in the current phase. | | `data.progress.total` | number | no | The number of items to process in the current phase. | | `data.invoices` | array of object | yes | The invoices the run created. Each has its own currency, so there is no summed run total. | | `data.invoices[].id` | string | yes | The unique identifier for the invoice. See id on GET accounting/invoices/:id. | | `data.invoices[].invoiceNumber` | string | no | The invoice number, for example 'INV-20841'. | | `data.invoices[].totalAmount` | string (decimal) | no | The total amount of the invoice including tax, in the currency named by 'currency'. | | `data.invoices[].currency` | string | no | The currency of the invoice. Format ISO 4217, for example 'EUR'. | | `data.errors` | array of object | yes | The lines the run could not bill, one entry each. A non-empty array with a non-zero 'invoicesCreated' is the 'completeAndError' outcome. | | `data.errors[].type` | string | yes | Why the line could not be billed. Possible values: 'system', 'avalaraAddress', 'taxCode', 'none', 'taxCalculation', 'vatEntity', 'accountConfig', 'vatCustomer', 'invoiceSetupIncomplete', 'invoiceDelivery'. | | `data.errors[].detail` | string | no | A description of why the line failed. | | `data.errors[].customerId` | string | no | The customer the failing line belongs to. See id on GET accounting/customers/:id. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Retries the failed lines of an invoice run. `POST /accounting/invoice-runs/{id}/rerun` Group: Accounting › Invoice Runs · Operation ID: `rerunInvoiceRun` Retries the lines that failed in one invoice run, named by the ':id' path segment. This is not the same as starting a fresh run over the same selection, and a fresh run is not a substitute. A new run excludes any line that already failed, so repeating POST accounting/invoice-runs after a failure bills nothing and leaves the failures in place. Rerun is the only endpoint that picks them up: it selects the lines whose failures belong to this run, discards their old error records and bills them again. The retry reuses the same run rather than creating another, so the run's history stays in one place and the response reports that run's new status. A rerun that succeeds moves it to 'completed'. One that fails again leaves it at 'error' or 'completeAndError' with fresh error records, and can be rerun once more after the underlying cause, such as a missing tax code or an incomplete customer setup, is fixed. Only a run that failed can be rerun. Anything else is refused with RUN_NOT_FAILED. The retry runs inline: this call returns once the run has settled, so no polling is needed afterward. [Open in the interactive reference](https://docs.api.lensing.ai/?op=rerunInvoiceRun) ## 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. | | `id` | path | string | yes | The unique identifier for the invoice run to retry. | ## Response `200` Success Under `data` (rerunInvoiceRunOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the invoice run retried. It is the same run, not a new one. | | `status` | string | yes | The status the run settled at after the retry. Possible values: 'completed', 'error', 'completeAndError'. 'error' or 'completeAndError' means the retry hit the same problem, and the run can be rerun again once the cause is fixed. | | `invoicesCreated` | number | yes | The total number of invoices the run has created, including those from before the retry. | ## 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 | | --- | --- | | `RUN_NOT_FOUND` | 400 | | `RUN_NOT_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: RUN_NOT_FOUND, RUN_NOT_FAILED - `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 --- # Invoices `GET /accounting/invoices` Group: Accounting › Invoices · Operation ID: `invoices` Returns a list of invoices. To filter by number, customer, address, email, document number or message, use POST accounting/invoices/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=invoices) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `invoiceDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `dueDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastPaidDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `invoiceStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `approvalStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `deliveryStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `disputeStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `dataSource` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentTermDays` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `exchangeRate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `excludeFromTaxEngine` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `customerId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `salesOrderId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `subtotal` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `totalTax` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `totalDutiesAndCustoms` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `totalAmount` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `totalAmountDue` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `totalCredited` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `totalLateFeeAmount` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lineDiscounts` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `oneTimeDiscount` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentsApplied` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `adjustments` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, invoiceDate, dueDate, lastPaidDate, invoiceStatus, approvalStatus, deliveryStatus, disputeStatus, dataSource, currency, customerId, entityId, salesOrderId, paymentTermDays, exchangeRate, excludeFromTaxEngine, subtotal, totalTax, totalDutiesAndCustoms, totalAmount, totalAmountDue, totalCredited, totalLateFeeAmount, lineDiscounts, oneTimeDiscount, paymentsApplied, adjustments, lastModifiedDate - Selectable: id, invoiceNumber, invoiceDate, dueDate, lastPaidDate, invoiceStatus, approvalStatus, deliveryStatus, disputeStatus, dataSource, currency, customerMessage, externalInvoiceId, invoiceEmail, purchaseOrderOverride, paymentTermDays, exchangeRate, excludeFromTaxEngine, customerId, customerName, customerNumber, entityId, entityName, salesOrderId, salesOrderNumber, subtotal, totalTax, totalDutiesAndCustoms, totalAmount, totalAmountDue, totalCredited, totalLateFeeAmount, lineDiscounts, oneTimeDiscount, paymentsApplied, adjustments, lastModifiedDate ## Response `200` Success Under `data`, an array of invoicesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the invoice. | | `invoiceNumber` | string or null | no | The invoice number, for example 'INV-1', unique within the tenant. Absent while none has been assigned. | | `invoiceDate` | string (date) | no | The date the invoice was issued. Format YYYY-MM-DD. Payment terms and the general ledger impact are both based on this date. | | `dueDate` | string (date) or null | no | The date payment falls due, the invoice date plus the payment term. Format YYYY-MM-DD. Absent when no term has been resolved. | | `lastPaidDate` | string (date) or null | no | The date the most recent payment was applied. Format YYYY-MM-DD. Absent on an invoice that has never been paid against. | | `invoiceStatus` | string or null | no | The settlement status of the invoice. Possible values: 'unpaid', 'partiallyPaid', 'paidInFull', 'creditedInFull', 'isOverdue', 'disputed'. | | `approvalStatus` | string or null | no | The finance approval status of the invoice. Possible values: 'draft', 'pendingApproval', 'approved', 'revise', 'rejected'. | | `deliveryStatus` | string or null | no | How far the invoice has got in being sent to the customer. Possible values: 'created', 'toBeSent', 'delivered', 'read', 'failed'. | | `disputeStatus` | string or null | no | The dispute status of the invoice, set only when payments against it are under dispute. Possible values: 'disputed', 'chargedBack'. | | `dataSource` | string or null | no | The system the invoice record was imported from, for invoices that arrived through an integration or migration rather than being created in Everest. For example 'NetSuite', 'Quickbooks', 'Stripe' or 'MigrationFile'. Absent on invoices created in Everest. | | `currency` | string | no | The currency of the invoice. Format ISO 4217, for example 'USD'. It applies to every amount on the record, on the header and on every line. | | `customerMessage` | string or null | no | A free-form message printed on the invoice for the customer. | | `externalInvoiceId` | string or null | no | The invoice's identifier in an external source system, kept for reconciliation. For identifiers owned by your other systems, prefer externalIdentifiers below. | | `invoiceEmail` | string or null | no | The email address of the business contact that receives this invoice. | | `purchaseOrderOverride` | string or null | no | A manually entered PO number that overrides the sales-order-derived values in purchaseOrders. | | `paymentTermDays` | number or null | no | The number of days after the invoice date that payment falls due, resolved from the invoice's payment term. The term record itself is not published, only its day count. | | `exchangeRate` | string (decimal) or null | no | The rate used to convert the invoice's currency to the entity's functional currency. Absent when no conversion applies. | | `excludeFromTaxEngine` | boolean or null | no | Whether the invoice is left out of the tax engine. Derived from the sales order. | | `customerId` | string or null | no | The customer the invoice is issued to. See id on GET accounting/customers/:id. | | `customerName` | string or null | no | The display name of the customer, as it appears on the document. | | `customerNumber` | string or null | no | The customer number of the customer, unique within the tenant. | | `entityId` | string or null | no | The legal entity issuing the invoice. See id on GET base/entities. | | `entityName` | string or null | no | The name of the issuing legal entity. | | `salesOrderId` | string or null | no | The sales order the invoice was billed from. Absent on invoices created directly. See id on GET accounting/sales-orders/:id. | | `salesOrderNumber` | string or null | no | The number of the source sales order. Present whenever salesOrderId is. | | `subtotal` | string (decimal) or null | no | The amount before taxes and discounts, in the invoice currency. Absent when no subtotal has been computed. | | `totalTax` | string (decimal) or null | no | The total tax across all lines, in the invoice currency. | | `totalDutiesAndCustoms` | string (decimal) or null | no | The total duties and customs across all lines, in the invoice currency. | | `totalAmount` | string (decimal) or null | no | The total invoiced, including taxes and duties, in the invoice currency. | | `totalAmountDue` | string (decimal) or null | no | The amount that was due when the invoice was created, in the invoice currency. | | `totalCredited` | string (decimal) or null | no | The total credited back against this invoice by credit memos, in the invoice currency. | | `totalLateFeeAmount` | string (decimal) or null | no | The total late fees charged on this invoice, in the invoice currency. | | `lineDiscounts` | string (decimal) or null | no | The sum of discount amounts across all invoice lines, in the invoice currency. | | `oneTimeDiscount` | string (decimal) or null | no | The one-time discount applied to the invoice as a whole, in the invoice currency. | | `paymentsApplied` | string (decimal) or null | no | The amount settled by payments so far, in the invoice currency. | | `adjustments` | string (decimal) or null | no | The sum of small-balance write-offs and early-payment discounts recorded against this invoice, in the invoice currency. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the invoice record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Get Invoice `GET /accounting/invoices/{id}` Group: Accounting › Invoices · Operation ID: `getInvoice` Returns the invoice with the given identifier. Use the invoice's id, not its invoiceNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getInvoice) ## 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`. | | `id` | path | string | yes | The unique identifier for the invoice to return. | ## Response `200` Success Under `data` (getInvoiceOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the invoice. | | `invoiceNumber` | string | no | The invoice number, for example 'INV-1', unique within the tenant. Absent while none has been assigned. | | `invoiceDate` | string (date) | yes | The date the invoice was issued. Format YYYY-MM-DD. Payment terms and the general ledger impact are both based on this date. | | `dueDate` | string (date) | no | The date payment falls due, the invoice date plus the payment term. Format YYYY-MM-DD. Absent when no term has been resolved. | | `lastPaidDate` | string (date) | no | The date the most recent payment was applied. Format YYYY-MM-DD. Absent on an invoice that has never been paid against. | | `invoiceStatus` | string | no | The settlement status of the invoice. Possible values: 'unpaid', 'partiallyPaid', 'paidInFull', 'creditedInFull', 'isOverdue', 'disputed'. | | `approvalStatus` | string | no | The finance approval status of the invoice. Possible values: 'draft', 'pendingApproval', 'approved', 'revise', 'rejected'. | | `deliveryStatus` | string | no | How far the invoice has got in being sent to the customer. Possible values: 'created', 'toBeSent', 'delivered', 'read', 'failed'. | | `disputeStatus` | string | no | The dispute status of the invoice, set only when payments against it are under dispute. Possible values: 'disputed', 'chargedBack'. | | `dataSource` | string | no | The system the invoice record was imported from, for invoices that arrived through an integration or migration rather than being created in Everest. For example 'NetSuite', 'Quickbooks', 'Stripe' or 'MigrationFile'. Absent on invoices created in Everest. | | `currency` | string | yes | The currency of the invoice. Format ISO 4217, for example 'USD'. It applies to every amount on the record, on the header and on every line. | | `customerMessage` | string | no | A free-form message printed on the invoice for the customer. | | `externalInvoiceId` | string | no | The invoice's identifier in an external source system, kept for reconciliation. For identifiers owned by your other systems, prefer externalIdentifiers below. | | `customerId` | string | yes | The customer the invoice is issued to. See id on GET accounting/customers/:id. | | `customerName` | string | no | The display name of the customer, as it appears on the document. | | `customerNumber` | string | no | The customer number of the customer, unique within the tenant. | | `entityId` | string | yes | The legal entity issuing the invoice. See id on GET base/entities. | | `entityName` | string | no | The name of the issuing legal entity. | | `salesOrderId` | string | no | The sales order the invoice was billed from. Absent on invoices created directly. See id on GET accounting/sales-orders/:id. | | `salesOrderNumber` | string | no | The number of the source sales order. Present whenever salesOrderId is. | | `billToAddress` | object | no | The bill-to address the invoice is sent to. Absent when the invoice references no bill-to record. | | `billToAddress.addressId` | string | no | The unique identifier for the address record. Absent when none is stored. | | `billToAddress.line1` | string | yes | The first address line: street number and name, or PO box. | | `billToAddress.line2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `billToAddress.city` | string | yes | The city or locality. | | `billToAddress.stateProvince` | string | no | The state, province or region. | | `billToAddress.country` | string | yes | The country, as a name or ISO 3166-1 code. | | `billToAddress.zipCode` | string | yes | The postal or ZIP code. | | `soldToAddress` | object | no | The sold-to address the product or service was delivered to, used for tax determination. Absent when the invoice references no sold-to record. | | `soldToAddress.addressId` | string | no | The unique identifier for the address record. Absent when none is stored. | | `soldToAddress.line1` | string | yes | The first address line: street number and name, or PO box. | | `soldToAddress.line2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `soldToAddress.city` | string | yes | The city or locality. | | `soldToAddress.stateProvince` | string | no | The state, province or region. | | `soldToAddress.country` | string | yes | The country, as a name or ISO 3166-1 code. | | `soldToAddress.zipCode` | string | yes | The postal or ZIP code. | | `invoiceEmail` | string | no | The email address of the business contact that receives this invoice. | | `purchaseOrders` | array of string | no | The purchase order references the customer supplied, derived from the sales order. These are the customer's own document numbers. An empty array when none were supplied. | | `purchaseOrderOverride` | string | no | A manually entered PO number that overrides the sales-order-derived values in purchaseOrders. | | `paymentTermDays` | number | no | The number of days after the invoice date that payment falls due, resolved from the invoice's payment term. The term record itself is not published, only its day count. | | `exchangeRate` | string (decimal) | no | The rate used to convert the invoice's currency to the entity's functional currency. Absent when no conversion applies. | | `excludeFromTaxEngine` | boolean | no | Whether the invoice is left out of the tax engine. Derived from the sales order. | | `subtotal` | string (decimal) | no | The amount before taxes and discounts, in the invoice currency. Absent when no subtotal has been computed. | | `totalTax` | string (decimal) | no | The total tax across all lines, in the invoice currency. | | `totalDutiesAndCustoms` | string (decimal) | no | The total duties and customs across all lines, in the invoice currency. | | `totalAmount` | string (decimal) | no | The total invoiced, including taxes and duties, in the invoice currency. | | `totalAmountDue` | string (decimal) | no | The amount that was due when the invoice was created, in the invoice currency. | | `totalCredited` | string (decimal) | no | The total credited back against this invoice by credit memos, in the invoice currency. | | `totalLateFeeAmount` | string (decimal) | no | The total late fees charged on this invoice, in the invoice currency. | | `lineDiscounts` | string (decimal) | no | The sum of discount amounts across all invoice lines, in the invoice currency. | | `oneTimeDiscount` | string (decimal) | no | The one-time discount applied to the invoice as a whole, in the invoice currency. | | `paymentsApplied` | string (decimal) | no | The amount settled by payments so far, in the invoice currency. | | `adjustments` | string (decimal) | no | The sum of small-balance write-offs and early-payment discounts recorded against this invoice, in the invoice currency. | | `lastModifiedDate` | string (date-time) | yes | The date and time the invoice record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | no | The lines of the invoice, each billing one product or free-text item. An empty array when the invoice has no lines. All line amounts are in the invoice currency. | | `lines[].id` | string | yes | The unique identifier for the invoice line. | | `lines[].invoiceLineNumber` | string | no | The sequential line number within the invoice. | | `lines[].description` | string | no | The line description, as shown to the customer. | | `lines[].lineType` | string | no | The kind of charge the line is. Possible values: 'recurring', 'oneTime', 'oneTimeDiscount', 'usageOverage', 'overage', 'payAsYouGo', 'commit', 'lateFee', 'creditCardProcessingFee', 'billableTime', 'billablePassThrough', 'billableTravelExpense'. | | `lines[].productId` | string | no | The billed product. Set only on lines created from a product reference, absent on free-text lines. See id on GET accounting/products/:id. | | `lines[].productCode` | string | no | The product code as it stood when the line was created. A snapshot, so it does not follow later changes to the product. | | `lines[].productDescription` | string | no | The product description as it stood when the line was created. A snapshot, as with productCode. | | `lines[].quantity` | string (decimal) | no | The quantity billed. A count, not an amount, so it has no currency. | | `lines[].unitOfMeasure` | string | no | The unit the quantity is measured in, for example 'each', 'hour' or 'gigabyte'. | | `lines[].unitPrice` | string (decimal) | no | The price per unit actually charged, in the invoice currency. | | `lines[].listPrice` | string (decimal) | no | The undiscounted list price per unit, in the invoice currency. | | `lines[].totalPrice` | string (decimal) | no | The line total excluding tax and duties, in the invoice currency. | | `lines[].discountAmount` | string (decimal) | no | The discount granted on the line, in the invoice currency. | | `lines[].discountPercentage` | string (decimal) | no | The discount granted on the line as a percentage. A rate, not an amount, so it has no currency. | | `lines[].taxAmount` | string (decimal) | no | The tax on the line, in the invoice currency. | | `lines[].dutiesAndCustomsAmount` | string (decimal) | no | The duties and customs charged on the line, in the invoice currency. | | `lines[].amount` | string (decimal) | no | The total billed for the line, including tax and duties, in the invoice currency. | | `lines[].totalAmountDue` | string (decimal) | no | The amount of the line still due, in the invoice currency. | | `lines[].taxCode` | string | no | The tax code applied to the line. | | `lines[].taxDate` | string (date) | no | The date the line's tax was determined on. Format YYYY-MM-DD. | | `lines[].serviceStartDate` | string (date) | no | The first day of the service period the line covers. Format YYYY-MM-DD. | | `lines[].serviceEndDate` | string (date) | no | The last day of the service period the line covers, inclusive. Format YYYY-MM-DD. | | `externalIdentifiers` | array of object | no | External identifiers owned by your other systems. At most one entry per fieldName. An empty array when the invoice has none. | | `externalIdentifiers[].fieldName` | string | yes | The configured key of the external identifier, a stable camelCase slug such as 'netsuiteInvoiceId', not the human-readable label. It must match an active external-identifier configuration for invoices. | | `externalIdentifiers[].value` | string | yes | The identifier as the source system stores it. | | `externalIdentifiers[].externalUrl` | string | no | An absolute http or https link to the object in the originating system. Absent when none is stored. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Invoice `PATCH /accounting/invoices/{id}` Group: Accounting › Invoices · Operation ID: `updateInvoice` Updates the invoice with the given identifier. Only the fields in the request body change. Use the invoice's id, not its invoiceNumber. Edits an existing invoice. The invoice is named by the :id path segment. The response contains that id and nothing else, so read the invoice back with GET accounting/invoices/:id to see the recalculated totals. Four header fields can be edited: invoiceDate, paymentTerm, customerMessage and purchaseOrderOverride, plus two fields on each line, description and dutiesAndCustomsAmount. Everything else is determined by the sales order and the billing run. Amounts, tax, the set of lines, and the delivery status the sending pipeline owns are all read-only here. Addresses and the invoice email cannot be edited. Every field is omit-means-unchanged. Clearing is not uniform. purchaseOrderOverride treats an empty string as clear it and fall back to the sales order's PO references, while paymentTerm, customerMessage and invoiceDate ignore an empty value rather than storing it, so customerMessage cannot be cleared through this endpoint at all. Changing invoiceDate re-derives the due date from the payment term and moves the invoice's tax period, so it is not a cosmetic edit. Lines are addressed by id, inside updateLines. A line is matched only when it belongs to this invoice. A line id from another invoice is refused with LINE_NOT_ON_INVOICE rather than silently ignored, and the same line may not appear twice in one request. Omit updateLines to leave the lines untouched. dutiesAndCustomsAmount requires the entity's Duties and Customs feature to be enabled, and setting it recalculates the invoice's journal entry. The call runs in one transaction. If any part fails, none of the edits are applied. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateInvoice) ## 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. | | `id` | path | string | yes | The unique identifier for the invoice to update. | ## Request body Required, `application/json`: updateInvoiceInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `invoiceDate` | string (date) | no | The date the invoice was issued. Format YYYY-MM-DD. Omit to leave unchanged. Changing it re-derives the due date from the payment term and moves which tax period the invoice falls in. | | `paymentTerm` | string | no | The payment term governing when payment falls due, as the term's configured code. Omit to leave unchanged. An empty string is ignored rather than clearing it. This field is write-only: the read endpoints publish the resolved paymentTermDays instead of the term itself. | | `customerMessage` | string | no | A free-form message printed on the invoice for the customer. Omit to leave unchanged. It cannot be cleared through this endpoint. An empty string is ignored, not stored. | | `purchaseOrderOverride` | string | no | A manually entered PO number that overrides the references derived from the sales order. Omit to leave unchanged. Unlike the other fields here, pass an empty string to clear it and fall back to the sales order's own PO references. | | `updateLines` | array of object | no | The line edits to apply, each addressing one existing line of this invoice by its id. Omit to leave the lines untouched. Lines cannot be added or removed here, because the set of lines comes from the sales order's invoice plan. | | `updateLines[].id` | string | yes | The unique identifier for the invoice line to edit, as returned by GET accounting/invoices/:id. It must be a line of this invoice, and may appear at most once in the request. | | `updateLines[].description` | string | no | The new line description, as shown to the customer. Omit to leave unchanged. | | `updateLines[].dutiesAndCustomsAmount` | string (decimal) | no | The duties and customs charged on this line, in the invoice's currency. Requires the entity's Duties and Customs feature to be enabled, and setting it recalculates the invoice's journal entry. Omit to leave unchanged. | Example: ```json { "invoiceDate": "2026-09-05", "paymentTerm": "", "customerMessage": "Thank you for your business.", "purchaseOrderOverride": "", "updateLines": [ { "id": "5e2a7c81-9d34-4b06-8f19-2c7e4a0d3b58", "description": "Platform subscription — September 2026", "dutiesAndCustomsAmount": "45.00" } ] } ``` ## Response `200` Success Under `data` (updateInvoiceOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the updated invoice. Read the invoice back with GET accounting/invoices/:id to see the recalculated totals. | ## 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 | | --- | --- | | `INVOICE_NOT_FOUND` | 400 | | `LINE_NOT_ON_INVOICE` | 400 | | `DUPLICATE_LINE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: INVOICE_NOT_FOUND, LINE_NOT_ON_INVOICE, DUPLICATE_LINE - `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 --- # Records a payment against an invoice and applies it immediately. `POST /accounting/invoices/{id}/pay` Group: Accounting › Invoices · Operation ID: `payInvoice` Records a payment against one invoice and applies it immediately. The invoice is named by the :id path segment. The response contains the id of the payment created. This is a settlement, not a bookkeeping note. A payment record is created against the invoice's customer and entity, applied to the invoice, and posted to the ledger against the entity's default receivable account. Read the invoice back with GET accounting/invoices/:id to see the reduced totalAmountDue and the increased paymentsApplied, or read the payment with GET accounting/payments/:id. The amount is in the invoice's currency and must be greater than zero. An over-payment is refused with AMOUNT_EXCEEDS_DUE, whose details contain the amount still outstanding. A partial payment is accepted and is the normal case for installments. Call the endpoint again for each one. bankFeeAmount is what the bank or processor deducted. It is recorded on the payment rather than subtracted from it. Send the gross amount the customer paid in amount, and the fee beside it. Send an Idempotency-Key header. A retry carrying the same key and body replays the original response instead of recording a second payment, so a timed-out call is safe to retry. Without the header, a retried call that had in fact succeeded records a second payment. The call runs in one transaction. A failure writes neither the payment nor its application. [Open in the interactive reference](https://docs.api.lensing.ai/?op=payInvoice) ## 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. | | `id` | path | string | yes | The unique identifier for the invoice to settle. | ## Request body Required, `application/json`: payInvoiceInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `amount` | string (decimal) | yes | The amount paid, in the invoice's currency. Must be greater than zero and may not exceed the amount still due. | | `paymentDate` | string (date) | yes | The date the payment was received or settled. Format YYYY-MM-DD. It drives the ledger posting and the reporting period. | | `paymentType` | string, one of `"check"`, `"ach"`, `"wire"`, `"creditCard"`, `"directDebit"`, `"sepaCreditTransfer"` | yes | How the payment was tendered. | | `paymentNote` | string | no | A free-text note recorded on the payment, for example a bank reference or check number. | | `bankFeeAmount` | string (decimal) | no | The fee the bank or processor deducted, in the invoice's currency. Recorded on the payment rather than subtracted from it. Send the gross amount in amount. | Example: ```json { "amount": "1210.00", "paymentDate": "2026-09-15", "paymentType": "wire", "paymentNote": "Remittance ref 88213", "bankFeeAmount": "12.50" } ``` ## Response `200` Success Under `data` (payInvoiceOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `paymentId` | string | yes | The unique identifier for the payment created. Read it back with GET accounting/payments/:id, or re-read the invoice for its updated balance. | ## 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 | | --- | --- | | `INVOICE_NOT_FOUND` | 400 | | `INVALID_AMOUNT` | 400 | | `AMOUNT_EXCEEDS_DUE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: INVOICE_NOT_FOUND, INVALID_AMOUNT, AMOUNT_EXCEEDS_DUE - `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 --- # 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 --- # Returns the invoices that match the given filters. `POST /accounting/invoices/search` Group: Accounting › Invoices · Operation ID: `searchInvoice` [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchInvoice) ## 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`. | ## Request body Required, `application/json`: searchInvoiceInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. An invoice must match every filter you send. Omit to return all invoices. | | `filters.id` | string | no | Limits results to the invoice with this id. | | `filters.invoiceNumber` | string | no | Limits results to the invoice with this invoice number, for example 'INV-1'. The match is exact. | | `filters.customerId` | string | no | Limits results to invoices issued to this customer, given as the customer id. An id that matches no customer matches no invoice. | | `filters.customerName` | string | no | Limits results to invoices whose customer name contains this text. The match is case-insensitive. | | `filters.customerNumber` | string | no | Limits results to invoices issued to the customer with this customer number. The match is exact. | | `filters.entityId` | string | no | Limits results to invoices issued by this legal entity, given as the entity id. An id that matches no entity matches no invoice. | | `filters.entityName` | string | no | Limits results to invoices whose issuing legal entity's name contains this text. The match is case-insensitive. | | `filters.salesOrderId` | string | no | Limits results to invoices billed from this sales order, given as the sales order id. An id that matches no sales order matches no invoice. | | `filters.salesOrderNumber` | string | no | Limits results to invoices billed from the sales order with this number. The match is exact. | | `filters.invoiceStatus` | array of string | no | Limits results to invoices in any of these settlement statuses. Accepted values: 'unpaid', 'partiallyPaid', 'paidInFull', 'creditedInFull', 'isOverdue', 'disputed'. Any other value matches no invoice. An empty array does not constrain. | | `filters.approvalStatus` | array of string | no | Limits results to invoices in any of these approval statuses. Accepted values: 'draft', 'pendingApproval', 'approved', 'revise', 'rejected'. Any other value matches no invoice. An empty array does not constrain. | | `filters.deliveryStatus` | array of string | no | Limits results to invoices in any of these delivery statuses. Accepted values: 'created', 'toBeSent', 'delivered', 'read', 'failed'. Any other value matches no invoice. An empty array does not constrain. | | `filters.disputeStatus` | array of string | no | Limits results to invoices in any of these dispute statuses. Accepted values: 'disputed', 'chargedBack'. Any other value matches no invoice. An empty array does not constrain. | | `filters.customerMessage` | string | no | Limits results to invoices whose customer message contains this text. The match is case-insensitive. | | `filters.billToAddress` | string | no | Limits results to invoices whose bill-to address contains this text across line1, line2, city, stateProvince and zipCode. The match is case-insensitive. Country is excluded, because it is an ISO code and a short term would match every address in the country. | | `filters.soldToAddress` | string | no | Limits results to invoices whose sold-to address contains this text across line1, line2, city, stateProvince and zipCode. The match is case-insensitive. Country is excluded, as with billToAddress. | | `filters.invoiceEmail` | string | no | Limits results to invoices with this billing email. The match is exact and case-sensitive. | | `filters.purchaseOrderOverride` | string | no | Limits results to invoices with this PO override. The match is exact. | | `filters.externalInvoiceId` | string | no | Limits results to invoices with this identifier in an external source system. The match is exact. | | `filters.invoiceDate` | object | no | Limits results to invoices whose invoiceDate falls within these bounds. Omit either bound to leave that side open. | | `filters.invoiceDate.start` | string (date) | no | The inclusive lower bound. Format YYYY-MM-DD. | | `filters.invoiceDate.end` | string (date) | no | The inclusive upper bound. Format YYYY-MM-DD. | | `filters.dueDate` | object | no | Limits results to invoices whose dueDate falls within these bounds. Omit either bound to leave that side open. An aging report is this filter with an open lower bound. | | `filters.dueDate.start` | string (date) | no | The inclusive lower bound. Format YYYY-MM-DD. | | `filters.dueDate.end` | string (date) | no | The inclusive upper bound. Format YYYY-MM-DD. | | `filters.lastModifiedDate` | object | no | Limits results to invoices changed within this window, for polling an incremental sync. Omit either bound to leave that side open. | | `filters.lastModifiedDate.start` | string | no | The inclusive lower bound, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `filters.lastModifiedDate.end` | string | no | The inclusive upper bound, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `filters.externalIdentifier` | object | no | Matches one identifier owned by another system. Both members are required, because a fieldName on its own would match every value stored under that key. Several invoices may share one value, so this returns the items array, not a single record. | | `filters.externalIdentifier.fieldName` | string | yes | The configured key of the external identifier, a stable camelCase slug such as 'netsuiteInvoiceId', not the human-readable label. It must match an active external-identifier configuration for invoices, or the call fails with UNKNOWN_EXTERNAL_IDENTIFIER. | | `filters.externalIdentifier.value` | string | yes | The identifier as the source system stores it. The match is exact and case-sensitive. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first request. | | `page.size` | number | no | The number of invoices per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "invoiceNumber": "", "customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "customerName": "", "customerNumber": "", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityName": "", "salesOrderId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "salesOrderNumber": "", "invoiceStatus": [ "" ], "approvalStatus": [ "" ], "deliveryStatus": [ "" ], "disputeStatus": [ "" ], "customerMessage": "", "billToAddress": "", "soldToAddress": "", "invoiceEmail": "", "purchaseOrderOverride": "", "externalInvoiceId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "invoiceDate": { "start": "2026-01-15", "end": "2026-01-15" }, "dueDate": { "start": "2026-01-15", "end": "2026-01-15" }, "lastModifiedDate": { "start": "", "end": "" }, "externalIdentifier": { "fieldName": "", "value": "" } }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchInvoiceOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The invoices on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the invoice. | | `items[].invoiceNumber` | string | no | The invoice number, for example 'INV-1', unique within the tenant. Absent while none has been assigned. | | `items[].invoiceDate` | string (date) | yes | The date the invoice was issued. Format YYYY-MM-DD. Payment terms and the general ledger impact are both based on this date. | | `items[].dueDate` | string (date) | no | The date payment falls due, the invoice date plus the payment term. Format YYYY-MM-DD. Absent when no term has been resolved. | | `items[].lastPaidDate` | string (date) | no | The date the most recent payment was applied. Format YYYY-MM-DD. Absent on an invoice that has never been paid against. | | `items[].invoiceStatus` | string | no | The settlement status of the invoice. Possible values: 'unpaid', 'partiallyPaid', 'paidInFull', 'creditedInFull', 'isOverdue', 'disputed'. | | `items[].approvalStatus` | string | no | The finance approval status of the invoice. Possible values: 'draft', 'pendingApproval', 'approved', 'revise', 'rejected'. | | `items[].deliveryStatus` | string | no | How far the invoice has got in being sent to the customer. Possible values: 'created', 'toBeSent', 'delivered', 'read', 'failed'. | | `items[].disputeStatus` | string | no | The dispute status of the invoice, set only when payments against it are under dispute. Possible values: 'disputed', 'chargedBack'. | | `items[].dataSource` | string | no | The system the invoice record was imported from, for invoices that arrived through an integration or migration rather than being created in Everest. For example 'NetSuite', 'Quickbooks', 'Stripe' or 'MigrationFile'. Absent on invoices created in Everest. | | `items[].currency` | string | yes | The currency of the invoice. Format ISO 4217, for example 'USD'. It applies to every amount on the record, on the header and on every line. | | `items[].customerMessage` | string | no | A free-form message printed on the invoice for the customer. | | `items[].externalInvoiceId` | string | no | The invoice's identifier in an external source system, kept for reconciliation. For identifiers owned by your other systems, prefer externalIdentifiers below. | | `items[].customerId` | string | yes | The customer the invoice is issued to. See id on GET accounting/customers/:id. | | `items[].customerName` | string | no | The display name of the customer, as it appears on the document. | | `items[].customerNumber` | string | no | The customer number of the customer, unique within the tenant. | | `items[].entityId` | string | yes | The legal entity issuing the invoice. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the issuing legal entity. | | `items[].salesOrderId` | string | no | The sales order the invoice was billed from. Absent on invoices created directly. See id on GET accounting/sales-orders/:id. | | `items[].salesOrderNumber` | string | no | The number of the source sales order. Present whenever salesOrderId is. | | `items[].billToAddress` | object | no | The bill-to address the invoice is sent to. Absent when the invoice references no bill-to record. | | `items[].billToAddress.addressId` | string | no | The unique identifier for the address record. Absent when none is stored. | | `items[].billToAddress.line1` | string | yes | The first address line: street number and name, or PO box. | | `items[].billToAddress.line2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `items[].billToAddress.city` | string | yes | The city or locality. | | `items[].billToAddress.stateProvince` | string | no | The state, province or region. | | `items[].billToAddress.country` | string | yes | The country, as a name or ISO 3166-1 code. | | `items[].billToAddress.zipCode` | string | yes | The postal or ZIP code. | | `items[].soldToAddress` | object | no | The sold-to address the product or service was delivered to, used for tax determination. Absent when the invoice references no sold-to record. | | `items[].soldToAddress.addressId` | string | no | The unique identifier for the address record. Absent when none is stored. | | `items[].soldToAddress.line1` | string | yes | The first address line: street number and name, or PO box. | | `items[].soldToAddress.line2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `items[].soldToAddress.city` | string | yes | The city or locality. | | `items[].soldToAddress.stateProvince` | string | no | The state, province or region. | | `items[].soldToAddress.country` | string | yes | The country, as a name or ISO 3166-1 code. | | `items[].soldToAddress.zipCode` | string | yes | The postal or ZIP code. | | `items[].invoiceEmail` | string | no | The email address of the business contact that receives this invoice. | | `items[].purchaseOrders` | array of string | no | The purchase order references the customer supplied, derived from the sales order. These are the customer's own document numbers. An empty array when none were supplied. | | `items[].purchaseOrderOverride` | string | no | A manually entered PO number that overrides the sales-order-derived values in purchaseOrders. | | `items[].paymentTermDays` | number | no | The number of days after the invoice date that payment falls due, resolved from the invoice's payment term. The term record itself is not published, only its day count. | | `items[].exchangeRate` | string (decimal) | no | The rate used to convert the invoice's currency to the entity's functional currency. Absent when no conversion applies. | | `items[].excludeFromTaxEngine` | boolean | no | Whether the invoice is left out of the tax engine. Derived from the sales order. | | `items[].subtotal` | string (decimal) | no | The amount before taxes and discounts, in the invoice currency. Absent when no subtotal has been computed. | | `items[].totalTax` | string (decimal) | no | The total tax across all lines, in the invoice currency. | | `items[].totalDutiesAndCustoms` | string (decimal) | no | The total duties and customs across all lines, in the invoice currency. | | `items[].totalAmount` | string (decimal) | no | The total invoiced, including taxes and duties, in the invoice currency. | | `items[].totalAmountDue` | string (decimal) | no | The amount that was due when the invoice was created, in the invoice currency. | | `items[].totalCredited` | string (decimal) | no | The total credited back against this invoice by credit memos, in the invoice currency. | | `items[].totalLateFeeAmount` | string (decimal) | no | The total late fees charged on this invoice, in the invoice currency. | | `items[].lineDiscounts` | string (decimal) | no | The sum of discount amounts across all invoice lines, in the invoice currency. | | `items[].oneTimeDiscount` | string (decimal) | no | The one-time discount applied to the invoice as a whole, in the invoice currency. | | `items[].paymentsApplied` | string (decimal) | no | The amount settled by payments so far, in the invoice currency. | | `items[].adjustments` | string (decimal) | no | The sum of small-balance write-offs and early-payment discounts recorded against this invoice, in the invoice currency. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the invoice record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | no | The lines of the invoice, each billing one product or free-text item. An empty array when the invoice has no lines. All line amounts are in the invoice currency. | | `items[].lines[].id` | string | yes | The unique identifier for the invoice line. | | `items[].lines[].invoiceLineNumber` | string | no | The sequential line number within the invoice. | | `items[].lines[].description` | string | no | The line description, as shown to the customer. | | `items[].lines[].lineType` | string | no | The kind of charge the line is. Possible values: 'recurring', 'oneTime', 'oneTimeDiscount', 'usageOverage', 'overage', 'payAsYouGo', 'commit', 'lateFee', 'creditCardProcessingFee', 'billableTime', 'billablePassThrough', 'billableTravelExpense'. | | `items[].lines[].productId` | string | no | The billed product. Set only on lines created from a product reference, absent on free-text lines. See id on GET accounting/products/:id. | | `items[].lines[].productCode` | string | no | The product code as it stood when the line was created. A snapshot, so it does not follow later changes to the product. | | `items[].lines[].productDescription` | string | no | The product description as it stood when the line was created. A snapshot, as with productCode. | | `items[].lines[].quantity` | string (decimal) | no | The quantity billed. A count, not an amount, so it has no currency. | | `items[].lines[].unitOfMeasure` | string | no | The unit the quantity is measured in, for example 'each', 'hour' or 'gigabyte'. | | `items[].lines[].unitPrice` | string (decimal) | no | The price per unit actually charged, in the invoice currency. | | `items[].lines[].listPrice` | string (decimal) | no | The undiscounted list price per unit, in the invoice currency. | | `items[].lines[].totalPrice` | string (decimal) | no | The line total excluding tax and duties, in the invoice currency. | | `items[].lines[].discountAmount` | string (decimal) | no | The discount granted on the line, in the invoice currency. | | `items[].lines[].discountPercentage` | string (decimal) | no | The discount granted on the line as a percentage. A rate, not an amount, so it has no currency. | | `items[].lines[].taxAmount` | string (decimal) | no | The tax on the line, in the invoice currency. | | `items[].lines[].dutiesAndCustomsAmount` | string (decimal) | no | The duties and customs charged on the line, in the invoice currency. | | `items[].lines[].amount` | string (decimal) | no | The total billed for the line, including tax and duties, in the invoice currency. | | `items[].lines[].totalAmountDue` | string (decimal) | no | The amount of the line still due, in the invoice currency. | | `items[].lines[].taxCode` | string | no | The tax code applied to the line. | | `items[].lines[].taxDate` | string (date) | no | The date the line's tax was determined on. Format YYYY-MM-DD. | | `items[].lines[].serviceStartDate` | string (date) | no | The first day of the service period the line covers. Format YYYY-MM-DD. | | `items[].lines[].serviceEndDate` | string (date) | no | The last day of the service period the line covers, inclusive. Format YYYY-MM-DD. | | `items[].externalIdentifiers` | array of object | no | External identifiers owned by your other systems. At most one entry per fieldName. An empty array when the invoice has none. | | `items[].externalIdentifiers[].fieldName` | string | yes | The configured key of the external identifier, a stable camelCase slug such as 'netsuiteInvoiceId', not the human-readable label. It must match an active external-identifier configuration for invoices. | | `items[].externalIdentifiers[].value` | string | yes | The identifier as the source system stores it. | | `items[].externalIdentifiers[].externalUrl` | string | no | An absolute http or https link to the object in the originating system. Absent when none is stored. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Errors | Code | Status | | --- | --- | | `UNKNOWN_EXTERNAL_IDENTIFIER` | 400 | | `INVALID_TIMESTAMP` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: UNKNOWN_EXTERNAL_IDENTIFIER, INVALID_TIMESTAMP - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Accounting/invoices/created event `accounting/invoices/created` Group: Accounting › Invoices · Operation ID: `accounting_invoices_created` An invoice was issued. Carries the invoice id only; read the invoice for everything else. Delivered once the invoice, its lines, its journal entry and any customer credit applied automatically have all settled, so the invoice is complete and readable when this arrives. It fires however the invoice came about: a scheduled, manual or ad-hoc billing run, a create through the API, milestone invoicing, a credit-and-rebill, a subscription cancellation, or a load from an external system. A billing run issues many invoices and publishes one event for each, so expect bursts. Invoices brought in by a one-time historical migration do **not** publish this event. They record invoices that were issued, sent and usually settled long ago in another system, so treating them as newly issued would trigger work that has already happened. Load that history from the invoicing read API instead. If your own integration wrote the invoice, you will receive an event for it. Filter those out using the invoice's data source or its external identifier. A delivery may be retried, so deduplicate on `event.uuid`, the id of the emission itself. Do not confuse it with `event.data.id`, which is the invoice: one invoice produces many emissions over its life. [Open in the interactive reference](https://docs.api.lensing.ai/?op=accounting_invoices_created) Delivered as `POST` to the webhook endpoint of a subscription to `accounting/invoices/created`. ## Payload Required, `application/json`: object. | Field | Type | Required | Description | | --- | --- | --- | --- | | `subscription` | object | yes | The subscription this delivery was made for. | | `subscription.uuid` | string | yes | Stable identifier of the subscription. | | `subscription.type` | string | yes | Transport the subscription delivers through; `webhook` for deliveries carried by this request. | | `subscription.name` | string | yes | Authored name of the subscription. | | `event` | object | yes | The emitted event being delivered. | | `event.uuid` | string | yes | Stable identifier of this emission — the idempotency key for a receiver, since a delivery may be retried. | | `event.identifier` | `"accounting/invoices/created"` | yes | Public identifier of the event type. | | `event.emittedAt` | string (date-time) | yes | When the event was emitted (ISO 8601). | | `event.data` | accounting_invoices_createdPayload | yes | | | `event.data.id` | string | yes | | Example: ```json { "subscription": { "uuid": "", "type": "", "name": "" }, "event": { "uuid": "", "identifier": "accounting/invoices/created", "emittedAt": "2026-01-15T09:30:00.000Z", "data": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } } } ``` --- # Accounting/invoices/updated event `accounting/invoices/updated` Group: Accounting › Invoices · Operation ID: `accounting_invoices_updated` An invoice changed after it was issued. Carries the invoice id only; re-read the invoice for its new state. Delivered whenever something about the invoice changes after it is issued: an edit to its header or lines, a payment applied to it, a credit memo applied to it, the invoice being emailed, delivered, failed or opened, and a chargeback dispute opening or resolving. This is a change notification rather than a description of the change. The payload names the invoice but not what changed about it, and there is no before-and-after, so re-read the invoice and take its current amounts and status from there. A subscriber that needs a diff keeps its own last-known copy, because the delivery cannot supply one. There are deliberately no separate events for payment, credit, delivery or dispute. To tell those apart, compare what you read against your last-known copy: the invoice's status, amount due, delivery status and dispute state are all readable. More specific events can be added later without breaking this one. Several changes in quick succession are several deliveries, and a re-read after the first may already show the last. Deduplicate on `event.uuid`, the id of the emission itself, and do that before anything with a financial side effect. Take balances from a read rather than from adding up events. Invoices touched by a one-time historical migration do not publish this event, the same as `accounting/invoices/created`. [Open in the interactive reference](https://docs.api.lensing.ai/?op=accounting_invoices_updated) Delivered as `POST` to the webhook endpoint of a subscription to `accounting/invoices/updated`. ## Payload Required, `application/json`: object. | Field | Type | Required | Description | | --- | --- | --- | --- | | `subscription` | object | yes | The subscription this delivery was made for. | | `subscription.uuid` | string | yes | Stable identifier of the subscription. | | `subscription.type` | string | yes | Transport the subscription delivers through; `webhook` for deliveries carried by this request. | | `subscription.name` | string | yes | Authored name of the subscription. | | `event` | object | yes | The emitted event being delivered. | | `event.uuid` | string | yes | Stable identifier of this emission — the idempotency key for a receiver, since a delivery may be retried. | | `event.identifier` | `"accounting/invoices/updated"` | yes | Public identifier of the event type. | | `event.emittedAt` | string (date-time) | yes | When the event was emitted (ISO 8601). | | `event.data` | accounting_invoices_updatedPayload | yes | | | `event.data.id` | string | yes | | Example: ```json { "subscription": { "uuid": "", "type": "", "name": "" }, "event": { "uuid": "", "identifier": "accounting/invoices/updated", "emittedAt": "2026-01-15T09:30:00.000Z", "data": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } } } ``` --- # Journal Entries `GET /accounting/journal-entries` Group: Accounting › Journal Entries · Operation ID: `journalEntries` Returns a list of journal entries, each as a header without its lines. To read an entry with its lines, use GET accounting/journal-entries/:id or POST accounting/journal-entries/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=journalEntries) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `type` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `postingDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `bookName` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, status, type, postingDate, bookName, lastModifiedDate - Selectable: id, name, status, type, approvalStatus, postingDate, reversalDate, reversalReason, reversalDescription, description, url, bookName, lastModifiedDate ## Response `200` Success Under `data`, an array of journalEntriesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the journal entry. | | `name` | string or null | no | The display name of the journal entry. | | `status` | string | no | The posting status of the journal entry. Possible values: 'posted', 'unposted', 'voided'. | | `type` | string or null | no | How the entry originated, for example 'manual', 'invoice', 'revaluation' or 'reversal'. A system-assigned value. | | `approvalStatus` | string or null | no | The approval status of the journal entry. Possible values: 'draft', 'pendingApproval', 'revise', 'approved', 'rejected'. Absent on entries that never entered the approval workflow. | | `postingDate` | string (date) | no | The date the entry posts to the general ledger. Format YYYY-MM-DD. | | `reversalDate` | string (date) or null | no | The reversal date recorded on the entry. Format YYYY-MM-DD. Set only on a reversed entry or a reversal entry. | | `reversalReason` | string or null | no | The reason recorded for the reversal. Possible values: 'accrualReversal', 'revaluationReversal', 'estimate', 'rebook', 'reversingSFD', 'other'. | | `reversalDescription` | string or null | no | The description recorded on the reversal. | | `description` | string or null | no | A free-form description of the journal entry. | | `url` | string or null | no | A link to the source document this entry comes from. | | `bookName` | string or null | no | The name of the accounting book this entry posts to. See name on GET accounting/books/:id. | | `lastModifiedDate` | string (date-time) | no | The date and time the journal entry last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Journal Entry `POST /accounting/journal-entries` Group: Accounting › Journal Entries · Operation ID: `createJournalEntry` Creates one journal entry with its debit and credit lines. The response contains only the new entry's id. Read the entry with GET accounting/journal-entries/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createJournalEntry) ## 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`: createJournalEntryInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The display name of the journal entry. | | `description` | string | no | A free-form description of the journal entry. | | `postingDate` | string (date) | yes | The date the entry posts to the general ledger. Use format YYYY-MM-DD. Each line takes this as its transaction date. | | `reversalDate` | string (date) | no | The reversal date recorded on the header. Use format YYYY-MM-DD. This does not post a reversal. To post one, use POST accounting/journal-entries/:id/reverse. | | `reversalReason` | string | no | The reason recorded on the header for a later reversal. Accepted values: 'accrualReversal', 'revaluationReversal', 'estimate', 'rebook', 'reversingSFD', 'other'. | | `reversalDescription` | string | no | A free-form reversal description recorded on the header. | | `url` | string | no | A link to the source document this entry comes from. | | `bookId` | string | yes | The accounting book this entry posts to. See id on GET accounting/books/:id. | | `deriveToAllBooks` | boolean | no | Whether to replicate the entry to every accounting book. Defaults to false. Has no effect unless multi-book accounting is enabled. | | `lines` | array of object | yes | The debit and credit lines of the entry. Each line is a debit or a credit in its transactionalCurrency. | | `lines[].accountId` | string | yes | The general ledger account this line posts to. See id on GET accounting/accounts/:id. | | `lines[].entityId` | string | yes | The legal entity this line posts to. See id on GET base/entities. | | `lines[].transactionalCurrency` | string | yes | The currency of this line's debit or credit amount. Format ISO 4217, for example 'USD'. | | `lines[].description` | string | no | A free-form description of the line. | | `lines[].intercompanyPartnerId` | string | no | The intercompany counterparty for this line. Set on intercompany lines. | | `lines[].debit` | string (decimal) | no | The debit amount for this line, in its transactionalCurrency. A line sets either debit or credit, not both. | | `lines[].credit` | string (decimal) | no | The credit amount for this line, in its transactionalCurrency. A line sets either debit or credit, not both. | | `lines[].exchangeRate` | string (decimal) | no | The exchange rate from the line's transactionalCurrency to the entity's functional currency. It applies only when the two currencies differ. For a same-currency line it is 1 and any value you send is ignored. When you omit it on a foreign-currency line, it defaults to the latest rate on or before the posting date. | | `lines[].reportingTags` | array of object | no | The reporting tags for this line, one per reporting dimension. Each tag pairs a dimensionCode with a modelId. A line has at most one tag per dimensionCode. The referenced record must exist and be active, or the call fails with REFERENCED_RECORD_NOT_FOUND. Omit or send an empty array for an untagged line. | | `lines[].reportingTags[].dimensionCode` | string | yes | The reporting dimension this tag is for. It determines which model modelId points to. Accepted values: 'department', 'businessUnit', 'team', 'location', 'region', 'employee', 'vendor', 'customer', 'valueCenter', 'custom'. | | `lines[].reportingTags[].modelId` | string | yes | The dimension record this tag points to, on the model that dimensionCode names. For example, a department record when dimensionCode is 'department'. It must be an existing, active record. | Example: ```json { "name": "", "description": "", "postingDate": "2026-01-15", "reversalDate": "2026-01-15", "reversalReason": "", "reversalDescription": "", "url": "", "bookId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "deriveToAllBooks": true, "lines": [ { "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "transactionalCurrency": "", "description": "", "intercompanyPartnerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "debit": "19.99", "credit": "19.99", "exchangeRate": "19.99", "reportingTags": [ { "dimensionCode": "", "modelId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ] } ] } ``` ## Response `200` Success Under `data` (createJournalEntryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created journal entry. | ## 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 | | --- | --- | | `REFERENCED_RECORD_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCED_RECORD_NOT_FOUND - `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 --- # Get Journal Entry `GET /accounting/journal-entries/{id}` Group: Accounting › Journal Entries · Operation ID: `getJournalEntry` Returns the journal entry with the given identifier, including its lines. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getJournalEntry) ## 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`. | | `id` | path | string | yes | The unique identifier for the journal entry to return. | ## Response `200` Success Under `data` (getJournalEntryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the journal entry. | | `approvalStatus` | string | no | The approval status of the journal entry. Possible values: 'draft', 'pendingApproval', 'revise', 'approved', 'rejected'. Absent on entries that never entered the approval workflow. | | `bookId` | string | no | The accounting book this entry posts to. See id on GET accounting/books/:id. | | `description` | string | no | A free-form description of the journal entry. | | `name` | string | no | The display name of the journal entry. | | `postingDate` | string (date) | yes | The date the entry posts to the general ledger. Format YYYY-MM-DD. | | `reversalDate` | string (date) | no | The reversal date recorded on the entry. Format YYYY-MM-DD. Set only on a reversed entry or a reversal entry. | | `reversalDescription` | string | no | The description recorded on the reversal. | | `reversalReason` | string | no | The reason recorded for the reversal. Possible values: 'accrualReversal', 'revaluationReversal', 'estimate', 'rebook', 'reversingSFD', 'other'. | | `status` | string | yes | The posting status of the journal entry. Possible values: 'posted', 'unposted', 'voided'. | | `type` | string | no | How the entry originated, for example 'manual', 'invoice', 'revaluation' or 'reversal'. A system-assigned value. | | `url` | string | no | A link to the source document this entry comes from. | | `lastModifiedDate` | string (date-time) | yes | The date and time the journal entry last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The debit and credit lines of the entry. | | `lines[].id` | string | yes | The unique identifier for the line. | | `lines[].accountId` | string | yes | The general ledger account this line posts to. See id on GET accounting/accounts/:id. | | `lines[].credit` | string (decimal) | no | The credit amount for this line, in its transactionalCurrency. A line has either a debit or a credit, not both. | | `lines[].debit` | string (decimal) | no | The debit amount for this line, in its transactionalCurrency. A line has either a debit or a credit, not both. | | `lines[].description` | string | no | A free-form description of the line. | | `lines[].entityId` | string | yes | The legal entity this line posts to. See id on GET base/entities. | | `lines[].intercompanyPartnerId` | string | no | The intercompany counterparty for this line. Set on intercompany lines. | | `lines[].exchangeRate` | string (decimal) | no | The exchange rate used to convert the line's transactional amount to its functional amount. | | `lines[].functionalCredit` | string (decimal) | no | The credit amount for this line, in the functional currency. | | `lines[].functionalCurrency` | string | no | The functional currency of the posting entity. Format ISO 4217, for example 'USD'. | | `lines[].functionalDebit` | string (decimal) | no | The debit amount for this line, in the functional currency. | | `lines[].journalEntryId` | string | no | The journal entry this line belongs to. See id on GET accounting/journal-entries/:id. | | `lines[].lineNumber` | number | no | The line's sequential number within the entry. | | `lines[].lineType` | string | no | The classification of the line, for example 'standard', 'tax', 'offset' or 'fxRoundingDifference'. A system-assigned value. Absent when the line has no classification. | | `lines[].postingPeriodId` | string | no | The accounting period this line posts to. See id on GET accounting/accounting-periods/:id. | | `lines[].revaluation` | boolean | no | Whether a currency-revaluation process created this line. | | `lines[].sourceTransaction` | string | no | The identifier of the source transaction that generated this line, for example an invoice or payment number. | | `lines[].transactionalCurrency` | string | no | The currency of the line's transactional amounts. Format ISO 4217, for example 'USD'. | | `lines[].transactionDate` | string (date) | no | The date of the underlying transaction. Format YYYY-MM-DD. It can differ from the posting date. | | `lines[].reportingTags` | array of object | no | The reporting tags assigned to this line. Set them with create or update. | | `lines[].reportingTags[].dimensionCode` | string | yes | The reporting dimension this tag is for. Possible values: 'department', 'businessUnit', 'team', 'location', 'region', 'employee', 'vendor', 'customer', 'valueCenter', 'custom'. | | `lines[].reportingTags[].modelId` | string | yes | The dimension record this tag points to, on the model that dimensionCode names. | | `lines[].reportingTags[].customDimensionId` | string | no | The custom dimension this tag belongs to. Present only for a custom-dimension tag. See id on GET base/custom-dimensions. | | `lines[].reportingTags[].isActive` | boolean | yes | Whether the referenced dimension record is currently active. A historical entry can have tags that point at records since made inactive. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Journal Entry `PATCH /accounting/journal-entries/{id}` Group: Accounting › Journal Entries · Operation ID: `updateJournalEntry` Updates a journal entry, changing header fields and adding, changing or removing its lines. Only unposted entries can be updated. Only the fields you send change. Returns the entry's id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateJournalEntry) ## 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. | | `id` | path | string | yes | The unique identifier for the journal entry to update. | ## Request body Required, `application/json`: updateJournalEntryInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The display name of the journal entry. | | `description` | string | no | A free-form description of the journal entry. | | `postingDate` | string (date) | no | The date the entry posts to the general ledger. Use format YYYY-MM-DD. | | `reversalDate` | string (date) | no | The reversal date recorded on the header. Use format YYYY-MM-DD. | | `reversalReason` | string | no | The reason recorded on the header for a later reversal. Accepted values: 'accrualReversal', 'revaluationReversal', 'estimate', 'rebook', 'reversingSFD', 'other'. | | `reversalDescription` | string | no | A free-form reversal description recorded on the header. | | `url` | string | no | A link to the source document this entry comes from. | | `createJournalEntryLines` | array of object | no | The lines to add to the entry. Each is a new line with no id. | | `createJournalEntryLines[].accountId` | string | yes | The general ledger account this line posts to. See id on GET accounting/accounts/:id. | | `createJournalEntryLines[].entityId` | string | yes | The legal entity this line posts to. See id on GET base/entities. | | `createJournalEntryLines[].transactionalCurrency` | string | yes | The currency of this line's amount. Format ISO 4217, for example 'USD'. | | `createJournalEntryLines[].description` | string | no | A free-form description of the line. | | `createJournalEntryLines[].intercompanyPartnerId` | string | no | The intercompany counterparty for this line. Set on intercompany lines. | | `createJournalEntryLines[].debit` | string (decimal) | no | The debit amount for this line, in its transactionalCurrency. A line sets either debit or credit, not both. | | `createJournalEntryLines[].credit` | string (decimal) | no | The credit amount for this line, in its transactionalCurrency. A line sets either debit or credit, not both. | | `createJournalEntryLines[].exchangeRate` | string (decimal) | no | The exchange rate from the line's transactionalCurrency to the entity's functional currency. It applies only when the two currencies differ. For a same-currency line it is 1 and any value you send is ignored. | | `createJournalEntryLines[].reportingTags` | array of object | no | The reporting tags for this new line, one per reporting dimension. Each tag pairs a dimensionCode with a modelId, and the referenced record must exist and be active. A line has at most one tag per dimensionCode. Omit or send an empty array for an untagged line. | | `createJournalEntryLines[].reportingTags[].dimensionCode` | string | yes | The reporting dimension this tag is for. It determines which model modelId points to. Accepted values: 'department', 'businessUnit', 'team', 'location', 'region', 'employee', 'vendor', 'customer', 'valueCenter', 'custom'. | | `createJournalEntryLines[].reportingTags[].modelId` | string | yes | The dimension record this tag points to, on the model that dimensionCode names. For example, a department record when dimensionCode is 'department'. It must be an existing, active record. | | `updateJournalEntryLines` | array of object | no | The existing lines to change, each matched by its id. Only the fields you send on a line change. | | `updateJournalEntryLines[].id` | string | yes | The line to change. It must belong to this entry. | | `updateJournalEntryLines[].accountId` | string | no | The general ledger account this line posts to. See id on GET accounting/accounts/:id. | | `updateJournalEntryLines[].entityId` | string | no | The legal entity this line posts to. See id on GET base/entities. | | `updateJournalEntryLines[].intercompanyPartnerId` | string | no | The intercompany counterparty for this line. Set on intercompany lines. | | `updateJournalEntryLines[].description` | string | no | A free-form description of the line. | | `updateJournalEntryLines[].debit` | string (decimal) | no | The debit amount for this line, in its transactionalCurrency. A line sets either debit or credit, not both. | | `updateJournalEntryLines[].credit` | string (decimal) | no | The credit amount for this line, in its transactionalCurrency. A line sets either debit or credit, not both. | | `updateJournalEntryLines[].transactionalCurrency` | string | no | The currency of this line's amount. Format ISO 4217, for example 'USD'. | | `updateJournalEntryLines[].exchangeRate` | string (decimal) | no | The exchange rate from the line's transactionalCurrency to the entity's functional currency. It applies only when the two currencies differ. For a same-currency line it is 1 and any value you send is ignored. | | `updateJournalEntryLines[].reportingTags` | array of object | no | The reporting tags for the line, one per reporting dimension. Each tag pairs a dimensionCode with a modelId referencing an existing, active record, at most one per dimensionCode. Omit to keep the current tags. Send an empty array to clear them. Send a list to replace them. | | `updateJournalEntryLines[].reportingTags[].dimensionCode` | string | yes | The reporting dimension this tag is for. It determines which model modelId points to. Accepted values: 'department', 'businessUnit', 'team', 'location', 'region', 'employee', 'vendor', 'customer', 'valueCenter', 'custom'. | | `updateJournalEntryLines[].reportingTags[].modelId` | string | yes | The dimension record this tag points to, on the model that dimensionCode names. For example, a department record when dimensionCode is 'department'. It must be an existing, active record. | | `deleteJournalEntryLines` | array of string | no | The lines to delete from the entry, each by its id. Deleting a line also deletes its reporting tags. | Example: ```json { "name": "", "description": "", "postingDate": "2026-01-15", "reversalDate": "2026-01-15", "reversalReason": "", "reversalDescription": "", "url": "", "createJournalEntryLines": [ { "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "transactionalCurrency": "", "description": "", "intercompanyPartnerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "debit": "19.99", "credit": "19.99", "exchangeRate": "19.99", "reportingTags": [ { "dimensionCode": "", "modelId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ] } ], "updateJournalEntryLines": [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "intercompanyPartnerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "description": "", "debit": "19.99", "credit": "19.99", "transactionalCurrency": "", "exchangeRate": "19.99", "reportingTags": [ { "dimensionCode": "", "modelId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ] } ], "deleteJournalEntryLines": [ "" ] } ``` ## Response `200` Success Under `data` (updateJournalEntryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the updated journal entry. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCED_RECORD_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCED_RECORD_NOT_FOUND - `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 --- # Reverse Journal Entry `POST /accounting/journal-entries/{id}/reverse` Group: Accounting › Journal Entries · Operation ID: `reverseJournalEntry` Reverses a posted journal entry by posting a new entry with its debits and credits swapped. Fails when the entry cannot be reversed, such as one that is not manual, is already reversed, is itself a reversal, or was generated by revaluation. Returns the new reversal entry's id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=reverseJournalEntry) ## 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. | | `id` | path | string | yes | The unique identifier for the journal entry to reverse. | ## Request body Required, `application/json`: reverseJournalEntryInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `reversalDate` | string (date) | yes | The posting date of the reversal entry. Use format YYYY-MM-DD. It must be on or after the original entry's posting date. | | `reversalReason` | string | yes | The reason for the reversal. Accepted values: 'accrualReversal', 'revaluationReversal', 'estimate', 'rebook', 'reversingSFD', 'other'. | | `reversalDescription` | string | no | A free-form reversal description recorded on the original entry's header. Required unless reversalReason is 'other', which fails with REVERSAL_DESCRIPTION_REQUIRED. | Example: ```json { "reversalDate": "2026-01-15", "reversalReason": "", "reversalDescription": "" } ``` ## Response `200` Success Under `data` (reverseJournalEntryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `reversalJournalEntryId` | string | yes | The unique identifier for the created reversal journal entry. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REVERSAL_DESCRIPTION_REQUIRED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REVERSAL_DESCRIPTION_REQUIRED - `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 --- # Void Journal Entry `POST /accounting/journal-entries/{id}/void` Group: Accounting › Journal Entries · Operation ID: `voidJournalEntry` Voids a posted journal entry. Fails when the entry is already voided. Returns the voided entry's id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=voidJournalEntry) ## 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. | | `id` | path | string | yes | The unique identifier for the journal entry to void. | ## Request body Required, `application/json`: voidJournalEntryInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `voidReason` | string | yes | The reason recorded on the voided entry. | Example: ```json { "voidReason": "" } ``` ## Response `200` Success Under `data` (voidJournalEntryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the voided journal entry. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Search Journal Entries `POST /accounting/journal-entries/search` Group: Accounting › Journal Entries · Operation ID: `searchJournalEntries` Returns the journal entries that match the given filters, each with its lines. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchJournalEntries) ## 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`. | ## Request body Required, `application/json`: searchJournalEntriesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A journal entry must match every filter you send. Omit to return all journal entries. | | `filters.id` | string | no | Limits results to the journal entry with this identifier. | | `filters.bookId` | string | no | Limits results to entries in this accounting book. See id on GET accounting/books/:id. | | `filters.status` | string | no | Limits results to entries with this posting status. Accepted values: 'posted', 'unposted', 'voided'. Any other value matches no entry. | | `filters.type` | string | no | Limits results to entries of this origin type, for example 'manual' or 'invoice'. | | `filters.name` | string | no | Limits results to entries whose name contains this text. The match is case-insensitive. | | `filters.description` | string | no | Limits results to entries whose description contains this text. The match is case-insensitive. | | `filters.postingDate` | object | no | A window on the posting date. Both bounds are inclusive. Omit a bound to leave that side open. | | `filters.postingDate.start` | string (date) | no | The inclusive start of the posting-date window. Use format YYYY-MM-DD. | | `filters.postingDate.end` | string (date) | no | The inclusive end of the posting-date window. Use format YYYY-MM-DD. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of entries per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "bookId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "status": "", "type": "", "name": "", "description": "", "postingDate": { "start": "2026-01-15", "end": "2026-01-15" } }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchJournalEntriesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The journal entries that match, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the journal entry. | | `items[].approvalStatus` | string | no | The approval status of the journal entry. Possible values: 'draft', 'pendingApproval', 'revise', 'approved', 'rejected'. Absent on entries that never entered the approval workflow. | | `items[].bookId` | string | no | The accounting book this entry posts to. See id on GET accounting/books/:id. | | `items[].description` | string | no | A free-form description of the journal entry. | | `items[].name` | string | no | The display name of the journal entry. | | `items[].postingDate` | string (date) | yes | The date the entry posts to the general ledger. Format YYYY-MM-DD. | | `items[].reversalDate` | string (date) | no | The reversal date recorded on the entry. Format YYYY-MM-DD. Set only on a reversed entry or a reversal entry. | | `items[].reversalDescription` | string | no | The description recorded on the reversal. | | `items[].reversalReason` | string | no | The reason recorded for the reversal. Possible values: 'accrualReversal', 'revaluationReversal', 'estimate', 'rebook', 'reversingSFD', 'other'. | | `items[].status` | string | yes | The posting status of the journal entry. Possible values: 'posted', 'unposted', 'voided'. | | `items[].type` | string | no | How the entry originated, for example 'manual', 'invoice', 'revaluation' or 'reversal'. A system-assigned value. | | `items[].url` | string | no | A link to the source document this entry comes from. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the journal entry last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | yes | The debit and credit lines of the entry. | | `items[].lines[].id` | string | yes | The unique identifier for the line. | | `items[].lines[].accountId` | string | yes | The general ledger account this line posts to. See id on GET accounting/accounts/:id. | | `items[].lines[].credit` | string (decimal) | no | The credit amount for this line, in its transactionalCurrency. A line has either a debit or a credit, not both. | | `items[].lines[].debit` | string (decimal) | no | The debit amount for this line, in its transactionalCurrency. A line has either a debit or a credit, not both. | | `items[].lines[].description` | string | no | A free-form description of the line. | | `items[].lines[].entityId` | string | yes | The legal entity this line posts to. See id on GET base/entities. | | `items[].lines[].intercompanyPartnerId` | string | no | The intercompany counterparty for this line. Set on intercompany lines. | | `items[].lines[].exchangeRate` | string (decimal) | no | The exchange rate used to convert the line's transactional amount to its functional amount. | | `items[].lines[].functionalCredit` | string (decimal) | no | The credit amount for this line, in the functional currency. | | `items[].lines[].functionalCurrency` | string | no | The functional currency of the posting entity. Format ISO 4217, for example 'USD'. | | `items[].lines[].functionalDebit` | string (decimal) | no | The debit amount for this line, in the functional currency. | | `items[].lines[].journalEntryId` | string | no | The journal entry this line belongs to. See id on GET accounting/journal-entries/:id. | | `items[].lines[].lineNumber` | number | no | The line's sequential number within the entry. | | `items[].lines[].lineType` | string | no | The classification of the line, for example 'standard', 'tax', 'offset' or 'fxRoundingDifference'. A system-assigned value. Absent when the line has no classification. | | `items[].lines[].postingPeriodId` | string | no | The accounting period this line posts to. See id on GET accounting/accounting-periods/:id. | | `items[].lines[].revaluation` | boolean | no | Whether a currency-revaluation process created this line. | | `items[].lines[].sourceTransaction` | string | no | The identifier of the source transaction that generated this line, for example an invoice or payment number. | | `items[].lines[].transactionalCurrency` | string | no | The currency of the line's transactional amounts. Format ISO 4217, for example 'USD'. | | `items[].lines[].transactionDate` | string (date) | no | The date of the underlying transaction. Format YYYY-MM-DD. It can differ from the posting date. | | `items[].lines[].reportingTags` | array of object | no | The reporting tags assigned to this line. Set them with create or update. | | `items[].lines[].reportingTags[].dimensionCode` | string | yes | The reporting dimension this tag is for. Possible values: 'department', 'businessUnit', 'team', 'location', 'region', 'employee', 'vendor', 'customer', 'valueCenter', 'custom'. | | `items[].lines[].reportingTags[].modelId` | string | yes | The dimension record this tag points to, on the model that dimensionCode names. | | `items[].lines[].reportingTags[].customDimensionId` | string | no | The custom dimension this tag belongs to. Present only for a custom-dimension tag. See id on GET base/custom-dimensions. | | `items[].lines[].reportingTags[].isActive` | boolean | yes | Whether the referenced dimension record is currently active. A historical entry can have tags that point at records since made inactive. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more entries remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Outbound Payments `GET /accounting/outbound-payments` Group: Accounting › Outbound Payments · Operation ID: `outboundPayments` Returns a list of outbound payments. Each entry is the payment header. Read one payment with its lines and payee details from GET accounting/outbound-payments/:id or POST accounting/outbound-payments/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=outboundPayments) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `outboundPaymentNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentCurrency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `approvalStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `applicationStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `externalId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `origin` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `singlePayment` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, outboundPaymentNumber, paymentDate, paymentCurrency, paymentType, status, approvalStatus, applicationStatus, lastModifiedDate - Selectable: id, outboundPaymentNumber, description, paymentDate, paymentCurrency, paymentType, status, approvalStatus, applicationStatus, paymentId, externalId, origin, singlePayment, paymentAccountName, entityName, lastModifiedDate ## Response `200` Success Under `data`, an array of outboundPaymentsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the outbound payment. | | `outboundPaymentNumber` | string or null | no | The outbound payment number. Absent until a number has been assigned. | | `description` | string or null | no | A description of the payment. | | `paymentDate` | string (date) or null | no | The date the payment was made. Format YYYY-MM-DD. | | `paymentCurrency` | string or null | no | The currency the payment is denominated in. Format ISO 4217, for example 'USD'. | | `paymentType` | string or null | no | How the money was sent, for example 'ach', 'wire' or 'check'. | | `status` | string or null | no | The processing status of the payment. | | `approvalStatus` | string or null | no | Where the payment sits in the approval workflow. | | `applicationStatus` | string or null | no | How much of the payment has been applied to bills. Values include 'unapplied', 'partiallyApplied' and 'applied'. | | `paymentId` | string or null | no | The payment reference assigned by the bank or payment provider. It is not filterable. Match on it through POST accounting/outbound-payments/search. | | `externalId` | string or null | no | The identifier this payment has in the upstream system that produced it. It is not unique. | | `origin` | string or null | no | The source system this payment came from. Recorded when an integration created the payment. | | `singlePayment` | boolean or null | no | Whether the payment covers a single bill rather than a batch. | | `paymentAccountName` | string or null | no | The name of the bank GL account the money left. It is not filterable. To scope by account, use paymentAccountId on POST accounting/outbound-payments/search. | | `entityName` | string or null | no | The name of the legal entity that made the payment. It is not filterable. To scope by entity, use entityId on POST accounting/outbound-payments/search. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the payment last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates an outbound payment. `POST /accounting/outbound-payments` Group: Accounting › Outbound Payments · Operation ID: `createOutboundPayment` The response contains only the new payment's id. Read the record with GET accounting/outbound-payments/:id if you need its stored state, where outboundPaymentNumber appears once assigned. This records a payment and does not execute one. The payment is created with status manuallyRecorded, and initiating a bank payment is not part of this version. Every item pays one document, and documentType names the one kind of document the whole payment pays. Every document must be payable and all in one currency, or the call fails with DOCUMENT_NOT_PAYABLE or DOCUMENT_CURRENCY_MISMATCH before anything is written. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createOutboundPayment) ## 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`: createOutboundPaymentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `accountId` | string | yes | The bank or cash account the payment is made from. See id on GET accounting/accounts/:id. | | `entityId` | string | yes | The legal entity the payment is posted under. See id on GET base/entities. | | `paymentDate` | string (date) | yes | The date the payment is made. Use format YYYY-MM-DD. | | `paymentCurrency` | string | yes | The currency of the payment. Format ISO 4217, for example 'USD'. Governs each item's amount unless the item names its own currency. | | `documentType` | string | yes | The one kind of document every item pays, either 'VendorBillHeaderBase' or 'ExpenseReportBase'. A payment pays one kind, not a mixture. | | `singlePayment` | boolean | no | Whether this is a single payment rather than a batch. | | `commonDescription` | string | no | A description applied to the payment as a whole. | | `skipJournalEntry` | boolean | no | Whether to skip journal-entry creation for this payment. Defaults to false. | | `externalUrl` | string | no | A link back to the payment in the upstream system. | | `origin` | string | no | The source system this payment came from. | | `senderAccountId` | string | no | A bank account identifier for the sender, supplied by an integration. It is free-form text, not a reference to an account record. | | `actualExchangeRate` | string (decimal) | no | The exchange rate the bank actually applied, as a decimal. | | `systemExchangeRate` | string (decimal) | no | The exchange rate the system derived, as a decimal. | | `paymentItems` | array of object | yes | The documents this payment settles. At least one is required. | | `paymentItems[].documentId` | string | yes | The vendor bill or expense report being paid, of the kind named by documentType. See id on GET accounting/vendor-bills/:id or GET accounting/expense-reports/:id. | | `paymentItems[].entityId` | string | yes | The legal entity the document is posted under. See id on GET base/entities. | | `paymentItems[].amount` | string (decimal) | yes | The amount applied to this document, as a decimal. | | `paymentItems[].currency` | string | no | The currency for this item. Format ISO 4217, for example 'USD'. Omit to use the payment's currency. | | `paymentItems[].description` | string | yes | What this item pays for. Required. | | `attachments` | array of object | no | Files to attach to the payment. Each names an already-uploaded file by its id. | | `attachments[].fileName` | string | yes | The display name of the attached file. | | `attachments[].fileId` | string | yes | The identifier of the already-uploaded file. There is no endpoint for it. | Example: ```json { "accountId": "6d1e4b27-8a90-4c31-b5f2-7e8a9c0d1b23", "entityId": "8f2c1d40-5a6b-4c7d-9e80-1f2a3b4c5d6e", "paymentDate": "2026-08-27", "paymentCurrency": "USD", "documentType": "VendorBillHeaderBase", "singlePayment": true, "commonDescription": "", "skipJournalEntry": true, "externalUrl": "", "origin": "", "senderAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "actualExchangeRate": "19.99", "systemExchangeRate": "19.99", "paymentItems": [ { "documentId": "c41d0b58-2e97-4f6a-8d13-7b5e9c204af6", "entityId": "8f2c1d40-5a6b-4c7d-9e80-1f2a3b4c5d6e", "amount": "1200.00", "description": "Invoice 5512" } ], "attachments": [ { "fileName": "", "fileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ] } ``` ## Response `200` Success Under `data` (createOutboundPaymentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created payment. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `DOCUMENT_NOT_PAYABLE` | 400 | | `PAYMENT_AMOUNT_EXCEEDS_OPEN_AMOUNT` | 400 | | `DOCUMENT_CURRENCY_MISMATCH` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, DOCUMENT_NOT_PAYABLE, PAYMENT_AMOUNT_EXCEEDS_OPEN_AMOUNT, DOCUMENT_CURRENCY_MISMATCH - `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 --- # Returns the outbound payment with the given identifier. `GET /accounting/outbound-payments/{id}` Group: Accounting › Outbound Payments · Operation ID: `getOutboundPayment` An outbound payment is money paid out to a vendor or employee, with one or more lines. It shares its read with POST accounting/outbound-payments/search, which returns the same record. The list at GET accounting/outbound-payments is header-only, so the payment lines come back here and on search. Each line's payee bank account and sort code are returned here and on search but never on the list and never filterable. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getOutboundPayment) ## 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`. | | `id` | path | string | yes | The unique identifier for the outbound payment to return. | ## Response `200` Success Under `data` (getOutboundPaymentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the outbound payment. | | `outboundPaymentNumber` | string | no | The outbound payment number. Absent until a number has been assigned. | | `description` | string | no | A description of the payment. | | `paymentDate` | string (date) | yes | The date the payment was made. Format YYYY-MM-DD. | | `paymentCurrency` | string | yes | The currency the payment is denominated in. Format ISO 4217, for example 'USD'. Governs totalPaid, appliedAmount and unappliedAmount. | | `paymentType` | string | yes | How the money was sent, for example 'ach', 'wire' or 'check'. | | `status` | string | yes | The processing status of the payment. | | `approvalStatus` | string | no | Where the payment sits in the approval workflow. | | `applicationStatus` | string | no | How much of the payment has been applied to bills. Values include 'unapplied', 'partiallyApplied' and 'applied'. | | `totalPaid` | string (decimal) | no | The total amount paid, in the payment currency. | | `appliedAmount` | string (decimal) | no | The portion already applied against vendor bills, in the payment currency. | | `unappliedAmount` | string (decimal) | no | The portion not yet applied to any bill, in the payment currency. | | `bankCurrency` | string | no | The currency of the bank account the money left, when it differs from the payment currency. Format ISO 4217, for example 'USD'. | | `paymentId` | string | no | The payment reference assigned by the bank or payment provider. | | `externalId` | string | no | The identifier this payment has in the upstream system that produced it. It is not unique. | | `externalUrl` | string | no | A link to the payment in an external system, when one is stored. | | `origin` | string | no | The source system this payment came from. Recorded when an integration created the payment. | | `singlePayment` | boolean | no | Whether the payment covers a single bill rather than a batch. | | `paymentAccountId` | string | no | The bank GL account the money left. See id on GET accounting/accounts/:id. | | `paymentAccountName` | string | no | The name of the bank GL account the money left. | | `entityId` | string | no | The legal entity that made the payment. See id on GET base/entities. | | `entityName` | string | no | The name of the legal entity that made the payment. | | `lastModifiedDate` | string (date-time) | yes | The date and time the payment last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | no | The payment lines, one per payee or bill covered. An empty array when the payment has none. | | `lines[].id` | string | yes | The unique identifier for the line. | | `lines[].lineNumber` | string | no | The line number. | | `lines[].description` | string | yes | A description of the line. | | `lines[].amount` | string (decimal) | no | The amount paid on this line, in the line currency. | | `lines[].currency` | string | yes | The currency for the line. Format ISO 4217, for example 'USD'. | | `lines[].paymentDate` | string (date) | no | The date this line was paid, when it differs from the header. Format YYYY-MM-DD. | | `lines[].paymentType` | string | no | How this line was sent, for example 'ach', 'wire' or 'check'. | | `lines[].receiverName` | string | yes | The name of the payee. | | `lines[].receiverAccount` | string | no | The payee bank account number. It is returned here and on search but never on the collection endpoint and never filterable. | | `lines[].receiverBankCode` | string | no | The payee sort or routing code. It is returned here and on search but never on the collection endpoint and never filterable. | | `lines[].customReference` | string | no | A reference you supplied, carried on the line. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Outbound Payment `PATCH /accounting/outbound-payments/{id}` Group: Accounting › Outbound Payments · Operation ID: `updateOutboundPayment` Updates the outbound payment with the given identifier. Only the fields in the request body change. Only the fields in the request body change. Only a payment that has not been executed can be changed, the same states the delete endpoint accepts. A payment that has been applied, is processing, or has otherwise moved money fails with PAYMENT_NOT_EDITABLE. Void it instead with POST accounting/outbound-payments/:id/void. Payment items change through three arrays, applied in the order delete, update, create. updatePaymentItems and deletePaymentItems address items by the id the payment publishes, and an item id belonging to another payment fails with ITEM_NOT_FOUND. An update that would leave the payment with no items fails with NO_PAYMENT_ITEMS. Documents named by added or changed items go through the same payability checks as on create, DOCUMENT_NOT_PAYABLE, PAYMENT_AMOUNT_EXCEEDS_OPEN_AMOUNT and DOCUMENT_CURRENCY_MISMATCH. When an item's amount changes, the amount it already applies is credited back first, so re-sending the same amount is not read as a second payment. The response is empty. Read the record with GET accounting/outbound-payments/:id if you need its state after the update. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateOutboundPayment) ## 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. | | `id` | path | string | yes | The unique identifier for the outbound payment to update. | ## Request body Required, `application/json`: updateOutboundPaymentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `accountId` | string | no | The bank or cash account the payment is made from. See id on GET accounting/accounts/:id. | | `paymentDate` | string (date) | no | The date the payment is made. Use format YYYY-MM-DD. | | `paymentCurrency` | string | no | The currency of the payment. Format ISO 4217, for example 'USD'. Governs each item's amount unless the item names its own currency. | | `commonDescription` | string | no | A description applied to the payment as a whole. | | `externalUrl` | string | no | A link back to the payment in the upstream system. | | `origin` | string | no | The source system this payment came from. | | `senderAccountId` | string | no | A bank account identifier for the sender, supplied by an integration. It is free-form text, not a reference to an account record. | | `actualExchangeRate` | string (decimal) | no | The exchange rate the bank actually applied, as a decimal. | | `systemExchangeRate` | string (decimal) | no | The exchange rate the system derived, as a decimal. | | `skipJournalEntry` | boolean | no | Whether to skip journal-entry creation for this payment. | | `createPaymentItems` | array of object | no | Documents to add to this payment. Each has the same shape as on create, without an id. | | `createPaymentItems[].documentId` | string | yes | The document being paid, of the kind this payment already pays. See id on GET accounting/vendor-bills/:id or GET accounting/expense-reports/:id. | | `createPaymentItems[].entityId` | string | yes | The legal entity the document is posted under. See id on GET base/entities. | | `createPaymentItems[].amount` | string (decimal) | yes | The amount applied to this document, as a decimal. | | `createPaymentItems[].currency` | string | no | The currency for this item. Format ISO 4217, for example 'USD'. Omit to use the payment's currency. | | `createPaymentItems[].description` | string | yes | What this item pays for. Required. | | `updatePaymentItems` | array of object | no | Payment items to change, each addressed by the id the payment publishes. | | `updatePaymentItems[].id` | string | yes | The unique identifier for the payment item to change. It must belong to this payment. | | `updatePaymentItems[].documentId` | string | no | The document being paid. See id on GET accounting/vendor-bills/:id or GET accounting/expense-reports/:id. | | `updatePaymentItems[].entityId` | string | no | The legal entity the document is posted under. See id on GET base/entities. | | `updatePaymentItems[].amount` | string (decimal) | no | The amount applied to this document, as a decimal. | | `updatePaymentItems[].currency` | string | no | The currency for this item. Format ISO 4217, for example 'USD'. | | `updatePaymentItems[].description` | string | no | What this item pays for. | | `deletePaymentItems` | array of string | no | The identifiers of payment items to remove, applied before the updates and creates. Each must belong to this payment. Removing them all is refused, so delete the payment instead. | Example: ```json { "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "paymentDate": "2026-08-28", "paymentCurrency": "", "commonDescription": "August vendor run (revised)", "externalUrl": "", "origin": "", "senderAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "actualExchangeRate": "19.99", "systemExchangeRate": "19.99", "skipJournalEntry": true, "createPaymentItems": [ { "documentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "amount": "19.99", "currency": "", "description": "" } ], "updatePaymentItems": [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "documentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "amount": "19.99", "currency": "", "description": "" } ], "deletePaymentItems": [ "" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `PAYMENT_NOT_EDITABLE` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `ITEM_NOT_FOUND` | 400 | | `NO_PAYMENT_ITEMS` | 400 | | `DOCUMENT_NOT_PAYABLE` | 400 | | `PAYMENT_AMOUNT_EXCEEDS_OPEN_AMOUNT` | 400 | | `DOCUMENT_CURRENCY_MISMATCH` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, PAYMENT_NOT_EDITABLE, REFERENCE_NOT_FOUND, ITEM_NOT_FOUND, NO_PAYMENT_ITEMS, DOCUMENT_NOT_PAYABLE, PAYMENT_AMOUNT_EXCEEDS_OPEN_AMOUNT, DOCUMENT_CURRENCY_MISMATCH - `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 --- # Deletes an outbound payment. `DELETE /accounting/outbound-payments/{id}` Group: Accounting › Outbound Payments · Operation ID: `deleteOutboundPayment` The response is empty. The documents the payment settled are released back to unpaid. Only a payment that has not moved money can be deleted, one that is manually recorded, scheduled, rejected, cancelled or of unknown status. Any other status fails with PAYMENT_NOT_EDITABLE, and that payment must be voided instead with POST accounting/outbound-payments/:id/void. A payment whose posting period is closed or locked fails with POSTING_PERIOD_CLOSED. A draft payment skips both checks. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteOutboundPayment) ## 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. | | `id` | path | string | yes | The unique identifier for the outbound payment to delete. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `PAYMENT_NOT_EDITABLE` | 400 | | `POSTING_PERIOD_CLOSED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, PAYMENT_NOT_EDITABLE, POSTING_PERIOD_CLOSED - `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 --- # Voids an outbound payment. `POST /accounting/outbound-payments/{id}/void` Group: Accounting › Outbound Payments · Operation ID: `voidOutboundPayment` Voids one outbound payment and the journal entry behind it, so the money movement is withdrawn from the ledger. Void and delete cover different states. Delete accepts a payment that has not been executed, one that is manuallyRecorded, scheduled, unknown, rejected or cancelled. Void covers a payment that has been executed, one that is successful, partiallySuccessful, manuallyRecorded or processing. manuallyRecorded is in both. Any other status fails with PAYMENT_NOT_VOIDABLE. Voiding is terminal, so a second void fails with PAYMENT_ALREADY_VOIDED. A voidReason is required and is stored on the payment for an auditor to read, so an empty or whitespace-only string is refused. The applications the payment made are released, so the bills or expense reports it paid become open again. The posting period must be open. The response is empty. Read the payment with GET accounting/outbound-payments/:id to see its voided state. [Open in the interactive reference](https://docs.api.lensing.ai/?op=voidOutboundPayment) ## 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. | | `id` | path | string | yes | The unique identifier for the outbound payment to void. | ## Request body Required, `application/json`: voidOutboundPaymentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `voidReason` | string | yes | Why the payment is being voided. It is stored on the payment and shown in the audit trail, and an empty or whitespace-only string is refused. | Example: ```json { "voidReason": "Paid the wrong vendor" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `VOID_REASON_REQUIRED` | 400 | | `PAYMENT_ALREADY_VOIDED` | 400 | | `PAYMENT_NOT_VOIDABLE` | 400 | | `POSTING_PERIOD_CLOSED` | 400 | | `VOID_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, VOID_REASON_REQUIRED, PAYMENT_ALREADY_VOIDED, PAYMENT_NOT_VOIDABLE, POSTING_PERIOD_CLOSED, VOID_FAILED - `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 --- # Search Outbound Payment `POST /accounting/outbound-payments/search` Group: Accounting › Outbound Payments · Operation ID: `searchOutboundPayment` Returns the outbound payments that match the given filters, each with its lines. Searches outbound payments and pages through the matches, returning the full record, header and lines, for each. Matching nothing returns an empty items array, not an error. Payee bank account and sort code come back on the matches but cannot be searched on. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchOutboundPayment) ## 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`. | ## Request body Required, `application/json`: searchOutboundPaymentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. An outbound payment must match every filter you send. Omit to return all outbound payments. | | `filters.id` | string | no | Limits results to the outbound payment with this identifier. | | `filters.outboundPaymentNumber` | string | no | Limits results to the payment with this number. | | `filters.paymentId` | string | no | Limits results to payments with this bank or provider reference. | | `filters.status` | string | no | Limits results to payments in this processing status. Any other value matches no payment. | | `filters.approvalStatus` | string | no | Limits results to payments in this approval state. Any other value matches no payment. | | `filters.applicationStatus` | string | no | Limits results by how much of the payment has been applied. Values include 'unapplied', 'partiallyApplied' and 'applied'. | | `filters.paymentType` | string | no | Limits results to payments sent this way, for example 'ach', 'wire' or 'check'. | | `filters.paymentCurrency` | string | no | Limits results to payments denominated in this currency. Format ISO 4217, for example 'USD'. | | `filters.paymentAccountId` | string | no | Limits results to payments made from this bank GL account. See id on GET accounting/accounts/:id. | | `filters.entityId` | string | no | Limits results to payments made by this legal entity. See id on GET base/entities. | | `filters.entityName` | string | no | Limits results to payments made by the legal entity with this name. The match is exact. Use entityId when you already have the id. | | `filters.origin` | string | no | Limits results to payments created by this source system. The match is exact. | | `filters.externalId` | string | no | Limits results to payments with this upstream identifier. It is not unique, so this can match several payments. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of payments per page. Defaults to 50, and a value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "applicationStatus": "unapplied" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchOutboundPaymentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The outbound payments on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the outbound payment. | | `items[].outboundPaymentNumber` | string | no | The outbound payment number. Absent until a number has been assigned. | | `items[].description` | string | no | A description of the payment. | | `items[].paymentDate` | string (date) | yes | The date the payment was made. Format YYYY-MM-DD. | | `items[].paymentCurrency` | string | yes | The currency the payment is denominated in. Format ISO 4217, for example 'USD'. Governs totalPaid, appliedAmount and unappliedAmount. | | `items[].paymentType` | string | yes | How the money was sent, for example 'ach', 'wire' or 'check'. | | `items[].status` | string | yes | The processing status of the payment. | | `items[].approvalStatus` | string | no | Where the payment sits in the approval workflow. | | `items[].applicationStatus` | string | no | How much of the payment has been applied to bills. Values include 'unapplied', 'partiallyApplied' and 'applied'. | | `items[].totalPaid` | string (decimal) | no | The total amount paid, in the payment currency. | | `items[].appliedAmount` | string (decimal) | no | The portion already applied against vendor bills, in the payment currency. | | `items[].unappliedAmount` | string (decimal) | no | The portion not yet applied to any bill, in the payment currency. | | `items[].bankCurrency` | string | no | The currency of the bank account the money left, when it differs from the payment currency. Format ISO 4217, for example 'USD'. | | `items[].paymentId` | string | no | The payment reference assigned by the bank or payment provider. | | `items[].externalId` | string | no | The identifier this payment has in the upstream system that produced it. It is not unique. | | `items[].externalUrl` | string | no | A link to the payment in an external system, when one is stored. | | `items[].origin` | string | no | The source system this payment came from. Recorded when an integration created the payment. | | `items[].singlePayment` | boolean | no | Whether the payment covers a single bill rather than a batch. | | `items[].paymentAccountId` | string | no | The bank GL account the money left. See id on GET accounting/accounts/:id. | | `items[].paymentAccountName` | string | no | The name of the bank GL account the money left. | | `items[].entityId` | string | no | The legal entity that made the payment. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the legal entity that made the payment. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the payment last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | no | The payment lines, one per payee or bill covered. An empty array when the payment has none. | | `items[].lines[].id` | string | yes | The unique identifier for the line. | | `items[].lines[].lineNumber` | string | no | The line number. | | `items[].lines[].description` | string | yes | A description of the line. | | `items[].lines[].amount` | string (decimal) | no | The amount paid on this line, in the line currency. | | `items[].lines[].currency` | string | yes | The currency for the line. Format ISO 4217, for example 'USD'. | | `items[].lines[].paymentDate` | string (date) | no | The date this line was paid, when it differs from the header. Format YYYY-MM-DD. | | `items[].lines[].paymentType` | string | no | How this line was sent, for example 'ach', 'wire' or 'check'. | | `items[].lines[].receiverName` | string | yes | The name of the payee. | | `items[].lines[].receiverAccount` | string | no | The payee bank account number. It is returned here and on search but never on the collection endpoint and never filterable. | | `items[].lines[].receiverBankCode` | string | no | The payee sort or routing code. It is returned here and on search but never on the collection endpoint and never filterable. | | `items[].lines[].customReference` | string | no | A reference you supplied, carried on the line. | | `pagination` | object | yes | The pagination details for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Payment Terms `GET /accounting/payment-terms` Group: Accounting › Payment Terms · Operation ID: `paymentTerms` List payment terms with structured filters. GET data-set exposing filter[field][op]= (eq/ne/gt/gte/lt/lte/in/notIn/between/like), sort=-numberOfDays,code, sparse fields=, and page[size]/page[cursor] cursor pagination. Returns the catalogue of agreed payment schedules and their early-payment discounts. Every request must carry the Api-Version header: a missing or empty header is rejected with 400, an identifier naming no version of the company with 400, and a route that exists but not in the requested version with 404. [Open in the interactive reference](https://docs.api.lensing.ai/?op=paymentTerms) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `code` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `numberOfDays` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `discountPercentage` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `discountValidityDays` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, code, label, numberOfDays, discountPercentage, discountValidityDays, lastModifiedDate - Selectable: id, code, label, description, numberOfDays, discountPercentage, discountValidityDays, lastModifiedDate ## Response `200` Success Under `data`, an array of paymentTermsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | Stable, platform-assigned id (a UUID) of the payment term — the public identifier other accounting contracts reference a term by. | | `code` | string | no | Stable machine code of the payment term, unique within the tenant and conventionally 'net' plus the day count, e.g. 'net30'. This is the key other records associate on, and the value integrations hold. Filter target: filter[code][eq]=net30, or filter[code][in]=net30,net60. | | `label` | string | no | Human-readable label of the payment term, e.g. 'Net 30'. Orderable for alphabetical listing but not filterable — filter on `code` instead, which identifies the same term. | | `description` | string | no | Longer description of the payment term. Returned only: in neither filterableFields nor orderableFields, because it is free-form prose (R7.2). | | `numberOfDays` | number | no | Number of days after the invoice date on which payment becomes due. Filter target: filter[numberOfDays][lte]=30, or filter[numberOfDays][between]=30,60. A day count, not an amount. | | `discountPercentage` | string (decimal) or null | no | Early-payment discount granted when the invoice is settled within discountValidityDays, as a FRACTION of the invoice amount — 0.02 means 2%, not 2. Bounded to the range 0..1 inclusive, 1 being a 100% discount, and held to two decimal places — so the practical granularity is whole percentage points: 0.02 (2%) round-trips, while 0.025 (2.5%) is rounded to 0.03. A ratio, so it carries no currency (R9.4c). Zero, not absent, on a term that grants no discount: the column defaults to 0, so treat 0 rather than omission as 'no discount'. Above zero it is always paired with a non-zero discountValidityDays. Filter target: filter[discountPercentage][gt]=0 to find terms that discount at all. | | `discountValidityDays` | number or null | no | Number of days after the invoice date within which payment earns discountPercentage. A day count, not an amount. Zero, not absent, on a term that grants no discount — the column defaults to 0 — and above zero exactly when discountPercentage is above zero, since the two are only meaningful together. | | `lastModifiedDate` | string (date-time) | no | Instant the payment term last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Filter and sort on this to sync incrementally: filter[lastModifiedDate][gte]=2026-08-01T00:00:00.000Z with sort=lastModifiedDate. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Payments `GET /accounting/payments` Group: Accounting › Payments · Operation ID: `payments` Returns a list of payments, each with its amounts. For the customer, entity and account references, the payment reference and the note, use GET accounting/payments/:id or POST accounting/payments/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=payments) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `customerPaymentStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `disputeStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, paymentDate, paymentType, currency, customerPaymentStatus, disputeStatus, amount, unappliedAmount, lastModifiedDate - Selectable: id, paymentNumber, paymentDate, paymentType, currency, paymentAccountCurrency, exchangeRate, amount, appliedAmount, unappliedAmount, refundAmount, bankFeeAmount, smallBalanceWriteOffAmount, totalEarlyPaymentDiscount, customerPaymentStatus, disputeStatus, customerName, accountName, lastModifiedDate ## Response `200` Success Under `data`, an array of paymentsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the payment. | | `paymentNumber` | string or null | no | The payment number, unique within the tenant. A human-readable reference used for reconciliation. | | `paymentDate` | string (date) | no | The date the payment was made. Format YYYY-MM-DD. | | `paymentType` | string | no | The payment method. Possible values: 'check', 'ach', 'wire', 'creditCard', 'directDebit', 'sepaCreditTransfer'. | | `currency` | string | no | The currency the customer paid in. Every amount on the payment is in this currency. Format ISO 4217, for example 'USD'. | | `paymentAccountCurrency` | string or null | no | The currency of the bank account the payment was received into. Present only when it differs from the payment currency. Format ISO 4217, for example 'USD'. | | `exchangeRate` | string (decimal) or null | no | The rate used to convert the payment currency to the entity's functional currency. A ratio, not an amount, with no currency of its own. | | `amount` | string (decimal) or null | no | The amount paid, in the payment currency. | | `appliedAmount` | string (decimal) or null | no | The portion already applied to invoices, in the payment currency. The sum of the payment's applications. | | `unappliedAmount` | string (decimal) or null | no | The portion not yet applied to any invoice, in the payment currency. unappliedAmount = amount - appliedAmount - refundAmount. | | `refundAmount` | string (decimal) or null | no | The portion refunded through approved refunds, in the payment currency. | | `bankFeeAmount` | string (decimal) or null | no | The bank fee deducted from the payment, in the payment currency. | | `smallBalanceWriteOffAmount` | string (decimal) or null | no | The total written off as a small residual balance across the payment's applications, in the payment currency. | | `totalEarlyPaymentDiscount` | string (decimal) or null | no | The total early-payment discount granted across the payment's applications, in the payment currency. | | `customerPaymentStatus` | string or null | no | The derived lifecycle status of the payment. Possible values: 'unapplied', 'partiallyApplied', 'appliedInFull', 'partiallyRefunded', 'refunded', 'disputed', 'chargedBack'. | | `disputeStatus` | string or null | no | The dispute state of the payment. Possible values: 'disputed', 'chargedBack'. Absent on an undisputed payment. | | `customerName` | string or null | no | The name of the customer the payment came from. | | `accountName` | string or null | no | The name of the ledger account the payment was posted to. | | `lastModifiedDate` | string (date-time) | no | The date and time the payment last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a payment for a customer. `POST /accounting/payments` Group: Accounting › Payments · Operation ID: `createPayment` [Open in the interactive reference](https://docs.api.lensing.ai/?op=createPayment) ## 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`: createPaymentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `customerId` | string | yes | The customer making the payment. See id on GET accounting/customers/:id. | | `entityId` | string | yes | The legal entity receiving the payment. See id on GET base/entities. | | `amount` | string (decimal) | yes | The total payment amount, in the payment currency. Must be greater than 0. | | `currency` | string | yes | The currency the customer paid in. Every amount on the payment is in this currency. Format ISO 4217, for example 'USD'. | | `paymentDate` | string (date) | yes | The date the payment was made. Use format YYYY-MM-DD. | | `paymentType` | string, one of `"check"`, `"ach"`, `"wire"`, `"creditCard"`, `"directDebit"`, `"sepaCreditTransfer"` | yes | The payment method. | | `accountId` | string | no | The ledger account to post the payment to. Omit to use the entity's configured electronic payment account, which fails with DEFAULT_ACCOUNT_NOT_CONFIGURED when it is not set. See id on GET accounting/accounts/:id. | | `paymentReference` | string | no | The reference the payer supplied with the payment, for example a check or wire reference. | | `paymentNote` | string | no | A free-form note to record on the payment. | | `bankFeeAmount` | string (decimal) | no | The bank fee deducted from the payment, in the payment currency. | | `thirdPartyPayerId` | string | no | The customer actually making the payment, when a third party pays on the invoiced customer's behalf. See id on GET accounting/customers/:id. | Example: ```json { "customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "amount": "19.99", "currency": "", "paymentDate": "2026-01-15", "paymentType": "check", "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "paymentReference": "", "paymentNote": "", "bankFeeAmount": "19.99", "thirdPartyPayerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` ## Response `200` Success Under `data` (createPaymentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created payment. | ## 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 | | --- | --- | | `INVALID_AMOUNT` | 400 | | `CUSTOMER_NOT_FOUND` | 400 | | `ENTITY_NOT_FOUND` | 400 | | `ACCOUNT_NOT_FOUND` | 400 | | `THIRD_PARTY_PAYER_NOT_FOUND` | 400 | | `DEFAULT_ACCOUNT_NOT_CONFIGURED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_AMOUNT, CUSTOMER_NOT_FOUND, ENTITY_NOT_FOUND, ACCOUNT_NOT_FOUND, THIRD_PARTY_PAYER_NOT_FOUND, DEFAULT_ACCOUNT_NOT_CONFIGURED - `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 --- # Get Payment `GET /accounting/payments/{id}` Group: Accounting › Payments · Operation ID: `getPayment` Returns the payment with the given identifier. Use the payment's id, not its paymentNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getPayment) ## 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`. | | `id` | path | string | yes | The unique identifier for the payment to return. | ## Response `200` Success Under `data` (getPaymentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the payment. | | `paymentNumber` | string | no | The payment number, unique within the tenant. A human-readable reference used for reconciliation. | | `paymentDate` | string (date) | yes | The date the payment was made. Format YYYY-MM-DD. | | `paymentType` | string | yes | The payment method. Possible values: 'check', 'ach', 'wire', 'creditCard', 'directDebit', 'sepaCreditTransfer'. | | `currency` | string | yes | The currency the customer paid in. Every amount on the payment is in this currency. Format ISO 4217, for example 'USD'. | | `paymentAccountCurrency` | string | no | The currency of the bank account the payment was received into. Present only when it differs from the payment currency. Format ISO 4217, for example 'USD'. | | `amount` | string (decimal) | no | The amount paid, in the payment currency. | | `appliedAmount` | string (decimal) | no | The portion already applied to invoices, in the payment currency. The sum of the payment's applications. | | `unappliedAmount` | string (decimal) | no | The portion not yet applied to any invoice, in the payment currency. unappliedAmount = amount - appliedAmount - refundAmount. | | `refundAmount` | string (decimal) | no | The portion refunded through approved refunds, in the payment currency. | | `bankFeeAmount` | string (decimal) | no | The bank fee deducted from the payment, in the payment currency. | | `smallBalanceWriteOffAmount` | string (decimal) | no | The total written off as a small residual balance across the payment's applications, in the payment currency. | | `totalEarlyPaymentDiscount` | string (decimal) | no | The total early-payment discount granted across the payment's applications, in the payment currency. | | `exchangeRate` | string (decimal) | no | The rate used to convert the payment currency to the entity's functional currency. A ratio, not an amount, with no currency of its own. | | `customerPaymentStatus` | string | no | The derived lifecycle status of the payment. Possible values: 'unapplied', 'partiallyApplied', 'appliedInFull', 'partiallyRefunded', 'refunded', 'disputed', 'chargedBack'. | | `disputeStatus` | string | no | The dispute state of the payment. Possible values: 'disputed', 'chargedBack'. Absent on an undisputed payment. | | `paymentReference` | string | no | The reference the payer supplied with the payment, for example a remittance or wire reference. Absent when none was supplied. | | `paymentNote` | string | no | A free-form note recorded on the payment. Absent when none was set. | | `customerId` | string | no | The customer the payment came from. See id on GET accounting/customers/:id. | | `customerName` | string | no | The name of the customer the payment came from. | | `thirdPartyPayerId` | string | no | The customer who actually paid, when a third party paid on the invoiced customer's behalf. Absent when the invoiced customer paid. See id on GET accounting/customers/:id. | | `entityId` | string | no | The legal entity that received the payment. See id on GET base/entities. | | `accountId` | string | no | The ledger account the payment was posted to. See id on GET accounting/accounts/:id. | | `accountName` | string | no | The name of the ledger account the payment was posted to. | | `lastModifiedDate` | string (date-time) | yes | The date and time the payment last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Payment `PATCH /accounting/payments/{id}` Group: Accounting › Payments · Operation ID: `updatePayment` Updates the payment with the given identifier. Only the fields in the request body change. Use the payment's id, not its paymentNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updatePayment) ## 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. | | `id` | path | string | yes | The unique identifier for the payment to update. | ## Request body Required, `application/json`: updatePaymentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `paymentDate` | string (date) | no | The new payment date. Use format YYYY-MM-DD. Omit to leave it unchanged. | | `paymentType` | string, one of `"check"`, `"ach"`, `"wire"`, `"creditCard"`, `"directDebit"`, `"sepaCreditTransfer"` | no | The new payment method. Omit to leave it unchanged. | | `amount` | string (decimal) | no | The new total payment amount, in the payment's currency. Must be greater than 0. Omit to leave it unchanged. | | `accountId` | string | no | A different ledger account to post the payment to. Omit to leave it unchanged. See id on GET accounting/accounts/:id. | | `paymentReference` | string or null | no | A new payer-supplied reference. Omit to leave it unchanged. Pass null to clear it. | | `paymentNote` | string or null | no | A new free-form note. Omit to leave it unchanged. Pass null to clear it. | | `bankFeeAmount` | string (decimal) or null | no | A new bank fee, in the payment's currency. Omit to leave it unchanged. Pass null to clear it. | | `thirdPartyPayerId` | string or null | no | A different third-party payer. Omit to leave it unchanged. Pass null to clear it, attributing the payment back to the invoiced customer. See id on GET accounting/customers/:id. | Example: ```json { "paymentDate": "2026-01-15", "paymentType": "check", "amount": "19.99", "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "paymentReference": "", "paymentNote": "", "bankFeeAmount": "19.99", "thirdPartyPayerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `INVALID_AMOUNT` | 400 | | `ACCOUNT_NOT_FOUND` | 400 | | `THIRD_PARTY_PAYER_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, INVALID_AMOUNT, ACCOUNT_NOT_FOUND, THIRD_PARTY_PAYER_NOT_FOUND - `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 --- # Applies a payment to one or more invoices. `POST /accounting/payments/{id}/apply` Group: Accounting › Payments · Operation ID: `applyPayment` [Open in the interactive reference](https://docs.api.lensing.ai/?op=applyPayment) ## 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. | | `id` | path | string | yes | The unique identifier for the payment to apply. | ## Request body Required, `application/json`: applyPaymentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `invoices` | array of object | yes | The per-invoice amounts to apply from the payment. | | `invoices[].invoiceId` | string | yes | The invoice to apply the payment to. See id on GET accounting/invoices/:id. | | `invoices[].amount` | string (decimal) | yes | The amount to apply to this invoice, in the payment currency. Must be greater than 0. | | `invoices[].appliedDate` | string (date) | yes | The date the application is recognized. Use format YYYY-MM-DD. | | `invoices[].earlyPaymentDiscount` | string (decimal) | no | The early-payment discount granted on this application, in the payment currency. | | `invoices[].smallBalanceWriteOffAmount` | string (decimal) | no | The small-balance write-off amount on this application, in the payment currency. | Example: ```json { "invoices": [ { "invoiceId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "amount": "19.99", "appliedDate": "2026-01-15", "earlyPaymentDiscount": "19.99", "smallBalanceWriteOffAmount": "19.99" } ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `INVALID_AMOUNT` | 400 | | `INVOICE_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, INVALID_AMOUNT, INVOICE_NOT_FOUND - `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 --- # Reverses a payment's application to one or more invoices. `POST /accounting/payments/{id}/unapply` Group: Accounting › Payments · Operation ID: `unapplyPayment` [Open in the interactive reference](https://docs.api.lensing.ai/?op=unapplyPayment) ## 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. | | `id` | path | string | yes | The unique identifier for the payment to unapply. | ## Request body Required, `application/json`: unapplyPaymentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `invoices` | array of object | yes | The per-invoice amounts to unapply from the payment. | | `invoices[].invoiceId` | string | yes | The invoice to unapply the payment from. See id on GET accounting/invoices/:id. | | `invoices[].amount` | string (decimal) | yes | The amount to unapply from this invoice, in the payment currency. Must be greater than 0. | | `invoices[].unappliedDate` | string (date) | yes | The date the reversal is recognized. Use format YYYY-MM-DD. | | `invoices[].earlyPaymentDiscount` | string (decimal) | no | The early-payment discount to reverse for this invoice, in the payment currency. | | `invoices[].smallBalanceWriteOffAmount` | string (decimal) | no | The small-balance write-off amount to reverse for this invoice, in the payment currency. | Example: ```json { "invoices": [ { "invoiceId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "amount": "19.99", "unappliedDate": "2026-01-15", "earlyPaymentDiscount": "19.99", "smallBalanceWriteOffAmount": "19.99" } ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `INVALID_AMOUNT` | 400 | | `INVOICE_NOT_FOUND` | 400 | | `LOCKED_ACCOUNTING_PERIOD` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, INVALID_AMOUNT, INVOICE_NOT_FOUND, LOCKED_ACCOUNTING_PERIOD - `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 --- # Returns the payments that match the given filters. `POST /accounting/payments/search` Group: Accounting › Payments · Operation ID: `searchPayment` [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchPayment) ## 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`. | ## Request body Required, `application/json`: searchPaymentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A payment must match every filter you send. Omit to return all payments. | | `filters.id` | string | no | Limits results to the payment with this identifier. | | `filters.paymentNumber` | string | no | Limits results to the payment with this payment number. | | `filters.paymentType` | string | no | Limits results to payments tendered this way. Possible values: 'check', 'ach', 'wire', 'creditCard', 'directDebit', 'sepaCreditTransfer'. Any other value matches no payment. | | `filters.customerPaymentStatus` | string | no | Limits results to payments in this lifecycle status. Possible values: 'unapplied', 'partiallyApplied', 'appliedInFull', 'partiallyRefunded', 'refunded', 'disputed', 'chargedBack'. Any other value matches no payment. | | `filters.disputeStatus` | string | no | Limits results to payments in this dispute state. Possible values: 'disputed', 'chargedBack'. Any other value matches no payment. | | `filters.paymentReference` | string | no | Limits results to payments with this payer-supplied reference. | | `filters.customerId` | string | no | Limits results to payments from this customer. See id on GET accounting/customers/:id. | | `filters.entityId` | string | no | Limits results to payments received by this legal entity. See id on GET base/entities. | | `filters.paymentDate` | object | no | Limits results to payments whose paymentDate falls in this range. Both bounds are inclusive. Omit either bound to leave that side open. | | `filters.paymentDate.start` | string (date) | no | The earliest paymentDate to include, inclusive. Format YYYY-MM-DD. | | `filters.paymentDate.end` | string (date) | no | The latest paymentDate to include, inclusive. Format YYYY-MM-DD. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of payments per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "paymentNumber": "", "paymentType": "", "customerPaymentStatus": "", "disputeStatus": "", "paymentReference": "", "customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "paymentDate": { "start": "2026-01-15", "end": "2026-01-15" } }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchPaymentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The payments that match, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the payment. | | `items[].paymentNumber` | string | no | The payment number, unique within the tenant. A human-readable reference used for reconciliation. | | `items[].paymentDate` | string (date) | yes | The date the payment was made. Format YYYY-MM-DD. | | `items[].paymentType` | string | yes | The payment method. Possible values: 'check', 'ach', 'wire', 'creditCard', 'directDebit', 'sepaCreditTransfer'. | | `items[].currency` | string | yes | The currency the customer paid in. Every amount on the payment is in this currency. Format ISO 4217, for example 'USD'. | | `items[].paymentAccountCurrency` | string | no | The currency of the bank account the payment was received into. Present only when it differs from the payment currency. Format ISO 4217, for example 'USD'. | | `items[].amount` | string (decimal) | no | The amount paid, in the payment currency. | | `items[].appliedAmount` | string (decimal) | no | The portion already applied to invoices, in the payment currency. The sum of the payment's applications. | | `items[].unappliedAmount` | string (decimal) | no | The portion not yet applied to any invoice, in the payment currency. unappliedAmount = amount - appliedAmount - refundAmount. | | `items[].refundAmount` | string (decimal) | no | The portion refunded through approved refunds, in the payment currency. | | `items[].bankFeeAmount` | string (decimal) | no | The bank fee deducted from the payment, in the payment currency. | | `items[].smallBalanceWriteOffAmount` | string (decimal) | no | The total written off as a small residual balance across the payment's applications, in the payment currency. | | `items[].totalEarlyPaymentDiscount` | string (decimal) | no | The total early-payment discount granted across the payment's applications, in the payment currency. | | `items[].exchangeRate` | string (decimal) | no | The rate used to convert the payment currency to the entity's functional currency. A ratio, not an amount, with no currency of its own. | | `items[].customerPaymentStatus` | string | no | The derived lifecycle status of the payment. Possible values: 'unapplied', 'partiallyApplied', 'appliedInFull', 'partiallyRefunded', 'refunded', 'disputed', 'chargedBack'. | | `items[].disputeStatus` | string | no | The dispute state of the payment. Possible values: 'disputed', 'chargedBack'. Absent on an undisputed payment. | | `items[].paymentReference` | string | no | The reference the payer supplied with the payment, for example a remittance or wire reference. Absent when none was supplied. | | `items[].paymentNote` | string | no | A free-form note recorded on the payment. Absent when none was set. | | `items[].customerId` | string | no | The customer the payment came from. See id on GET accounting/customers/:id. | | `items[].customerName` | string | no | The name of the customer the payment came from. | | `items[].thirdPartyPayerId` | string | no | The customer who actually paid, when a third party paid on the invoiced customer's behalf. Absent when the invoiced customer paid. See id on GET accounting/customers/:id. | | `items[].entityId` | string | no | The legal entity that received the payment. See id on GET base/entities. | | `items[].accountId` | string | no | The ledger account the payment was posted to. See id on GET accounting/accounts/:id. | | `items[].accountName` | string | no | The name of the ledger account the payment was posted to. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the payment last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Payroll Details `GET /accounting/payroll-details` Group: Accounting › Payroll Details · Operation ID: `payrollDetails` Returns a list of payroll runs. Each entry is the run header. Read one run with its lines from GET accounting/payroll-details/:id or POST accounting/payroll-details/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=payrollDetails) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `payrollDetailNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `periodName` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `payrollDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `startDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `endDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `grouping` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isImported` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, payrollDetailNumber, periodName, payrollDate, startDate, endDate, currency, grouping, paymentStatus, lastModifiedDate - Selectable: id, payrollDetailNumber, description, periodName, payrollDate, startDate, endDate, currency, grouping, paymentStatus, isImported, entityName, lastModifiedDate ## Response `200` Success Under `data`, an array of payrollDetailsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the payroll run. | | `payrollDetailNumber` | string or null | no | The payroll run number. Absent until a number has been assigned. | | `description` | string or null | no | A description of the payroll run. | | `periodName` | string or null | no | The name of the payroll period this run covers, for example '2026-08'. | | `payrollDate` | string (date) or null | no | The date the payroll is dated. Format YYYY-MM-DD. | | `startDate` | string (date) or null | no | The first day of the period the run covers. Format YYYY-MM-DD. | | `endDate` | string (date) or null | no | The last day of the period the run covers. Format YYYY-MM-DD. | | `currency` | string or null | no | The currency the run is denominated in. Format ISO 4217, for example 'USD'. | | `grouping` | string or null | no | How the payroll run is grouped. Values include 'Employee', 'Department' and 'Multi-level'. | | `paymentStatus` | string or null | no | The payment status of the run. Values include 'unpaid', 'partiallyPaid', 'fullyPaid', 'pending', 'notApplicable' and 'voided'. | | `isImported` | boolean or null | no | Whether the run was imported from a payroll provider rather than entered by hand. | | `entityName` | string or null | no | The name of the legal entity the run belongs to. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the payroll run last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a payroll detail. `POST /accounting/payroll-details` Group: Accounting › Payroll Details · Operation ID: `createPayrollDetail` The response contains only the new run's id. Read the record with GET accounting/payroll-details/:id if you need its stored state, where payrollDetailNumber appears once assigned. The payroll must post in balance: gross pay must equal deductions plus net pay across every line, or the request is refused with AMOUNT_NOT_BALANCED. The posting period is derived, not supplied: the template's journal-entry-date rule picks the payroll date or the period end date, and the entity's default transactions book supplies the period, so a missing book, a date no period covers, and a closed or locked period are three distinct errors. The payroll is created non-draft and, unless status says otherwise, pending approval. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createPayrollDetail) ## 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`: createPayrollDetailInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `templateId` | string | yes | The payroll template this run uses. See id on GET accounting/payroll-templates/:id. It supplies the categories, the journal-entry-date rule, the group-by and the clearing account. | | `entityId` | string | yes | The legal entity that owns the payroll. See id on GET base/entities. | | `grouping` | string | yes | How the payroll is grouped. Values include 'Employee', 'Department' and 'Multi-level'. | | `status` | string | no | The status of the payroll. Defaults to 'pendingApproval' when omitted. | | `payrollDate` | string (date) | yes | The issue date of the payroll. Use format YYYY-MM-DD. | | `startDate` | string (date) | no | The first day of the payroll period. Use format YYYY-MM-DD. | | `endDate` | string (date) | no | The last day of the payroll period, which must not be before the start date. Use format YYYY-MM-DD. | | `currency` | string | yes | The currency the payroll is denominated in. Format ISO 4217, for example 'USD'. | | `description` | string | no | A description of the payroll. | | `isNetPayCategoryMapped` | boolean | no | Whether the net-pay category is matched in the account mapping. Defaults to false. | | `origin` | string | no | The source system this record came from, as free-form text, for example 'Ramp'. Omit it and it stays unset. The server does not set a value. | | `lines` | array of object | yes | The payroll's lines, one per employee or department depending on the grouping. | | `lines[].employeeId` | string | no | The employee this line is for. There is no endpoint for it. With grouping 'Employee', a line that names an employee but no department has the department backfilled from that employee's primary job. | | `lines[].departmentId` | string | no | The department this line is charged to. See id on GET departments/:id. | | `lines[].lineItems` | array of object | yes | The amounts on this line, one per payroll category. Each category must be reachable from the payroll's template. An item whose amount is null is dropped before the payroll is saved. | | `lines[].lineItems[].categoryId` | string | yes | The payroll category this amount belongs to. There is no endpoint for it. It must be an active category of the payroll's template. | | `lines[].lineItems[].amount` | string (decimal) | yes | The amount for this category, as a decimal in the payroll's currency. | | `attachments` | array of object | no | Files to attach to the payroll. Omit for none, unless your tenant requires an attachment, in which case omitting them fails with ATTACHMENT_REQUIRED. | | `attachments[].fileName` | string | yes | The display name of the attached file. | | `attachments[].fileId` | string | yes | The identifier of the uploaded file in file storage, as a text reference. There is no endpoint for it. | Example: ```json { "templateId": "b7e4c1a9-3f52-4d86-9017-5a2c8e0b4d61", "entityId": "7c1a9e40-2b53-4d68-9f01-3a5b7c9d1e24", "grouping": "Employee", "status": "", "payrollDate": "2026-08-31", "startDate": "2026-08-01", "endDate": "2026-08-31", "currency": "USD", "description": "August 2026 payroll", "isNetPayCategoryMapped": true, "origin": "", "lines": [ { "employeeId": "3f1a8c25-9d47-4e60-b892-0c5a7e3b1d84", "lineItems": [ { "categoryId": "9b2d5e10-4c78-4a36-8f51-6e0a3c7b2d95", "amount": "5000.00" }, { "categoryId": "e5c7a390-1d64-4b28-9a07-2f8b6d4c1e53", "amount": "5000.00" } ] } ], "attachments": [ { "fileName": "", "fileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ] } ``` ## Response `200` Success Under `data` (createPayrollDetailOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created payroll run. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `CATEGORY_NOT_IN_TEMPLATE` | 400 | | `INVALID_DATE_RANGE` | 400 | | `AMOUNT_NOT_BALANCED` | 400 | | `NO_ACCOUNTING_BOOK` | 400 | | `NO_ACCOUNTING_PERIOD` | 400 | | `POSTING_PERIOD_CLOSED` | 400 | | `TOO_MANY_LINES` | 400 | | `ATTACHMENT_REQUIRED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, CATEGORY_NOT_IN_TEMPLATE, INVALID_DATE_RANGE, AMOUNT_NOT_BALANCED, NO_ACCOUNTING_BOOK, NO_ACCOUNTING_PERIOD, POSTING_PERIOD_CLOSED, TOO_MANY_LINES, ATTACHMENT_REQUIRED - `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 --- # Returns the payroll run with the given identifier. `GET /accounting/payroll-details/{id}` Group: Accounting › Payroll Details · Operation ID: `getPayrollDetail` A payroll run is a set of payroll lines, one per employee or department, posted for one legal entity. It shares its read with POST accounting/payroll-details/search, which returns the same record. The list at GET accounting/payroll-details is header-only, so the lines come back here and on search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getPayrollDetail) ## 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`. | | `id` | path | string | yes | The unique identifier for the payroll run to return. | ## Response `200` Success Under `data` (getPayrollDetailOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the payroll run. | | `payrollDetailNumber` | string | no | The payroll run number. Absent until a number has been assigned. | | `description` | string | no | A description of the payroll run. | | `periodName` | string | no | The name of the payroll period this run covers, for example '2026-08'. | | `payrollDate` | string (date) | yes | The date the payroll is dated. Format YYYY-MM-DD. | | `startDate` | string (date) | no | The first day of the period the run covers. Format YYYY-MM-DD. | | `endDate` | string (date) | no | The last day of the period the run covers. Format YYYY-MM-DD. | | `currency` | string | yes | The currency the run is denominated in. Format ISO 4217, for example 'USD'. Governs openAmount and every line amount. | | `grouping` | string | no | How the payroll run is grouped. Values include 'Employee', 'Department' and 'Multi-level'. | | `paymentStatus` | string | no | The payment status of the run. Values include 'unpaid', 'partiallyPaid', 'fullyPaid', 'pending', 'notApplicable' and 'voided'. | | `openAmount` | string (decimal) | no | The amount still unpaid on this run, in currency. | | `isImported` | boolean | no | Whether the run was imported from a payroll provider rather than entered by hand. | | `entityId` | string | no | The legal entity the run belongs to. See id on GET base/entities. | | `entityName` | string | no | The name of the legal entity the run belongs to. | | `templateId` | string | no | The payroll template this run was created from. See id on GET accounting/payroll-templates/:id. | | `lastModifiedDate` | string (date-time) | yes | The date and time the payroll run last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `origin` | string | no | The source system this record came from, as free-form text. Absent when none was supplied. | | `lines` | array of object | no | The payroll lines, one per employee or department depending on grouping. An empty array when the run has none. | | `lines[].id` | string | yes | The unique identifier for the line. | | `lines[].lineNumber` | string | no | The line number. | | `lines[].lineLevel` | string | no | The level this line sits at. | | `lines[].employeeName` | string | no | The name of the employee this line pays. | | `lines[].unmatchedEmployeeName` | string | no | The employee name as it arrived from the payroll provider when it matched no employee on file. | | `lines[].departmentName` | string | no | The name of the department this line is charged to. | | `lines[].departmentCode` | string | no | The code of that department. | | `lines[].departmentNumber` | string | no | The number of that department. | | `lines[].totalAmount` | string (decimal) | no | The total amount on the line, in the run currency. | | `lines[].totalGrossAmount` | string (decimal) | no | The gross pay on the line, in the run currency. | | `lines[].totalNetAmount` | string (decimal) | no | The net pay on the line, in the run currency. | | `lines[].totalEmployerAmount` | string (decimal) | no | The employer-side contributions on the line, in the run currency. | | `lines[].errorMessage` | string | no | Why this line could not be processed, when it failed. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Payroll Detail `PATCH /accounting/payroll-details/{id}` Group: Accounting › Payroll Details · Operation ID: `updatePayrollDetail` Updates the payroll run with the given identifier. Only the fields in the request body change. Only the fields in the request body change. entityId cannot be changed, so a payroll cannot change owner. lines and attachments are complete replacement sets, not verb arrays: sending lines replaces all of them, omitting it keeps the stored lines, and sending an empty array clears the set. Child ids are neither published nor preserved. The same guards as create apply to the result: the payroll must still balance, every category must still be reachable from the template, and the posting period must still be open for the resulting posting date. The response is empty. Read the record with GET accounting/payroll-details/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updatePayrollDetail) ## 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. | | `id` | path | string | yes | The unique identifier for the payroll run to update. | ## Request body Required, `application/json`: updatePayrollDetailInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `templateId` | string | no | The payroll template this run uses. See id on GET accounting/payroll-templates/:id. Omit to leave unchanged. | | `grouping` | string | no | How the payroll is grouped. Values include 'Employee', 'Department' and 'Multi-level'. Omit to leave unchanged. | | `status` | string | no | The status of the payroll. Omit to leave unchanged. | | `payrollDate` | string (date) | no | The issue date of the payroll. Use format YYYY-MM-DD. Omit to leave unchanged. | | `startDate` | string (date) | no | The first day of the payroll period. Use format YYYY-MM-DD. Omit to leave unchanged. | | `endDate` | string (date) | no | The last day of the payroll period. Use format YYYY-MM-DD. Omit to leave unchanged. | | `currency` | string | no | The currency the payroll is denominated in. Format ISO 4217, for example 'USD'. Omit to leave unchanged. | | `description` | string | no | A description of the payroll. Omit to leave unchanged. | | `isNetPayCategoryMapped` | boolean | no | Whether the net-pay category is matched in the account mapping. Omit to leave unchanged. | | `origin` | string | no | The source system this record came from, as free-form text, for example 'Ramp'. Omit it and it stays unset. The server does not set a value. | | `lines` | array of object | no | The complete replacement set of lines. Omit to keep the stored lines. Send an empty array to clear them. Child ids are neither published nor preserved. | | `lines[].employeeId` | string | no | The employee this line is for. There is no endpoint for it. With grouping 'Employee', a line that names an employee but no department has the department backfilled from that employee's primary job. | | `lines[].departmentId` | string | no | The department this line is charged to. See id on GET departments/:id. | | `lines[].lineItems` | array of object | yes | The amounts on this line, one per payroll category. Each category must be reachable from the payroll's template. An item whose amount is null is dropped before the payroll is saved. | | `lines[].lineItems[].categoryId` | string | yes | The payroll category this amount belongs to. There is no endpoint for it. It must be an active category of the payroll's template, or one the payroll already uses. | | `lines[].lineItems[].amount` | string (decimal) | yes | The amount for this category, as a decimal in the payroll's currency. | | `attachments` | array of object | no | Files to attach to the payroll, as a complete replacement set. Omit to keep the stored attachments. Send an empty array to clear them. | | `attachments[].fileName` | string | yes | The display name of the attached file. | | `attachments[].fileId` | string | yes | The identifier of the uploaded file in file storage, as a text reference. There is no endpoint for it. | Example: ```json { "templateId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "grouping": "", "status": "", "payrollDate": "2026-01-15", "startDate": "2026-01-15", "endDate": "2026-01-15", "currency": "", "description": "August 2026 payroll (revised)", "isNetPayCategoryMapped": true, "origin": "", "lines": [ { "employeeId": "3f1a8c25-9d47-4e60-b892-0c5a7e3b1d84", "lineItems": [ { "categoryId": "9b2d5e10-4c78-4a36-8f51-6e0a3c7b2d95", "amount": "5200.00" }, { "categoryId": "e5c7a390-1d64-4b28-9a07-2f8b6d4c1e53", "amount": "5200.00" } ] } ], "attachments": [ { "fileName": "", "fileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `CATEGORY_NOT_IN_TEMPLATE` | 400 | | `INVALID_DATE_RANGE` | 400 | | `AMOUNT_NOT_BALANCED` | 400 | | `NO_ACCOUNTING_BOOK` | 400 | | `NO_ACCOUNTING_PERIOD` | 400 | | `POSTING_PERIOD_CLOSED` | 400 | | `TOO_MANY_LINES` | 400 | | `ATTACHMENT_REQUIRED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND, CATEGORY_NOT_IN_TEMPLATE, INVALID_DATE_RANGE, AMOUNT_NOT_BALANCED, NO_ACCOUNTING_BOOK, NO_ACCOUNTING_PERIOD, POSTING_PERIOD_CLOSED, TOO_MANY_LINES, ATTACHMENT_REQUIRED - `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 --- # Deletes a payroll detail. `DELETE /accounting/payroll-details/{id}` Group: Accounting › Payroll Details · Operation ID: `deletePayrollDetail` The response is empty. The delete cascades: lines, line items, attachments, multi-book account mappings, and every payroll payment application together with the outbound payment that carried it. A payroll that has moved money cannot be deleted: a recorded payment gives PAYROLL_HAS_PAYMENTS and a pending one PAYROLL_HAS_PENDING_PAYMENT, so remove the payments first. A payroll whose posting period is closed or locked gives POSTING_PERIOD_CLOSED. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deletePayrollDetail) ## 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. | | `id` | path | string | yes | The unique identifier for the payroll run to delete. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `PAYROLL_HAS_PAYMENTS` | 400 | | `PAYROLL_HAS_PENDING_PAYMENT` | 400 | | `POSTING_PERIOD_CLOSED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, PAYROLL_HAS_PAYMENTS, PAYROLL_HAS_PENDING_PAYMENT, POSTING_PERIOD_CLOSED - `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 --- # Search Payroll Detail `POST /accounting/payroll-details/search` Group: Accounting › Payroll Details · Operation ID: `searchPayrollDetail` Returns the payroll runs that match the given filters, each with its lines. Searches payroll runs and pages through the matches, returning the full record, header and lines, for each. Matching nothing returns an empty items array, not an error. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchPayrollDetail) ## 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`. | ## Request body Required, `application/json`: searchPayrollDetailInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A payroll run must match every filter you send. Omit to return all payroll runs. | | `filters.id` | string | no | Limits results to the payroll run with this identifier. | | `filters.payrollDetailNumber` | string | no | Limits results to the payroll run with this number. | | `filters.periodName` | string | no | Limits results to runs whose payroll period has this name. | | `filters.currency` | string | no | Limits results to runs in this currency. Format ISO 4217, for example 'USD'. | | `filters.grouping` | string | no | Limits results to runs grouped this way. Values include 'Employee', 'Department' and 'Multi-level'. Any other value matches no run. | | `filters.paymentStatus` | string | no | Limits results to runs in this payment status. Values include 'unpaid', 'partiallyPaid', 'fullyPaid', 'pending', 'notApplicable' and 'voided'. Any other value matches no run. | | `filters.isImported` | boolean | no | Limits results to runs that were imported (true) or entered by hand (false). | | `filters.entityId` | string | no | Limits results to runs for this legal entity. See id on GET base/entities. | | `filters.entityName` | string | no | Limits results to runs for a legal entity with this name. | | `filters.templateId` | string | no | Limits results to runs created from this payroll template. See id on GET accounting/payroll-templates/:id. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first request. | | `page.size` | number | no | The number of payroll runs per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "paymentStatus": "unpaid" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchPayrollDetailOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The payroll runs that match, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the payroll run. | | `items[].payrollDetailNumber` | string | no | The payroll run number. Absent until a number has been assigned. | | `items[].description` | string | no | A description of the payroll run. | | `items[].periodName` | string | no | The name of the payroll period this run covers, for example '2026-08'. | | `items[].payrollDate` | string (date) | yes | The date the payroll is dated. Format YYYY-MM-DD. | | `items[].startDate` | string (date) | no | The first day of the period the run covers. Format YYYY-MM-DD. | | `items[].endDate` | string (date) | no | The last day of the period the run covers. Format YYYY-MM-DD. | | `items[].currency` | string | yes | The currency the run is denominated in. Format ISO 4217, for example 'USD'. Governs openAmount and every line amount. | | `items[].grouping` | string | no | How the payroll run is grouped. Values include 'Employee', 'Department' and 'Multi-level'. | | `items[].paymentStatus` | string | no | The payment status of the run. Values include 'unpaid', 'partiallyPaid', 'fullyPaid', 'pending', 'notApplicable' and 'voided'. | | `items[].openAmount` | string (decimal) | no | The amount still unpaid on this run, in currency. | | `items[].isImported` | boolean | no | Whether the run was imported from a payroll provider rather than entered by hand. | | `items[].entityId` | string | no | The legal entity the run belongs to. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the legal entity the run belongs to. | | `items[].templateId` | string | no | The payroll template this run was created from. See id on GET accounting/payroll-templates/:id. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the payroll run last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].origin` | string | no | The source system this record came from, as free-form text. Absent when none was supplied. | | `items[].lines` | array of object | no | The payroll lines, one per employee or department depending on grouping. An empty array when the run has none. | | `items[].lines[].id` | string | yes | The unique identifier for the line. | | `items[].lines[].lineNumber` | string | no | The line number. | | `items[].lines[].lineLevel` | string | no | The level this line sits at. | | `items[].lines[].employeeName` | string | no | The name of the employee this line pays. | | `items[].lines[].unmatchedEmployeeName` | string | no | The employee name as it arrived from the payroll provider when it matched no employee on file. | | `items[].lines[].departmentName` | string | no | The name of the department this line is charged to. | | `items[].lines[].departmentCode` | string | no | The code of that department. | | `items[].lines[].departmentNumber` | string | no | The number of that department. | | `items[].lines[].totalAmount` | string (decimal) | no | The total amount on the line, in the run currency. | | `items[].lines[].totalGrossAmount` | string (decimal) | no | The gross pay on the line, in the run currency. | | `items[].lines[].totalNetAmount` | string (decimal) | no | The net pay on the line, in the run currency. | | `items[].lines[].totalEmployerAmount` | string (decimal) | no | The employer-side contributions on the line, in the run currency. | | `items[].lines[].errorMessage` | string | no | Why this line could not be processed, when it failed. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more runs remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Payroll Payments `GET /accounting/payroll-payments` Group: Accounting › Payroll Payments · Operation ID: `payrollPayments` Returns a list of payroll payments. Each entry is the payment header. Read one payment with its applications from GET accounting/payroll-payments/:id or POST accounting/payroll-payments/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=payrollPayments) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `payrollPaymentNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentCurrency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `approvalStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `externalId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, payrollPaymentNumber, paymentDate, paymentCurrency, paymentType, status, totalPaid, lastModifiedDate - Selectable: id, payrollPaymentNumber, paymentDate, paymentCurrency, paymentType, status, approvalStatus, description, totalPaid, externalId, paymentAccountName, entityName, lastModifiedDate ## Response `200` Success Under `data`, an array of payrollPaymentsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the payroll payment. | | `payrollPaymentNumber` | string or null | no | The payment number. Absent until a number has been assigned. | | `paymentDate` | string (date) | no | The date the payment is made. Format YYYY-MM-DD. | | `paymentCurrency` | string | no | The currency the payment is made in. Format ISO 4217, for example 'USD'. Governs totalPaid. | | `paymentType` | string | no | How the payment was made: the instrument, not the origin. Values include 'manual' and 'bank'. | | `status` | string | no | The status of the payment. Values include 'successful', 'rejected', 'cancelled', 'processing', 'partiallySuccessful', 'unknown', 'unsigned', 'manuallyRecorded', 'scheduled' and 'voided'. | | `approvalStatus` | string or null | no | The approval status of the payment. | | `description` | string or null | no | A description recorded on the payment. | | `totalPaid` | string (decimal) or null | no | The total amount paid, in paymentCurrency. | | `externalId` | string or null | no | The identifier this payment has in the upstream system that produced it. It is not unique. | | `paymentAccountName` | string or null | no | The name of the GL account the payment is booked against. | | `entityName` | string or null | no | The name of the legal entity that made the payment. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the payroll payment last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Records a payroll payment. `POST /accounting/payroll-payments` Group: Accounting › Payroll Payments · Operation ID: `createPayrollPayment` The response contains only the new payment's id. Read the record with GET accounting/payroll-payments/:id if you need its stored state, where payrollPaymentNumber appears once assigned. A payment that would overpay a payroll run, per payee or overall, is refused, but the guard runs after the write and returns no declared error code. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createPayrollPayment) ## 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`: createPayrollPaymentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `entityId` | string | yes | The legal entity the payment is posted under. See id on GET base/entities. | | `paymentDate` | string (date) | yes | The date the payment is made. Use format YYYY-MM-DD. | | `paymentCurrency` | string | yes | The currency of the payment. Format ISO 4217, for example 'USD'. Governs every application amount. | | `paymentStatus` | string | yes | The status to record the payment with, for example 'manuallyRecorded' or 'scheduled'. | | `paymentType` | string | no | How the payment is made. Values include 'manual' and 'bank'. Defaults to 'manual'. | | `accountId` | string | no | The bank or cash account the payment is drawn from. See id on GET accounting/accounts/:id. | | `commonDescription` | string | no | A description applied to the payment as a whole. | | `origin` | string | no | The source system this record came from, as free-form text, for example 'Ramp'. Omit it and it stays unset. The server does not set a value. | | `applications` | array of object | yes | The payroll runs this payment settles, one application per payroll detail. At least one is required. | | `applications[].payrollDetailId` | string | yes | The payroll detail being paid. See id on GET accounting/payroll-details/:id. | | `applications[].accountId` | string | yes | The account the payment is booked against. See id on GET accounting/accounts/:id. | | `applications[].payeeCode` | string | yes | The payee, as a payroll-payee code from your tenant's configuration, for example 'employee' or 'taxOffice'. | | `applications[].amountPaid` | string (decimal) | yes | The amount applied to this payroll detail, as a decimal in the payment's currency. | | `applications[].vendorId` | string | no | The vendor being paid, when the payee is a third party. See id on GET accounting/vendors/:id. | | `applications[].employeeId` | string | no | The employee being paid. There is no endpoint for it. | | `applications[].description` | string | no | A description for this application. | Example: ```json { "entityId": "8f2c1d40-5a6b-4c7d-9e80-1f2a3b4c5d6e", "paymentDate": "2026-08-31", "paymentCurrency": "EUR", "paymentStatus": "manuallyRecorded", "paymentType": "manual", "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "commonDescription": "", "origin": "", "applications": [ { "payrollDetailId": "b7e4c1a9-3f52-4d86-9017-5a2c8e0b4d61", "accountId": "7c1a9e40-2b53-4d68-9f01-3a5b7c9d1e24", "payeeCode": "employee", "amountPaid": "12500.00" } ] } ``` ## Response `200` Success Under `data` (createPayrollPaymentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created payroll payment. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `NO_APPLICATIONS` | 400 | | `UNSUPPORTED_PAYMENT_TYPE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, NO_APPLICATIONS, UNSUPPORTED_PAYMENT_TYPE - `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 --- # Returns the payroll payment with the given identifier. `GET /accounting/payroll-payments/{id}` Group: Accounting › Payroll Payments · Operation ID: `getPayrollPayment` A payroll payment is an outbound payment that settles a payroll run, each application tied to one payroll detail and one payee. It shares its underlying record with accounting/outbound-payments, so the same id is readable there too, but this endpoint returns only payments that settle payroll and includes their applications, which GET accounting/outbound-payments/:id does not. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getPayrollPayment) ## 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`. | | `id` | path | string | yes | The unique identifier for the payroll payment to return. | ## Response `200` Success Under `data` (getPayrollPaymentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the payroll payment. The same record is also readable as an outbound payment under this id. | | `payrollPaymentNumber` | string | no | The payment number. Absent until a number has been assigned. | | `paymentDate` | string (date) | yes | The date the payment is made. Format YYYY-MM-DD. | | `paymentCurrency` | string | yes | The currency the payment is made in. Format ISO 4217, for example 'USD'. Governs totalPaid and every application amount. | | `paymentType` | string | yes | How the payment was made: the instrument, not the origin. Values include 'manual' and 'bank'. | | `status` | string | yes | The status of the payment. Values include 'successful', 'rejected', 'cancelled', 'processing', 'partiallySuccessful', 'unknown', 'unsigned', 'manuallyRecorded', 'scheduled' and 'voided'. | | `approvalStatus` | string | no | The approval status of the payment. | | `description` | string | no | A description recorded on the payment. | | `totalPaid` | string (decimal) | no | The total amount paid, in paymentCurrency. | | `externalId` | string | no | The identifier this payment has in the upstream system that produced it. It is not unique. | | `paymentAccountId` | string | no | The GL account the payment is booked against. See id on GET accounting/accounts/:id. | | `paymentAccountName` | string | no | The name of the GL account the payment is booked against. | | `entityId` | string | no | The legal entity that made the payment. See id on GET base/entities. | | `entityName` | string | no | The name of the legal entity that made the payment. | | `lastModifiedDate` | string (date-time) | yes | The date and time the payroll payment last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `origin` | string | no | The source system this record came from, as free-form text. Absent when none was supplied. | | `applications` | array of object | no | The payroll payment applications, each tying part of this payment to one payroll detail and one payee. An empty array when the payment has none. | | `applications[].id` | string | yes | The unique identifier for the payroll payment application. | | `applications[].applicationNumber` | string | no | The application number. | | `applications[].description` | string | no | A description of the application. | | `applications[].amountPaid` | string (decimal) | no | The amount paid by this application, as a decimal in currency. | | `applications[].currency` | string | no | The currency governing amountPaid, the payroll run's currency. Format ISO 4217, for example 'USD'. | | `applications[].paymentStatus` | string | no | The status of the paying payment item, mirrored onto the application. It uses the same values as the payment's status. | | `applications[].payrollDetailId` | string | no | The payroll detail this application settles. See id on GET accounting/payroll-details/:id. | | `applications[].accountId` | string | no | The GL account this application is booked against. See id on GET accounting/accounts/:id. | | `applications[].accountName` | string | no | The name of the GL account this application is booked against. | | `applications[].accountNumber` | string | no | The number of the GL account this application is booked against. | | `applications[].payeeCode` | string | no | The payee, as a payroll-payee code from your tenant's configuration, for example 'employee' or 'taxOffice'. | | `applications[].payeeLabel` | string | no | The display label for the payee. | | `applications[].vendorId` | string | no | The vendor being paid, when the payee is a vendor. See id on GET accounting/vendors/:id. | | `applications[].vendorName` | string | no | The name of the vendor being paid. | | `applications[].employeeId` | string | no | The employee being paid, when the payee is an employee. There is no endpoint for it. | | `applications[].employeeName` | string | no | The name of the employee being paid. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Payroll Payment `PATCH /accounting/payroll-payments/{id}` Group: Accounting › Payroll Payments · Operation ID: `updatePayrollPayment` Updates the payroll payment with the given identifier. Only the fields in the request body change. Only the fields in the request body change. entityId cannot be changed. Applications change through three arrays, applied in the order delete, then update, then create. updateApplications and deleteApplications address applications by the id this payment publishes, and an id belonging to another payment is refused with APPLICATION_NOT_FOUND. A change that would leave the payment with no applications is refused with NO_APPLICATIONS. Delete the payment instead. Only a payment that has not moved money can be changed. Anything else is refused with PAYMENT_NOT_EDITABLE. The response is empty. Read the record with GET accounting/payroll-payments/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updatePayrollPayment) ## 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. | | `id` | path | string | yes | The unique identifier for the payroll payment to update. | ## Request body Required, `application/json`: updatePayrollPaymentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `paymentDate` | string (date) | no | The date the payment is made. Use format YYYY-MM-DD. Omit to leave unchanged. | | `paymentCurrency` | string | no | The currency of the payment. Format ISO 4217, for example 'USD'. Omit to leave unchanged. | | `paymentStatus` | string | no | The status of the payment, for example 'manuallyRecorded' or 'scheduled'. Omit to leave unchanged. | | `paymentType` | string | no | How the payment is made. Values include 'manual' and 'bank'. Omit to leave unchanged. | | `accountId` | string | no | The bank or cash account the payment is drawn from. See id on GET accounting/accounts/:id. Omit to leave unchanged. | | `commonDescription` | string | no | A description applied to the payment as a whole. Omit to leave unchanged. | | `origin` | string | no | The source system this record came from, as free-form text, for example 'Ramp'. Omit it and it stays unset. The server does not set a value. | | `createApplications` | array of object | no | Applications to add to this payment, each with the same shape as on create and no id. Omit for no additions. | | `createApplications[].payrollDetailId` | string | yes | The payroll detail being paid. See id on GET accounting/payroll-details/:id. | | `createApplications[].accountId` | string | yes | The account the payment is booked against. See id on GET accounting/accounts/:id. | | `createApplications[].payeeCode` | string | yes | The payee, as a payroll-payee code from your tenant's configuration, for example 'employee' or 'taxOffice'. | | `createApplications[].amountPaid` | string (decimal) | yes | The amount applied to this payroll detail, as a decimal in the payment's currency. | | `createApplications[].vendorId` | string | no | The vendor being paid, when the payee is a third party. See id on GET accounting/vendors/:id. | | `createApplications[].employeeId` | string | no | The employee being paid. There is no endpoint for it. | | `createApplications[].description` | string | no | A description for this application. | | `updateApplications` | array of object | no | Applications to change, each addressed by the id this payment publishes. Every field but the id is optional and left unchanged when omitted. Omit for no changes. | | `updateApplications[].id` | string | yes | The unique identifier for the application to change. It must belong to this payment. | | `updateApplications[].payrollDetailId` | string | no | The payroll detail being paid. See id on GET accounting/payroll-details/:id. Omit to leave unchanged. | | `updateApplications[].accountId` | string | no | The account the payment is booked against. See id on GET accounting/accounts/:id. Omit to leave unchanged. | | `updateApplications[].payeeCode` | string | no | The payee, as a payroll-payee code from your tenant's configuration. Omit to leave unchanged. | | `updateApplications[].amountPaid` | string (decimal) | no | The amount applied to this payroll detail, as a decimal. Omit to leave unchanged. | | `updateApplications[].vendorId` | string | no | The vendor being paid, when the payee is a third party. See id on GET accounting/vendors/:id. | | `updateApplications[].employeeId` | string | no | The employee being paid. There is no endpoint for it. | | `updateApplications[].description` | string | no | A description for this application. | | `deleteApplications` | array of string | no | The identifiers of applications to remove, applied before the updates and creates. Each must belong to this payment. Removing them all is refused. Delete the payment instead. | Example: ```json { "paymentDate": "2026-01-15", "paymentCurrency": "", "paymentStatus": "", "paymentType": "", "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "commonDescription": "August payroll run (revised)", "origin": "", "createApplications": [ { "payrollDetailId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "payeeCode": "", "amountPaid": "19.99", "vendorId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "employeeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "description": "" } ], "updateApplications": [ { "id": "9b2d5e10-4c78-4a36-8f51-6e0a3c7b2d95", "amountPaid": "12750.00" } ], "deleteApplications": [ "" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `APPLICATION_NOT_FOUND` | 400 | | `NO_APPLICATIONS` | 400 | | `UNSUPPORTED_PAYMENT_TYPE` | 400 | | `PAYMENT_NOT_EDITABLE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND, APPLICATION_NOT_FOUND, NO_APPLICATIONS, UNSUPPORTED_PAYMENT_TYPE, PAYMENT_NOT_EDITABLE - `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 --- # Deletes a payroll payment. `DELETE /accounting/payroll-payments/{id}` Group: Accounting › Payroll Payments · Operation ID: `deletePayrollPayment` The response is empty. Deleting releases the payroll runs the payment settled back to unpaid. Only a payment that has not moved money can be deleted: one that is manually recorded, scheduled, rejected, cancelled or of unknown status. Anything else is refused with PAYMENT_NOT_EDITABLE, which names the status. Void it instead, which keeps the audit trail. Only payroll payments are reachable here. An outbound payment that settles a vendor bill or expense report returns NOT_FOUND. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deletePayrollPayment) ## 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. | | `id` | path | string | yes | The unique identifier for the payroll payment to delete. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `PAYMENT_NOT_EDITABLE` | 400 | | `POSTING_PERIOD_CLOSED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, PAYMENT_NOT_EDITABLE, POSTING_PERIOD_CLOSED - `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 --- # Search Payroll Payment `POST /accounting/payroll-payments/search` Group: Accounting › Payroll Payments · Operation ID: `searchPayrollPayment` Returns the payroll payments that match the given filters, each with its applications. Only outbound payments that settle payroll are in scope: a payment qualifies when its items point at a payroll payment application. Every filter is applied on top of that, so this returns strictly fewer records than POST accounting/outbound-payments/search. Matching nothing returns an empty items array, not an error. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchPayrollPayment) ## 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`. | ## Request body Required, `application/json`: searchPayrollPaymentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A payroll payment must match every filter you send. Omit to return all payroll payments. | | `filters.id` | string | no | Limits results to the payroll payment with this identifier. | | `filters.payrollPaymentNumber` | string | no | Limits results to the payroll payment with this number. | | `filters.entityId` | string | no | Limits results to payments made by this legal entity. See id on GET base/entities. | | `filters.entityName` | string | no | Limits results to payments made by a legal entity with this name. The match is exact and case-sensitive. | | `filters.paymentAccountId` | string | no | Limits results to payments booked against this GL account. See id on GET accounting/accounts/:id. | | `filters.paymentType` | string | no | Limits results to payments made this way. Values include 'manual' and 'bank'. Any other value matches no payment. | | `filters.paymentCurrency` | string | no | Limits results to payments in this currency. Format ISO 4217, for example 'USD'. | | `filters.status` | string | no | Limits results to payments in this status. Values include 'successful', 'rejected', 'cancelled', 'processing', 'partiallySuccessful', 'unknown', 'unsigned', 'manuallyRecorded', 'scheduled' and 'voided'. Any other value matches no payment. | | `filters.approvalStatus` | string | no | Limits results to payments in this approval status. | | `filters.externalId` | string | no | Limits results to payments with this upstream identifier. It is not unique, so this can match several payments. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first request. | | `page.size` | number | no | The number of payroll payments per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "status": "successful" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchPayrollPaymentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The payroll payments that match, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the payroll payment. | | `items[].payrollPaymentNumber` | string | no | The payment number. Absent until a number has been assigned. | | `items[].paymentDate` | string (date) | yes | The date the payment is made. Format YYYY-MM-DD. | | `items[].paymentCurrency` | string | yes | The currency the payment is made in. Format ISO 4217, for example 'USD'. Governs totalPaid and every application amount. | | `items[].paymentType` | string | yes | How the payment was made: the instrument, not the origin. Values include 'manual' and 'bank'. | | `items[].status` | string | yes | The status of the payment. Values include 'successful', 'rejected', 'cancelled', 'processing', 'partiallySuccessful', 'unknown', 'unsigned', 'manuallyRecorded', 'scheduled' and 'voided'. | | `items[].approvalStatus` | string | no | The approval status of the payment. | | `items[].description` | string | no | A description recorded on the payment. | | `items[].totalPaid` | string (decimal) | no | The total amount paid, in paymentCurrency. | | `items[].externalId` | string | no | The identifier this payment has in the upstream system that produced it. It is not unique. | | `items[].paymentAccountId` | string | no | The GL account the payment is booked against. See id on GET accounting/accounts/:id. | | `items[].paymentAccountName` | string | no | The name of the GL account the payment is booked against. | | `items[].entityId` | string | no | The legal entity that made the payment. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the legal entity that made the payment. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the payroll payment last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].origin` | string | no | The source system this record came from, as free-form text. Absent when none was supplied. | | `items[].applications` | array of object | no | The payroll payment applications, each tying part of this payment to one payroll detail and one payee. An empty array when the payment has none. | | `items[].applications[].id` | string | yes | The unique identifier for the payroll payment application. | | `items[].applications[].applicationNumber` | string | no | The application number. | | `items[].applications[].description` | string | no | A description of the application. | | `items[].applications[].amountPaid` | string (decimal) | no | The amount paid by this application, as a decimal in currency. | | `items[].applications[].currency` | string | no | The currency governing amountPaid, the payroll run's currency. Format ISO 4217, for example 'USD'. | | `items[].applications[].paymentStatus` | string | no | The status of the paying payment item, mirrored onto the application. It uses the same values as the payment's status. | | `items[].applications[].payrollDetailId` | string | no | The payroll detail this application settles. See id on GET accounting/payroll-details/:id. | | `items[].applications[].accountId` | string | no | The GL account this application is booked against. See id on GET accounting/accounts/:id. | | `items[].applications[].accountName` | string | no | The name of the GL account this application is booked against. | | `items[].applications[].accountNumber` | string | no | The number of the GL account this application is booked against. | | `items[].applications[].payeeCode` | string | no | The payee, as a payroll-payee code from your tenant's configuration, for example 'employee' or 'taxOffice'. | | `items[].applications[].payeeLabel` | string | no | The display label for the payee. | | `items[].applications[].vendorId` | string | no | The vendor being paid, when the payee is a vendor. See id on GET accounting/vendors/:id. | | `items[].applications[].vendorName` | string | no | The name of the vendor being paid. | | `items[].applications[].employeeId` | string | no | The employee being paid, when the payee is an employee. There is no endpoint for it. | | `items[].applications[].employeeName` | string | no | The name of the employee being paid. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more payments remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Payroll Templates `GET /accounting/payroll-templates` Group: Accounting › Payroll Templates · Operation ID: `payrollTemplates` Returns a list of payroll templates. Each item is the header only. For the categories, use GET accounting/payroll-templates/:id or POST accounting/payroll-templates/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=payrollTemplates) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `name` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `groupBy` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `journalEntryDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `journalEntryGroupBy` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `deductionsType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `source` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, name, groupBy, journalEntryDate, journalEntryGroupBy, deductionsType, source, status, lastModifiedDate - Selectable: id, name, description, groupBy, journalEntryDate, journalEntryGroupBy, deductionsType, source, status, lastModifiedDate ## Response `200` Success Under `data`, an array of payrollTemplatesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the payroll template. | | `name` | string | no | The name of the payroll template. Unique across payroll templates. | | `description` | string or null | no | A free-form description of the payroll template. | | `groupBy` | string | no | The level at which payroll amounts are entered. One of 'Employee', 'Department' or 'Multi-level'. | | `journalEntryDate` | string | no | The date the generated journal entry is posted on. Possible values: 'payrollDate', 'endDate'. | | `journalEntryGroupBy` | string | no | The default consolidation level for generated journal lines. One of 'account', 'accountAndPayrollCategory', 'payrollCategory' or 'payrollCategoryByEmployee'. | | `deductionsType` | string | no | Whether deduction amounts in imported data arrive positive or negative. Possible values: 'positive', 'negative'. | | `source` | string or null | no | The payroll system this template imports from. Possible values: 'datev', 'datevEmployeePayroll', 'datevEmployeePayrollPersonLevel', 'rippling', 'lexware', 'briars', 'gusto', 'adpGLReport', 'adpPayrollHistory', 'sage', 'none'. | | `status` | string or null | no | The lifecycle status of the payroll template. One of 'create', 'active', 'inactive'. It is computed from the template's categories, not set through the API. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the payroll template last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Payroll Template `POST /accounting/payroll-templates` Group: Accounting › Payroll Templates · Operation ID: `createPayrollTemplate` Creates a payroll template with its categories and entity assignments. The name must be unique across payroll templates. The name must be unique across payroll templates, and a clash fails with DUPLICATE_NAME. At least one category and at least one entity are required. Category names must be unique within the template, and at most one category may be netPay. entityIds is the complete set of entities the template serves. status is not an input. It is computed as 'active' when the template has both an active netPay and an active grossPay category, and 'inactive' otherwise. A category may use 'payrollCategoryByEmployee' aggregation only when groupBy is 'Employee'. The response contains only the new template's id. Read the record with GET accounting/payroll-templates/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createPayrollTemplate) ## 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`: createPayrollTemplateInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The name of the payroll template. Unique across all payroll templates. | | `groupBy` | string | yes | How payroll lines are entered. One of 'Employee', 'Department' or 'Multi-level'. | | `journalEntryDate` | string | yes | The date the journal entry uses. Accepted values: 'payrollDate', 'endDate'. | | `journalEntryGroupBy` | string | yes | How journal lines are aggregated by default. One of 'account', 'accountAndPayrollCategory', 'payrollCategory' or 'payrollCategoryByEmployee'. | | `description` | string | no | A free-form description of the template. | | `clearingAccountId` | string | no | The clearing account used when posting. See id on GET accounting/accounts/:id. | | `source` | string | no | The payroll system the template imports from. Accepted values: 'datev', 'datevEmployeePayroll', 'datevEmployeePayrollPersonLevel', 'rippling', 'lexware', 'briars', 'gusto', 'adpGLReport', 'adpPayrollHistory', 'sage', 'none'. | | `entityIds` | array of string | yes | The complete set of legal entities this template serves. At least one is required. See id on GET base/entities. | | `categories` | array of object | yes | The categories this template defines. At least one is required. Category names must be unique, and at most one category may be netPay. | | `categories[].name` | string | yes | The name of the category. Unique within the template. | | `categories[].subType` | string | yes | What the category represents. One of 'grossPay', 'deduction', 'netPay' or 'employer'. At most one netPay category per template. | | `categories[].status` | string | no | Whether the category is usable. One of 'active' or 'inactive'. Defaults to 'active'. | | `categories[].payee` | string | no | Who gets paid, as a payee code from your tenant's configured payee list, for example 'employee', 'payrollProvider', 'benefitsProvider' or 'taxOffice'. | | `categories[].isManagedFromPayroll` | boolean | yes | Whether payment for this category is managed from payroll. Required, the category model has no default. | | `categories[].debitAccountId` | string | no | The account debited for this category. See id on GET accounting/accounts/:id. | | `categories[].creditAccountId` | string | no | The account credited for this category. See id on GET accounting/accounts/:id. | | `categories[].debitJournalAggregation` | string | no | How debit journal lines are aggregated. One of 'account', 'accountAndPayrollCategory', 'payrollCategory' or 'payrollCategoryByEmployee'. Defaults to the template's setting. 'payrollCategoryByEmployee' requires groupBy 'Employee'. | | `categories[].creditJournalAggregation` | string | no | How credit journal lines are aggregated. The same values and rule as the debit side apply. | Example: ```json { "name": "Germany monthly payroll", "groupBy": "Employee", "journalEntryDate": "payrollDate", "journalEntryGroupBy": "account", "description": "", "clearingAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "source": "", "entityIds": [ "8f2c1d40-5a6b-4c7d-9e80-1f2a3b4c5d6e" ], "categories": [ { "name": "Gross Pay", "subType": "grossPay", "payee": "employee", "isManagedFromPayroll": false, "debitAccountId": "7c1a9e40-2b53-4d68-9f01-3a5b7c9d1e24" }, { "name": "Net Pay", "subType": "netPay", "payee": "employee", "isManagedFromPayroll": false, "creditAccountId": "7c1a9e40-2b53-4d68-9f01-3a5b7c9d1e24" } ] } ``` ## Response `200` Success Under `data` (createPayrollTemplateOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created payroll template. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `DUPLICATE_NAME` | 400 | | `NO_CATEGORIES` | 400 | | `DUPLICATE_CATEGORY_NAME` | 400 | | `MULTIPLE_NET_PAY_CATEGORIES` | 400 | | `AGGREGATION_REQUIRES_EMPLOYEE_GROUPING` | 400 | | `NO_ENTITIES` | 400 | | `NO_CATEGORY_ACCOUNTS` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, DUPLICATE_NAME, NO_CATEGORIES, DUPLICATE_CATEGORY_NAME, MULTIPLE_NET_PAY_CATEGORIES, AGGREGATION_REQUIRES_EMPLOYEE_GROUPING, NO_ENTITIES, NO_CATEGORY_ACCOUNTS - `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 --- # Get Payroll Template `GET /accounting/payroll-templates/{id}` Group: Accounting › Payroll Templates · Operation ID: `getPayrollTemplate` Returns the payroll template with the given identifier, including its categories. A template describes how an imported payroll file is turned into a journal entry. groupBy says at what level amounts are entered, journalEntryGroupBy how journal lines are consolidated, and each category maps one payroll line type onto a debit and a credit account. To look up a template by name, which is unique, use POST accounting/payroll-templates/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getPayrollTemplate) ## 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`. | | `id` | path | string | yes | The unique identifier for the payroll template to return. | ## Response `200` Success Under `data` (getPayrollTemplateOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the payroll template. | | `name` | string | yes | The name of the payroll template. Unique across payroll templates. | | `description` | string | no | A free-form description of the payroll template. | | `groupBy` | string | yes | The level at which payroll amounts are entered. One of 'Employee', 'Department' or 'Multi-level'. | | `journalEntryDate` | string | yes | The date the generated journal entry is posted on. Possible values: 'payrollDate', 'endDate'. | | `journalEntryGroupBy` | string | yes | The default consolidation level for generated journal lines. One of 'account', 'accountAndPayrollCategory', 'payrollCategory' or 'payrollCategoryByEmployee'. | | `deductionsType` | string | yes | Whether deduction amounts in imported data arrive positive or negative. Possible values: 'positive', 'negative'. | | `source` | string | no | The payroll system this template imports from. Possible values: 'datev', 'datevEmployeePayroll', 'datevEmployeePayrollPersonLevel', 'rippling', 'lexware', 'briars', 'gusto', 'adpGLReport', 'adpPayrollHistory', 'sage', 'none'. | | `status` | string | no | The lifecycle status of the payroll template. One of 'create', 'active', 'inactive'. It is computed from the template's categories, not set through the API. | | `clearingAccountId` | string | no | The GL account payroll amounts are cleared through. See id on GET accounting/accounts/:id. | | `entities` | array of string | yes | The names of the legal entities this template is mapped to. An empty array when it is mapped to none. | | `lastModifiedDate` | string (date-time) | yes | The date and time the payroll template last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `categories` | array of object | no | The payroll categories defined on this template, each mapping one payroll line type onto a debit and a credit account. An empty array when the template has none. | | `categories[].id` | string | yes | The unique identifier for the payroll category. | | `categories[].name` | string | yes | The name of the category, for example 'Gross Salary'. Unique within the template. | | `categories[].subType` | string | yes | The kind of payroll amount this category represents. One of 'grossPay', 'deduction', 'netPay' or 'employer'. | | `categories[].status` | string | no | The lifecycle status of the category. One of 'active' or 'inactive'. | | `categories[].payee` | string | no | Who the amount is paid to, as a payee code from your tenant's configured payee list, for example 'employee', 'payrollProvider', 'benefitsProvider' or 'taxOffice'. | | `categories[].isManagedFromPayroll` | boolean | no | Whether payments for this category are recorded through payroll rather than separately. | | `categories[].debitAccountId` | string | no | The GL account this category debits. See id on GET accounting/accounts/:id. | | `categories[].debitJournalAggregation` | string | no | The consolidation level for journal lines posted to the debit account. Absent means the template default applies. | | `categories[].creditAccountId` | string | no | The GL account this category credits. See id on GET accounting/accounts/:id. | | `categories[].creditJournalAggregation` | string | no | The consolidation level for journal lines posted to the credit account. Absent means the template default applies. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Payroll Template `PATCH /accounting/payroll-templates/{id}` Group: Accounting › Payroll Templates · Operation ID: `updatePayrollTemplate` Updates the payroll template with the given identifier. Only the header fields in the request body change. Header fields are optional and omitting one leaves it unchanged. categories and entityIds are each a complete set. Send categories and the template ends up with exactly those. A stored category whose id is left out is deleted, and the deletion is refused while any payroll item still uses it. To keep a category, include it with its id. To add one, include it without an id. Send entityIds and the template serves exactly those entities. Omit either to leave the stored set unchanged. status is not an input. It is recomputed on every write from whether an active netPay and an active grossPay category are present. The response is empty. Read the updated record with GET accounting/payroll-templates/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updatePayrollTemplate) ## 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. | | `id` | path | string | yes | The unique identifier for the payroll template to update. | ## Request body Required, `application/json`: updatePayrollTemplateInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The new name. Omit to leave it unchanged. It must stay unique across payroll templates. | | `groupBy` | string | no | The new grouping. One of 'Employee', 'Department' or 'Multi-level'. Omit to leave it unchanged. | | `journalEntryDate` | string | no | The new journal entry date rule. Accepted values: 'payrollDate', 'endDate'. Omit to leave it unchanged. | | `journalEntryGroupBy` | string | no | The new default journal aggregation. One of 'account', 'accountAndPayrollCategory', 'payrollCategory' or 'payrollCategoryByEmployee'. Omit to leave it unchanged. | | `description` | string | no | The new description. Omit to leave it unchanged. | | `clearingAccountId` | string | no | The new clearing account. Omit to leave it unchanged. See id on GET accounting/accounts/:id. | | `source` | string | no | The new source. Accepted values: 'datev', 'datevEmployeePayroll', 'datevEmployeePayrollPersonLevel', 'rippling', 'lexware', 'briars', 'gusto', 'adpGLReport', 'adpPayrollHistory', 'sage', 'none'. Omit to leave it unchanged. | | `entityIds` | array of string | no | The complete set of legal entities this template serves. Sending it replaces the assignment. Omit it to leave the current set unchanged. At least one entity if sent. See id on GET base/entities. | | `categories` | array of object | no | The complete set of categories. Sending it replaces them. Include an existing category with its id to keep it, and leave one out to delete it. Omit the field to leave the stored categories unchanged. | | `categories[].id` | string | no | The identifier of an existing category to keep. Omit for a new category. It must belong to this template. | | `categories[].name` | string | yes | The name of the category. Unique within the template. | | `categories[].subType` | string | yes | What the category represents. One of 'grossPay', 'deduction', 'netPay' or 'employer'. At most one netPay category per template. | | `categories[].status` | string | no | Whether the category is usable. One of 'active' or 'inactive'. Defaults to 'active'. | | `categories[].payee` | string | no | Who gets paid, as a payee code from your tenant's configured payee list, for example 'employee', 'payrollProvider', 'benefitsProvider' or 'taxOffice'. | | `categories[].isManagedFromPayroll` | boolean | yes | Whether payment for this category is managed from payroll. Required, the category model has no default. | | `categories[].debitAccountId` | string | no | The account debited for this category. See id on GET accounting/accounts/:id. | | `categories[].creditAccountId` | string | no | The account credited for this category. See id on GET accounting/accounts/:id. | | `categories[].debitJournalAggregation` | string | no | How debit journal lines are aggregated. One of 'account', 'accountAndPayrollCategory', 'payrollCategory' or 'payrollCategoryByEmployee'. Defaults to the template's setting. 'payrollCategoryByEmployee' requires groupBy 'Employee'. | | `categories[].creditJournalAggregation` | string | no | How credit journal lines are aggregated. The same values and rule as the debit side apply. | Example: ```json { "name": "", "groupBy": "", "journalEntryDate": "", "journalEntryGroupBy": "", "description": "Revised for 2027", "clearingAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "source": "", "entityIds": [ "" ], "categories": [ { "id": "6c9e2b41-8a35-4f70-b183-2d5a7c04e916", "name": "Gross Pay", "subType": "grossPay", "isManagedFromPayroll": false, "debitAccountId": "7c1a9e40-2b53-4d68-9f01-3a5b7c9d1e24" }, { "name": "Pension", "subType": "deduction", "payee": "benefitsProvider", "isManagedFromPayroll": true, "creditAccountId": "7c1a9e40-2b53-4d68-9f01-3a5b7c9d1e24" } ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `CATEGORY_NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `DUPLICATE_NAME` | 400 | | `NO_CATEGORIES` | 400 | | `DUPLICATE_CATEGORY_NAME` | 400 | | `MULTIPLE_NET_PAY_CATEGORIES` | 400 | | `AGGREGATION_REQUIRES_EMPLOYEE_GROUPING` | 400 | | `NO_ENTITIES` | 400 | | `NO_CATEGORY_ACCOUNTS` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, CATEGORY_NOT_FOUND, REFERENCE_NOT_FOUND, DUPLICATE_NAME, NO_CATEGORIES, DUPLICATE_CATEGORY_NAME, MULTIPLE_NET_PAY_CATEGORIES, AGGREGATION_REQUIRES_EMPLOYEE_GROUPING, NO_ENTITIES, NO_CATEGORY_ACCOUNTS - `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 --- # Deletes the payroll template with the given identifier. `DELETE /accounting/payroll-templates/{id}` Group: Accounting › Payroll Templates · Operation ID: `deletePayrollTemplate` Deletes one payroll template with its categories, account maps and entity assignments. While any payroll detail uses the template, the call fails with TEMPLATE_IN_USE, which reports how many payroll details reference it, drafts included. To stop the template being chosen without removing the payrolls that reference it, set it to 'inactive' instead. The response is empty. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deletePayrollTemplate) ## 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. | | `id` | path | string | yes | The unique identifier for the payroll template to delete. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `TEMPLATE_IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, TEMPLATE_IN_USE - `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 --- # Search Payroll Template `POST /accounting/payroll-templates/search` Group: Accounting › Payroll Templates · Operation ID: `searchPayrollTemplate` Returns the payroll templates that match the given filters, each including its categories. All filters are optional and are combined with AND. Omit every filter to match all payroll templates. Lookup by name lives here because a path takes only the id, and entityId lives here because a template is mapped to entities through a join, not a column. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchPayrollTemplate) ## 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`. | ## Request body Required, `application/json`: searchPayrollTemplateInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A template must match every filter you send. Omit to return all payroll templates. | | `filters.id` | string | no | Limits results to the template with this identifier. | | `filters.name` | string | no | Limits results to the template with this exact name. The name is unique across payroll templates, so this returns at most one template. | | `filters.groupBy` | string | no | Limits results to templates that enter amounts at this level. One of 'Employee', 'Department' or 'Multi-level'. Any other value matches no template. | | `filters.status` | string | no | Limits results to templates in this lifecycle status. One of 'create', 'active', 'inactive'. Any other value matches no template. | | `filters.source` | string | no | Limits results to templates importing from this payroll system. Accepted values: 'datev', 'datevEmployeePayroll', 'datevEmployeePayrollPersonLevel', 'rippling', 'lexware', 'briars', 'gusto', 'adpGLReport', 'adpPayrollHistory', 'sage', 'none'. Any other value matches no template. | | `filters.journalEntryGroupBy` | string | no | Limits results to templates with this default journal consolidation level. One of 'account', 'accountAndPayrollCategory', 'payrollCategory' or 'payrollCategoryByEmployee'. Any other value matches no template. | | `filters.entityId` | string | no | Limits results to templates mapped to this legal entity. An id that matches no entity returns no template. See id on GET base/entities. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of payroll templates per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "status": "active" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchPayrollTemplateOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The payroll templates on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the payroll template. | | `items[].name` | string | yes | The name of the payroll template. Unique across payroll templates. | | `items[].description` | string | no | A free-form description of the payroll template. | | `items[].groupBy` | string | yes | The level at which payroll amounts are entered. One of 'Employee', 'Department' or 'Multi-level'. | | `items[].journalEntryDate` | string | yes | The date the generated journal entry is posted on. Possible values: 'payrollDate', 'endDate'. | | `items[].journalEntryGroupBy` | string | yes | The default consolidation level for generated journal lines. One of 'account', 'accountAndPayrollCategory', 'payrollCategory' or 'payrollCategoryByEmployee'. | | `items[].deductionsType` | string | yes | Whether deduction amounts in imported data arrive positive or negative. Possible values: 'positive', 'negative'. | | `items[].source` | string | no | The payroll system this template imports from. Possible values: 'datev', 'datevEmployeePayroll', 'datevEmployeePayrollPersonLevel', 'rippling', 'lexware', 'briars', 'gusto', 'adpGLReport', 'adpPayrollHistory', 'sage', 'none'. | | `items[].status` | string | no | The lifecycle status of the payroll template. One of 'create', 'active', 'inactive'. It is computed from the template's categories, not set through the API. | | `items[].clearingAccountId` | string | no | The GL account payroll amounts are cleared through. See id on GET accounting/accounts/:id. | | `items[].entities` | array of string | yes | The names of the legal entities this template is mapped to. An empty array when it is mapped to none. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the payroll template last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].categories` | array of object | no | The payroll categories defined on this template, each mapping one payroll line type onto a debit and a credit account. An empty array when the template has none. | | `items[].categories[].id` | string | yes | The unique identifier for the payroll category. | | `items[].categories[].name` | string | yes | The name of the category, for example 'Gross Salary'. Unique within the template. | | `items[].categories[].subType` | string | yes | The kind of payroll amount this category represents. One of 'grossPay', 'deduction', 'netPay' or 'employer'. | | `items[].categories[].status` | string | no | The lifecycle status of the category. One of 'active' or 'inactive'. | | `items[].categories[].payee` | string | no | Who the amount is paid to, as a payee code from your tenant's configured payee list, for example 'employee', 'payrollProvider', 'benefitsProvider' or 'taxOffice'. | | `items[].categories[].isManagedFromPayroll` | boolean | no | Whether payments for this category are recorded through payroll rather than separately. | | `items[].categories[].debitAccountId` | string | no | The GL account this category debits. See id on GET accounting/accounts/:id. | | `items[].categories[].debitJournalAggregation` | string | no | The consolidation level for journal lines posted to the debit account. Absent means the template default applies. | | `items[].categories[].creditAccountId` | string | no | The GL account this category credits. See id on GET accounting/accounts/:id. | | `items[].categories[].creditJournalAggregation` | string | no | The consolidation level for journal lines posted to the credit account. Absent means the template default applies. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Performance Obligations `GET /accounting/performance-obligations` Group: Accounting › Performance Obligations · Operation ID: `performanceObligations` Returns a list of performance obligations. Each entry is a header. The full record, with standalone selling price ranges, comes from GET accounting/performance-obligations/:id or POST accounting/performance-obligations/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=performanceObligations) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `performanceObligationNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `effectiveStartDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `effectiveEndDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `allocationStartDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isAllocationExcluded` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isNetAccounting` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `defaultAmortizationMethod` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `standaloneSellingPriceRangeType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `revenueSchedulingTemplateName` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, performanceObligationNumber, name, effectiveStartDate, effectiveEndDate, allocationStartDate, isAllocationExcluded, isNetAccounting, defaultAmortizationMethod, standaloneSellingPriceRangeType, revenueSchedulingTemplateName, lastModifiedDate - Selectable: id, performanceObligationNumber, name, description, effectiveStartDate, effectiveEndDate, allocationStartDate, isAllocationExcluded, isNetAccounting, defaultAmortizationMethod, standaloneSellingPriceRangeType, revenueSchedulingTemplateName, lastModifiedDate ## Response `200` Success Under `data`, an array of performanceObligationsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the performance obligation. | | `performanceObligationNumber` | string or null | no | The performance obligation number. Absent while none has been assigned. | | `name` | string | no | The display name of the performance obligation, as it appears on revenue contracts and schedules. | | `description` | string or null | no | A free-form description of what the obligation covers. Absent when none is set. | | `effectiveStartDate` | string (date) | no | The first date this obligation is in effect. Format YYYY-MM-DD. | | `effectiveEndDate` | string (date) or null | no | The last date this obligation is in effect. Format YYYY-MM-DD. Absent when the obligation is open-ended. | | `allocationStartDate` | string (date) or null | no | The date from which revenue for this obligation takes part in allocation. Format YYYY-MM-DD. Absent when none is set. | | `isAllocationExcluded` | boolean | no | Whether the revenue contract lines of this obligation are left out of fair-value allocation. | | `isNetAccounting` | boolean | no | Whether the obligation is accounted net rather than gross. | | `defaultAmortizationMethod` | string or null | no | The default amortization method used when scheduling this obligation's revenue. Possible values: 'exactDays', 'pointInTime', 'straightLineProRateFirstLast', 'straightLine', 'usageBased'. | | `standaloneSellingPriceRangeType` | string or null | no | How the obligation's standalone selling price ranges are expressed. Possible values: 'percentage', 'amount'. | | `revenueSchedulingTemplateName` | string or null | no | The name of the revenue scheduling template that schedules this obligation's revenue. | | `lastModifiedDate` | string (date-time) | no | The date and time the performance obligation last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Get Performance Obligation `GET /accounting/performance-obligations/{id}` Group: Accounting › Performance Obligations · Operation ID: `getPerformanceObligation` Returns the performance obligation with the given identifier, including its standalone selling price ranges. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getPerformanceObligation) ## 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`. | | `id` | path | string | yes | The unique identifier for the performance obligation to return. | ## Response `200` Success Under `data` (getPerformanceObligationOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the performance obligation. | | `performanceObligationNumber` | string | no | The performance obligation number. Absent while none has been assigned. | | `name` | string | yes | The display name of the performance obligation, as it appears on revenue contracts and schedules. | | `description` | string | no | A free-form description of what the obligation covers. Absent when none is set. | | `effectiveStartDate` | string (date) | yes | The first date this obligation is in effect. Format YYYY-MM-DD. | | `effectiveEndDate` | string (date) | no | The last date this obligation is in effect. Format YYYY-MM-DD. Absent when the obligation is open-ended. | | `allocationStartDate` | string (date) | no | The date from which revenue for this obligation takes part in allocation. Format YYYY-MM-DD. Absent when none is set. | | `isAllocationExcluded` | boolean | yes | Whether the revenue contract lines of this obligation are left out of fair-value allocation. | | `isNetAccounting` | boolean | yes | Whether the obligation is accounted net rather than gross. | | `defaultAmortizationMethod` | string | no | The default amortization method used when scheduling this obligation's revenue. Possible values: 'exactDays', 'pointInTime', 'straightLineProRateFirstLast', 'straightLine', 'usageBased'. | | `standaloneSellingPriceRangeType` | string | no | How the obligation's standalone selling price ranges are expressed. Possible values: 'percentage', 'amount'. | | `revenueSchedulingTemplateId` | string | no | The unique identifier for the revenue scheduling template that schedules this obligation's revenue. See id on GET accounting/revenue-scheduling-templates/:id. | | `revenueSchedulingTemplateName` | string | no | The name of the revenue scheduling template that schedules this obligation's revenue. | | `standaloneSellingPriceStratificationConfigurationId` | string | no | The unique identifier for the standalone selling price stratification configuration this obligation stratifies by. Absent when none is set. There is no endpoint for it. | | `lastModifiedDate` | string (date-time) | yes | The date and time the performance obligation last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `standaloneSellingPriceRanges` | array of object | no | The obligation's standalone selling price ranges, one per product and effective period. An empty array when none are configured. Which of the amount and percentage fields are set follows each range's rangeType. | | `standaloneSellingPriceRanges[].id` | string | no | The unique identifier for the standalone selling price range. | | `standaloneSellingPriceRanges[].rangeNumber` | string | no | The standalone selling price range number. | | `standaloneSellingPriceRanges[].rangeType` | string | no | How this range is expressed. Possible values: 'percentage', 'amount'. | | `standaloneSellingPriceRanges[].currency` | string | no | The currency the amounts are given in. Format ISO 4217, for example 'USD'. Absent on a percentage range. | | `standaloneSellingPriceRanges[].effectiveStartDate` | string (date) | yes | The first date this range applies. Format YYYY-MM-DD. | | `standaloneSellingPriceRanges[].effectiveEndDate` | string (date) | no | The last date this range applies. Format YYYY-MM-DD. Absent when the range is open-ended. | | `standaloneSellingPriceRanges[].lowAmount` | string (decimal) | no | The lower bound of the standalone selling price, in the range currency. Set on an amount range. | | `standaloneSellingPriceRanges[].midAmount` | string (decimal) | no | The midpoint of the standalone selling price, in the range currency. Set on an amount range. | | `standaloneSellingPriceRanges[].highAmount` | string (decimal) | no | The upper bound of the standalone selling price, in the range currency. Set on an amount range. | | `standaloneSellingPriceRanges[].lowPercentage` | string (decimal) | no | The lower bound of the standalone selling price, as a percentage of list price. Set on a percentage range. | | `standaloneSellingPriceRanges[].midPercentage` | string (decimal) | no | The midpoint of the standalone selling price, as a percentage of list price. Set on a percentage range. | | `standaloneSellingPriceRanges[].highPercentage` | string (decimal) | no | The upper bound of the standalone selling price, as a percentage of list price. Set on a percentage range. | | `standaloneSellingPriceRanges[].term` | string | no | The period the range's prices are quoted over. Possible values: 'monthly', 'annually', 'none'. | | `standaloneSellingPriceRanges[].unitOfMeasure` | string | no | The unit the range's prices are quoted per. | | `standaloneSellingPriceRanges[].productId` | string | no | The product this range prices. Absent on a range that applies to the whole obligation. See id on GET accounting/products/:id. | | `standaloneSellingPriceRanges[].productCode` | string | no | The code of that product. | | `standaloneSellingPriceRanges[].productName` | string | no | The name of that product. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # List Performance Obligation Products `GET /accounting/performance-obligations/{id}/products` Group: Accounting › Performance Obligations · Operation ID: `listPerformanceObligationProducts` Returns the products mapped to the given performance obligation, one entry per product and entity book. The same product appears once for each entity book it is mapped in. [Open in the interactive reference](https://docs.api.lensing.ai/?op=listPerformanceObligationProducts) ## 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`. | | `id` | path | string | yes | The unique identifier for the performance obligation whose products to return. | | `cursor` | query | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `pageSize` | query | number | no | The number of mappings per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | ## Response `200` Success Under `data` (listPerformanceObligationProductsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The obligation's product mappings on this page, newest first. An empty array when the obligation maps no products. | | `items[].id` | string | no | The unique identifier for this product mapping. | | `items[].productId` | string | no | The mapped product. See id on GET accounting/products/:id. | | `items[].productCode` | string | no | The code of that product. | | `items[].productName` | string | no | The name of that product. | | `items[].entityBookId` | string | no | The entity book this mapping applies in. A product mapped in several entity books has one entry per book. There is no endpoint for it. | | `items[].entityName` | string | no | The name of the legal entity of that entity book. | | `items[].bookName` | string | no | The name of the accounting book of that entity book. | | `items[].effectiveStartDate` | string (date) | yes | The first date this mapping applies. Format YYYY-MM-DD. | | `items[].effectiveEndDate` | string (date) | no | The last date this mapping applies. Format YYYY-MM-DD. Absent when the mapping is open-ended. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time this mapping last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as the cursor query parameter. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more mappings remain beyond this page. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Search Performance Obligation `POST /accounting/performance-obligations/search` Group: Accounting › Performance Obligations · Operation ID: `searchPerformanceObligation` Returns the performance obligations that match the given filters, each with its standalone selling price ranges. The obligation's product mappings come from GET accounting/performance-obligations/:id/products. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchPerformanceObligation) ## 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`. | ## Request body Required, `application/json`: searchPerformanceObligationInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A performance obligation must match every filter you send. Omit to return all performance obligations. | | `filters.id` | string | no | Limits results to the performance obligation with this identifier. | | `filters.performanceObligationNumber` | string | no | Limits results to the performance obligation with this number, for example 'POB-1042'. | | `filters.name` | string | no | Limits results to performance obligations whose name contains this text. The match is case-insensitive. | | `filters.revenueSchedulingTemplateId` | string | no | Limits results to obligations scheduled by the revenue scheduling template with this identifier. An identifier matching no template returns an empty page. | | `filters.standaloneSellingPriceRangeType` | string | no | Limits results to obligations whose standalone selling price ranges are expressed this way. Accepted values: 'percentage', 'amount'. Any other value matches nothing. | | `filters.isAllocationExcluded` | boolean | no | Limits results by whether the obligation's revenue contract lines are left out of fair-value allocation. | | `filters.productId` | string | no | Limits results to obligations that have a product line for the product with this identifier. An identifier matching no product, or a product no obligation covers, returns an empty page. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of performance obligations per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "performanceObligationNumber": "", "name": "", "revenueSchedulingTemplateId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "standaloneSellingPriceRangeType": "", "isAllocationExcluded": true, "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchPerformanceObligationOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The performance obligations on this page, newest first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the performance obligation. | | `items[].performanceObligationNumber` | string | no | The performance obligation number. Absent while none has been assigned. | | `items[].name` | string | yes | The display name of the performance obligation, as it appears on revenue contracts and schedules. | | `items[].description` | string | no | A free-form description of what the obligation covers. Absent when none is set. | | `items[].effectiveStartDate` | string (date) | yes | The first date this obligation is in effect. Format YYYY-MM-DD. | | `items[].effectiveEndDate` | string (date) | no | The last date this obligation is in effect. Format YYYY-MM-DD. Absent when the obligation is open-ended. | | `items[].allocationStartDate` | string (date) | no | The date from which revenue for this obligation takes part in allocation. Format YYYY-MM-DD. Absent when none is set. | | `items[].isAllocationExcluded` | boolean | yes | Whether the revenue contract lines of this obligation are left out of fair-value allocation. | | `items[].isNetAccounting` | boolean | yes | Whether the obligation is accounted net rather than gross. | | `items[].defaultAmortizationMethod` | string | no | The default amortization method used when scheduling this obligation's revenue. Possible values: 'exactDays', 'pointInTime', 'straightLineProRateFirstLast', 'straightLine', 'usageBased'. | | `items[].standaloneSellingPriceRangeType` | string | no | How the obligation's standalone selling price ranges are expressed. Possible values: 'percentage', 'amount'. | | `items[].revenueSchedulingTemplateId` | string | no | The unique identifier for the revenue scheduling template that schedules this obligation's revenue. See id on GET accounting/revenue-scheduling-templates/:id. | | `items[].revenueSchedulingTemplateName` | string | no | The name of the revenue scheduling template that schedules this obligation's revenue. | | `items[].standaloneSellingPriceStratificationConfigurationId` | string | no | The unique identifier for the standalone selling price stratification configuration this obligation stratifies by. Absent when none is set. There is no endpoint for it. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the performance obligation last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].standaloneSellingPriceRanges` | array of object | no | The obligation's standalone selling price ranges, one per product and effective period. An empty array when none are configured. Which of the amount and percentage fields are set follows each range's rangeType. | | `items[].standaloneSellingPriceRanges[].id` | string | no | The unique identifier for the standalone selling price range. | | `items[].standaloneSellingPriceRanges[].rangeNumber` | string | no | The standalone selling price range number. | | `items[].standaloneSellingPriceRanges[].rangeType` | string | no | How this range is expressed. Possible values: 'percentage', 'amount'. | | `items[].standaloneSellingPriceRanges[].currency` | string | no | The currency the amounts are given in. Format ISO 4217, for example 'USD'. Absent on a percentage range. | | `items[].standaloneSellingPriceRanges[].effectiveStartDate` | string (date) | yes | The first date this range applies. Format YYYY-MM-DD. | | `items[].standaloneSellingPriceRanges[].effectiveEndDate` | string (date) | no | The last date this range applies. Format YYYY-MM-DD. Absent when the range is open-ended. | | `items[].standaloneSellingPriceRanges[].lowAmount` | string (decimal) | no | The lower bound of the standalone selling price, in the range currency. Set on an amount range. | | `items[].standaloneSellingPriceRanges[].midAmount` | string (decimal) | no | The midpoint of the standalone selling price, in the range currency. Set on an amount range. | | `items[].standaloneSellingPriceRanges[].highAmount` | string (decimal) | no | The upper bound of the standalone selling price, in the range currency. Set on an amount range. | | `items[].standaloneSellingPriceRanges[].lowPercentage` | string (decimal) | no | The lower bound of the standalone selling price, as a percentage of list price. Set on a percentage range. | | `items[].standaloneSellingPriceRanges[].midPercentage` | string (decimal) | no | The midpoint of the standalone selling price, as a percentage of list price. Set on a percentage range. | | `items[].standaloneSellingPriceRanges[].highPercentage` | string (decimal) | no | The upper bound of the standalone selling price, as a percentage of list price. Set on a percentage range. | | `items[].standaloneSellingPriceRanges[].term` | string | no | The period the range's prices are quoted over. Possible values: 'monthly', 'annually', 'none'. | | `items[].standaloneSellingPriceRanges[].unitOfMeasure` | string | no | The unit the range's prices are quoted per. | | `items[].standaloneSellingPriceRanges[].productId` | string | no | The product this range prices. Absent on a range that applies to the whole obligation. See id on GET accounting/products/:id. | | `items[].standaloneSellingPriceRanges[].productCode` | string | no | The code of that product. | | `items[].standaloneSellingPriceRanges[].productName` | string | no | The name of that product. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more performance obligations remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Price Books `GET /accounting/price-books` Group: Accounting › Price Books · Operation ID: `priceBooks` Returns a list of price books. To match on name or price book number, use POST accounting/price-books/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=priceBooks) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `active` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isStandard` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `dataSource` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `createdDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, priceBookName, isStandard, active, dataSource, createdDate, lastModifiedDate - Selectable: id, priceBookName, priceBookNumber, description, active, isStandard, dataSource, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of priceBooksRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the price book. | | `priceBookName` | string | no | The display name of the price book, unique within the tenant and compared case-insensitively. | | `priceBookNumber` | string or null | no | The price book number, for example 'PB-000123'. Absent until a number range assigns one. | | `description` | string or null | no | A free-form description of the price book. Absent when none is set. | | `active` | boolean or null | no | Whether the price book is active and usable in transactions. | | `isStandard` | boolean or null | no | Whether this is the standard (default) price book. Only one price book is the standard at a time. It cannot be set through this API. | | `dataSource` | string or null | no | The source system that provided the price book data. Absent when none is set. | | `createdDate` | string (date-time) or null | no | The date and time the price book was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the price book record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a price book. `POST /accounting/price-books` Group: Accounting › Price Books · Operation ID: `createPriceBook` Creates a price book and returns its id. The response contains only the id, so read the record with GET accounting/price-books/:id. priceBookName must be unique within the tenant, compared case-insensitively, and a collision fails with DUPLICATE_NAME. isStandard is not an input: exactly one standard price book exists per tenant, assigned by seed data, and this endpoint cannot reassign it. priceBookNumber is not an input either: it is system-assigned once a number range applies, and may be absent on the response until then. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createPriceBook) ## 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`: createPriceBookInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `priceBookName` | string | yes | The display name of the price book. Required, and must be unique within the tenant, compared case-insensitively. | | `active` | boolean | no | Whether the price book is active and usable in transactions. Omit to use the default. | | `description` | string | no | A free-form description of the price book. | | `dataSource` | string | no | The source system that provided the price book data. | Example: ```json { "priceBookName": "", "active": true, "description": "", "dataSource": "" } ``` ## Response `200` Success Under `data` (createPriceBookOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created price book. | ## 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 | | --- | --- | | `DUPLICATE_NAME` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: DUPLICATE_NAME - `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 --- # Get Price Book `GET /accounting/price-books/{id}` Group: Accounting › Price Books · Operation ID: `getPriceBook` Returns the price book with the given identifier. Use the price book's id, not its priceBookNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getPriceBook) ## 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`. | | `id` | path | string | yes | The unique identifier for the price book to return. | ## Response `200` Success Under `data` (getPriceBookOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the price book. | | `priceBookName` | string | yes | The display name of the price book, unique within the tenant and compared case-insensitively. | | `priceBookNumber` | string | no | The price book number, for example 'PB-000123'. Absent until a number range assigns one. | | `description` | string | no | A free-form description of the price book. Absent when none is set. | | `active` | boolean | no | Whether the price book is active and usable in transactions. | | `isStandard` | boolean | no | Whether this is the standard (default) price book. Only one price book is the standard at a time. It cannot be set through this API. | | `dataSource` | string | no | The source system that provided the price book data. Absent when none is set. | | `createdDate` | string (date-time) | no | The date and time the price book was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | yes | The date and time the price book record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Price Book `PATCH /accounting/price-books/{id}` Group: Accounting › Price Books · Operation ID: `updatePriceBook` Updates the price book with the given identifier. Only the fields in the request body change. Use the price book's id, not its priceBookNumber. Everything omitted from the request body is left as it was. priceBookName, when sent, must stay unique within the tenant, compared case-insensitively. The price book's own current name is not a collision, and renaming onto a name another price book already has fails with DUPLICATE_NAME, which names that other price book. isStandard and priceBookNumber cannot be changed through this endpoint. An unknown id fails with NOT_FOUND. The response contains only the id, so read the updated record with GET accounting/price-books/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updatePriceBook) ## 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. | | `id` | path | string | yes | The unique identifier for the price book to update. | ## Request body Required, `application/json`: updatePriceBookInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `priceBookName` | string | no | A new display name. Omit to leave it unchanged. It must stay unique within the tenant, compared case-insensitively. The price book's own current name is not a collision. | | `active` | boolean | no | A new active flag. Omit to leave it unchanged. | | `description` | string | no | A new description. Omit to leave it unchanged. | | `dataSource` | string | no | A new data source. Omit to leave it unchanged. | Example: ```json { "priceBookName": "", "active": true, "description": "", "dataSource": "" } ``` ## Response `200` Success Under `data` (updatePriceBookOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the updated price book. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `DUPLICATE_NAME` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, DUPLICATE_NAME - `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 --- # Returns the price books that match the given filters. `POST /accounting/price-books/search` Group: Accounting › Price Books · Operation ID: `searchPriceBook` [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchPriceBook) ## 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`. | ## Request body Required, `application/json`: searchPriceBookInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A price book must match every filter you send. Omit to return all price books. | | `filters.id` | string | no | Limits results to the price book with this identifier. The match is exact. | | `filters.priceBookNumber` | string | no | Limits results to the price book with this price book number, for example 'PB-000123'. The match is exact. | | `filters.priceBookName` | string | no | Limits results to price books whose name contains this text. The match is case-insensitive. | | `filters.isStandard` | boolean | no | Limits results to the standard price book when true, or to non-standard price books when false. | | `filters.active` | boolean | no | Limits results to active price books when true, or inactive ones when false. | | `filters.dataSource` | string | no | Limits results to price books whose data source is this value. | | `filters.createdDate` | object | no | Limits results to price books created within this date range. Both bounds are inclusive, and omitting one leaves that side open. | | `filters.createdDate.start` | string (date-time) | no | The inclusive start of the range, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `filters.createdDate.end` | string (date-time) | no | The inclusive end of the range, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `filters.lastModifiedDate` | object | no | Limits results to price books last changed within this date range. Both bounds are inclusive, and omitting one leaves that side open. | | `filters.lastModifiedDate.start` | string (date-time) | no | The inclusive start of the range, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `filters.lastModifiedDate.end` | string (date-time) | no | The inclusive end of the range, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of price books per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "priceBookNumber": "", "priceBookName": "", "isStandard": true, "active": true, "dataSource": "", "createdDate": { "start": "2026-01-15T09:30:00.000Z", "end": "2026-01-15T09:30:00.000Z" }, "lastModifiedDate": { "start": "2026-01-15T09:30:00.000Z", "end": "2026-01-15T09:30:00.000Z" } }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchPriceBookOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The price books that match, ordered by id. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the price book. | | `items[].priceBookName` | string | yes | The display name of the price book, unique within the tenant and compared case-insensitively. | | `items[].priceBookNumber` | string | no | The price book number, for example 'PB-000123'. Absent until a number range assigns one. | | `items[].description` | string | no | A free-form description of the price book. Absent when none is set. | | `items[].active` | boolean | no | Whether the price book is active and usable in transactions. | | `items[].isStandard` | boolean | no | Whether this is the standard (default) price book. Only one price book is the standard at a time. It cannot be set through this API. | | `items[].dataSource` | string | no | The source system that provided the price book data. Absent when none is set. | | `items[].createdDate` | string (date-time) | no | The date and time the price book was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the price book record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Products `GET /accounting/products` Group: Accounting › Products · Operation ID: `products` Returns a list of products. To match by free text or filter by CRM id, use POST accounting/products/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=products) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `productCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `effectiveStartDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `effectiveEndDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `productFamily` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `productType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `productStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isInventory` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, productCode, productName, productStatus, productFamily, productType, isInventory, effectiveStartDate, effectiveEndDate, lastModifiedDate - Selectable: id, productName, productCode, productNumber, description, internalDescription, crmProductId, effectiveStartDate, effectiveEndDate, productFamily, productType, productStatus, isInventory, lastModifiedDate ## Response `200` Success Under `data`, an array of productsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the product. | | `productName` | string | no | The product's display name. | | `productCode` | string | no | The product's code, unique within the tenant. | | `productNumber` | string or null | no | The product number, assigned by the system and unique within the tenant. Absent while none has been assigned. | | `description` | string or null | no | A description of the product shown to customers. | | `internalDescription` | string or null | no | A description of the product for internal use, not shown to customers. | | `crmProductId` | string or null | no | The product's identifier in an external CRM system. | | `effectiveStartDate` | string (date) | no | The date the product becomes available. Format YYYY-MM-DD. | | `effectiveEndDate` | string (date) or null | no | The date the product is no longer available. Format YYYY-MM-DD. Absent for a product with no end date. | | `productFamily` | string or null | no | The product family. Possible values: 'software', 'services', 'physicalProducts'. | | `productType` | string or null | no | The product type. Possible values: 'sub', 'termLicense', 'support', 'professionalServices', 'training', 'usage', 'billableTravelExpense', 'bundle', 'hardwareConsumables', 'billablePassThrough', 'merchandise', 'shipping'. | | `productStatus` | string or null | no | The product's availability status, computed by the system. Possible values: 'draft', 'available', 'unavailable', 'incomplete'. It is derived from the product's lines, pricing and effective dates and cannot be set directly. | | `isInventory` | boolean or null | no | Whether the product is tracked as inventory. | | `lastModifiedDate` | string (date-time) | no | The date and time the product record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a product. `POST /accounting/products` Group: Accounting › Products · Operation ID: `createAccountingProduct` A productCode that another product already uses fails with DUPLICATE_PRODUCT_CODE, which names the id of the product that already has it. The whole call runs as one transaction, so any failure writes nothing. The response contains only the new product's id. Read the record with GET accounting/products/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createAccountingProduct) ## 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`: createAccountingProductInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `productName` | string | yes | The product's display name. | | `productCode` | string | yes | The product's code, unique within the tenant. A code another product already uses fails with DUPLICATE_PRODUCT_CODE. | | `effectiveStartDate` | string (date) | yes | The date the product becomes available. Use format YYYY-MM-DD. | | `effectiveEndDate` | string (date) | no | The date the product is no longer available. Use format YYYY-MM-DD. Omit for a product with no end date. | | `productFamily` | string | no | The product family. Accepted values: 'software', 'services', 'physicalProducts'. | | `productType` | string | no | The product type. Accepted values: 'sub', 'termLicense', 'support', 'professionalServices', 'training', 'usage', 'billableTravelExpense', 'bundle', 'hardwareConsumables', 'billablePassThrough', 'merchandise', 'shipping'. | | `description` | string | no | A description of the product shown to customers. | | `internalDescription` | string | no | A description of the product for internal use, not shown to customers. | | `crmProductId` | string | no | The product's identifier in an external CRM system. | | `isInventory` | boolean | no | Whether the product is tracked as inventory. Defaults to false. | | `itemIds` | array of string | no | The inventory items to link to this product. Used only when isInventory is true. There is no endpoint for inventory items. | | `entityId` | string | no | The legal entity this product is scoped to. See id on GET base/entities. Omit to default to your current entity when entity-based visibility is enabled, otherwise the product is shared across all entities. | | `productLines` | array of object | no | The product lines to create with this product, each with optional pricing. A 'prepaid' line also creates a paired 'usage' line, with that line's pricing supplied in priceBookHeadersForUsage. | | `productLines[].lineType` | string | yes | The type of product line. Accepted values: 'recurring', 'oneTime', 'usage', 'prepaid', 'billableTime', 'billableTravelExpense', 'billablePassThrough'. 'commitWithOverage' is not supported and fails with UNSUPPORTED_LINE_TYPE. | | `productLines[].priceModel` | string | yes | How the product line is priced. Accepted values: 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. | | `productLines[].unitOfMeasure` | string | yes | The unit the product line's quantities are measured in. Accepted values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `productLines[].name` | string | no | A name for the product line. Omit to have one generated. | | `productLines[].pricingTerm` | string | no | The pricing term of the product line. Accepted values: 'monthly', 'biMonthly', 'once', 'quarterly', 'semiAnnually', 'annually'. | | `productLines[].enforceListPrice` | boolean | no | Whether the list price is enforced on the product line. When true, sales transactions cannot override it. | | `productLines[].invoiceFrequency` | string | no | How often the product line is invoiced. Accepted values: 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `productLines[].prepaidBalanceOverConsumptionBehavior` | string | no | What happens when usage exceeds the prepaid balance. Accepted values: 'allowNegative', 'overage'. 'allowNegative' lets the balance go negative and reconciles it at renewal. 'overage' switches to pay-as-you-go invoicing when the balance reaches 0. | | `productLines[].prepaidBalanceRolloverAtRenewal` | boolean | no | Whether the remaining prepaid balance rolls over at renewal. | | `productLines[].crmProductId` | string | no | The product line's identifier in an external CRM system. | | `productLines[].dataSource` | string | no | The upstream system or import job that produced the product line, for example 'Salesforce', 'NetSuite' or 'Stripe'. | | `productLines[].productLineTaxes` | array of object | no | The tax records to attach to the product line. Provide only when a tax provider is configured. | | `productLines[].productLineTaxes[].taxCode` | string | yes | The tax code, as defined by the configured tax provider. | | `productLines[].productLineTaxes[].entityId` | string | yes | The legal entity this tax record applies under. See id on GET base/entities. | | `productLines[].productLineTaxes[].engine` | string | yes | The tax engine that computes this tax. Accepted values: 'avalara', 'anrok', 'everestTaxEngine', 'manualTax', 'none'. | | `productLines[].productLineTaxes[].country` | string | no | The country the tax record applies in. Format ISO 3166-1, for example 'US'. | | `productLines[].priceBookHeadersToCreate` | array of object | no | The pricing headers to create for the product line. | | `productLines[].priceBookHeadersToCreate[].priceBookId` | string | yes | The price book this header belongs to. See id on GET accounting/price-books/:id. | | `productLines[].priceBookHeadersToCreate[].currency` | string | yes | The currency the header's prices are in. Format ISO 4217, for example 'USD'. | | `productLines[].priceBookHeadersToCreate[].validFrom` | string (date) | yes | The first date the header's prices are effective, inclusive. Use format YYYY-MM-DD. | | `productLines[].priceBookHeadersToCreate[].validTo` | string (date) | no | The last date the header's prices are effective, inclusive. Use format YYYY-MM-DD. Omit for a header with no end date. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries` | array of object | yes | | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].listPrice` | string (decimal) | no | The list price, in the header currency. Required except on tiered and rate-card entries. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].fromQuantity` | string (decimal) | no | The inclusive lower bound of the tier's quantity range. Provide only on tiered entries. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].toQuantity` | string (decimal) | no | The inclusive upper bound of the tier's quantity range. Omit on the top tier, which has no upper bound. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].priceModel` | string | no | How the tier prices quantity. Provide only on tiered entries. Accepted values: 'flat', 'perUnit'. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry` | object | no | The rate-card entry to attach. Provide only on rate-card-priced entries. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry.billableComponentId` | string | no | The billable component this rate-card entry prices. There is no endpoint for it. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry.unitOfMeasure` | string | yes | The unit the rate is quoted in. Accepted values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry.description` | string | no | A free-form qualifier that distinguishes this rate-card entry from others on the same billable component. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry.usageType` | string | no | How usage is measured against this entry. Accepted values: 'consumption', 'overage', 'thresholdCounter'. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry.billableUnitOfMeasure` | string | no | The unit used when billing this entry. Accepted values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry.conversion` | string (decimal) | no | The multiplier that converts unitOfMeasure quantities into billableUnitOfMeasure quantities. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry.invoiceFrequency` | string | no | How often usage on this entry is invoiced. Accepted values: 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry.priceModel` | string | no | How the rate-card entry computes a price. Accepted values: 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. | | `productLines[].priceBookHeadersForUsage` | array of object | no | The pricing for the auto-created paired usage line. Provide only when lineType is 'prepaid' or priceModel is 'thresholdBased'. Otherwise it is ignored. | | `productLines[].priceBookHeadersForUsage[].priceBookId` | string | yes | The price book this header belongs to. See id on GET accounting/price-books/:id. | | `productLines[].priceBookHeadersForUsage[].currency` | string | yes | The currency the header's prices are in. Format ISO 4217, for example 'USD'. | | `productLines[].priceBookHeadersForUsage[].validFrom` | string (date) | yes | The first date the header's prices are effective, inclusive. Use format YYYY-MM-DD. | | `productLines[].priceBookHeadersForUsage[].validTo` | string (date) | no | The last date the header's prices are effective, inclusive. Use format YYYY-MM-DD. Omit for a header with no end date. | | `productLines[].priceBookHeadersForUsage[].rateCardEntries` | array of object | yes | The rate-card entries on the usage-line header. Usage-line pricing is always rate-card based. | | `productLines[].priceBookHeadersForUsage[].rateCardEntries[].billableComponentId` | string | no | The billable component this rate-card entry prices. There is no endpoint for it. | | `productLines[].priceBookHeadersForUsage[].rateCardEntries[].usageType` | string | yes | How usage is measured against this entry. Accepted values: 'consumption', 'overage', 'thresholdCounter'. | | `productLines[].priceBookHeadersForUsage[].rateCardEntries[].unitOfMeasure` | string | yes | The unit the rate is quoted in. Accepted values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `productLines[].priceBookHeadersForUsage[].rateCardEntries[].billableUnitOfMeasure` | string | no | The unit used when billing this entry. Accepted values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `productLines[].priceBookHeadersForUsage[].rateCardEntries[].conversion` | string (decimal) | no | The multiplier that converts unitOfMeasure quantities into billableUnitOfMeasure quantities. | | `productLines[].priceBookHeadersForUsage[].rateCardEntries[].listPrice` | string (decimal) | no | The list price for this rate-card entry, in the price book currency. | Example: ```json { "productName": "", "productCode": "", "effectiveStartDate": "2026-01-15", "effectiveEndDate": "2026-01-15", "productFamily": "", "productType": "", "description": "", "internalDescription": "", "crmProductId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "isInventory": true, "itemIds": [ "" ], "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "productLines": [ { "lineType": "", "priceModel": "", "unitOfMeasure": "", "name": "", "pricingTerm": "", "enforceListPrice": true, "invoiceFrequency": "", "prepaidBalanceOverConsumptionBehavior": "", "prepaidBalanceRolloverAtRenewal": true, "crmProductId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "dataSource": "", "productLineTaxes": [ { "taxCode": "", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "engine": "", "country": "" } ], "priceBookHeadersToCreate": [ { "priceBookId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "currency": "", "validFrom": "2026-01-15", "validTo": "2026-01-15", "priceBookEntries": [ { "listPrice": null, "fromQuantity": null, "toQuantity": null, "priceModel": null, "rateCardEntry": null } ] } ], "priceBookHeadersForUsage": [ { "priceBookId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "currency": "", "validFrom": "2026-01-15", "validTo": "2026-01-15", "rateCardEntries": [ { "billableComponentId": null, "usageType": null, "unitOfMeasure": null, "billableUnitOfMeasure": null, "conversion": null, "listPrice": null } ] } ] } ] } ``` ## Response `200` Success Under `data` (createAccountingProductOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created product. | ## 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 | | --- | --- | | `INVALID_ID` | 400 | | `DUPLICATE_PRODUCT_CODE` | 400 | | `UNKNOWN_ITEM_IDS` | 400 | | `UNKNOWN_ENTITY_ID` | 400 | | `UNKNOWN_PRICE_BOOK_IDS` | 400 | | `UNKNOWN_BILLABLE_COMPONENT_IDS` | 400 | | `UNKNOWN_TAX_ENTITY_IDS` | 400 | | `UNSUPPORTED_LINE_TYPE` | 400 | | `VALIDATION_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_ID, DUPLICATE_PRODUCT_CODE, UNKNOWN_ITEM_IDS, UNKNOWN_ENTITY_ID, UNKNOWN_PRICE_BOOK_IDS, UNKNOWN_BILLABLE_COMPONENT_IDS, UNKNOWN_TAX_ENTITY_IDS, UNSUPPORTED_LINE_TYPE, VALIDATION_FAILED - `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 --- # Returns the product with the given identifier. `GET /accounting/products/{id}` Group: Accounting › Products · Operation ID: `getAccountingProduct` [Open in the interactive reference](https://docs.api.lensing.ai/?op=getAccountingProduct) ## 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`. | | `id` | path | string | yes | The unique identifier for the product to return. | ## Response `200` Success Under `data` (getAccountingProductOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the product. | | `productName` | string | yes | The product's display name. | | `productCode` | string | yes | The product's code, unique within the tenant. | | `productNumber` | string | no | The product number, assigned by the system and unique within the tenant. Absent while none has been assigned. | | `description` | string | no | A description of the product shown to customers. | | `internalDescription` | string | no | A description of the product for internal use, not shown to customers. | | `crmProductId` | string | no | The product's identifier in an external CRM system. | | `effectiveStartDate` | string (date) | yes | The date the product becomes available. Format YYYY-MM-DD. | | `effectiveEndDate` | string (date) | no | The date the product is no longer available. Format YYYY-MM-DD. Absent for a product with no end date. | | `productFamily` | string | no | The product family. Possible values: 'software', 'services', 'physicalProducts'. | | `productType` | string | no | The product type. Possible values: 'sub', 'termLicense', 'support', 'professionalServices', 'training', 'usage', 'billableTravelExpense', 'bundle', 'hardwareConsumables', 'billablePassThrough', 'merchandise', 'shipping'. | | `productStatus` | string | no | The product's availability status, computed by the system. Possible values: 'draft', 'available', 'unavailable', 'incomplete'. It is derived from the product's lines, pricing and effective dates and cannot be set directly. | | `isInventory` | boolean | no | Whether the product is tracked as inventory. | | `itemIds` | array of string | no | The inventory items linked to this product. Used only when isInventory is true. Absent when the product has none. There is no endpoint for inventory items. | | `createdDate` | string (date-time) | no | The date and time the product record was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | yes | The date and time the product record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `externalIdentifiers` | array of object | no | External identifiers owned by other systems, at most one per fieldName. Absent when the product has none. This collection is read-only. Create and update do not accept it. | | `externalIdentifiers[].fieldName` | string | yes | The configured key of the external identifier, a camelCase slug rather than the display label. It must match an active external-identifier configuration for products. | | `externalIdentifiers[].value` | string | yes | The identifier's value in the source system. | | `externalIdentifiers[].externalUrl` | string | no | An absolute http or https link to the object in the source system. Absent when none is stored. | ## Errors | Code | Status | | --- | --- | | `INVALID_ID` | 400 | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_ID, NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Accounting Product `PATCH /accounting/products/{id}` Group: Accounting › Products · Operation ID: `updateAccountingProduct` Updates the product with the given identifier. Only the fields in the request body change. Only the fields in the request body change. Everything omitted is left as it was, so you never have to read the product first to change one field. itemIds, when present, replaces the whole set of linked inventory items, so pass every item that should stay linked, and send an empty array to unlink every item. Changing productCode to one another product already uses fails with DUPLICATE_PRODUCT_CODE. A product's legal entity cannot be reassigned through this endpoint. The whole call runs as one transaction, so any failure leaves the product unchanged. The response is empty. Read the updated record with GET accounting/products/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateAccountingProduct) ## 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. | | `id` | path | string | yes | The unique identifier for the product to update. | ## Request body Required, `application/json`: updateAccountingProductInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `productName` | string | no | The product's display name. | | `productCode` | string | no | The product's code, unique within the tenant. A code another product already uses fails with DUPLICATE_PRODUCT_CODE. | | `effectiveStartDate` | string (date) | no | The date the product becomes available. Use format YYYY-MM-DD. | | `effectiveEndDate` | string (date) | no | The date the product is no longer available. Use format YYYY-MM-DD. | | `productFamily` | string | no | The product family. Accepted values: 'software', 'services', 'physicalProducts'. | | `productType` | string | no | The product type. Accepted values: 'sub', 'termLicense', 'support', 'professionalServices', 'training', 'usage', 'billableTravelExpense', 'bundle', 'hardwareConsumables', 'billablePassThrough', 'merchandise', 'shipping'. | | `description` | string | no | A description of the product shown to customers. | | `internalDescription` | string | no | A description of the product for internal use, not shown to customers. | | `crmProductId` | string | no | The product's identifier in an external CRM system. | | `isInventory` | boolean | no | Whether the product is tracked as inventory. | | `itemIds` | array of string | no | The full set of inventory items linked to the product. This array replaces the whole set. Pass every item that should stay linked, and send an empty array to unlink every item. There is no endpoint for inventory items. | Example: ```json { "productName": "", "productCode": "", "effectiveStartDate": "2026-01-15", "effectiveEndDate": "2026-01-15", "productFamily": "", "productType": "", "description": "", "internalDescription": "", "crmProductId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "isInventory": true, "itemIds": [ "" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `INVALID_ID` | 400 | | `NOT_FOUND` | 400 | | `DUPLICATE_PRODUCT_CODE` | 400 | | `UNKNOWN_ITEM_IDS` | 400 | | `VALIDATION_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_ID, NOT_FOUND, DUPLICATE_PRODUCT_CODE, UNKNOWN_ITEM_IDS, VALIDATION_FAILED - `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 --- # Returns the products that match the given free-text term and filters. `POST /accounting/products/search` Group: Accounting › Products · Operation ID: `searchAccountingProducts` [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchAccountingProducts) ## 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`. | ## Request body Required, `application/json`: searchAccountingProductsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A product must match every filter you send. Omit to return all products. | | `filters.term` | string | no | Limits results to products whose name, code or description contains this text. The match is case-insensitive. | | `filters.productCode` | string | no | Limits results to the product with this product code. The match is exact. | | `filters.crmProductId` | string | no | Limits results to the product with this CRM identifier. The match is exact. | | `filters.productStatus` | string | no | Limits results to products with this status. Accepted values: 'draft', 'available', 'unavailable', 'incomplete'. | | `filters.productFamily` | string | no | Limits results to products of this family. Accepted values: 'software', 'services', 'physicalProducts'. | | `filters.productType` | string | no | Limits results to products of this type. Accepted values: 'sub', 'termLicense', 'support', 'professionalServices', 'training', 'usage', 'billableTravelExpense', 'bundle', 'hardwareConsumables', 'billablePassThrough', 'merchandise', 'shipping'. | | `filters.isInventory` | boolean | no | Limits results to products with this inventory-tracking flag. | | `filters.effectiveStartDate` | object | no | Limits results by the date the product becomes available. Bounds are inclusive. | | `filters.effectiveStartDate.start` | string | no | The inclusive lower bound. Use format YYYY-MM-DD. | | `filters.effectiveStartDate.end` | string | no | The inclusive upper bound. Use format YYYY-MM-DD. | | `filters.effectiveEndDate` | object | no | Limits results by the date the product is no longer available. Bounds are inclusive. A product with no end date matches any lower bound. | | `filters.effectiveEndDate.start` | string | no | The inclusive lower bound. Use format YYYY-MM-DD. | | `filters.effectiveEndDate.end` | string | no | The inclusive upper bound. Use format YYYY-MM-DD. | | `filters.lastModifiedDate` | object | no | Limits results by the date and time the product last changed. Bounds are inclusive. | | `filters.lastModifiedDate.start` | string | no | The inclusive lower bound, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `filters.lastModifiedDate.end` | string | no | The inclusive upper bound, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `filters.entityId` | string | no | Limits results to products visible to this legal entity: shared products and products assigned to it. See id on GET base/entities. An identifier that matches no entity returns an empty page, not an error. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of products per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "term": "", "productCode": "", "crmProductId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "productStatus": "", "productFamily": "", "productType": "", "isInventory": true, "effectiveStartDate": { "start": "", "end": "" }, "effectiveEndDate": { "start": "", "end": "" }, "lastModifiedDate": { "start": "", "end": "" }, "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchAccountingProductsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The products on this page. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the product. | | `items[].productName` | string | yes | The product's display name. | | `items[].productCode` | string | yes | The product's code, unique within the tenant. | | `items[].productNumber` | string | no | The product number, assigned by the system and unique within the tenant. Absent while none has been assigned. | | `items[].description` | string | no | A description of the product shown to customers. | | `items[].internalDescription` | string | no | A description of the product for internal use, not shown to customers. | | `items[].crmProductId` | string | no | The product's identifier in an external CRM system. | | `items[].effectiveStartDate` | string (date) | yes | The date the product becomes available. Format YYYY-MM-DD. | | `items[].effectiveEndDate` | string (date) | no | The date the product is no longer available. Format YYYY-MM-DD. Absent for a product with no end date. | | `items[].productFamily` | string | no | The product family. Possible values: 'software', 'services', 'physicalProducts'. | | `items[].productType` | string | no | The product type. Possible values: 'sub', 'termLicense', 'support', 'professionalServices', 'training', 'usage', 'billableTravelExpense', 'bundle', 'hardwareConsumables', 'billablePassThrough', 'merchandise', 'shipping'. | | `items[].productStatus` | string | no | The product's availability status, computed by the system. Possible values: 'draft', 'available', 'unavailable', 'incomplete'. It is derived from the product's lines, pricing and effective dates and cannot be set directly. | | `items[].isInventory` | boolean | no | Whether the product is tracked as inventory. | | `items[].itemIds` | array of string | no | The inventory items linked to this product. Used only when isInventory is true. Absent when the product has none. There is no endpoint for inventory items. | | `items[].createdDate` | string (date-time) | no | The date and time the product record was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the product record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].externalIdentifiers` | array of object | no | External identifiers owned by other systems, at most one per fieldName. Absent when the product has none. | | `items[].externalIdentifiers[].fieldName` | string | yes | The configured key of the external identifier, a camelCase slug rather than the display label. | | `items[].externalIdentifiers[].value` | string | yes | The identifier's value in the source system. | | `items[].externalIdentifiers[].externalUrl` | string | no | An absolute http or https link to the object in the source system. Absent when none is stored. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more products remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Pricing `GET /accounting/pricing` Group: Accounting › Products › Product Lines · Operation ID: `pricing` List pricing headers with structured filters. GET data-set exposing filter[field][op]= (eq/ne/gt/gte/lt/lte/in/notIn/between/like), sort=-validFrom,priceBookName, sparse fields=, and page[size]/page[cursor] cursor pagination. Filter by product line with filter[productLineId][eq]=, by product with filter[productId][eq]=, by price book with filter[priceBookId][eq]=. Returns the header of each record; entries, rate cards and external identifiers come from GET accounting/pricing/:id or POST accounting/pricing/search. Every request must carry the Api-Version header. [Open in the interactive reference](https://docs.api.lensing.ai/?op=pricing) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `productLineId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `productId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `priceBookId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `validFrom` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `validTo` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `dataSource` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `hasBeenUsed` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `createdDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, productLineId, productLineName, productId, productName, priceBookId, priceBookName, currency, dataSource, hasBeenUsed, validFrom, validTo, createdDate, lastModifiedDate - Selectable: id, headerNumber, productLineId, productLineName, productLineNumber, productId, productName, priceBookId, priceBookName, currency, validFrom, validTo, dataSource, hasBeenUsed, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of pricingRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | Stable, platform-assigned id (a UUID) of the pricing header — the public identifier used by GET accounting/pricing/:id. Filter e.g. filter[id][in]=,. | | `headerNumber` | string or null | no | System-generated pricing header number. Returned only — neither filterable nor orderable; look a header up by number through POST accounting/pricing/search. | | `productLineId` | string | no | Id of the product line this header prices (a UUID). Filter target: filter[productLineId][eq]=. | | `productLineName` | string or null | no | Display name of that product line. Orderable only. | | `productLineNumber` | string or null | no | System-generated number of that product line. Returned only. | | `productId` | string or null | no | Id of the product the line belongs to (a UUID). Filter target: filter[productId][eq]=. | | `productName` | string or null | no | Display name of that product. Orderable only. | | `priceBookId` | string | no | Id of the price book this header belongs to (a UUID). Filter target: filter[priceBookId][eq]=. | | `priceBookName` | string or null | no | Name of that price book. Orderable only. | | `currency` | string | no | ISO 4217 currency code governing every amount in this header. Filter target: filter[currency][eq]=USD. | | `validFrom` | string (date) | no | First date the header's prices are effective. Filter target: filter[validFrom][lte]=2026-12-31. | | `validTo` | string (date) or null | no | Last effective date. Absent when open-ended. Filter target: filter[validTo][gte]=2026-01-01. | | `dataSource` | string or null | no | Upstream system or import job that produced this header, e.g. 'Everest', 'Salesforce'. Filter target: filter[dataSource][eq]=Salesforce. | | `hasBeenUsed` | boolean or null | no | True once the header has priced at least one sales order line. Computed; never settable. Filter target: filter[hasBeenUsed][eq]=false. | | `createdDate` | string (date-time) or null | no | Instant the record was created, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Filter target: filter[createdDate][gte]=2026-01-01T00:00:00.000Z. | | `lastModifiedDate` | string (date-time) | no | Instant the record last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Filter target: filter[lastModifiedDate][gte]=2026-08-01T00:00:00.000Z, with sort=lastModifiedDate, is a complete incremental-sync cursor. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Get Accounting Pricing `GET /accounting/pricing/{id}` Group: Accounting › Products › Product Lines · Operation ID: `getAccountingPricing` Fetch a single pricing header by its id (a UUID), bound from the :id path segment. Returns the header with its price-book entries, rate cards and entry external identifiers — the same item schema as POST accounting/pricing/search. Read-only. Raises NOT_FOUND when no pricing header has that id (should map to 404). Every request must carry the Api-Version header. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getAccountingPricing) ## 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`. | | `id` | path | string | yes | Id of the pricing header to fetch (a UUID); bound from the :id path segment. | ## Response `200` Success Under `data` (getAccountingPricingOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Id of the pricing header (a UUID) — the public identifier used by this route. | | `headerNumber` | string | no | System-generated pricing header number. Absent until one has been assigned. | | `productLineId` | string | yes | Id of the product line this header prices (a UUID) — GET accounting/product-lines/:id. | | `productLineName` | string | no | Display name of that product line. | | `productLineNumber` | string | no | System-generated number of that product line. | | `productId` | string | no | Id of the product the line belongs to (a UUID) — GET accounting/products/:id. | | `productName` | string | no | Display name of that product. | | `priceBookId` | string | yes | Id of the price book this header belongs to (a UUID) — GET accounting/price-books/:id. | | `priceBookName` | string | no | Name of that price book. | | `currency` | string | yes | ISO 4217 currency code governing every amount in this header. | | `validFrom` | string (date) | yes | First date the header's prices are effective (YYYY-MM-DD, inclusive). | | `validTo` | string (date) | no | Last effective date (YYYY-MM-DD, inclusive). Absent when open-ended. | | `dataSource` | string | no | Upstream system or import job that produced this header, e.g. 'Everest', 'Salesforce'. Tenant-configurable, not exhaustively enumerable. | | `hasBeenUsed` | boolean | no | True once the header has priced at least one sales order line. Computed; never settable. | | `createdDate` | string (date-time) | no | Instant the record was created, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ), when recorded. | | `lastModifiedDate` | string (date-time) | yes | Instant the record last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Poll on this to sync incrementally. | | `priceBookEntries` | array of object | yes | Price-book entries of the header: one for flat pricing, one per tier for tiered models, one per rate card for rate-card pricing. Empty when the header has none. | | `priceBookEntries[].id` | string | yes | Id of the entry (a UUID). | | `priceBookEntries[].priceBookEntryNumber` | string | no | System-generated entry number. | | `priceBookEntries[].listPrice` | string (decimal) | no | List price, or the rate for a rate-card entry, as a decimal in the header currency (see currency). | | `priceBookEntries[].fromQuantity` | string (decimal) | no | Inclusive lower bound of this tier's quantity range. Tiered models only. | | `priceBookEntries[].toQuantity` | string (decimal) | no | Inclusive upper bound of this tier's quantity range. Absent on the open-ended top tier. | | `priceBookEntries[].priceModel` | string | no | How this tier prices quantity. One of 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. Tiered and rate-card models only. | | `priceBookEntries[].rateCardEntry` | object | no | Rate card priced by this entry. Rate-card pricing only. | | `priceBookEntries[].rateCardEntry.id` | string | yes | Id of the rate-card entry (a UUID). | | `priceBookEntries[].rateCardEntry.billableComponentId` | string | no | Id of the billable component this rate prices (a UUID). | | `priceBookEntries[].rateCardEntry.unitOfMeasure` | string | yes | Unit of measure of the rate card. One of 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `priceBookEntries[].rateCardEntry.description` | string | no | Rate-card description. | | `priceBookEntries[].rateCardEntry.usageType` | string | no | Usage type the rate applies to. One of 'consumption', 'overage', 'thresholdCounter', 'payg'. | | `priceBookEntries[].rateCardEntry.billableUnitOfMeasure` | string | no | Unit of measure the usage is billed in, when it differs from unitOfMeasure. | | `priceBookEntries[].rateCardEntry.conversion` | string (decimal) | no | Conversion factor from unitOfMeasure to billableUnitOfMeasure. A quantity, not an amount. | | `priceBookEntries[].rateCardEntry.invoiceFrequency` | string | no | Invoice frequency of the rate card, when it differs from the line. One of 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `priceBookEntries[].rateCardEntry.priceModel` | string | no | Price model of the rate card, when it differs from the line. One of 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. | | `priceBookEntries[].externalIdentifiers` | array of object | no | External identifiers owned by other systems for this entry. At most one entry per fieldName. Absent when it has none. | | `priceBookEntries[].externalIdentifiers[].fieldName` | string | yes | Configured key of the external identifier — a stable camelCase slug, not the human-readable label. | | `priceBookEntries[].externalIdentifiers[].value` | string | yes | The identifier as the source system holds it. | | `priceBookEntries[].externalIdentifiers[].externalUrl` | string | no | Absolute http(s) link to the object in the originating system, when one is stored. | ## Errors | Code | Status | | --- | --- | | `INVALID_ID` | 400 | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_ID, NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Search Accounting Pricing `POST /accounting/pricing/search` Group: Accounting › Products › Product Lines · Operation ID: `searchAccountingPricing` Search pricing headers by free text (price book name, product line name, header number) plus structured filters, and page through the matches. POST so `term` and identifiers travel in the JSON body rather than the URL. Read-only. Returns the full record for each match, including entries, rate cards and entry external identifiers; matching nothing is an empty items array with HTTP 200, not a 404. Every request must carry the Api-Version header. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchAccountingPricing) ## 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`. | ## Request body Required, `application/json`: searchAccountingPricingInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | Search filters, nested as one object so they map onto the POST body. All are optional and are ANDed; omit the whole object to match all pricing headers. | | `filters.term` | string | no | Case-insensitive SUBSTRING match against the price book name, the product line name and the header number. | | `filters.headerNumber` | string | no | Exact match on the system-generated header number. | | `filters.productLineId` | string | no | Restrict to headers of this product line (a UUID). An id that matches nothing yields an empty page, not an error. | | `filters.productId` | string | no | Restrict to headers of any line of this product (a UUID). An id that matches nothing yields an empty page. | | `filters.priceBookId` | string | no | Restrict to headers in this price book (a UUID). An id that matches nothing yields an empty page. | | `filters.currency` | string | no | Exact match on the ISO 4217 currency code. | | `filters.dataSource` | string | no | Exact match on the upstream system or import job that produced the header, e.g. 'Salesforce'. | | `filters.hasBeenUsed` | boolean | no | Restrict to headers that have (or have not) priced a sales order line. | | `filters.validFrom` | object | no | Filter by effective start date. Bounds are inclusive. | | `filters.validFrom.start` | string | no | Inclusive lower bound (YYYY-MM-DD). | | `filters.validFrom.end` | string | no | Inclusive upper bound (YYYY-MM-DD). | | `filters.validTo` | object | no | Filter by effective end date. Bounds are inclusive; an open-ended header has no end date and never matches this filter. | | `filters.validTo.start` | string | no | Inclusive lower bound (YYYY-MM-DD). | | `filters.validTo.end` | string | no | Inclusive upper bound (YYYY-MM-DD). | | `filters.createdDate` | object | no | Filter by creation instant. Bounds are inclusive. | | `filters.createdDate.start` | string | no | Inclusive lower bound (ISO 8601 UTC, YYYY-MM-DDTHH:mm:ss.sssZ). | | `filters.createdDate.end` | string | no | Inclusive upper bound (ISO 8601 UTC, YYYY-MM-DDTHH:mm:ss.sssZ). | | `filters.lastModifiedDate` | object | no | Filter by last-changed instant. Bounds are inclusive. | | `filters.lastModifiedDate.start` | string | no | Inclusive lower bound (ISO 8601 UTC, YYYY-MM-DDTHH:mm:ss.sssZ). | | `filters.lastModifiedDate.end` | string | no | Inclusive upper bound (ISO 8601 UTC, YYYY-MM-DDTHH:mm:ss.sssZ). | | `filters.externalIdentifier` | object | no | Match headers owning a price-book entry that carries this identifier from another system. Both members are required. Not unique across entries, so this returns the ordinary items array. | | `filters.externalIdentifier.fieldName` | string | yes | Configured key of the external identifier — a stable camelCase slug, not the human-readable label. Must match an active PriceBookEntryExternalIdentifierConfiguration or the call fails with UNKNOWN_EXTERNAL_IDENTIFIER. | | `filters.externalIdentifier.value` | string | yes | The identifier as the source system holds it. Matched exactly, case-sensitively. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | Opaque forward cursor from a previous response's pagination.nextCursor; omit for the first page. Do not construct manually. | | `page.size` | number | no | Maximum number of pricing headers to return. Defaults to 50, capped at 200; the server may return fewer, but never more. | Example: ```json { "filters": { "term": "", "headerNumber": "", "productLineId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "priceBookId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "currency": "", "dataSource": "", "hasBeenUsed": true, "validFrom": { "start": "", "end": "" }, "validTo": { "start": "", "end": "" }, "createdDate": { "start": "", "end": "" }, "lastModifiedDate": { "start": "", "end": "" }, "externalIdentifier": { "fieldName": "", "value": "" } }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchAccountingPricingOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | Matching pricing headers for this page. Empty when nothing matches. | | `items[].id` | string | yes | Id of the pricing header (a UUID) — the public identifier of this record. | | `items[].headerNumber` | string | no | System-generated pricing header number. Absent until one has been assigned. | | `items[].productLineId` | string | yes | Id of the product line this header prices (a UUID) — GET accounting/product-lines/:id. | | `items[].productLineName` | string | no | Display name of that product line. | | `items[].productLineNumber` | string | no | System-generated number of that product line. | | `items[].productId` | string | no | Id of the product the line belongs to (a UUID) — GET accounting/products/:id. | | `items[].productName` | string | no | Display name of that product. | | `items[].priceBookId` | string | yes | Id of the price book this header belongs to (a UUID) — GET accounting/price-books/:id. | | `items[].priceBookName` | string | no | Name of that price book. | | `items[].currency` | string | yes | ISO 4217 currency code governing every amount in this header. | | `items[].validFrom` | string (date) | yes | First date the header's prices are effective (YYYY-MM-DD, inclusive). | | `items[].validTo` | string (date) | no | Last effective date (YYYY-MM-DD, inclusive). Absent when open-ended. | | `items[].dataSource` | string | no | Upstream system or import job that produced this header, e.g. 'Everest', 'Salesforce'. Tenant-configurable, not exhaustively enumerable. | | `items[].hasBeenUsed` | boolean | no | True once the header has priced at least one sales order line. Computed; never settable. | | `items[].createdDate` | string (date-time) | no | Instant the record was created, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ), when recorded. | | `items[].lastModifiedDate` | string (date-time) | yes | Instant the record last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Poll on this to sync incrementally. | | `items[].priceBookEntries` | array of object | yes | Price-book entries of the header: one for flat pricing, one per tier for tiered models, one per rate card for rate-card pricing. Empty when the header has none. | | `items[].priceBookEntries[].id` | string | yes | Id of the entry (a UUID). | | `items[].priceBookEntries[].priceBookEntryNumber` | string | no | System-generated entry number. | | `items[].priceBookEntries[].listPrice` | string (decimal) | no | List price, or the rate for a rate-card entry, as a decimal in the header currency (see currency). | | `items[].priceBookEntries[].fromQuantity` | string (decimal) | no | Inclusive lower bound of this tier's quantity range. Tiered models only. | | `items[].priceBookEntries[].toQuantity` | string (decimal) | no | Inclusive upper bound of this tier's quantity range. Absent on the open-ended top tier. | | `items[].priceBookEntries[].priceModel` | string | no | How this tier prices quantity. One of 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. Tiered and rate-card models only. | | `items[].priceBookEntries[].rateCardEntry` | object | no | Rate card priced by this entry. Rate-card pricing only. | | `items[].priceBookEntries[].rateCardEntry.id` | string | yes | Id of the rate-card entry (a UUID). | | `items[].priceBookEntries[].rateCardEntry.billableComponentId` | string | no | Id of the billable component this rate prices (a UUID). | | `items[].priceBookEntries[].rateCardEntry.unitOfMeasure` | string | yes | Unit of measure of the rate card. One of 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `items[].priceBookEntries[].rateCardEntry.description` | string | no | Rate-card description. | | `items[].priceBookEntries[].rateCardEntry.usageType` | string | no | Usage type the rate applies to. One of 'consumption', 'overage', 'thresholdCounter', 'payg'. | | `items[].priceBookEntries[].rateCardEntry.billableUnitOfMeasure` | string | no | Unit of measure the usage is billed in, when it differs from unitOfMeasure. | | `items[].priceBookEntries[].rateCardEntry.conversion` | string (decimal) | no | Conversion factor from unitOfMeasure to billableUnitOfMeasure. A quantity, not an amount. | | `items[].priceBookEntries[].rateCardEntry.invoiceFrequency` | string | no | Invoice frequency of the rate card, when it differs from the line. One of 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `items[].priceBookEntries[].rateCardEntry.priceModel` | string | no | Price model of the rate card, when it differs from the line. One of 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. | | `items[].priceBookEntries[].externalIdentifiers` | array of object | no | External identifiers owned by other systems for this entry. At most one entry per fieldName. Absent when it has none. | | `items[].priceBookEntries[].externalIdentifiers[].fieldName` | string | yes | Configured key of the external identifier — a stable camelCase slug, not the human-readable label. | | `items[].priceBookEntries[].externalIdentifiers[].value` | string | yes | The identifier as the source system holds it. | | `items[].priceBookEntries[].externalIdentifiers[].externalUrl` | string | no | Absolute http(s) link to the object in the originating system, when one is stored. | | `pagination` | object | yes | Pagination cursor for this page. | | `pagination.nextCursor` | string | no | Opaque cursor to pass as page.cursor for the next page; absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Errors | Code | Status | | --- | --- | | `INVALID_DATE` | 400 | | `INVALID_TIMESTAMP` | 400 | | `UNKNOWN_EXTERNAL_IDENTIFIER` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_DATE, INVALID_TIMESTAMP, UNKNOWN_EXTERNAL_IDENTIFIER - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Product Lines `GET /accounting/product-lines` Group: Accounting › Products › Product Lines · Operation ID: `productLines` List product lines with structured filters. GET data-set exposing filter[field][op]= (eq/ne/gt/gte/lt/lte/in/notIn/between/like), sort=-lastModifiedDate,name, sparse fields=, and page[size]/page[cursor] cursor pagination. Filter by parent product with filter[productId][eq]=. Returns the scalar header of each line; tax records, pricing and external identifiers come from GET accounting/product-lines/:id or POST accounting/product-lines/search. Every request must carry the Api-Version header. [Open in the interactive reference](https://docs.api.lensing.ai/?op=productLines) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `productId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lineType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `priceModel` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `pricingTerm` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `invoiceFrequency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `unitOfMeasure` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `taxMode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `enforceListPrice` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `hasUsage` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `hasBeenUsed` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `parentProductLineId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `dataSource` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `createdDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, productId, productName, name, lineType, priceModel, pricingTerm, invoiceFrequency, unitOfMeasure, taxMode, enforceListPrice, hasUsage, hasBeenUsed, parentProductLineId, dataSource, createdDate, lastModifiedDate - Selectable: id, productId, productName, productLineNumber, name, lineType, priceModel, pricingTerm, invoiceFrequency, unitOfMeasure, taxMode, enforceListPrice, hasUsage, hasBeenUsed, commitInvoicing, prepaidBalanceOverConsumptionBehavior, prepaidBalanceRolloverAtRenewal, parentProductLineId, crmProductId, dataSource, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of productLinesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | Stable, platform-assigned id (a UUID) of the product line — the public identifier used by GET accounting/product-lines/:id. Filter e.g. filter[id][in]=,. | | `productId` | string | no | Id of the parent product (a UUID). Filter target: filter[productId][eq]=. | | `productName` | string or null | no | Display name of the parent product. Orderable only. | | `productLineNumber` | string or null | no | System-generated product line number. Returned only — neither filterable nor orderable; look a line up by number through POST accounting/product-lines/search. | | `name` | string | no | Product line display name. Orderable only; substring matching is a search concern (POST accounting/product-lines/search term). | | `lineType` | string or null | no | Line type. One of 'recurring', 'oneTime', 'commitWithOverage', 'payAsYouGo', 'usage', 'prepaid', 'billableTime', 'billableTravelExpense', 'billablePassThrough'. Filter target: filter[lineType][in]=recurring,prepaid. | | `priceModel` | string or null | no | Price model. One of 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. Filter target: filter[priceModel][eq]=tiered. | | `pricingTerm` | string or null | no | Pricing term. One of 'monthly', 'biMonthly', 'once', 'quarterly', 'semiAnnually', 'annually'. | | `invoiceFrequency` | string or null | no | Invoice frequency. One of 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `unitOfMeasure` | string or null | no | Unit of measure quantities are tracked in, e.g. 'user', 'seat', 'hour', 'each'. See GET accounting/product-lines/:id for the full set. | | `taxMode` | string or null | no | Tax mode. One of 'TaxInclusive', 'TaxExclusive' (PascalCase members). | | `enforceListPrice` | boolean or null | no | When true, sales transactions on this line cannot override the list price. Filter target: filter[enforceListPrice][eq]=true. | | `hasUsage` | boolean or null | no | True when this Prepaid line has a paired Usage line. | | `hasBeenUsed` | boolean or null | no | True once the line has been sold on at least one sales order. Computed; never settable. | | `commitInvoicing` | string or null | no | Invoice timing of a commit line. One of 'advance', 'arrears'. Absent on other line types. | | `prepaidBalanceOverConsumptionBehavior` | string or null | no | What happens when usage exceeds the prepaid balance. One of 'allowNegative', 'overage'. Prepaid lines only. | | `prepaidBalanceRolloverAtRenewal` | boolean or null | no | Whether the remaining prepaid balance rolls over at renewal. Prepaid lines only. | | `parentProductLineId` | string or null | no | Id of the parent Prepaid product line (a UUID) when this is its paired Usage line. Filter target: filter[parentProductLineId][eq]=. | | `crmProductId` | string or null | no | CRM system's own identifier for this line, when integrated from one. Returned only — neither filterable nor orderable; filter on it through POST accounting/product-lines/search. | | `dataSource` | string or null | no | Upstream system or import job that produced this line, e.g. 'Everest', 'Salesforce'. Filter target: filter[dataSource][eq]=Salesforce. | | `createdDate` | string (date-time) or null | no | Instant the record was created, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). | | `lastModifiedDate` | string (date-time) | no | Instant the record last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Filter target: filter[lastModifiedDate][gte]=2026-08-01T00:00:00.000Z, with sort=lastModifiedDate, is a complete incremental-sync cursor. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Get Accounting Product Line `GET /accounting/product-lines/{id}` Group: Accounting › Products › Product Lines · Operation ID: `getAccountingProductLine` Fetch a single product line by its id (a UUID), bound from the :id path segment. Returns the full record including tax records, pricing headers with their entries and rate cards, and external identifiers — the same item schema as POST accounting/product-lines/search. Read-only. Raises NOT_FOUND when no product line has that id (should map to 404). Every request must carry the Api-Version header. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getAccountingProductLine) ## 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`. | | `id` | path | string | yes | Id of the product line to fetch (a UUID); bound from the :id path segment. | ## Response `200` Success Under `data` (getAccountingProductLineOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Id of the product line (a UUID) — the public identifier used by this route. | | `productId` | string | yes | Id of the parent product (a UUID) — GET accounting/products/:id. | | `productName` | string | no | Display name of the parent product. | | `productLineNumber` | string | no | System-generated product line number, unique per tenant. Absent until one has been assigned. | | `name` | string | yes | Product line display name. | | `lineType` | string | no | Line type. One of 'recurring', 'oneTime', 'commitWithOverage', 'payAsYouGo', 'usage', 'prepaid', 'billableTime', 'billableTravelExpense', 'billablePassThrough'. | | `priceModel` | string | no | Price model. One of 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. | | `pricingTerm` | string | no | Pricing term. One of 'monthly', 'biMonthly', 'once', 'quarterly', 'semiAnnually', 'annually'. | | `invoiceFrequency` | string | no | Invoice frequency. One of 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `unitOfMeasure` | string | no | Unit of measure quantities are tracked in. One of 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `taxMode` | string | no | Tax mode. One of 'TaxInclusive', 'TaxExclusive' (PascalCase members). | | `enforceListPrice` | boolean | no | When true, sales transactions on this line cannot override the list price. | | `hasUsage` | boolean | no | True when this Prepaid line has a paired Usage line. | | `hasBeenUsed` | boolean | no | True once the line has been sold on at least one sales order. Computed; never settable. | | `commitInvoicing` | string | no | Invoice timing of a commit line. One of 'advance', 'arrears'. Absent on other line types. | | `prepaidBalanceOverConsumptionBehavior` | string | no | What happens when usage exceeds the prepaid balance. One of 'allowNegative', 'overage'. Prepaid lines only. | | `prepaidBalanceRolloverAtRenewal` | boolean | no | Whether the remaining prepaid balance rolls over at renewal. Prepaid lines only. | | `parentProductLineId` | string | no | Id of the parent Prepaid product line (a UUID) when this is its paired Usage line. | | `crmProductId` | string | no | CRM system's own identifier for this product line, when integrated from one. | | `dataSource` | string | no | Upstream system or import job that produced this line, e.g. 'Everest', 'Salesforce'. Tenant-configurable, not exhaustively enumerable. | | `createdDate` | string (date-time) | no | Instant the record was created, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ), when recorded. | | `lastModifiedDate` | string (date-time) | yes | Instant the record last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Poll on this to sync incrementally. | | `productLineTaxes` | array of object | no | Tax records attached to this line. Absent when it has none. | | `productLineTaxes[].id` | string | yes | Id of the tax record (a UUID). | | `productLineTaxes[].taxCode` | string | yes | Tax code as defined by the configured tax provider. | | `productLineTaxes[].engine` | string | yes | Tax engine. One of 'avalara', 'anrok', 'everestTaxEngine', 'manualTax', 'none'. | | `productLineTaxes[].country` | string | no | ISO 3166-1 country code the record applies in. | | `productLineTaxes[].entityId` | string | no | Id of the legal entity the record applies under (a UUID). | | `pricingHeaders` | array of object | no | Pricing headers of this line, each with its price-book entries. Absent when the line has no pricing. Amounts are in the header's currency. | | `pricingHeaders[].id` | string | yes | Id of the pricing header (a UUID) — the :pricingId of the pricing routes. | | `pricingHeaders[].headerNumber` | string | no | System-generated header number. | | `pricingHeaders[].priceBookId` | string | no | Id of the price book this header belongs to (a UUID). | | `pricingHeaders[].priceBookName` | string | no | Name of that price book. | | `pricingHeaders[].currency` | string | yes | ISO 4217 currency code governing every amount in this header. | | `pricingHeaders[].validFrom` | string (date) | yes | First date the header's prices are effective (YYYY-MM-DD, inclusive). | | `pricingHeaders[].validTo` | string (date) | no | Last effective date (YYYY-MM-DD, inclusive). Absent when open-ended. | | `pricingHeaders[].priceBookEntries` | array of object | yes | Price-book entries of the header: one for flat pricing, one per tier for tiered models, one per rate card for rate-card pricing. | | `pricingHeaders[].priceBookEntries[].id` | string | yes | Id of the entry (a UUID). | | `pricingHeaders[].priceBookEntries[].priceBookEntryNumber` | string | no | System-generated entry number. | | `pricingHeaders[].priceBookEntries[].listPrice` | string (decimal) | no | List price, or the rate for a rate-card entry, as a decimal in the header currency (see currency). | | `pricingHeaders[].priceBookEntries[].fromQuantity` | string (decimal) | no | Inclusive lower bound of this tier's quantity range. Tiered models only. | | `pricingHeaders[].priceBookEntries[].toQuantity` | string (decimal) | no | Inclusive upper bound of this tier's quantity range. Absent on the open-ended top tier. | | `pricingHeaders[].priceBookEntries[].priceModel` | string | no | How this tier prices quantity. One of 'flat', 'perUnit'. Tiered models only. | | `pricingHeaders[].priceBookEntries[].rateCardEntry` | object | no | Rate card priced by this entry. Rate-card pricing only. | | `pricingHeaders[].priceBookEntries[].rateCardEntry.id` | string | yes | Id of the rate-card entry (a UUID). | | `pricingHeaders[].priceBookEntries[].rateCardEntry.billableComponentId` | string | no | Id of the billable component this rate prices (a UUID). | | `pricingHeaders[].priceBookEntries[].rateCardEntry.unitOfMeasure` | string | yes | Unit of measure of the rate card. | | `pricingHeaders[].priceBookEntries[].rateCardEntry.description` | string | no | Rate-card description. | | `pricingHeaders[].priceBookEntries[].rateCardEntry.usageType` | string | no | Usage type the rate applies to. | | `pricingHeaders[].priceBookEntries[].rateCardEntry.billableUnitOfMeasure` | string | no | Unit of measure the usage is billed in, when it differs from unitOfMeasure. | | `pricingHeaders[].priceBookEntries[].rateCardEntry.conversion` | string (decimal) | no | Conversion factor from unitOfMeasure to billableUnitOfMeasure. A quantity, not an amount. | | `pricingHeaders[].priceBookEntries[].rateCardEntry.invoiceFrequency` | string | no | Invoice frequency of the rate card, when it differs from the line. | | `pricingHeaders[].priceBookEntries[].rateCardEntry.priceModel` | string | no | Price model of the rate card, when it differs from the line. | | `externalIdentifiers` | array of object | no | External identifiers owned by other systems. At most one entry per fieldName. Absent when the line has none. | | `externalIdentifiers[].fieldName` | string | yes | Configured key of the external identifier — a stable camelCase slug, not the human-readable label. | | `externalIdentifiers[].value` | string | yes | The identifier as the source system holds it. | | `externalIdentifiers[].externalUrl` | string | no | Absolute http(s) link to the object in the originating system, when one is stored. | ## Errors | Code | Status | | --- | --- | | `INVALID_ID` | 400 | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_ID, NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Search Accounting Product Lines `POST /accounting/product-lines/search` Group: Accounting › Products › Product Lines · Operation ID: `searchAccountingProductLines` Search product lines by free text (name, product line number) plus structured filters, and page through the matches. POST so `term` and identifiers travel in the JSON body rather than the URL. Read-only. Returns the full record for each match, including tax records, pricing and external identifiers; matching nothing is an empty items array with HTTP 200, not a 404. Every request must carry the Api-Version header. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchAccountingProductLines) ## 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`. | ## Request body Required, `application/json`: searchAccountingProductLinesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | Search filters, nested as one object so they map onto the POST body. All are optional and are ANDed; omit the whole object to match all product lines. | | `filters.term` | string | no | Case-insensitive SUBSTRING match against name and productLineNumber. | | `filters.productId` | string | no | Restrict to lines of this product (a UUID). An id that matches no product yields an empty page, not an error. | | `filters.productLineNumber` | string | no | Exact match on the system-generated product line number. | | `filters.crmProductId` | string | no | Exact match on the CRM system's own identifier for the line. | | `filters.lineType` | string | no | Restrict to this line type. One of 'recurring', 'oneTime', 'commitWithOverage', 'payAsYouGo', 'usage', 'prepaid', 'billableTime', 'billableTravelExpense', 'billablePassThrough'. | | `filters.priceModel` | string | no | Restrict to this price model. One of 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. | | `filters.pricingTerm` | string | no | Restrict to this pricing term. One of 'monthly', 'biMonthly', 'once', 'quarterly', 'semiAnnually', 'annually'. | | `filters.invoiceFrequency` | string | no | Restrict to this invoice frequency. One of 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `filters.unitOfMeasure` | string | no | Restrict to this unit of measure, e.g. 'user', 'seat', 'hour'. | | `filters.taxMode` | string | no | Restrict to this tax mode. One of 'TaxInclusive', 'TaxExclusive'. | | `filters.enforceListPrice` | boolean | no | Restrict to lines that do (or do not) enforce the list price. | | `filters.hasUsage` | boolean | no | Restrict to Prepaid lines with (or without) a paired Usage line. | | `filters.parentProductLineId` | string | no | Restrict to the Usage line(s) paired with this Prepaid product line (a UUID). An id that matches nothing yields an empty page. | | `filters.dataSource` | string | no | Exact match on the upstream system or import job that produced the line, e.g. 'Salesforce'. | | `filters.createdDate` | object | no | Filter by creation instant. Bounds are inclusive. | | `filters.createdDate.start` | string | no | Inclusive lower bound (ISO 8601 UTC, YYYY-MM-DDTHH:mm:ss.sssZ). | | `filters.createdDate.end` | string | no | Inclusive upper bound (ISO 8601 UTC, YYYY-MM-DDTHH:mm:ss.sssZ). | | `filters.lastModifiedDate` | object | no | Filter by last-changed instant. Bounds are inclusive. | | `filters.lastModifiedDate.start` | string | no | Inclusive lower bound (ISO 8601 UTC, YYYY-MM-DDTHH:mm:ss.sssZ). | | `filters.lastModifiedDate.end` | string | no | Inclusive upper bound (ISO 8601 UTC, YYYY-MM-DDTHH:mm:ss.sssZ). | | `filters.externalIdentifier` | object | no | Match one identifier owned by another system. Both members are required. Not unique across lines, so this returns the ordinary items array. | | `filters.externalIdentifier.fieldName` | string | yes | Configured key of the external identifier — a stable camelCase slug, not the human-readable label. Must match an active ProductLineExternalIdentifierConfiguration or the call fails with UNKNOWN_EXTERNAL_IDENTIFIER. | | `filters.externalIdentifier.value` | string | yes | The identifier as the source system holds it. Matched exactly, case-sensitively. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | Opaque forward cursor from a previous response's pagination.nextCursor; omit for the first page. Do not construct manually. | | `page.size` | number | no | Maximum number of product lines to return. Defaults to 50, capped at 200; the server may return fewer, but never more. | Example: ```json { "filters": { "term": "", "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "productLineNumber": "", "crmProductId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "lineType": "", "priceModel": "", "pricingTerm": "", "invoiceFrequency": "", "unitOfMeasure": "", "taxMode": "", "enforceListPrice": true, "hasUsage": true, "parentProductLineId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "dataSource": "", "createdDate": { "start": "", "end": "" }, "lastModifiedDate": { "start": "", "end": "" }, "externalIdentifier": { "fieldName": "", "value": "" } }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchAccountingProductLinesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | Matching product lines for this page. Empty when nothing matches. | | `items[].id` | string | yes | Id of the product line (a UUID) — the public identifier of this record. | | `items[].productId` | string | yes | Id of the parent product (a UUID) — GET accounting/products/:id. | | `items[].productName` | string | no | Display name of the parent product. | | `items[].productLineNumber` | string | no | System-generated product line number, unique per tenant. Absent until one has been assigned. | | `items[].name` | string | yes | Product line display name. | | `items[].lineType` | string | no | Line type. One of 'recurring', 'oneTime', 'commitWithOverage', 'payAsYouGo', 'usage', 'prepaid', 'billableTime', 'billableTravelExpense', 'billablePassThrough'. | | `items[].priceModel` | string | no | Price model. One of 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. | | `items[].pricingTerm` | string | no | Pricing term. One of 'monthly', 'biMonthly', 'once', 'quarterly', 'semiAnnually', 'annually'. | | `items[].invoiceFrequency` | string | no | Invoice frequency. One of 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `items[].unitOfMeasure` | string | no | Unit of measure quantities are tracked in. One of 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `items[].taxMode` | string | no | Tax mode. One of 'TaxInclusive', 'TaxExclusive' (PascalCase members). | | `items[].enforceListPrice` | boolean | no | When true, sales transactions on this line cannot override the list price. | | `items[].hasUsage` | boolean | no | True when this Prepaid line has a paired Usage line. | | `items[].hasBeenUsed` | boolean | no | True once the line has been sold on at least one sales order. Computed; never settable. | | `items[].commitInvoicing` | string | no | Invoice timing of a commit line. One of 'advance', 'arrears'. Absent on other line types. | | `items[].prepaidBalanceOverConsumptionBehavior` | string | no | What happens when usage exceeds the prepaid balance. One of 'allowNegative', 'overage'. Prepaid lines only. | | `items[].prepaidBalanceRolloverAtRenewal` | boolean | no | Whether the remaining prepaid balance rolls over at renewal. Prepaid lines only. | | `items[].parentProductLineId` | string | no | Id of the parent Prepaid product line (a UUID) when this is its paired Usage line. | | `items[].crmProductId` | string | no | CRM system's own identifier for this product line, when integrated from one. | | `items[].dataSource` | string | no | Upstream system or import job that produced this line, e.g. 'Everest', 'Salesforce'. Tenant-configurable, not exhaustively enumerable. | | `items[].createdDate` | string (date-time) | no | Instant the record was created, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ), when recorded. | | `items[].lastModifiedDate` | string (date-time) | yes | Instant the record last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Poll on this to sync incrementally. | | `items[].productLineTaxes` | array of object | no | Tax records attached to this line. Absent when it has none. | | `items[].productLineTaxes[].id` | string | yes | Id of the tax record (a UUID). | | `items[].productLineTaxes[].taxCode` | string | yes | Tax code as defined by the configured tax provider. | | `items[].productLineTaxes[].engine` | string | yes | Tax engine. One of 'avalara', 'anrok', 'everestTaxEngine', 'manualTax', 'none'. | | `items[].productLineTaxes[].country` | string | no | ISO 3166-1 country code the record applies in. | | `items[].productLineTaxes[].entityId` | string | no | Id of the legal entity the record applies under (a UUID). | | `items[].pricingHeaders` | array of object | no | Pricing headers of this line, each with its price-book entries. Absent when the line has no pricing. Amounts are in the header's currency. | | `items[].pricingHeaders[].id` | string | yes | Id of the pricing header (a UUID) — the :pricingId of the pricing routes. | | `items[].pricingHeaders[].headerNumber` | string | no | System-generated header number. | | `items[].pricingHeaders[].priceBookId` | string | no | Id of the price book this header belongs to (a UUID). | | `items[].pricingHeaders[].priceBookName` | string | no | Name of that price book. | | `items[].pricingHeaders[].currency` | string | yes | ISO 4217 currency code governing every amount in this header. | | `items[].pricingHeaders[].validFrom` | string (date) | yes | First date the header's prices are effective (YYYY-MM-DD, inclusive). | | `items[].pricingHeaders[].validTo` | string (date) | no | Last effective date (YYYY-MM-DD, inclusive). Absent when open-ended. | | `items[].pricingHeaders[].priceBookEntries` | array of object | yes | Price-book entries of the header: one for flat pricing, one per tier for tiered models, one per rate card for rate-card pricing. | | `items[].pricingHeaders[].priceBookEntries[].id` | string | yes | Id of the entry (a UUID). | | `items[].pricingHeaders[].priceBookEntries[].priceBookEntryNumber` | string | no | System-generated entry number. | | `items[].pricingHeaders[].priceBookEntries[].listPrice` | string (decimal) | no | List price, or the rate for a rate-card entry, as a decimal in the header currency (see currency). | | `items[].pricingHeaders[].priceBookEntries[].fromQuantity` | string (decimal) | no | Inclusive lower bound of this tier's quantity range. Tiered models only. | | `items[].pricingHeaders[].priceBookEntries[].toQuantity` | string (decimal) | no | Inclusive upper bound of this tier's quantity range. Absent on the open-ended top tier. | | `items[].pricingHeaders[].priceBookEntries[].priceModel` | string | no | How this tier prices quantity. One of 'flat', 'perUnit'. Tiered models only. | | `items[].pricingHeaders[].priceBookEntries[].rateCardEntry` | object | no | Rate card priced by this entry. Rate-card pricing only. | | `items[].pricingHeaders[].priceBookEntries[].rateCardEntry.id` | string | yes | Id of the rate-card entry (a UUID). | | `items[].pricingHeaders[].priceBookEntries[].rateCardEntry.billableComponentId` | string | no | Id of the billable component this rate prices (a UUID). | | `items[].pricingHeaders[].priceBookEntries[].rateCardEntry.unitOfMeasure` | string | yes | Unit of measure of the rate card. | | `items[].pricingHeaders[].priceBookEntries[].rateCardEntry.description` | string | no | Rate-card description. | | `items[].pricingHeaders[].priceBookEntries[].rateCardEntry.usageType` | string | no | Usage type the rate applies to. | | `items[].pricingHeaders[].priceBookEntries[].rateCardEntry.billableUnitOfMeasure` | string | no | Unit of measure the usage is billed in, when it differs from unitOfMeasure. | | `items[].pricingHeaders[].priceBookEntries[].rateCardEntry.conversion` | string (decimal) | no | Conversion factor from unitOfMeasure to billableUnitOfMeasure. A quantity, not an amount. | | `items[].pricingHeaders[].priceBookEntries[].rateCardEntry.invoiceFrequency` | string | no | Invoice frequency of the rate card, when it differs from the line. | | `items[].pricingHeaders[].priceBookEntries[].rateCardEntry.priceModel` | string | no | Price model of the rate card, when it differs from the line. | | `items[].externalIdentifiers` | array of object | no | External identifiers owned by other systems. At most one entry per fieldName. Absent when the line has none. | | `items[].externalIdentifiers[].fieldName` | string | yes | Configured key of the external identifier — a stable camelCase slug, not the human-readable label. | | `items[].externalIdentifiers[].value` | string | yes | The identifier as the source system holds it. | | `items[].externalIdentifiers[].externalUrl` | string | no | Absolute http(s) link to the object in the originating system, when one is stored. | | `pagination` | object | yes | Pagination cursor for this page. | | `pagination.nextCursor` | string | no | Opaque cursor to pass as page.cursor for the next page; absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Errors | Code | Status | | --- | --- | | `INVALID_TIMESTAMP` | 400 | | `UNKNOWN_EXTERNAL_IDENTIFIER` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_TIMESTAMP, UNKNOWN_EXTERNAL_IDENTIFIER - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Add product lines to a product `POST /accounting/products/{id}/product-lines` Group: Accounting › Products › Product Lines · Operation ID: `addAccountingProductLines` Adds one or more product lines (with optional pricing) to an existing product, in the same transaction. Same nested shape as `productLinesToCreate` on POST accounting/products — see that endpoint's explanation for the full rules (Prepaid/Usage pairing, price-book/billable-component/tax-entity resolution, CommitWithOverage rejection). `id` must resolve to an existing product or the call fails with NOT_FOUND. [Open in the interactive reference](https://docs.api.lensing.ai/?op=addAccountingProductLines) ## 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. | | `id` | path | string | yes | Id of the product to add lines to (a UUID); bound from the :id path segment. | ## Request body Required, `application/json`: addAccountingProductLinesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `productLines` | array of object | yes | Product lines to create, with optional pricing. A `Prepaid` line automatically also creates a paired `Usage` line. | | `productLines[].lineType` | string | yes | Type of product line. One of 'recurring', 'oneTime', 'usage', 'prepaid', 'billableTime', 'billableTravelExpense', 'billablePassThrough'. CommitWithOverage is not supported on this endpoint. | | `productLines[].priceModel` | string | yes | How this product line is priced. One of 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. | | `productLines[].unitOfMeasure` | string | yes | Unit of measure quantities of this product line are tracked in. One of 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `productLines[].name` | string | no | Human-readable name for the product line. Omit to let the platform auto-generate one. | | `productLines[].pricingTerm` | string | no | Pricing term of the product line. One of 'monthly', 'biMonthly', 'once', 'quarterly', 'semiAnnually', 'annually'. | | `productLines[].enforceListPrice` | boolean | no | When true, sales transactions on this product line cannot override the list price. | | `productLines[].invoiceFrequency` | string | no | How often this product line is invoiced. One of 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `productLines[].prepaidBalanceOverConsumptionBehavior` | string | no | Controls what happens when usage exceeds the prepaid balance. Accepted values: allowNegative (balance goes negative; reconciled at renewal) and overage (switches to PAYG invoicing when balance hits 0). | | `productLines[].prepaidBalanceRolloverAtRenewal` | boolean | no | Whether the remaining prepaid balance rolls over at renewal. | | `productLines[].crmProductId` | string | no | CRM system's own identifier for this product line, when integrating from one. | | `productLines[].dataSource` | string | no | Identifier of the upstream system or import job that produced this product line. E.g. 'Salesforce', 'NetSuite', 'Stripe' — the domain is tenant-configurable and not exhaustively enumerable here. | | `productLines[].productLineTaxes` | array of object | no | Tax records to attach to this product line. Provide only when a tax provider is configured. | | `productLines[].productLineTaxes[].taxCode` | string | yes | Tax code as defined by the configured tax provider. | | `productLines[].productLineTaxes[].entityId` | string | yes | Id of the legal entity this tax record applies under (a UUID). | | `productLines[].productLineTaxes[].engine` | string | yes | Identifier of the tax engine used to compute this tax. One of 'avalara', 'anrok', 'everestTaxEngine', 'manualTax', 'none'. | | `productLines[].productLineTaxes[].country` | string | no | ISO 3166-1 country code the tax record applies in. | | `productLines[].priceBookHeadersToCreate` | array of object | no | Pricing headers to create alongside this product line. | | `productLines[].priceBookHeadersToCreate[].priceBookId` | string | yes | Id of the price book this header belongs to (a UUID). | | `productLines[].priceBookHeadersToCreate[].currency` | string | yes | ISO 4217 three-letter currency code the prices in this header are denominated in. | | `productLines[].priceBookHeadersToCreate[].validFrom` | string (date) | yes | First date on which this header's prices are effective (inclusive). Format: YYYY-MM-DD. | | `productLines[].priceBookHeadersToCreate[].validTo` | string (date) | no | Last date on which this header's prices are effective (inclusive). Omit for an open-ended header. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries` | array of object | yes | | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].listPrice` | string (decimal) | no | List price as a decimal amount in the header currency. Required for non-tiered, non-rate-card entries. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].fromQuantity` | string (decimal) | no | Inclusive lower bound of this tier's quantity range. Provide only on tiered entries. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].toQuantity` | string (decimal) | no | Inclusive upper bound of this tier's quantity range. Omit on the top, open-ended tier. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].priceModel` | string | no | How this tier prices quantity. Provide only on tiered entries. One of 'flat', 'perUnit'. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry` | object | no | Rate-card entry to attach. Provide only on rate-card-priced entries. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry.billableComponentId` | string | no | Id of the billable component this rate-card entry prices (a UUID). | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry.unitOfMeasure` | string | yes | Unit of measure the rate is quoted in. One of 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry.description` | string | no | Free-form qualifier distinguishing this rate-card entry from siblings on the same billable component. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry.usageType` | string | no | How usage is measured against this entry. One of 'consumption', 'overage', 'thresholdCounter'. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry.billableUnitOfMeasure` | string | no | Unit of measure used when billing this entry. One of 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry.conversion` | string (decimal) | no | Multiplier applied to convert unitOfMeasure quantities into billableUnitOfMeasure quantities. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry.invoiceFrequency` | string | no | How often usage on this entry is invoiced. One of 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `productLines[].priceBookHeadersToCreate[].priceBookEntries[].rateCardEntry.priceModel` | string | no | How this rate-card entry computes a price (rate-card specific). One of 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. | | `productLines[].priceBookHeadersForUsage` | array of object | no | Pricing for the auto-created paired Usage line. Provide only when lineType is Prepaid; ignored otherwise. | | `productLines[].priceBookHeadersForUsage[].priceBookId` | string | yes | Id of the price book the Usage-line header belongs to (a UUID). | | `productLines[].priceBookHeadersForUsage[].currency` | string | yes | ISO 4217 three-letter currency code. | | `productLines[].priceBookHeadersForUsage[].validFrom` | string (date) | yes | First date on which the Usage-line header's prices are effective. Format: YYYY-MM-DD. | | `productLines[].priceBookHeadersForUsage[].validTo` | string (date) | no | Last date on which the Usage-line header's prices are effective. | | `productLines[].priceBookHeadersForUsage[].rateCardEntries` | array of object | yes | Rate-card entries on the Usage-line header. Usage-line pricing is always rate-card-based. | | `productLines[].priceBookHeadersForUsage[].rateCardEntries[].billableComponentId` | string | no | Id of the billable component this rate-card entry prices (a UUID). | | `productLines[].priceBookHeadersForUsage[].rateCardEntries[].usageType` | string | yes | How usage is measured against this entry. One of 'consumption', 'overage', 'thresholdCounter'. | | `productLines[].priceBookHeadersForUsage[].rateCardEntries[].unitOfMeasure` | string | yes | Unit of measure the rate is quoted in. One of 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `productLines[].priceBookHeadersForUsage[].rateCardEntries[].billableUnitOfMeasure` | string | no | Unit of measure used when billing this entry. One of 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `productLines[].priceBookHeadersForUsage[].rateCardEntries[].conversion` | string (decimal) | no | Multiplier applied to convert unitOfMeasure quantities into billableUnitOfMeasure quantities. | | `productLines[].priceBookHeadersForUsage[].rateCardEntries[].listPrice` | string (decimal) | no | The list price for this rate card entry, in the price book currency. | Example: ```json { "productLines": [ { "lineType": "", "priceModel": "", "unitOfMeasure": "", "name": "", "pricingTerm": "", "enforceListPrice": true, "invoiceFrequency": "", "prepaidBalanceOverConsumptionBehavior": "", "prepaidBalanceRolloverAtRenewal": true, "crmProductId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "dataSource": "", "productLineTaxes": [ { "taxCode": "", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "engine": "", "country": "" } ], "priceBookHeadersToCreate": [ { "priceBookId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "currency": "", "validFrom": "2026-01-15", "validTo": "2026-01-15", "priceBookEntries": [ { "listPrice": null, "fromQuantity": null, "toQuantity": null, "priceModel": null, "rateCardEntry": null } ] } ], "priceBookHeadersForUsage": [ { "priceBookId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "currency": "", "validFrom": "2026-01-15", "validTo": "2026-01-15", "rateCardEntries": [ { "billableComponentId": null, "usageType": null, "unitOfMeasure": null, "billableUnitOfMeasure": null, "conversion": null, "listPrice": null } ] } ] } ] } ``` ## Response `200` Success Under `data` (addAccountingProductLinesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `createdProductLineIds` | array of string | yes | Ids of the product lines created (each a UUID), in the same order as the input entries. | ## 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 | | --- | --- | | `INVALID_ID` | 400 | | `NOT_FOUND` | 400 | | `UNKNOWN_PRICE_BOOK_IDS` | 400 | | `UNKNOWN_BILLABLE_COMPONENT_IDS` | 400 | | `UNKNOWN_TAX_ENTITY_IDS` | 400 | | `UNSUPPORTED_LINE_TYPE` | 400 | | `VALIDATION_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_ID, NOT_FOUND, UNKNOWN_PRICE_BOOK_IDS, UNKNOWN_BILLABLE_COMPONENT_IDS, UNKNOWN_TAX_ENTITY_IDS, UNSUPPORTED_LINE_TYPE, VALIDATION_FAILED - `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 --- # Partially update a product line `PATCH /accounting/products/{id}/product-lines/{productLineId}` Group: Accounting › Products › Product Lines · Operation ID: `updateAccountingProductLine` Updates one existing product line's own fields (plus its tax records), scoped to that single line — other lines on the product are untouched. Writes only the fields the request body carries; everything omitted is left unchanged, EXCEPT `productLineTaxes`, which is a full replacement of the line's tax records whenever the field is present (pass every tax record that should remain, not just new ones) — omit the field entirely to leave existing taxes unchanged. This endpoint does not touch pricing — use createPricing/updatePricing/deletePricing (POST/PATCH/DELETE .../product-lines/:productLineId/pricing) for that. `lineType` is also not settable here: it is immutable after creation, so retyping a line means deleting it and creating a new one. The call runs in a transaction: a failure writes nothing. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateAccountingProductLine) ## 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. | | `id` | path | string | yes | Id of the product the line belongs to (a UUID); bound from the :id path segment. | | `productLineId` | path | string | yes | Id of the product line to update (a UUID); bound from the :productLineId path segment. | ## Request body Required, `application/json`: updateAccountingProductLineInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `priceModel` | string | no | New pricing model for the line. Omit to leave unchanged. One of 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. | | `unitOfMeasure` | string | no | New unit of measure. Omit to leave unchanged. One of 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `name` | string | no | New human-readable name. Omit to leave unchanged. | | `pricingTerm` | string | no | New pricing term. Omit to leave unchanged. One of 'monthly', 'biMonthly', 'once', 'quarterly', 'semiAnnually', 'annually'. | | `enforceListPrice` | boolean | no | Whether sales transactions on this line can override the list price. Omit to leave unchanged. | | `invoiceFrequency` | string | no | New invoice frequency. Omit to leave unchanged. One of 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `prepaidBalanceOverConsumptionBehavior` | string | no | New prepaid-overconsumption behavior. Accepted values: allowNegative, overage. Omit to leave unchanged. | | `prepaidBalanceRolloverAtRenewal` | boolean | no | Whether the remaining prepaid balance rolls over at renewal. Omit to leave unchanged. | | `crmProductId` | string | no | New CRM identifier for this line. Omit to leave unchanged. | | `dataSource` | string | no | New upstream-system identifier. Omit to leave unchanged. E.g. 'Salesforce', 'NetSuite', 'Stripe' — the domain is tenant-configurable and not exhaustively enumerable here. | | `productLineTaxes` | array of object | no | Full replacement of this line's tax records. Pass every record that should remain. Omit the field entirely to leave existing taxes unchanged; pass an empty array to clear them. | | `productLineTaxes[].taxCode` | string | yes | Tax code as defined by the configured tax provider. | | `productLineTaxes[].entityId` | string | yes | Id of the legal entity this tax record applies under (a UUID). | | `productLineTaxes[].engine` | string | yes | Identifier of the tax engine used to compute this tax. One of 'avalara', 'anrok', 'everestTaxEngine', 'manualTax', 'none'. | | `productLineTaxes[].country` | string | no | ISO 3166-1 country code the tax record applies in. | Example: ```json { "priceModel": "", "unitOfMeasure": "", "name": "", "pricingTerm": "", "enforceListPrice": true, "invoiceFrequency": "", "prepaidBalanceOverConsumptionBehavior": "", "prepaidBalanceRolloverAtRenewal": true, "crmProductId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "dataSource": "", "productLineTaxes": [ { "taxCode": "", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "engine": "", "country": "" } ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `INVALID_ID` | 400 | | `NOT_FOUND` | 400 | | `UNKNOWN_TAX_ENTITY_IDS` | 400 | | `UNSUPPORTED_LINE_TYPE` | 400 | | `VALIDATION_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_ID, NOT_FOUND, UNKNOWN_TAX_ENTITY_IDS, UNSUPPORTED_LINE_TYPE, VALIDATION_FAILED - `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 --- # Delete a product line `DELETE /accounting/products/{id}/product-lines/{productLineId}` Group: Accounting › Products › Product Lines · Operation ID: `deleteAccountingProductLine` Hard-deletes one product line, cascading to its pricing (headers, entries, rate cards). `id` and `productLineId` must resolve to an existing product line under that product or the call fails with NOT_FOUND. A `Prepaid` line and its paired `Usage` line must be deleted together — deleting one without the other fails the call (enforced by the underlying domain action, not this endpoint). The call runs in a transaction: a failure deletes nothing. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteAccountingProductLine) ## 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. | | `id` | path | string | yes | Id of the product the line belongs to (a UUID); bound from the :id path segment. | | `productLineId` | path | string | yes | Id of the product line to delete (a UUID); bound from the :productLineId path segment. | ## Response `200` Success ## 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 | | --- | --- | | `INVALID_ID` | 400 | | `NOT_FOUND` | 400 | | `VALIDATION_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_ID, NOT_FOUND, VALIDATION_FAILED - `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 --- # Add pricing headers to a product line `POST /accounting/products/{id}/product-lines/{productLineId}/pricing` Group: Accounting › Products › Product Lines · Operation ID: `createAccountingProductLinePricing` Adds one or more pricing headers (with entries) to an existing product line, in the same transaction. Same nested shape as `priceBookHeadersToCreate` on the product-line endpoints — see POST accounting/products for the full rules on tiers vs. rate cards. `id` and `productLineId` must resolve to an existing product line under that product or the call fails with NOT_FOUND. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createAccountingProductLinePricing) ## 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. | | `id` | path | string | yes | Id of the product the line belongs to (a UUID); bound from the :id path segment. | | `productLineId` | path | string | yes | Id of the product line to add pricing to (a UUID); bound from the :productLineId path segment. | ## Request body Required, `application/json`: createAccountingProductLinePricingInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `headers` | array of object | yes | Pricing headers to create. | | `headers[].priceBookId` | string | yes | Id of the price book this header belongs to (a UUID). | | `headers[].currency` | string | yes | ISO 4217 three-letter currency code the prices in this header are denominated in. | | `headers[].validFrom` | string (date) | yes | First date on which this header's prices are effective (inclusive). Format: YYYY-MM-DD. | | `headers[].validTo` | string (date) | no | Last date on which this header's prices are effective (inclusive). Omit for an open-ended header. | | `headers[].priceBookEntries` | array of object | yes | | | `headers[].priceBookEntries[].listPrice` | string (decimal) | no | List price as a decimal amount in the header currency. Required for non-tiered, non-rate-card entries. | | `headers[].priceBookEntries[].fromQuantity` | string (decimal) | no | Inclusive lower bound of this tier's quantity range. Provide only on tiered entries. | | `headers[].priceBookEntries[].toQuantity` | string (decimal) | no | Inclusive upper bound of this tier's quantity range. Omit on the top, open-ended tier. | | `headers[].priceBookEntries[].priceModel` | string | no | How this tier prices quantity. Provide only on tiered entries. One of 'flat', 'perUnit'. | | `headers[].priceBookEntries[].rateCardEntry` | object | no | Rate-card entry to attach. Provide only on rate-card-priced entries. | | `headers[].priceBookEntries[].rateCardEntry.billableComponentId` | string | no | Id of the billable component this rate-card entry prices (a UUID). | | `headers[].priceBookEntries[].rateCardEntry.unitOfMeasure` | string | yes | Unit of measure the rate is quoted in. One of 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `headers[].priceBookEntries[].rateCardEntry.description` | string | no | Free-form qualifier distinguishing this rate-card entry from siblings on the same billable component. | | `headers[].priceBookEntries[].rateCardEntry.usageType` | string | no | How usage is measured against this entry. One of 'consumption', 'overage', 'thresholdCounter'. | | `headers[].priceBookEntries[].rateCardEntry.billableUnitOfMeasure` | string | no | Unit of measure used when billing this entry. One of 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `headers[].priceBookEntries[].rateCardEntry.conversion` | string (decimal) | no | Multiplier applied to convert unitOfMeasure quantities into billableUnitOfMeasure quantities. | | `headers[].priceBookEntries[].rateCardEntry.invoiceFrequency` | string | no | How often usage on this entry is invoiced. One of 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `headers[].priceBookEntries[].rateCardEntry.priceModel` | string | no | How this rate-card entry computes a price (rate-card specific). One of 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. | Example: ```json { "headers": [ { "priceBookId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "currency": "", "validFrom": "2026-01-15", "validTo": "2026-01-15", "priceBookEntries": [ { "listPrice": "19.99", "fromQuantity": "19.99", "toQuantity": "19.99", "priceModel": "", "rateCardEntry": { "billableComponentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "unitOfMeasure": "", "description": "", "usageType": "", "billableUnitOfMeasure": "", "conversion": "19.99", "invoiceFrequency": "", "priceModel": "" } } ] } ] } ``` ## Response `200` Success Under `data` (createAccountingProductLinePricingOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `createdPricingHeaderIds` | array of string | yes | Ids of the pricing headers created (each a UUID), in the same order as the input entries. | ## 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 | | --- | --- | | `INVALID_ID` | 400 | | `NOT_FOUND` | 400 | | `UNKNOWN_PRICE_BOOK_IDS` | 400 | | `UNKNOWN_BILLABLE_COMPONENT_IDS` | 400 | | `VALIDATION_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_ID, NOT_FOUND, UNKNOWN_PRICE_BOOK_IDS, UNKNOWN_BILLABLE_COMPONENT_IDS, VALIDATION_FAILED - `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 --- # Partially update a pricing header and its entries `PATCH /accounting/products/{id}/product-lines/{productLineId}/pricing/{pricingId}` Group: Accounting › Products › Product Lines · Operation ID: `updateAccountingProductLinePricing` Patches one existing pricing header's own fields plus its entries and rate cards. Writes only the fields the request body carries; everything omitted is left unchanged. `priceBookEntriesToCreate` adds new entries. `priceBookEntriesToUpdate` patches existing entries by `entryId` (and, within one, `rateCardEntry` patches the entry's existing rate card by `rateCardEntryId`). `priceBookEntriesToDelete` removes entries by id. Every entry/rate-card id referenced must belong to this pricing header's own tree (an entry under a different header, or a rate card under a different entry, fails the same as an unknown id) — see UNKNOWN_PRICE_BOOK_ENTRY_IDS / UNKNOWN_RATE_CARD_ENTRY_IDS. `id`, `productLineId` and `pricingId` must resolve to an existing pricing header on that product line under that product or the call fails with NOT_FOUND. The call runs in a transaction: a failure writes nothing. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateAccountingProductLinePricing) ## 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. | | `id` | path | string | yes | Id of the product the line belongs to (a UUID); bound from the :id path segment. | | `productLineId` | path | string | yes | Id of the product line the pricing header belongs to (a UUID); bound from the :productLineId path segment. | | `pricingId` | path | string | yes | Id of the pricing header to update (a UUID); bound from the :pricingId path segment. | ## Request body Required, `application/json`: updateAccountingProductLinePricingInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `priceBookId` | string | no | New price book for this header (a UUID). Omit to leave unchanged. | | `currency` | string | no | New currency for this header. Omit to leave unchanged. | | `validFrom` | string (date) | no | New start of this header's effective range. Omit to leave unchanged. | | `validTo` | string (date) | no | New end of this header's effective range. Omit to leave unchanged. | | `priceBookEntriesToCreate` | array of object | no | New price-book entries to create under this header. | | `priceBookEntriesToCreate[].listPrice` | string (decimal) | no | List price as a decimal amount in the header currency. | | `priceBookEntriesToCreate[].fromQuantity` | string (decimal) | no | Inclusive lower bound of this tier's quantity range. Provide only on tiered entries. | | `priceBookEntriesToCreate[].toQuantity` | string (decimal) | no | Inclusive upper bound of this tier's quantity range. Omit on the top, open-ended tier. | | `priceBookEntriesToCreate[].priceModel` | string | no | How this tier prices quantity. Provide only on tiered entries. One of 'flat', 'perUnit'. | | `priceBookEntriesToCreate[].rateCardEntry` | object | no | Rate-card entry to attach. Provide only on rate-card-priced entries. | | `priceBookEntriesToCreate[].rateCardEntry.billableComponentId` | string | no | Id of the billable component this rate-card entry prices (a UUID). | | `priceBookEntriesToCreate[].rateCardEntry.unitOfMeasure` | string | yes | Unit of measure the rate is quoted in. One of 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `priceBookEntriesToCreate[].rateCardEntry.description` | string | no | Free-form qualifier distinguishing this rate-card entry from siblings on the same billable component. | | `priceBookEntriesToCreate[].rateCardEntry.usageType` | string | no | How usage is measured against this entry. One of 'consumption', 'overage', 'thresholdCounter'. | | `priceBookEntriesToCreate[].rateCardEntry.billableUnitOfMeasure` | string | no | Unit of measure used when billing this entry. One of 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `priceBookEntriesToCreate[].rateCardEntry.conversion` | string (decimal) | no | Multiplier applied to convert unitOfMeasure quantities into billableUnitOfMeasure quantities. | | `priceBookEntriesToCreate[].rateCardEntry.invoiceFrequency` | string | no | How often usage on this entry is invoiced. One of 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `priceBookEntriesToCreate[].rateCardEntry.priceModel` | string | no | How this rate-card entry computes a price (rate-card specific). One of 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. | | `priceBookEntriesToUpdate` | array of object | no | Patches to existing price-book entries under this header, identified by entryId. | | `priceBookEntriesToUpdate[].entryId` | string | yes | Id of the existing price-book entry to patch (a UUID). Must belong to this header. | | `priceBookEntriesToUpdate[].listPrice` | string (decimal) | no | New list price as a decimal amount in the header currency. Omit to leave unchanged. | | `priceBookEntriesToUpdate[].fromQuantity` | string (decimal) | no | New lower bound of this tier's quantity range. Omit to leave unchanged. | | `priceBookEntriesToUpdate[].toQuantity` | string (decimal) | no | New upper bound of this tier's quantity range. Omit to leave unchanged. | | `priceBookEntriesToUpdate[].priceModel` | string | no | New tier pricing model. Omit to leave unchanged. One of 'flat', 'perUnit'. | | `priceBookEntriesToUpdate[].rateCardEntry` | object | no | Patch to this entry's existing rate card. Provide only on rate-card-priced entries. | | `priceBookEntriesToUpdate[].rateCardEntry.rateCardEntryId` | string | yes | Id of the existing rate-card entry to patch (a UUID). Must belong to this price-book entry. | | `priceBookEntriesToUpdate[].rateCardEntry.billableComponentId` | string | no | New billable component for this rate-card entry (a UUID). Omit to leave unchanged. | | `priceBookEntriesToUpdate[].rateCardEntry.unitOfMeasure` | string | no | New unit of measure. Omit to leave unchanged. One of 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `priceBookEntriesToUpdate[].rateCardEntry.description` | string | no | New free-form qualifier. Omit to leave unchanged. | | `priceBookEntriesToUpdate[].rateCardEntry.usageType` | string | no | New usage-measurement type. Omit to leave unchanged. One of 'consumption', 'overage', 'thresholdCounter'. | | `priceBookEntriesToUpdate[].rateCardEntry.billableUnitOfMeasure` | string | no | New billing unit of measure. Omit to leave unchanged. One of 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `priceBookEntriesToUpdate[].rateCardEntry.conversion` | string (decimal) | no | New conversion multiplier. Omit to leave unchanged. | | `priceBookEntriesToUpdate[].rateCardEntry.invoiceFrequency` | string | no | New invoice frequency. Omit to leave unchanged. One of 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `priceBookEntriesToUpdate[].rateCardEntry.priceModel` | string | no | New rate-card-specific price model. Omit to leave unchanged. One of 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. | | `priceBookEntriesToDelete` | array of string | no | Ids of price-book entries under this header to delete (each a UUID). Cascades to each entry's rate-card entry, if any. | Example: ```json { "priceBookId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "currency": "", "validFrom": "2026-01-15", "validTo": "2026-01-15", "priceBookEntriesToCreate": [ { "listPrice": "19.99", "fromQuantity": "19.99", "toQuantity": "19.99", "priceModel": "", "rateCardEntry": { "billableComponentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "unitOfMeasure": "", "description": "", "usageType": "", "billableUnitOfMeasure": "", "conversion": "19.99", "invoiceFrequency": "", "priceModel": "" } } ], "priceBookEntriesToUpdate": [ { "entryId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "listPrice": "19.99", "fromQuantity": "19.99", "toQuantity": "19.99", "priceModel": "", "rateCardEntry": { "rateCardEntryId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "billableComponentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "unitOfMeasure": "", "description": "", "usageType": "", "billableUnitOfMeasure": "", "conversion": "19.99", "invoiceFrequency": "", "priceModel": "" } } ], "priceBookEntriesToDelete": [ "" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `INVALID_ID` | 400 | | `NOT_FOUND` | 400 | | `UNKNOWN_PRICE_BOOK_IDS` | 400 | | `UNKNOWN_BILLABLE_COMPONENT_IDS` | 400 | | `UNKNOWN_PRICE_BOOK_ENTRY_IDS` | 400 | | `UNKNOWN_RATE_CARD_ENTRY_IDS` | 400 | | `VALIDATION_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_ID, NOT_FOUND, UNKNOWN_PRICE_BOOK_IDS, UNKNOWN_BILLABLE_COMPONENT_IDS, UNKNOWN_PRICE_BOOK_ENTRY_IDS, UNKNOWN_RATE_CARD_ENTRY_IDS, VALIDATION_FAILED - `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 --- # Delete a pricing header `DELETE /accounting/products/{id}/product-lines/{productLineId}/pricing/{pricingId}` Group: Accounting › Products › Product Lines · Operation ID: `deleteAccountingProductLinePricing` Hard-deletes one pricing header, cascading to its entries and rate cards. `id`, `productLineId` and `pricingId` must resolve to an existing pricing header on that product line under that product or the call fails with NOT_FOUND. The call runs in a transaction: a failure deletes nothing. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteAccountingProductLinePricing) ## 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. | | `id` | path | string | yes | Id of the product the line belongs to (a UUID); bound from the :id path segment. | | `productLineId` | path | string | yes | Id of the product line the pricing header belongs to (a UUID); bound from the :productLineId path segment. | | `pricingId` | path | string | yes | Id of the pricing header to delete (a UUID); bound from the :pricingId path segment. | ## Response `200` Success ## 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 | | --- | --- | | `INVALID_ID` | 400 | | `NOT_FOUND` | 400 | | `VALIDATION_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_ID, NOT_FOUND, VALIDATION_FAILED - `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 --- # Purchase Orders `GET /accounting/purchase-orders` Group: Accounting › Purchase Orders · Operation ID: `purchaseOrders` Returns a list of purchase orders, header only. To filter by number, external reference, approval or budget status, currency, payment terms, vendor or legal entity, use POST accounting/purchase-orders/search. The full record with line items comes from GET accounting/purchase-orders/:id or search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=purchaseOrders) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `poNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `externalPONumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `approvalStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `budgetStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `startDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `endDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `needByDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentTerms` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, poNumber, approvalStatus, budgetStatus, status, currency, totalAmount, invoicedTotalAmount, receivedTotalAmount, startDate, endDate, needByDate, lastModifiedDate - Selectable: id, poNumber, description, internalDescription, externalMemo, externalPONumber, approvalStatus, budgetStatus, status, currency, totalAmount, invoicedTotalAmount, receivedTotalAmount, startDate, endDate, needByDate, paymentTerms, vendorName, vendorContactEmail, entityName, lastModifiedDate ## Response `200` Success Under `data`, an array of purchaseOrdersRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the purchase order. | | `poNumber` | string or null | no | The purchase order number, for example 'PO-3'. Absent while none has been assigned. | | `description` | string or null | no | The description of the order, shown to the vendor. | | `internalDescription` | string or null | no | Internal notes on the order, not shown to the vendor. | | `externalMemo` | string or null | no | A memo intended for the vendor. | | `externalPONumber` | string or null | no | The order reference in the vendor's or an upstream system. It is not unique. | | `approvalStatus` | string or null | no | The approval status of the order. Possible values: 'approved', 'pendingApproval', 'rejected'. | | `budgetStatus` | string or null | no | The budget-check status of the order. Possible values: 'approved', 'rejected', 'pendingApproval'. Independent of approvalStatus. | | `status` | string or null | no | The order lifecycle status. Possible values: 'open', 'inprogress', 'closed', 'overMatched'. This is not the vendor-bill matching status, which is per line in each line's matchStatus. | | `currency` | string or null | no | The currency the order is denominated in. Format ISO 4217, for example 'USD'. Governs every amount on the order and its lines. | | `totalAmount` | string (decimal) or null | no | The total ordered amount, the sum of the line totals, in the order currency. | | `invoicedTotalAmount` | string (decimal) or null | no | The portion of the order already invoiced by vendor bills, in the order currency. | | `receivedTotalAmount` | string (decimal) or null | no | The portion of the order already received as goods, in the order currency. | | `startDate` | string (date) or null | no | The first day of the period the order covers. Format YYYY-MM-DD. | | `endDate` | string (date) or null | no | The last day of the period the order covers. Format YYYY-MM-DD. | | `needByDate` | string (date) or null | no | The date the goods or services are needed by. Format YYYY-MM-DD. | | `paymentTerms` | string or null | no | The payment-terms code applied to this order, for example 'net30', 'net60', 'dueOnReceipt' or 'custom'. Inherited from the vendor unless overridden. | | `vendorName` | string or null | no | The name of the vendor this order is placed with. | | `vendorContactEmail` | string or null | no | The main contact email at the vendor. | | `entityName` | string or null | no | The name of the legal entity this order is placed by. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the purchase order last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a purchase order. `POST /accounting/purchase-orders` Group: Accounting › Purchase Orders · Operation ID: `createPurchaseOrder` The response contains only the new order's id. Read the record with GET accounting/purchase-orders/:id if you need its stored state, where poNumber appears once assigned. At least one line is required. A line is either an item line or an expense line, and isItem says which: an item line is priced by quantity times price and needs itemId and unitOfMeasureId, an expense line is priced by editableAmount and needs an accountId. currency governs every amount, which are decimals with no currency of their own. The order is created pendingApproval and then put through your tenant's approval policy, which may approve it at once when no approvers are configured, so do not assume a created order is still awaiting approval. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createPurchaseOrder) ## 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`: createPurchaseOrderInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `entityId` | string | yes | The legal entity that owns the order. See id on GET base/entities. | | `vendorId` | string | yes | The vendor the order is placed with. See id on GET accounting/vendors/:id. | | `currency` | string | yes | The currency of the order. Format ISO 4217, for example 'USD'. Governs every line amount. | | `description` | string | no | The description of the order, shown to the vendor. | | `internalDescription` | string | no | An internal description of the order, not shown to the vendor. | | `externalMemo` | string | no | A memo printed on vendor-facing documents. | | `externalPONumber` | string | no | The reference number this order has in the upstream system. | | `paymentTerms` | string | no | The payment-terms code agreed with the vendor, for example 'net30'. | | `startDate` | string (date) | no | The date the order starts. Use format YYYY-MM-DD. | | `endDate` | string (date) | no | The date the order ends. Use format YYYY-MM-DD. | | `needByDate` | string (date) | no | The date the goods or services are required by. Use format YYYY-MM-DD. | | `requestedById` | string | no | The employee who requested the order. There is no endpoint for it. | | `lines` | array of object | yes | The lines this order places. At least one is required. | | `lines[].isItem` | boolean | yes | Whether this is an item line. True for an item line, priced by quantity times price. False for an expense line, priced by editableAmount. | | `lines[].description` | string | no | A description of what is being ordered. | | `lines[].itemId` | string | no | The item being ordered. Expected on an item line. | | `lines[].accountId` | string | no | The ledger account the line is booked to. See id on GET accounting/accounts/:id. Expected on an expense line. | | `lines[].unitOfMeasureId` | string | no | The unit the quantity is expressed in. Expected on an item line. | | `lines[].departmentId` | string | no | The department the cost is allocated to. See id on GET departments/:id. | | `lines[].quantity` | string (decimal) | no | The ordered quantity, as a decimal. Expected on an item line. | | `lines[].price` | string (decimal) | no | The unit price, in the order currency, as a decimal. Expected on an item line. | | `lines[].editableAmount` | string (decimal) | no | The manually entered line amount, in the order currency, as a decimal. Expected on an expense line. | | `lines[].startDate` | string (date) | no | The date this line starts. Use format YYYY-MM-DD. | | `lines[].endDate` | string (date) | no | The date this line ends. Use format YYYY-MM-DD. | | `lines[].estimatedDeliveryDate` | string (date) | no | The expected delivery date for this line. Use format YYYY-MM-DD. | | `attachments` | array of object | no | Files to attach to the order. Each names an already-uploaded file by its id. | | `attachments[].fileName` | string | yes | The display name of the attached file. | | `attachments[].fileId` | string | yes | The identifier of the already-uploaded file. There is no endpoint for it. | Example: ```json { "entityId": "8f2c1d40-5a6b-4c7d-9e80-1f2a3b4c5d6e", "vendorId": "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "currency": "USD", "description": "Q4 office supplies", "internalDescription": "", "externalMemo": "", "externalPONumber": "", "paymentTerms": "", "startDate": "2026-01-15", "endDate": "2026-01-15", "needByDate": "2026-10-15", "requestedById": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "lines": [ { "isItem": false, "description": "Stationery", "editableAmount": "450.00" } ], "attachments": [ { "fileName": "", "fileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ] } ``` ## Response `200` Success Under `data` (createPurchaseOrderOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created purchase order. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `TOO_MANY_LINES` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, TOO_MANY_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 --- # Returns the purchase order with the given identifier. `GET /accounting/purchase-orders/{id}` Group: Accounting › Purchase Orders · Operation ID: `getPurchaseOrder` A purchase order records goods or services ordered from a vendor, with its header and line items. It shares its read with POST accounting/purchase-orders/search, which returns the same record. The list at GET accounting/purchase-orders is header-only, so the line items come back here and on search. The header status is the order lifecycle, and vendor-bill matching is per line in each line's matchStatus. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getPurchaseOrder) ## 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`. | | `id` | path | string | yes | The unique identifier for the purchase order to return. | ## Response `200` Success Under `data` (getPurchaseOrderOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the purchase order. | | `poNumber` | string | no | The purchase order number, for example 'PO-3'. Absent while none has been assigned. | | `description` | string | no | The description of the order, shown to the vendor. | | `internalDescription` | string | no | Internal notes on the order, not shown to the vendor. | | `externalMemo` | string | no | A memo intended for the vendor. | | `externalPONumber` | string | no | The order reference in the vendor's or an upstream system. It is not unique. | | `approvalStatus` | string | no | The approval status of the order. Possible values: 'approved', 'pendingApproval', 'rejected'. | | `budgetStatus` | string | no | The budget-check status of the order. Possible values: 'approved', 'rejected', 'pendingApproval'. Independent of approvalStatus. | | `status` | string | no | The order lifecycle status. Possible values: 'open', 'inprogress', 'closed', 'overMatched'. This is not the vendor-bill matching status, which is per line in each line's matchStatus. | | `currency` | string | no | The currency the order is denominated in. Format ISO 4217, for example 'USD'. Governs every amount on the order and its lines. | | `totalAmount` | string (decimal) | no | The total ordered amount, the sum of the line totals, in the order currency. | | `invoicedTotalAmount` | string (decimal) | no | The portion of the order already invoiced by vendor bills, in the order currency. | | `receivedTotalAmount` | string (decimal) | no | The portion of the order already received as goods, in the order currency. | | `startDate` | string (date) | no | The first day of the period the order covers. Format YYYY-MM-DD. | | `endDate` | string (date) | no | The last day of the period the order covers. Format YYYY-MM-DD. | | `needByDate` | string (date) | no | The date the goods or services are needed by. Format YYYY-MM-DD. | | `paymentTerms` | string | no | The payment-terms code applied to this order, for example 'net30', 'net60', 'dueOnReceipt' or 'custom'. Inherited from the vendor unless overridden. | | `hasInventoryItems` | boolean | no | Whether any line references an inventory item. | | `vendorId` | string | no | The vendor this order is placed with. See id on GET accounting/vendors/:id. | | `vendorName` | string | no | The name of the vendor this order is placed with. | | `vendorContactEmail` | string | no | The main contact email at the vendor. | | `entityId` | string | no | The legal entity this order is placed by. See id on GET base/entities. | | `entityName` | string | no | The name of the legal entity this order is placed by. | | `lastModifiedDate` | string (date-time) | yes | The date and time the purchase order last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `approvals` | array of object | no | The approval decisions on this order, oldest first. An empty array when no approval policy has matched the order. | | `approvals[].decision` | string | yes | What was decided. Values include 'approve' and 'reject'. | | `approvals[].employeeName` | string | no | The name of the employee who decided. | | `approvals[].message` | string | no | The reviewer's note, where one was given. | | `approvals[].decidedAt` | string (date-time) | yes | The date and time the decision was recorded, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | no | The order line items, in their order on the order. An empty array when the order has none. | | `lines[].id` | string | yes | The unique identifier for the line item. | | `lines[].lineNumber` | string | no | The line number, for example 'POL-3'. | | `lines[].description` | string | no | A description of the line item. | | `lines[].isItem` | boolean | no | Whether the line is an item line. True when the line references an item, false when it is an expense line. | | `lines[].quantity` | string (decimal) | no | The quantity ordered, as a decimal. | | `lines[].price` | string (decimal) | no | The unit price, in the order currency. Set on item lines. On an expense line the amount is in editableAmount instead. | | `lines[].editableAmount` | string (decimal) | no | The directly entered line amount, in the order currency. Used on expense lines, where there is no unit price to multiply. | | `lines[].total` | string (decimal) | no | The line total, in the order currency: price times quantity on an item line, editableAmount on an expense line. | | `lines[].discountPercentage` | string (decimal) | no | The discount applied to the line, as a percentage. 10 means 10%, not 0.10. | | `lines[].currency` | string | no | The currency of the line. Format ISO 4217, for example 'USD'. It always matches the order currency. | | `lines[].status` | string | no | The delivery status of the line. Possible values: 'undelivered', 'delivering', 'delivered'. | | `lines[].matchStatus` | string | no | The vendor-bill matching status of the line. Possible values: 'unmatched', 'partiallyMatched', 'matched', 'overMatched'. | | `lines[].invoicedStatus` | string | no | The invoicing status of the line. Possible values: 'uninvoiced', 'partiallyInvoiced', 'invoiced', 'overInvoiced'. | | `lines[].receivedStatus` | string | no | The goods-receipt status of the line. Possible values: 'unreceived', 'partiallyReceived', 'received', 'overReceived'. | | `lines[].invoicedQuantity` | string (decimal) | no | The quantity invoiced against this line so far, as a decimal. | | `lines[].invoicedAmount` | string (decimal) | no | The amount invoiced against this line so far, in the order currency. | | `lines[].receivedQuantity` | string (decimal) | no | The quantity received against this line so far, as a decimal. | | `lines[].receivedAmount` | string (decimal) | no | The amount received against this line so far, in the order currency. | | `lines[].remainingAmount` | string (decimal) | no | The amount still open on this line, in the order currency: the line total less what has been invoiced. | | `lines[].startDate` | string (date) | no | The first day of the period this line covers. Format YYYY-MM-DD. | | `lines[].endDate` | string (date) | no | The last day of the period this line covers. Format YYYY-MM-DD. | | `lines[].deliveryDate` | string (date) | no | The date this line was delivered. Format YYYY-MM-DD. | | `lines[].estimatedDeliveryDate` | string (date) | no | The expected delivery date for this line. Format YYYY-MM-DD. | | `lines[].unitOfMeasure` | string | no | The name of the unit of measure for the quantity. | | `lines[].unitOfMeasureId` | string | no | The unit of measure for the quantity. | | `lines[].itemId` | string | no | The item this line orders. Present on item lines. | | `lines[].departmentId` | string | no | The department this line is charged to. See id on GET departments/:id. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Purchase Order `PATCH /accounting/purchase-orders/{id}` Group: Accounting › Purchase Orders · Operation ID: `updatePurchaseOrder` Updates the purchase order with the given identifier. Only the fields in the request body change. Only the fields in the request body change. The entity that owns the order cannot be changed. Lines change through three arrays, applied delete, then update, then create, so one call can retire a line and add its replacement. updateLines and deleteLines address lines by the id the order publishes, and a line id belonging to another order is refused with LINE_NOT_FOUND. Omit all three to leave the lines untouched. An update that would leave the order with no lines is refused with NO_LINES, so delete the order instead. Saving re-opens approval: every write sets approvalStatus and budgetStatus back to pendingApproval, so changing a description un-approves the order. The response is empty. Read the record with GET accounting/purchase-orders/:id if you need its state after the update. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updatePurchaseOrder) ## 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. | | `id` | path | string | yes | The unique identifier for the purchase order to update. | ## Request body Required, `application/json`: updatePurchaseOrderInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `vendorId` | string | no | The vendor the order is placed with. See id on GET accounting/vendors/:id. Omit to leave unchanged. | | `currency` | string | no | The currency of the order. Format ISO 4217, for example 'USD'. Governs every line amount. Omit to leave unchanged. | | `description` | string | no | The description of the order, shown to the vendor. Omit to leave unchanged. | | `internalDescription` | string | no | An internal description of the order, not shown to the vendor. Omit to leave unchanged. | | `externalMemo` | string | no | A memo printed on vendor-facing documents. Omit to leave unchanged. | | `externalPONumber` | string | no | The reference number this order has in the upstream system. Omit to leave unchanged. | | `paymentTerms` | string | no | The payment-terms code agreed with the vendor, for example 'net30'. Omit to leave unchanged. | | `startDate` | string (date) | no | The date the order starts. Use format YYYY-MM-DD. Omit to leave unchanged. | | `endDate` | string (date) | no | The date the order ends. Use format YYYY-MM-DD. Omit to leave unchanged. | | `needByDate` | string (date) | no | The date the goods or services are required by. Use format YYYY-MM-DD. Omit to leave unchanged. | | `requestedById` | string | no | The employee who requested the order. There is no endpoint for it. Omit to leave unchanged. | | `createLines` | array of object | no | Lines to add to this order, in the same shape as on create but with no id. Omit for no additions. | | `createLines[].isItem` | boolean | yes | Whether this is an item line. True for an item line, priced by quantity times price. False for an expense line, priced by editableAmount. | | `createLines[].description` | string | no | A description of what is being ordered. | | `createLines[].itemId` | string | no | The item being ordered. Expected on an item line. | | `createLines[].accountId` | string | no | The ledger account the line is booked to. See id on GET accounting/accounts/:id. Expected on an expense line. | | `createLines[].unitOfMeasureId` | string | no | The unit the quantity is expressed in. Expected on an item line. | | `createLines[].departmentId` | string | no | The department the cost is allocated to. See id on GET departments/:id. | | `createLines[].quantity` | string (decimal) | no | The ordered quantity, as a decimal. Expected on an item line. | | `createLines[].price` | string (decimal) | no | The unit price, in the order currency, as a decimal. Expected on an item line. | | `createLines[].editableAmount` | string (decimal) | no | The manually entered line amount, in the order currency, as a decimal. Expected on an expense line. | | `createLines[].startDate` | string (date) | no | The date this line starts. Use format YYYY-MM-DD. | | `createLines[].endDate` | string (date) | no | The date this line ends. Use format YYYY-MM-DD. | | `createLines[].estimatedDeliveryDate` | string (date) | no | The expected delivery date for this line. Use format YYYY-MM-DD. | | `updateLines` | array of object | no | Lines to change, each addressed by the id the order publishes. Every field but the id is optional and omit leaves it unchanged. Omit for no changes. | | `updateLines[].id` | string | yes | The unique identifier for the line to change. It must belong to this order. | | `updateLines[].isItem` | boolean | no | Whether this is an item line. Omit to leave unchanged. | | `updateLines[].description` | string | no | A description of what is being ordered. Omit to leave unchanged. | | `updateLines[].itemId` | string | no | The item being ordered. Omit to leave unchanged. | | `updateLines[].accountId` | string | no | The ledger account the line is booked to. See id on GET accounting/accounts/:id. Omit to leave unchanged. | | `updateLines[].unitOfMeasureId` | string | no | The unit the quantity is expressed in. Omit to leave unchanged. | | `updateLines[].departmentId` | string | no | The department the cost is allocated to. See id on GET departments/:id. Omit to leave unchanged. | | `updateLines[].quantity` | string (decimal) | no | The ordered quantity, as a decimal. Omit to leave unchanged. | | `updateLines[].price` | string (decimal) | no | The unit price, in the order currency, as a decimal. Omit to leave unchanged. | | `updateLines[].editableAmount` | string (decimal) | no | The manually entered line amount, in the order currency, as a decimal. Omit to leave unchanged. | | `updateLines[].startDate` | string (date) | no | The date this line starts. Use format YYYY-MM-DD. Omit to leave unchanged. | | `updateLines[].endDate` | string (date) | no | The date this line ends. Use format YYYY-MM-DD. Omit to leave unchanged. | | `updateLines[].estimatedDeliveryDate` | string (date) | no | The expected delivery date for this line. Use format YYYY-MM-DD. Omit to leave unchanged. | | `deleteLines` | array of string | no | The identifiers of the lines to remove, applied before the updates and creates. Each must belong to this order. Removing them all is refused, so delete the order instead. | Example: ```json { "vendorId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "currency": "", "description": "", "internalDescription": "", "externalMemo": "", "externalPONumber": "", "paymentTerms": "", "startDate": "2026-01-15", "endDate": "2026-01-15", "needByDate": "2026-11-01", "requestedById": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "createLines": [ { "isItem": true, "description": "", "itemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "unitOfMeasureId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "departmentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "price": "19.99", "editableAmount": "19.99", "startDate": "2026-01-15", "endDate": "2026-01-15", "estimatedDeliveryDate": "2026-01-15" } ], "updateLines": [ { "id": "7a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9", "editableAmount": "520.00" } ], "deleteLines": [ "" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `LINE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `TOO_MANY_LINES` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND, LINE_NOT_FOUND, NO_LINES, TOO_MANY_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 --- # Deletes a purchase order. `DELETE /accounting/purchase-orders/{id}` Group: Accounting › Purchase Orders · Operation ID: `deletePurchaseOrder` The response is empty. Purchase requests consumed by this order are released as part of the delete, so their lines become available to a new order again. The whole operation is one transaction, so if the header delete fails the release rolls back with it. A line that already has a committed goods receipt or a matched vendor bill blocks the delete with LINES_NOT_DELETABLE, because deleting it would orphan that receiving or billing history. Amend the order instead of deleting it. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deletePurchaseOrder) ## 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. | | `id` | path | string | yes | The unique identifier for the purchase order to delete. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `LINES_NOT_DELETABLE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, LINES_NOT_DELETABLE - `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 --- # Approves the purchase order. `POST /accounting/purchase-orders/{id}/approve` Group: Accounting › Purchase Orders · Operation ID: `approvePurchaseOrder` Approves the purchase order through your tenant's approval policy. With no approvers configured the order goes straight to approved, and with a multi-step policy it advances one step and stays pendingApproval. The status after the operation is returned. An order that is already approved is refused with STATUS_CONFLICT. A refusal from the approval framework comes back as APPROVAL_FAILED, with its own message in reason. [Open in the interactive reference](https://docs.api.lensing.ai/?op=approvePurchaseOrder) ## 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. | | `id` | path | string | yes | The unique identifier for the purchase order to approve. | ## Response `200` Success Under `data` (approvePurchaseOrderOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string | yes | The approval status after the operation. The approval workflow decides it, so it is read back. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `STATUS_CONFLICT` | 400 | | `APPROVAL_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, STATUS_CONFLICT, APPROVAL_FAILED - `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 --- # Rejects the purchase order. `POST /accounting/purchase-orders/{id}/reject` Group: Accounting › Purchase Orders · Operation ID: `rejectPurchaseOrder` Rejects the purchase order through your tenant's approval policy, with an optional reviewMessage recorded against the decision. An order that is already rejected is refused with STATUS_CONFLICT. A refusal from the approval framework comes back as APPROVAL_FAILED, with its own message in reason. Rejecting is not deleting: the order stays, with its number and its audit trail. [Open in the interactive reference](https://docs.api.lensing.ai/?op=rejectPurchaseOrder) ## 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. | | `id` | path | string | yes | The unique identifier for the purchase order to reject. | ## Request body Required, `application/json`: rejectPurchaseOrderInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `reviewMessage` | string | no | Why the order is being rejected, recorded against the approval decision. Omit for none. | Example: ```json { "reviewMessage": "Budget not approved for this quarter" } ``` ## Response `200` Success Under `data` (rejectPurchaseOrderOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string | yes | The approval status after the operation. The approval workflow decides it, so it is read back. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `STATUS_CONFLICT` | 400 | | `APPROVAL_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, STATUS_CONFLICT, APPROVAL_FAILED - `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 --- # Returns the purchase orders that match the given filters. `POST /accounting/purchase-orders/search` Group: Accounting › Purchase Orders · Operation ID: `searchPurchaseOrder` Returns the full record for each match, header and line items. Every filter is optional and they are combined with AND, so omitting all of them matches all purchase orders. vendorName and entityName can be matched here but not on GET accounting/purchase-orders. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchPurchaseOrder) ## 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`. | ## Request body Required, `application/json`: searchPurchaseOrderInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A purchase order must match every filter you send. Omit to return all purchase orders. | | `filters.id` | string | no | Limits results to the purchase order with this identifier. GET accounting/purchase-orders/:id does the same more cheaply. | | `filters.poNumber` | string | no | Limits results to the purchase order with this number, for example 'PO-3'. The match is exact. | | `filters.externalPONumber` | string | no | Limits results to purchase orders with this external reference. The match is exact. It is not unique, so it can match several orders. | | `filters.approvalStatus` | string | no | Limits results to orders in this approval status. Accepted values: 'approved', 'pendingApproval', 'rejected'. Any other value matches no purchase order. | | `filters.budgetStatus` | string | no | Limits results to orders in this budget-check status. Accepted values: 'approved', 'rejected', 'pendingApproval'. Any other value matches no purchase order. | | `filters.currency` | string | no | Limits results to orders denominated in this currency. Format ISO 4217, for example 'USD'. | | `filters.paymentTerms` | string | no | Limits results to orders with this payment-terms code, for example 'net30'. The match is exact. | | `filters.vendorId` | string | no | Limits results to orders placed with this vendor. See id on GET accounting/vendors/:id. An id that matches no vendor returns an empty page, not an error. | | `filters.entityId` | string | no | Limits results to orders placed by this legal entity. See id on GET base/entities. An id that matches no entity returns an empty page, not an error. | | `filters.vendorName` | string | no | Limits results to orders whose vendor name is exactly this. The match is case-sensitive. Use vendorId when you already have the id. | | `filters.entityName` | string | no | Limits results to orders whose legal entity name is exactly this. The match is case-sensitive. Use entityId when you already have the id. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of purchase orders per page, between 1 and 200. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "approvalStatus": "pendingApproval" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchPurchaseOrderOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The purchase orders on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the purchase order. | | `items[].poNumber` | string | no | The purchase order number, for example 'PO-3'. Absent while none has been assigned. | | `items[].description` | string | no | The description of the order, shown to the vendor. | | `items[].internalDescription` | string | no | Internal notes on the order, not shown to the vendor. | | `items[].externalMemo` | string | no | A memo intended for the vendor. | | `items[].externalPONumber` | string | no | The order reference in the vendor's or an upstream system. It is not unique. | | `items[].approvalStatus` | string | no | The approval status of the order. Possible values: 'approved', 'pendingApproval', 'rejected'. | | `items[].budgetStatus` | string | no | The budget-check status of the order. Possible values: 'approved', 'rejected', 'pendingApproval'. Independent of approvalStatus. | | `items[].status` | string | no | The order lifecycle status. Possible values: 'open', 'inprogress', 'closed', 'overMatched'. This is not the vendor-bill matching status, which is per line in each line's matchStatus. | | `items[].currency` | string | no | The currency the order is denominated in. Format ISO 4217, for example 'USD'. Governs every amount on the order and its lines. | | `items[].totalAmount` | string (decimal) | no | The total ordered amount, the sum of the line totals, in the order currency. | | `items[].invoicedTotalAmount` | string (decimal) | no | The portion of the order already invoiced by vendor bills, in the order currency. | | `items[].receivedTotalAmount` | string (decimal) | no | The portion of the order already received as goods, in the order currency. | | `items[].startDate` | string (date) | no | The first day of the period the order covers. Format YYYY-MM-DD. | | `items[].endDate` | string (date) | no | The last day of the period the order covers. Format YYYY-MM-DD. | | `items[].needByDate` | string (date) | no | The date the goods or services are needed by. Format YYYY-MM-DD. | | `items[].paymentTerms` | string | no | The payment-terms code applied to this order, for example 'net30', 'net60', 'dueOnReceipt' or 'custom'. Inherited from the vendor unless overridden. | | `items[].hasInventoryItems` | boolean | no | Whether any line references an inventory item. | | `items[].vendorId` | string | no | The vendor this order is placed with. See id on GET accounting/vendors/:id. | | `items[].vendorName` | string | no | The name of the vendor this order is placed with. | | `items[].vendorContactEmail` | string | no | The main contact email at the vendor. | | `items[].entityId` | string | no | The legal entity this order is placed by. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the legal entity this order is placed by. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the purchase order last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | no | The order line items, in their order on the order. An empty array when the order has none. | | `items[].lines[].id` | string | yes | The unique identifier for the line item. | | `items[].lines[].lineNumber` | string | no | The line number, for example 'POL-3'. | | `items[].lines[].description` | string | no | A description of the line item. | | `items[].lines[].isItem` | boolean | no | Whether the line is an item line. True when the line references an item, false when it is an expense line. | | `items[].lines[].quantity` | string (decimal) | no | The quantity ordered, as a decimal. | | `items[].lines[].price` | string (decimal) | no | The unit price, in the order currency. Set on item lines. On an expense line the amount is in editableAmount instead. | | `items[].lines[].editableAmount` | string (decimal) | no | The directly entered line amount, in the order currency. Used on expense lines, where there is no unit price to multiply. | | `items[].lines[].total` | string (decimal) | no | The line total, in the order currency: price times quantity on an item line, editableAmount on an expense line. | | `items[].lines[].discountPercentage` | string (decimal) | no | The discount applied to the line, as a percentage. 10 means 10%, not 0.10. | | `items[].lines[].currency` | string | no | The currency of the line. Format ISO 4217, for example 'USD'. It always matches the order currency. | | `items[].lines[].status` | string | no | The delivery status of the line. Possible values: 'undelivered', 'delivering', 'delivered'. | | `items[].lines[].matchStatus` | string | no | The vendor-bill matching status of the line. Possible values: 'unmatched', 'partiallyMatched', 'matched', 'overMatched'. | | `items[].lines[].invoicedStatus` | string | no | The invoicing status of the line. Possible values: 'uninvoiced', 'partiallyInvoiced', 'invoiced', 'overInvoiced'. | | `items[].lines[].receivedStatus` | string | no | The goods-receipt status of the line. Possible values: 'unreceived', 'partiallyReceived', 'received', 'overReceived'. | | `items[].lines[].invoicedQuantity` | string (decimal) | no | The quantity invoiced against this line so far, as a decimal. | | `items[].lines[].invoicedAmount` | string (decimal) | no | The amount invoiced against this line so far, in the order currency. | | `items[].lines[].receivedQuantity` | string (decimal) | no | The quantity received against this line so far, as a decimal. | | `items[].lines[].receivedAmount` | string (decimal) | no | The amount received against this line so far, in the order currency. | | `items[].lines[].remainingAmount` | string (decimal) | no | The amount still open on this line, in the order currency: the line total less what has been invoiced. | | `items[].lines[].startDate` | string (date) | no | The first day of the period this line covers. Format YYYY-MM-DD. | | `items[].lines[].endDate` | string (date) | no | The last day of the period this line covers. Format YYYY-MM-DD. | | `items[].lines[].deliveryDate` | string (date) | no | The date this line was delivered. Format YYYY-MM-DD. | | `items[].lines[].estimatedDeliveryDate` | string (date) | no | The expected delivery date for this line. Format YYYY-MM-DD. | | `items[].lines[].unitOfMeasure` | string | no | The name of the unit of measure for the quantity. | | `items[].lines[].unitOfMeasureId` | string | no | The unit of measure for the quantity. | | `items[].lines[].itemId` | string | no | The item this line orders. Present on item lines. | | `items[].lines[].departmentId` | string | no | The department this line is charged to. See id on GET departments/:id. | | `pagination` | object | yes | The pagination details for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Purchase Requests `GET /accounting/purchase-requests` Group: Accounting › Purchase Requests · Operation ID: `purchaseRequests` Returns a list of purchase requests. Each row is the request header. For the full record with its line items use GET accounting/purchase-requests/:id, and to match on vendor, legal entity or department use POST accounting/purchase-requests/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=purchaseRequests) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `purchaseRequestNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `startDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `endDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `needByDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentTerms` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `externalId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, purchaseRequestNumber, status, currency, startDate, endDate, needByDate, totalAmount, lastModifiedDate - Selectable: id, purchaseRequestNumber, description, internalDescription, externalMemo, status, currency, totalAmount, startDate, endDate, needByDate, paymentTerms, externalId, entityName, departmentName, lastModifiedDate ## Response `200` Success Under `data`, an array of purchaseRequestsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the purchase request. | | `purchaseRequestNumber` | string or null | no | The purchase request number, for example 'PR-1', unique across purchase requests. Absent until one has been assigned. | | `description` | string or null | no | A description of what is being requested. It is shown to the vendor. | | `internalDescription` | string or null | no | Internal notes on the request, not shown to the vendor. | | `externalMemo` | string or null | no | A memo intended for the vendor. | | `status` | string or null | no | The lifecycle and approval state, in one field. Possible values: 'pendingApproval', 'approved', 'rejected', 'closed'. This resource has no separate approvalStatus. | | `currency` | string or null | no | The currency the request is denominated in. Format ISO 4217, for example 'USD'. Governs totalAmount and every line amount. | | `totalAmount` | string (decimal) or null | no | The total requested amount, the sum of the line totals, in the request currency. | | `startDate` | string (date) or null | no | The first day of the period the request covers. Format YYYY-MM-DD. | | `endDate` | string (date) or null | no | The last day of the period the request covers. Format YYYY-MM-DD. | | `needByDate` | string (date) or null | no | The date the goods or services are needed by. Format YYYY-MM-DD. | | `paymentTerms` | string or null | no | The payment-terms key requested for the resulting order, for example 'net30'. | | `externalId` | string or null | no | The identifier this request has in the upstream system that produced it. It is not unique. | | `entityName` | string or null | no | The name of the legal entity this request belongs to. | | `departmentName` | string or null | no | The name of the department raising the request. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the purchase request last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a purchase request. `POST /accounting/purchase-requests` Group: Accounting › Purchase Requests · Operation ID: `createPurchaseRequest` The response contains only the new request's id. Read the record with GET accounting/purchase-requests/:id if you need its stored state, where purchaseRequestNumber appears once assigned. At least one line is required. Each line is either an item line or an expense line, set by isItem: an item line is priced by quantity times price and takes itemId and unitOfMeasureId, and an expense line is priced by editableAmount. The request is created pendingApproval and then put through your tenant's approval policy, which may resolve it at once when no approvers are configured. Where your tenant marks a tag dimension required, omitting it fails the write with an untyped platform error rather than a declared code. There is no delete endpoint. Retire a request by rejecting it. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createPurchaseRequest) ## 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`: createPurchaseRequestInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `entityId` | string | yes | The legal entity that owns the request. See id on GET base/entities. | | `currency` | string | yes | The currency of the request. Format ISO 4217, for example 'USD'. Governs every line amount. | | `vendorId` | string | no | The vendor the request is aimed at, when one is known. See id on GET accounting/vendors/:id. | | `departmentId` | string | no | The department the request is raised for. See id on GET departments/:id. | | `description` | string | no | A public-facing description of the request. | | `internalDescription` | string | no | An internal description, visible only to internal users. | | `externalMemo` | string | no | A memo carried onto vendor-facing documents. | | `paymentTerms` | string | no | The payment-terms key requested for the resulting order, for example 'net30'. These are tenant data, not a fixed list of values. | | `externalId` | string | no | The identifier this request has in the upstream system that produced it. | | `needByDate` | string (date) | no | The date the goods or services are required by. Use format YYYY-MM-DD. | | `startDate` | string (date) | no | The date the request starts. Use format YYYY-MM-DD. | | `endDate` | string (date) | no | The date the request ends. Use format YYYY-MM-DD. | | `lines` | array of object | yes | The lines this request asks for. At least one is required. | | `lines[].isItem` | boolean | no | True for an item line, priced by quantity times price. False for an expense line, priced by editableAmount. | | `lines[].description` | string | no | A description of what is being requested. | | `lines[].itemId` | string | no | The item being requested. See id on GET inventory/items/:id. | | `lines[].unitOfMeasureId` | string | no | The unit the quantity is expressed in. See id on GET inventory/unit-of-measures/:id. | | `lines[].departmentId` | string | no | The department this line is allocated to. See id on GET departments/:id. | | `lines[].quantity` | string (decimal) | no | The requested quantity, as a decimal. | | `lines[].price` | string (decimal) | no | The unit price, in the request currency, as a decimal. | | `lines[].editableAmount` | string (decimal) | no | The manually entered line amount, in the request currency, as a decimal. | | `lines[].needByDate` | string (date) | no | The date this line is needed by. Use format YYYY-MM-DD. | | `lines[].deliveryDate` | string (date) | no | The expected delivery date for this line. Use format YYYY-MM-DD. | | `lines[].priority` | string | no | The urgency of this line. Accepted values: 'high', 'medium', 'low'. | | `attachments` | array of object | no | Files to attach to the request. Each names an already-uploaded file by its id. | | `attachments[].fileName` | string | yes | The display name of the attached file. | | `attachments[].fileId` | string | yes | The identifier of the already-uploaded file. There is no endpoint for it. | Example: ```json { "entityId": "8f2c1d40-5a6b-4c7d-9e80-1f2a3b4c5d6e", "currency": "EUR", "vendorId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "departmentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "description": "Replacement laptops", "internalDescription": "", "externalMemo": "", "paymentTerms": "", "externalId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "needByDate": "2026-10-15", "startDate": "2026-01-15", "endDate": "2026-01-15", "lines": [ { "isItem": false, "description": "Laptop", "editableAmount": "1800.00", "priority": "high" } ], "attachments": [ { "fileName": "", "fileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ] } ``` ## Response `200` Success Under `data` (createPurchaseRequestOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created purchase request. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `TOO_MANY_LINES` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, TOO_MANY_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 --- # Returns the purchase request with the given identifier. `GET /accounting/purchase-requests/{id}` Group: Accounting › Purchase Requests · Operation ID: `getPurchaseRequest` A purchase request is an internal request to buy, with its header and lines. It shares its read with POST accounting/purchase-requests/search, which returns the same record. The list at GET accounting/purchase-requests is header only, so the line items come back here and on search. status is both the lifecycle state and the approval outcome. There is no separate approvalStatus on this resource. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getPurchaseRequest) ## 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`. | | `id` | path | string | yes | The unique identifier for the purchase request to return. | ## Response `200` Success Under `data` (getPurchaseRequestOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the purchase request. | | `purchaseRequestNumber` | string | no | The purchase request number, for example 'PR-1', unique across purchase requests. Absent until one has been assigned. | | `description` | string | no | A description of what is being requested. It is shown to the vendor. | | `internalDescription` | string | no | Internal notes on the request, not shown to the vendor. | | `externalMemo` | string | no | A memo intended for the vendor. | | `status` | string | no | The lifecycle and approval state, in one field. Possible values: 'pendingApproval', 'approved', 'rejected', 'closed'. This resource has no separate approvalStatus. | | `currency` | string | no | The currency the request is denominated in. Format ISO 4217, for example 'USD'. Governs totalAmount and every line amount. | | `totalAmount` | string (decimal) | no | The total requested amount, the sum of the line totals, in the request currency. | | `startDate` | string (date) | no | The first day of the period the request covers. Format YYYY-MM-DD. | | `endDate` | string (date) | no | The last day of the period the request covers. Format YYYY-MM-DD. | | `needByDate` | string (date) | no | The date the goods or services are needed by. Format YYYY-MM-DD. | | `paymentTerms` | string | no | The payment-terms key requested for the resulting order, for example 'net30'. | | `externalId` | string | no | The identifier this request has in the upstream system that produced it. It is not unique. | | `vendorId` | string | no | The vendor this request is directed at. See id on GET accounting/vendors/:id. Absent when no vendor has been chosen yet. | | `vendorName` | string | no | The name of the vendor this request is directed at. | | `entityId` | string | no | The legal entity this request belongs to. See id on GET base/entities. | | `entityName` | string | no | The name of the legal entity this request belongs to. | | `departmentId` | string | no | The department raising the request. See id on GET departments/:id. | | `departmentName` | string | no | The name of the department raising the request. | | `lastModifiedDate` | string (date-time) | yes | The date and time the purchase request last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `approvals` | array of object | no | The approval decisions on this request, oldest first. An empty array when no approval policy has matched the request. | | `approvals[].decision` | string | yes | What was decided. Values include 'approve' and 'reject'. | | `approvals[].employeeName` | string | no | The name of the employee who decided. | | `approvals[].message` | string | no | The reviewer's note, where one was given. | | `approvals[].decidedAt` | string (date-time) | yes | The date and time the decision was recorded, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | no | The request line items, in their order on the request. An empty array when the request has none. | | `lines[].id` | string | yes | The unique identifier for the line item. | | `lines[].lineNumber` | string | no | The line number, for example 'PRL-1'. Unique across purchase request lines. | | `lines[].description` | string | no | A description of the line item. | | `lines[].isItem` | boolean | no | Whether the line references an inventory item. True for an item line, false for a free-text expense line. It is the item-versus-expense discriminator. There is no itemType field on this resource. | | `lines[].quantity` | string (decimal) | no | The quantity requested, as a decimal. | | `lines[].price` | string (decimal) | no | The unit price, in the request currency. Set on item lines. On expense lines the amount is on editableAmount instead. | | `lines[].editableAmount` | string (decimal) | no | The directly entered line amount, in the request currency. Used on expense lines, where there is no unit price to multiply. | | `lines[].total` | string (decimal) | no | The line total, in the request currency. price times quantity on an item line, editableAmount on an expense line. | | `lines[].currency` | string | no | The currency of the line. Format ISO 4217, for example 'USD'. Always the request currency, on each line so the line is self-describing. | | `lines[].status` | string | no | The delivery state of the line. Possible values: 'undelivered', 'delivering', 'delivered'. It is a delivery status, not an approval status. | | `lines[].priority` | string | no | The requested priority of the line. Possible values: 'high', 'medium', 'low'. | | `lines[].needByDate` | string (date) | no | The date this line is needed by, when it differs from the header. Format YYYY-MM-DD. | | `lines[].deliveryDate` | string (date) | no | The date this line was delivered. Format YYYY-MM-DD. | | `lines[].estimatedDeliveryDate` | string (date) | no | The expected delivery date for this line. Format YYYY-MM-DD. | | `lines[].startDate` | string (date) | no | The first day of the period this line covers. Format YYYY-MM-DD. | | `lines[].endDate` | string (date) | no | The last day of the period this line covers. Format YYYY-MM-DD. | | `lines[].unitOfMeasure` | string | no | The name of the unit of measure for the quantity. | | `lines[].unitOfMeasureId` | string | no | The unit of measure for the quantity. See id on GET inventory/unit-of-measures/:id. | | `lines[].itemId` | string | no | The item this line orders. See id on GET inventory/items/:id. Present only on an item line. | | `lines[].departmentId` | string | no | The department this line is charged to. See id on GET departments/:id. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Purchase Request `PATCH /accounting/purchase-requests/{id}` Group: Accounting › Purchase Requests · Operation ID: `updatePurchaseRequest` Updates the purchase request with the given identifier. Only the fields in the request body change. Only the fields in the request body change. entityId and currency are not among them: a request cannot change owner or currency. Lines change through createLines, updateLines and deleteLines, applied as a delete pass, then an update pass, then a create pass. updateLines and deleteLines address lines by the id the request publishes, and a line id belonging to another request is refused with LINE_NOT_FOUND. A line the arrays do not mention is left as it is, so there is no whole-set resend. A line already ordered on a purchase order cannot be removed, which is LINE_LINKED_TO_PURCHASE_ORDER. A guard upstream currently refuses every deleteLines, including on a request no order has touched, so treat deleteLines as unavailable until it is fixed. createLines and updateLines are unaffected. An update that would leave the request with no lines is refused with NO_LINES. status is not an input. The underlying action keeps the stored status, and approving or rejecting is a separate operation. There is no delete endpoint. Retire a request by rejecting it. The response is empty. Read the record with GET accounting/purchase-requests/:id if you need its state after the update. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updatePurchaseRequest) ## 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. | | `id` | path | string | yes | The unique identifier for the purchase request to update. | ## Request body Required, `application/json`: updatePurchaseRequestInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `vendorId` | string | no | A new vendor for the request. See id on GET accounting/vendors/:id. Omit to leave unchanged. | | `departmentId` | string | no | A new department for the request. See id on GET departments/:id. Omit to leave unchanged. | | `description` | string | no | A new public-facing description. Omit to leave unchanged. | | `internalDescription` | string | no | A new internal description. Omit to leave unchanged. | | `externalMemo` | string | no | A new external memo. Omit to leave unchanged. | | `paymentTerms` | string | no | New payment terms, as a payment-terms key. Omit to leave unchanged. | | `needByDate` | string (date) | no | A new need-by date. Use format YYYY-MM-DD. Omit to leave unchanged. | | `startDate` | string (date) | no | A new start date. Use format YYYY-MM-DD. Omit to leave unchanged. | | `endDate` | string (date) | no | A new end date. Use format YYYY-MM-DD. Omit to leave unchanged. | | `createLines` | array of object | no | Lines to add to this request. Same shape as on create, with no id, because the line does not exist yet. Omit for no additions. | | `createLines[].isItem` | boolean | no | True for an item line, priced by quantity times price. False for an expense line. | | `createLines[].description` | string | no | A description of what is being requested. | | `createLines[].itemId` | string | no | The item being requested. See id on GET inventory/items/:id. | | `createLines[].unitOfMeasureId` | string | no | The unit the quantity is expressed in. See id on GET inventory/unit-of-measures/:id. | | `createLines[].departmentId` | string | no | The department this line is allocated to. See id on GET departments/:id. | | `createLines[].quantity` | string (decimal) | no | The requested quantity, as a decimal. | | `createLines[].price` | string (decimal) | no | The unit price, as a decimal. | | `createLines[].editableAmount` | string (decimal) | no | The manually entered line amount, as a decimal. | | `createLines[].needByDate` | string (date) | no | The date this line is needed by. Use format YYYY-MM-DD. | | `createLines[].deliveryDate` | string (date) | no | The expected delivery date. Use format YYYY-MM-DD. | | `createLines[].priority` | string | no | The urgency of this line. Accepted values: 'high', 'medium', 'low'. | | `updateLines` | array of object | no | Lines to change, each addressed by the id the request publishes. Every field but the id is optional and omitting it leaves that field unchanged. Omit for no changes. | | `updateLines[].id` | string | yes | The unique identifier for the line to change. It must belong to this request. | | `updateLines[].isItem` | boolean | no | A new line kind. Omit to leave unchanged. | | `updateLines[].description` | string | no | A new description. Omit to leave unchanged. | | `updateLines[].itemId` | string | no | A new item. See id on GET inventory/items/:id. Omit to leave unchanged. | | `updateLines[].unitOfMeasureId` | string | no | A new unit of measure. See id on GET inventory/unit-of-measures/:id. Omit to leave unchanged. | | `updateLines[].departmentId` | string | no | A new department. See id on GET departments/:id. Omit to leave unchanged. | | `updateLines[].quantity` | string (decimal) | no | A new quantity, as a decimal. Omit to leave unchanged. | | `updateLines[].price` | string (decimal) | no | A new unit price, as a decimal. Omit to leave unchanged. | | `updateLines[].editableAmount` | string (decimal) | no | A new manually entered amount, as a decimal. Omit to leave unchanged. | | `updateLines[].needByDate` | string (date) | no | A new need-by date. Use format YYYY-MM-DD. Omit to leave unchanged. | | `updateLines[].deliveryDate` | string (date) | no | A new expected delivery date. Use format YYYY-MM-DD. Omit to leave unchanged. | | `updateLines[].priority` | string | no | A new urgency. Omit to leave unchanged. | | `deleteLines` | array of string | no | The ids of lines to remove, applied before the updates and creates. Each must belong to this request, and none may already be ordered on a purchase order. | Example: ```json { "vendorId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "departmentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "description": "", "internalDescription": "", "externalMemo": "", "paymentTerms": "", "needByDate": "2026-11-01", "startDate": "2026-01-15", "endDate": "2026-01-15", "createLines": [ { "isItem": true, "description": "", "itemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "unitOfMeasureId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "departmentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "price": "19.99", "editableAmount": "19.99", "needByDate": "2026-01-15", "deliveryDate": "2026-01-15", "priority": "" } ], "updateLines": [ { "id": "8b3c1d02-7e46-4f95-b2a7-6d04e1c9f378", "editableAmount": "1950.00" } ], "deleteLines": [ "" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `LINE_NOT_FOUND` | 400 | | `LINE_LINKED_TO_PURCHASE_ORDER` | 400 | | `NO_LINES` | 400 | | `TOO_MANY_LINES` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND, LINE_NOT_FOUND, LINE_LINKED_TO_PURCHASE_ORDER, NO_LINES, TOO_MANY_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 --- # Approves the purchase request. `POST /accounting/purchase-requests/{id}/approve` Group: Accounting › Purchase Requests · Operation ID: `approvePurchaseRequest` Approves the purchase request through your tenant's approval policy. With no approvers configured the request goes straight to approved, and with a multi-step policy it advances one step and stays pendingApproval. The status after the operation is returned. A request that is already approved is refused with STATUS_CONFLICT. A refusal from the approval framework comes back as APPROVAL_FAILED, with its own message in reason. [Open in the interactive reference](https://docs.api.lensing.ai/?op=approvePurchaseRequest) ## 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. | | `id` | path | string | yes | The unique identifier for the purchase request to approve. | ## Response `200` Success Under `data` (approvePurchaseRequestOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string | yes | The status after the operation. The approval workflow decides it, so it is read back. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `STATUS_CONFLICT` | 400 | | `APPROVAL_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, STATUS_CONFLICT, APPROVAL_FAILED - `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 --- # Rejects the purchase request. `POST /accounting/purchase-requests/{id}/reject` Group: Accounting › Purchase Requests · Operation ID: `rejectPurchaseRequest` Rejects the purchase request, with an optional reviewMessage recorded against the decision. A request that is already rejected is refused with STATUS_CONFLICT. A refusal from the approval framework comes back as APPROVAL_FAILED, with its own message in reason. Rejecting is how a purchase request is retired: this resource has no delete, so rejection is the terminal state. [Open in the interactive reference](https://docs.api.lensing.ai/?op=rejectPurchaseRequest) ## 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. | | `id` | path | string | yes | The unique identifier for the purchase request to reject. | ## Request body Required, `application/json`: rejectPurchaseRequestInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `reviewMessage` | string | no | Why the request is being rejected, recorded against the approval decision. Omit for none. | Example: ```json { "reviewMessage": "Not needed this quarter" } ``` ## Response `200` Success Under `data` (rejectPurchaseRequestOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string | yes | The status after the operation. The approval workflow decides it, so it is read back. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `STATUS_CONFLICT` | 400 | | `APPROVAL_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, STATUS_CONFLICT, APPROVAL_FAILED - `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 --- # Search Purchase Request `POST /accounting/purchase-requests/search` Group: Accounting › Purchase Requests · Operation ID: `searchPurchaseRequest` Returns the purchase requests that match the given filters, with their line items. This is the only endpoint that filters on vendor or legal entity name. Searches purchase requests and returns the full record, header and line items, for each match. It filters on keys the collection and GET accounting/purchase-requests/:id do not accept, including externalId, which is not unique, and the vendor and legal entity names. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchPurchaseRequest) ## 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`. | ## Request body Required, `application/json`: searchPurchaseRequestInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A purchase request must match every filter you send. Omit to return all purchase requests. | | `filters.id` | string | no | Limits results to the purchase request with this id. GET accounting/purchase-requests/:id does the same more cheaply. | | `filters.purchaseRequestNumber` | string | no | Limits results to the request with this number, for example 'PR-1'. The match is exact. It is unique, so this returns at most one. | | `filters.status` | string | no | Limits results to requests in this state. Accepted values: 'pendingApproval', 'approved', 'rejected', 'closed'. Any other value matches no request. | | `filters.currency` | string | no | Limits results to requests denominated in this currency. Format ISO 4217, for example 'USD'. | | `filters.paymentTerms` | string | no | Limits results to requests with this payment-terms key, for example 'net30'. The match is exact. | | `filters.externalId` | string | no | Limits results to requests with this external identifier. The match is exact. It is not unique, so this can return several requests. | | `filters.vendorId` | string | no | Limits results to requests directed at this vendor. See id on GET accounting/vendors/:id. An id that matches no vendor returns an empty page, not an error. | | `filters.entityId` | string | no | Limits results to requests of this legal entity. See id on GET base/entities. An id that matches no entity returns an empty page, not an error. | | `filters.departmentId` | string | no | Limits results to requests raised by this department. See id on GET departments/:id. An id that matches no department returns an empty page, not an error. | | `filters.vendorName` | string | no | Limits results to requests whose vendor has this name. The match is exact and case-sensitive. Use vendorId when you already have the id. | | `filters.entityName` | string | no | Limits results to requests whose legal entity has this name. The match is exact and case-sensitive. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of purchase requests per page. Defaults to 50, capped at 200. The server may return fewer, never more. | Example: ```json { "filters": { "status": "pendingApproval" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchPurchaseRequestOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The purchase requests on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the purchase request. | | `items[].purchaseRequestNumber` | string | no | The purchase request number, for example 'PR-1', unique across purchase requests. Absent until one has been assigned. | | `items[].description` | string | no | A description of what is being requested. It is shown to the vendor. | | `items[].internalDescription` | string | no | Internal notes on the request, not shown to the vendor. | | `items[].externalMemo` | string | no | A memo intended for the vendor. | | `items[].status` | string | no | The lifecycle and approval state, in one field. Possible values: 'pendingApproval', 'approved', 'rejected', 'closed'. This resource has no separate approvalStatus. | | `items[].currency` | string | no | The currency the request is denominated in. Format ISO 4217, for example 'USD'. Governs totalAmount and every line amount. | | `items[].totalAmount` | string (decimal) | no | The total requested amount, the sum of the line totals, in the request currency. | | `items[].startDate` | string (date) | no | The first day of the period the request covers. Format YYYY-MM-DD. | | `items[].endDate` | string (date) | no | The last day of the period the request covers. Format YYYY-MM-DD. | | `items[].needByDate` | string (date) | no | The date the goods or services are needed by. Format YYYY-MM-DD. | | `items[].paymentTerms` | string | no | The payment-terms key requested for the resulting order, for example 'net30'. | | `items[].externalId` | string | no | The identifier this request has in the upstream system that produced it. It is not unique. | | `items[].vendorId` | string | no | The vendor this request is directed at. See id on GET accounting/vendors/:id. Absent when no vendor has been chosen yet. | | `items[].vendorName` | string | no | The name of the vendor this request is directed at. | | `items[].entityId` | string | no | The legal entity this request belongs to. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the legal entity this request belongs to. | | `items[].departmentId` | string | no | The department raising the request. See id on GET departments/:id. | | `items[].departmentName` | string | no | The name of the department raising the request. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the purchase request last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | no | The request line items, in their order on the request. An empty array when the request has none. | | `items[].lines[].id` | string | yes | The unique identifier for the line item. | | `items[].lines[].lineNumber` | string | no | The line number, for example 'PRL-1'. Unique across purchase request lines. | | `items[].lines[].description` | string | no | A description of the line item. | | `items[].lines[].isItem` | boolean | no | Whether the line references an inventory item. True for an item line, false for a free-text expense line. It is the item-versus-expense discriminator. There is no itemType field on this resource. | | `items[].lines[].quantity` | string (decimal) | no | The quantity requested, as a decimal. | | `items[].lines[].price` | string (decimal) | no | The unit price, in the request currency. Set on item lines. On expense lines the amount is on editableAmount instead. | | `items[].lines[].editableAmount` | string (decimal) | no | The directly entered line amount, in the request currency. Used on expense lines, where there is no unit price to multiply. | | `items[].lines[].total` | string (decimal) | no | The line total, in the request currency. price times quantity on an item line, editableAmount on an expense line. | | `items[].lines[].currency` | string | no | The currency of the line. Format ISO 4217, for example 'USD'. Always the request currency, on each line so the line is self-describing. | | `items[].lines[].status` | string | no | The delivery state of the line. Possible values: 'undelivered', 'delivering', 'delivered'. It is a delivery status, not an approval status. | | `items[].lines[].priority` | string | no | The requested priority of the line. Possible values: 'high', 'medium', 'low'. | | `items[].lines[].needByDate` | string (date) | no | The date this line is needed by, when it differs from the header. Format YYYY-MM-DD. | | `items[].lines[].deliveryDate` | string (date) | no | The date this line was delivered. Format YYYY-MM-DD. | | `items[].lines[].estimatedDeliveryDate` | string (date) | no | The expected delivery date for this line. Format YYYY-MM-DD. | | `items[].lines[].startDate` | string (date) | no | The first day of the period this line covers. Format YYYY-MM-DD. | | `items[].lines[].endDate` | string (date) | no | The last day of the period this line covers. Format YYYY-MM-DD. | | `items[].lines[].unitOfMeasure` | string | no | The name of the unit of measure for the quantity. | | `items[].lines[].unitOfMeasureId` | string | no | The unit of measure for the quantity. See id on GET inventory/unit-of-measures/:id. | | `items[].lines[].itemId` | string | no | The item this line orders. See id on GET inventory/items/:id. Present only on an item line. | | `items[].lines[].departmentId` | string | no | The department this line is charged to. See id on GET departments/:id. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # List Refunds `GET /accounting/refunds` Group: Accounting › Refunds · Operation ID: `listRefunds` Returns a list of refunds. To match multiple approval statuses or filter in a request body, use POST accounting/refunds/search. The item schema here is the same as GET accounting/refunds/:id and POST accounting/refunds/search. Filtering by several statuses at once, or by the refund's id, is only on POST accounting/refunds/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=listRefunds) ## 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`. | | `status` | query | string | no | Limits results to refunds in this approval state. Possible values: 'draft', 'pendingApproval', 'revise', 'approved', 'rejected'. Omit to return every state. | | `paymentId` | query | string | no | Limits results to refunds against this source payment. Omit to return every payment. See id on GET accounting/payments/:id. | | `customerId` | query | string | no | Limits results to refunds for this customer. Omit to return every customer. See id on GET accounting/customers/:id. | | `refundDateFrom` | query | string | no | Limits results to refunds issued on or after this date, inclusive. Format YYYY-MM-DD. | | `refundDateTo` | query | string | no | Limits results to refunds issued on or before this date, inclusive. Format YYYY-MM-DD. | | `modifiedFrom` | query | string | no | Limits results to refunds changed at or after this time, inclusive. In UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `modifiedTo` | query | string | no | Limits results to refunds changed at or before this time, inclusive. In UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `cursor` | query | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `pageSize` | query | number | no | The number of refunds per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | ## Response `200` Success Under `data` (listRefundsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The refunds that match, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the refund. | | `items[].refundNumber` | string | no | The refund number, unique within the tenant, for example 'REF-1042'. | | `items[].refundDate` | string (date) | yes | The date the refund was issued. Format YYYY-MM-DD. | | `items[].approvalStatus` | string | no | The approval status. Possible values: 'draft', 'pendingApproval', 'revise', 'approved', 'rejected'. | | `items[].refundType` | string | yes | The payment method used for the refund. Possible values: 'check', 'ach', 'wire', 'creditCard', 'directDebit', 'sepaCreditTransfer'. | | `items[].currency` | string | yes | The currency of the refund. Every amount on it is in this currency. Format ISO 4217, for example 'USD'. | | `items[].amount` | string (decimal) | yes | The refund amount, in the refund currency. | | `items[].unappliedAmount` | string (decimal) | no | The unapplied balance remaining on the source payment, in the refund currency. | | `items[].refundNote` | string | no | A free-form note attached to the refund. Absent when none was set. | | `items[].customerId` | string | yes | The customer receiving the refund. See id on GET accounting/customers/:id. | | `items[].customerName` | string | no | The name of the customer receiving the refund. | | `items[].entityId` | string | yes | The legal entity issuing the refund. See id on GET base/entities. | | `items[].paymentId` | string | yes | The source payment being refunded. See id on GET accounting/payments/:id. | | `items[].paymentNumber` | string | no | The number of the source payment being refunded. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the refund last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as the cursor input. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more refunds remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a refund against a source payment. `POST /accounting/refunds` Group: Accounting › Refunds · Operation ID: `createRefund` Creates a refund against a source payment. When the payment lacks enough unapplied balance, list invoices in unapply to free balance first, in the same request. The refund is created approved, which triggers the external payment-provider sync, so there is no separate approval step over this API. customerId, entityId and currency are not inputs. They are derived from the source payment, so a refund cannot disagree with the payment it is against. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createRefund) ## 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`: createRefundInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `paymentId` | string | yes | The source payment being refunded. See id on GET accounting/payments/:id. | | `refundDate` | string (date) | yes | The date the refund is issued. Use format YYYY-MM-DD. Must fall in an open accounting period. | | `refundType` | string, one of `"check"`, `"ach"`, `"wire"`, `"creditCard"`, `"directDebit"`, `"sepaCreditTransfer"` | yes | The payment method used for the refund. | | `amount` | string (decimal) | yes | The refund amount, in the source payment's currency. Must be greater than 0. | | `refundNote` | string | no | A free-form note attached to the refund. | | `unapply` | array of object | no | Invoices to unapply the source payment from before the refund is created, freeing balance for the refund. The amount unapplied from each invoice is recognized on refundDate. | | `unapply[].invoiceId` | string | yes | The invoice to unapply the payment from. See id on GET accounting/invoices/:id. | | `unapply[].amount` | string (decimal) | yes | The amount to unapply from this invoice, in the payment currency. | Example: ```json { "paymentId": "5a0f3e6d-81b7-4c8e-9b21-9c2a6a107d34", "refundDate": "2026-08-01", "refundType": "ach", "amount": "150.00", "refundNote": "Customer requested partial refund", "unapply": [ { "invoiceId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "amount": "19.99" } ] } ``` ## Response `200` Success Under `data` (createRefundOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created refund. | | `refundNumber` | string | no | The refund number, unique within the tenant, for example 'REF-1042'. Assigned on create. | | `refundDate` | string (date) | yes | The date the refund was issued. Format YYYY-MM-DD. | | `approvalStatus` | string | no | The approval status, always 'approved' immediately after create. | | `refundType` | string | yes | The payment method used for the refund. Possible values: 'check', 'ach', 'wire', 'creditCard', 'directDebit', 'sepaCreditTransfer'. | | `currency` | string | yes | The currency of the refund, derived from the source payment. Every amount on it is in this currency. Format ISO 4217, for example 'USD'. | | `amount` | string (decimal) | yes | The refund amount, in the refund currency. | | `customerId` | string | yes | The customer receiving the refund, derived from the source payment. See id on GET accounting/customers/:id. | | `entityId` | string | yes | The legal entity issuing the refund, derived from the source payment. See id on GET base/entities. | | `paymentId` | string | yes | The source payment being refunded. See id on GET accounting/payments/:id. | | `lastModifiedDate` | string (date-time) | yes | The date and time the refund last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## 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 | | --- | --- | | `PAYMENT_NOT_FOUND` | 400 | | `INVOICE_NOT_FOUND` | 400 | | `INVALID_AMOUNT` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: PAYMENT_NOT_FOUND, INVOICE_NOT_FOUND, INVALID_AMOUNT - `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 --- # Get Refund `GET /accounting/refunds/{id}` Group: Accounting › Refunds · Operation ID: `getRefund` Returns the refund with the given identifier. Use the refund's id, not its refundNumber. The refund returned here has the same fields as GET accounting/refunds and POST accounting/refunds/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getRefund) ## 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`. | | `id` | path | string | yes | The unique identifier for the refund to return. | ## Response `200` Success Under `data` (getRefundOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the refund. | | `refundNumber` | string | no | The refund number, unique within the tenant, for example 'REF-1042'. Assigned on create. | | `refundDate` | string (date) | yes | The date the refund was issued. Format YYYY-MM-DD. | | `approvalStatus` | string | no | The approval status. Possible values: 'draft', 'pendingApproval', 'revise', 'approved', 'rejected'. A refund created through POST accounting/refunds is approved immediately. | | `refundType` | string | yes | The payment method used for the refund. Possible values: 'check', 'ach', 'wire', 'creditCard', 'directDebit', 'sepaCreditTransfer'. | | `currency` | string | yes | The currency of the refund. Every amount on it is in this currency. Format ISO 4217, for example 'USD'. | | `amount` | string (decimal) | yes | The refund amount, in the refund currency. | | `unappliedAmount` | string (decimal) | no | The unapplied balance remaining on the source payment, in the refund currency. | | `refundNote` | string | no | A free-form note attached to the refund. Absent when none was set. | | `customerId` | string | yes | The customer receiving the refund. See id on GET accounting/customers/:id. | | `customerName` | string | no | The name of the customer receiving the refund. | | `entityId` | string | yes | The legal entity issuing the refund. See id on GET base/entities. | | `paymentId` | string | yes | The source payment being refunded. See id on GET accounting/payments/:id. | | `paymentNumber` | string | no | The number of the source payment being refunded. | | `lastModifiedDate` | string (date-time) | yes | The date and time the refund last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Refund `PATCH /accounting/refunds/{id}` Group: Accounting › Refunds · Operation ID: `updateRefund` Updates the refund with the given identifier. Only the fields in the request body change. Use the refund's id, not its refundNumber. Only the fields in the request body change. Everything omitted is left unchanged. Only an unapproved refund can be updated: a refund created through POST accounting/refunds is approved automatically, so this endpoint succeeds only on a refund still in draft, pendingApproval or revise. Updating an already-approved refund fails with REFUND_ALREADY_APPROVED and changes nothing. Issue a new refund instead. customerId, entityId, paymentId and currency are not updatable. The response contains the updated refund's header fields. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateRefund) ## 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. | | `id` | path | string | yes | The unique identifier for the refund to update. | ## Request body Required, `application/json`: updateRefundInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `refundDate` | string (date) | no | The new issue date. Use format YYYY-MM-DD. Must fall in an open accounting period. Omit to leave it unchanged. | | `refundType` | string, one of `"check"`, `"ach"`, `"wire"`, `"creditCard"`, `"directDebit"`, `"sepaCreditTransfer"` | no | The new payment method. Omit to leave it unchanged. | | `refundNote` | string | no | A new free-form note. Omit to leave it unchanged. | | `amount` | string (decimal) | no | The new refund amount, in the refund currency. Must be greater than 0. Omit to leave it unchanged. | Example: ```json { "refundDate": "2026-01-15", "refundType": "check", "refundNote": "", "amount": "175.00" } ``` ## Response `200` Success Under `data` (updateRefundOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the refund. | | `refundNumber` | string | no | The refund number, unique within the tenant, for example 'REF-1042'. | | `refundDate` | string (date) | yes | The date the refund was issued. Format YYYY-MM-DD. | | `approvalStatus` | string | no | The approval status after the change. Possible values: 'draft', 'pendingApproval', 'revise', 'approved', 'rejected'. | | `refundType` | string | yes | The payment method used for the refund. Possible values: 'check', 'ach', 'wire', 'creditCard', 'directDebit', 'sepaCreditTransfer'. | | `currency` | string | yes | The currency of the refund. Every amount on it is in this currency. Format ISO 4217, for example 'USD'. | | `amount` | string (decimal) | yes | The refund amount, in the refund currency. | | `customerId` | string | yes | The customer receiving the refund. See id on GET accounting/customers/:id. | | `entityId` | string | yes | The legal entity issuing the refund. See id on GET base/entities. | | `paymentId` | string | yes | The source payment being refunded. See id on GET accounting/payments/:id. | | `lastModifiedDate` | string (date-time) | yes | The date and time the refund last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFUND_ALREADY_APPROVED` | 400 | | `INVALID_AMOUNT` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFUND_ALREADY_APPROVED, INVALID_AMOUNT - `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 --- # Search Refunds `POST /accounting/refunds/search` Group: Accounting › Refunds · Operation ID: `searchRefunds` Returns the refunds that match the given filters. Unlike the collection, it matches multiple approval statuses and takes its filters in a request body. GET accounting/refunds takes one status. This endpoint takes a list, combining with AND across criteria and OR within status. The item schema here is the same as GET accounting/refunds and GET accounting/refunds/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchRefunds) ## 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`. | ## Request body Required, `application/json`: searchRefundsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | Limits results to the refund with this identifier. | | `status` | array of string, one of `"draft"`, `"pendingApproval"`, `"revise"`, `"approved"`, `"rejected"` | no | Limits results to refunds in any of these approval states. Omit or pass an empty array to return every state. | | `paymentId` | string | no | Limits results to refunds against this source payment. See id on GET accounting/payments/:id. | | `customerId` | string | no | Limits results to refunds for this customer. See id on GET accounting/customers/:id. | | `refundDate` | object | no | Limits results to refunds whose refundDate falls in this range. Both bounds are inclusive. Omit either bound to leave that side open. | | `refundDate.start` | string (date) | no | The earliest refundDate to include, inclusive. Format YYYY-MM-DD. | | `refundDate.end` | string (date) | no | The latest refundDate to include, inclusive. Format YYYY-MM-DD. | | `lastModifiedDate` | object | no | Limits results to refunds whose lastModifiedDate falls in this range, in UTC. Both bounds are inclusive. Omit either bound to leave that side open. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate.start` | string | no | The earliest change time to include, inclusive. | | `lastModifiedDate.end` | string | no | The latest change time to include, inclusive. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of refunds per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "status": [ "pendingApproval", "revise" ], "paymentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "customerId": "9c2a6a10-7d34-4c8e-9b21-5a0f3e6d81b7", "refundDate": { "start": "2026-01-15", "end": "2026-01-15" }, "lastModifiedDate": { "start": "", "end": "" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchRefundsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The refunds that match, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the refund. | | `items[].refundNumber` | string | no | The refund number, unique within the tenant, for example 'REF-1042'. | | `items[].refundDate` | string (date) | yes | The date the refund was issued. Format YYYY-MM-DD. | | `items[].approvalStatus` | string | no | The approval status. Possible values: 'draft', 'pendingApproval', 'revise', 'approved', 'rejected'. | | `items[].refundType` | string | yes | The payment method used for the refund. Possible values: 'check', 'ach', 'wire', 'creditCard', 'directDebit', 'sepaCreditTransfer'. | | `items[].currency` | string | yes | The currency of the refund. Every amount on it is in this currency. Format ISO 4217, for example 'USD'. | | `items[].amount` | string (decimal) | yes | The refund amount, in the refund currency. | | `items[].unappliedAmount` | string (decimal) | no | The unapplied balance remaining on the source payment, in the refund currency. | | `items[].refundNote` | string | no | A free-form note attached to the refund. Absent when none was set. | | `items[].customerId` | string | yes | The customer receiving the refund. See id on GET accounting/customers/:id. | | `items[].customerName` | string | no | The name of the customer receiving the refund. | | `items[].entityId` | string | yes | The legal entity issuing the refund. See id on GET base/entities. | | `items[].paymentId` | string | yes | The source payment being refunded. See id on GET accounting/payments/:id. | | `items[].paymentNumber` | string | no | The number of the source payment being refunded. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the refund last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Run Financial Statement `POST /accounting/reports/financial-statement` Group: Accounting › Reports › Financial Statement · Operation ID: `runFinancialStatement` Returns the computed report for one financial statement template, over one entity, accounting book and set of accounting periods. [Open in the interactive reference](https://docs.api.lensing.ai/?op=runFinancialStatement) ## 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`. | ## Request body Required, `application/json`: runFinancialStatementInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `statementId` | string | yes | The configured statement template to run. A template that has been deleted is reported as STATEMENT_NOT_FOUND. This API has no endpoint that lists the templates. | | `entityId` | string | yes | The entity to report on. When consolidated is false, this is the reporting entity on its own. When true, this is the consolidating parent and the report covers its whole hierarchy. Exactly one entity per request. See id on GET base/entities. | | `bookId` | string | yes | The accounting book to report from. It must belong to entityId. A book that belongs to another entity is reported as BOOK_NOT_FOUND. Exactly one book per request, because amounts from different books are not comparable. See id on GET accounting/books/:id. | | `periodIds` | array of string | yes | The accounting periods to report over, at least one. Period names are not accepted, because their format varies per fiscal calendar. The periods need not be contiguous, and how they become columns depends on the template and on splitBy. See id on GET accounting/accounting-periods/:id. | | `consolidated` | boolean | no | Whether to report the consolidated statement for entityId and every entity beneath it, with amounts translated into the parent's currency. When false, only this entity is reported, with no translation. Defaults to false. | | `splitBy` | string, one of `"month"`, `"quarter"`, `"year"`, `"entity"`, `"department"`, `"aggregatedDepartment"`, `"businessUnit"`, `"vendor"`, `"customer"` | no | Adds a further column split to the report, beyond the periods it reports over. Omit for the plain statement with no split. The time splits 'month', 'quarter' and 'year' subdivide the requested periods. The dimension splits break each figure out by that dimension, such as entity, department, business unit, vendor or customer. 'entity' is meaningful only when consolidated is true. 'aggregatedDepartment' rolls the department hierarchy up to its top level, where 'department' keeps each department on its own. | Example: ```json { "statementId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "bookId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "periodIds": [ "" ], "consolidated": true, "splitBy": "month" } ``` ## Response `200` Success Under `data` (runFinancialStatementOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `statementId` | string | yes | The statement template that produced this report, echoing the statementId input. | | `statementName` | string | yes | The template's configured name. | | `statementType` | string | yes | The reporting standard and statement kind of the template that ran. Possible values: 'generic', 'coaIncomeStatement', 'coaBalanceSheet', 'gaapIncomeStatement', 'gaapBalanceSheet', 'cashFlow', 'hgbIncomeStatement', 'hgbBalanceSheet', 'ifrsStatementOfProfitOrLoss', 'ifrsStatementOfFinancialPosition', 'ifrsStatementOfOtherComprehensiveIncome', 'sfrsStatementOfProfitOrLoss', 'sfrsStatementOfFinancialPosition', 'sfrsStatementOfOtherComprehensiveIncome'. | | `data` | string or number or boolean or array of UnknownValue or UnknownObject | yes | The computed report, produced by the statement calculation. It has no fixed schema: the shape depends on the template's type and on splitBy. Expect a hierarchy of statement lines with their computed amounts per column, and account-level detail rows are always present, so you can aggregate or drill down. Treat unfamiliar fields as data rather than an error. | ## Errors | Code | Status | | --- | --- | | `STATEMENT_NOT_FOUND` | 400 | | `ENTITY_NOT_FOUND` | 400 | | `BOOK_NOT_FOUND` | 400 | | `PERIOD_NOT_FOUND` | 400 | | `PERIODS_REQUIRED` | 400 | | `ENTITY_SPLIT_REQUIRES_CONSOLIDATION` | 400 | | `MALFORMED_ID` | 400 | | `STATEMENT_CALCULATION_FAILED` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: STATEMENT_NOT_FOUND, ENTITY_NOT_FOUND, BOOK_NOT_FOUND, PERIOD_NOT_FOUND, PERIODS_REQUIRED, ENTITY_SPLIT_REQUIRES_CONSOLIDATION, MALFORMED_ID, STATEMENT_CALCULATION_FAILED - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Read General Ledger `POST /accounting/reports/general-ledger` Group: Accounting › Reports › General Ledger · Operation ID: `readGeneralLedger` Returns the general ledger for one book and entity over a bounded window: the posted journal entry lines and the lines the ledger derives so the statements tie out. It reports movements only, with no opening, closing or running balance. It is not paginated, so narrow the request until it returns in one response, or it fails with RESULT_TOO_LARGE. [Open in the interactive reference](https://docs.api.lensing.ai/?op=readGeneralLedger) ## 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`. | ## Request body Required, `application/json`: readGeneralLedgerInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `bookId` | string | yes | The accounting book to read. Exactly one book per request. An adjustment book automatically includes its base book. See id on GET accounting/books/:id. | | `entityId` | string | yes | The entity to read. When consolidated is false, this is the posting entity. When true, this is the consolidating parent, and the response covers its whole hierarchy, with each line also carrying preConsolidationEntityId naming the child that posted it. Exactly one entity per request. See id on GET base/entities. | | `consolidated` | boolean | yes | Whether to return the consolidated ledger for entityId and its hierarchy. When true, amounts are also translated into the parent's currency and elimination and CTA lines appear. When false, this returns the entity's own ledger with no translation, elimination or CTA. | | `postingDate` | object | no | A window on the posting date, the date a line was booked. Supply exactly one of postingDate or accountingPeriod. Both bounds are inclusive. The window may span at most 366 days. | | `postingDate.start` | string (date) | yes | The inclusive start of the window. Use format YYYY-MM-DD. | | `postingDate.end` | string (date) | yes | The inclusive end of the window. Use format YYYY-MM-DD. | | `accountingPeriod` | object | no | A window on the accounting period, the period a line was booked into. Supply exactly one of postingDate or accountingPeriod. Both bounds are inclusive. The resolved period dates may span at most 366 days. | | `accountingPeriod.startPeriodId` | string | yes | The first accounting period in the window. See accountingPeriodId on GET accounting/accounting-periods. | | `accountingPeriod.endPeriodId` | string | yes | The last accounting period in the window, read the same way. | | `filters` | object | no | Narrows which lines are returned, pushed down into the ledger computation wherever the ledger can express it. Derived amounts stay honest: a filter naming a derived line's offset account, such as CTA, translation elimination or retained earnings, is computed across every line in scope, and a filter that excludes those accounts drops those lines rather than returning a partial figure. Because derived lines carry almost no dimensions, any dimension filter drops nearly every retained-earnings, elimination and CTA line, so the returned lines no longer sum to a balanced ledger and the response carries a RESULT_DOES_NOT_TIE_OUT warning. lineOrigins is applied to the computed result rather than pushed down, as are tags on a dimension the ledger does not index. | | `filters.accountIds` | array of string | no | Restrict to these accounts. Naming a derived line's offset account returns that line with its amount computed across all accounts. Omitting it drops the line, so a partial CTA is never returned. See id on GET accounting/accounts/:id. | | `filters.lineOrigins` | array of string, one of `"posted"`, `"retainedEarnings"`, `"elimination"`, `"cta"`, `"ctaElimination"` | no | Restrict to these kinds of line. Omit for all kinds. Pass ['posted'] for posted journal entry lines only, excluding everything the ledger derives. | | `filters.reportingTags` | array of object | no | Restrict to lines carrying all of these dimension values. This is the one dimension filter: every dimension is filtered here, whether the ledger stores it as a column, such as department, business unit, value center, employee, customer or vendor, or as a tag, such as a custom dimension, team, location or region. Naming two values of the same dimension matches nothing, since a line has one value per dimension. A value that does not exist matches nothing rather than failing. | | `filters.reportingTags[].dimensionCode` | string | yes | The reporting dimension this filter is for. It determines which model modelId points to. Accepted values: 'department', 'businessUnit', 'team', 'location', 'region', 'employee', 'vendor', 'customer', 'valueCenter', 'custom'. | | `filters.reportingTags[].modelId` | string | yes | The dimension record this filter points to, on the model that dimensionCode names. | | `dimensions` | array of string, one of `"accounts"`, `"entities"`, `"periods"`, `"books"`, `"reportingTagDimensions"` | no | Which dimension dictionaries to return in dimensions. Omit for all of them, or pass an empty array for none. This never changes the lines: every id is always present on every line. An unrecognized name fails input validation. | Example: ```json { "bookId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "consolidated": true, "postingDate": { "start": "2026-01-15", "end": "2026-01-15" }, "accountingPeriod": { "startPeriodId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "endPeriodId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }, "filters": { "accountIds": [ "" ], "lineOrigins": [ "posted" ], "reportingTags": [ { "dimensionCode": "", "modelId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ] }, "dimensions": [ "accounts" ] } ``` ## Response `200` Success Under `data` (readGeneralLedgerOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `lines` | array of object | yes | The ledger's movements, ordered by posting date, then journal entry, then line. The ids on each line resolve in dimensions. A posted line has journalEntryLineId and journalEntryId to reach the underlying record. A derived line is computed per request and has no id of its own. | | `lines[].lineOrigin` | string, one of `"posted"`, `"retainedEarnings"`, `"elimination"`, `"cta"`, `"ctaElimination"` | yes | What kind of line this is, and the only field always present. 'posted' is a persisted journal entry line. The rest are derived by the ledger. 'elimination', 'cta' and 'ctaElimination' appear only when consolidated is true. | | `lines[].accountId` | string | yes | The account this line posts to. A derived line lands on the offset account configured for its accounting process, so this is always set. Resolve it in dimensions.accounts. | | `lines[].entityId` | string | yes | The entity this line belongs to. Resolve it in dimensions.entities. | | `lines[].postingPeriodId` | string | yes | The accounting period this line is booked into. Resolve it in dimensions.periods. | | `lines[].postingDate` | string (date) | yes | The posting date. Format YYYY-MM-DD. For a derived line this is the date the ledger assigns it, usually the end of the period it balances. | | `lines[].bookId` | string | yes | The accounting book this line belongs to. Resolve it in dimensions.books. | | `lines[].journalEntryLineId` | string | no | The unique identifier for the posted journal entry line. Present only when lineOrigin is 'posted'. | | `lines[].journalEntryId` | string | no | The journal entry this line belongs to. Present only when lineOrigin is 'posted'. See id on GET accounting/journal-entries/:id. | | `lines[].journalEntryLineReference` | string | no | A human-readable reference to this line within its journal entry. Present only when lineOrigin is 'posted', and can be absent on some posted lines, as the INCOMPLETE_TRANSACTION_REFERENCES warning reports. | | `lines[].transactionReference` | string | no | A reference to the business transaction that produced this line. Present only when lineOrigin is 'posted', and can be absent, as INCOMPLETE_TRANSACTION_REFERENCES reports. | | `lines[].lineType` | string | no | The classification of this line within its journal entry, for example 'standard', 'tax', 'offset' or 'fxRoundingDifference'. A system-assigned value. Absent on a derived line. | | `lines[].description` | string | no | The line description. | | `lines[].intercompanyPartnerId` | string | no | The intercompany counterparty for this line. Set on an intercompany line. | | `lines[].reportingTags` | array of object | no | Every dimension value on this line, one entry each: department, business unit, value center, employee, customer and vendor, alongside any custom dimension. Absent when the line has no dimension, which is normal for a derived line. Resolve each entry in dimensions.reportingTagDimensions. | | `lines[].reportingTags[].dimensionCode` | string | yes | The reporting dimension this value is for. Possible values: 'department', 'businessUnit', 'team', 'location', 'region', 'employee', 'vendor', 'customer', 'valueCenter', 'custom'. Every custom dimension uses the code 'custom', and dimensions.reportingTagDimensions names which one a value belongs to. | | `lines[].reportingTags[].modelId` | string | yes | The dimension record this value points to, on the model that dimensionCode names. | | `lines[].transactionalCurrency` | string | no | The currency the business event happened in. Format ISO 4217, for example 'USD'. Absent on a derived line, which has no transactional amount. | | `lines[].transactionalDebit` | string (decimal) | no | The debit in the transactional currency. A line has either a debit or a credit, not both. | | `lines[].transactionalCredit` | string (decimal) | no | The credit in the transactional currency. A line has either a debit or a credit, not both. | | `lines[].transactionalAmount` | string (decimal) | no | transactionalDebit minus transactionalCredit. Negative for a net credit. To read it in an account's natural direction, with revenue and expense both positive, multiply by the sign that normalBalance implies on the account in dimensions.accounts. | | `lines[].functionalDebit` | string (decimal) | no | The debit in the posting entity's own currency. The currency is functionalCurrency on the entity in dimensions.entities, since it does not vary per line. | | `lines[].functionalCredit` | string (decimal) | no | The credit in the posting entity's own currency. | | `lines[].functionalAmount` | string (decimal) | no | functionalDebit minus functionalCredit. | | `lines[].functionalExchangeRate` | string (decimal) | no | The rate used to convert the transactional amount into the functional currency. Absent on a derived line, which has no transactional amount. | | `lines[].consolidatedDebit` | string (decimal) | no | The debit in the consolidating parent's currency. Present only when consolidated is true. The currency is consolidationCurrency at the top of this response, since it is the same for every line. | | `lines[].consolidatedCredit` | string (decimal) | no | The credit in the consolidating parent's currency. Present only when consolidated is true. | | `lines[].consolidatedAmount` | string (decimal) | no | consolidatedDebit minus consolidatedCredit. Present only when consolidated is true. | | `lines[].consolidationExchangeRate` | string (decimal) | no | The rate used to convert the functional amount into the consolidation currency. Which rate this is depends on the account's exchangeRateType in dimensions.accounts. Present only when consolidated is true. | | `lines[].preConsolidationAccountId` | string | no | The account this line was posted to before consolidation account mapping. Present only when consolidated is true and the account was mapped. Resolve it in dimensions.accounts, which contains both the mapped and the pre-consolidation accounts. | | `lines[].preConsolidationEntityId` | string | no | The child entity that posted this line, before it rolled into the parent. Present only when consolidated is true. Resolve it in dimensions.entities. | | `lines[].preConsolidationPostingPeriodId` | string | no | The period this line sat in before it was re-dated into the parent's fiscal calendar. Differs from postingPeriodId when the parent and child keep different calendars. Present only when consolidated is true. Resolve it in dimensions.periods. | | `dimensions` | object | yes | Display values for the ids on the lines, each appearing once however many lines reference it. A collection is present only when its name was in the dimensions input, which defaults to all of them. Accounts, entities and periods contain both the consolidated and the pre-consolidation records, so every id on every line resolves. | | `dimensions.accounts` | array of object | no | The accounts referenced by any line, including pre-consolidation source accounts. The field names follow the accounts endpoint, so you can join the two without a mapping table. | | `dimensions.accounts[].id` | string | yes | The unique identifier for the account. See id on GET accounting/accounts/:id. | | `dimensions.accounts[].accountNumber` | string | yes | The account number. | | `dimensions.accounts[].accountName` | string | yes | The name of the account. | | `dimensions.accounts[].accountType` | string | yes | The top-level classification of the account. Possible values: 'asset', 'liability', 'equity', 'income', 'costOfGoodsSold', 'expense', 'otherIncome', 'otherExpense'. | | `dimensions.accounts[].accountSubType` | string | no | A finer classification within accountType. Possible values: 'bank', 'accountsReceivable', 'fixedAsset', 'cip', 'prepaidExpenses', 'deferredExpenses', 'otherCurrentAsset', 'otherAsset', 'unbilledReceivable', 'longTermAsset', 'accountsPayable', 'creditCard', 'deferredRevenue', 'otherCurrentLiability', 'longTermLiability', 'equity', 'income', 'costOfGoodsSold', 'expense', 'otherIncome', 'otherExpense', 'otherComprehensiveIncome'. Absent when none is assigned. | | `dimensions.accounts[].normalBalance` | string, one of `"debit"`, `"credit"` | yes | Whether the account's natural direction is a debit or a credit. Multiply a signed amount by +1 for 'debit' and -1 for 'credit' to read it with revenue and expense both positive. | | `dimensions.accounts[].exchangeRateType` | string, one of `"current"`, `"average"`, `"historical"` | no | Which rate translates this account's functional amounts into the consolidation currency. | | `dimensions.entities` | array of object | no | The entities referenced by any line, including pre-consolidation child entities. | | `dimensions.entities[].id` | string | yes | The unique identifier for the entity. See id on GET base/entities. | | `dimensions.entities[].entityName` | string | yes | The name of the entity. | | `dimensions.entities[].functionalCurrency` | string | no | The entity's own currency, the currency of every functional amount on its lines. Format ISO 4217, for example 'USD'. | | `dimensions.periods` | array of object | no | The accounting periods referenced by any line, including pre-consolidation periods. | | `dimensions.periods[].id` | string | yes | The unique identifier for the accounting period. See accountingPeriodId on GET accounting/accounting-periods. | | `dimensions.periods[].periodName` | string | yes | The period name, as its fiscal calendar defines it. | | `dimensions.periods[].fiscalYear` | string | yes | The fiscal year this period belongs to. | | `dimensions.periods[].startDate` | string (date) | yes | The first day of the period. Format YYYY-MM-DD. | | `dimensions.periods[].endDate` | string (date) | yes | The last day of the period. Format YYYY-MM-DD. | | `dimensions.books` | array of object | no | The books referenced by any line: the requested book, and its base book when an adjustment book was requested. | | `dimensions.books[].id` | string | yes | The unique identifier for the accounting book. See id on GET accounting/books/:id. | | `dimensions.books[].name` | string | yes | The name of the book. | | `dimensions.books[].bookType` | string, one of `"full"`, `"adjustment"` | yes | Whether this book is a full book or an adjustment book. An adjustment book has only the deltas layered over its base book, so read it together with its base and add the two rather than read either alone. | | `dimensions.books[].accountingStandard` | string | no | The accounting standard this book reports under, by name, for example 'IFRS', 'US GAAP' or a local standard. A tenant defines its own standards, so treat an unrecognized value as data rather than an error. Absent on a book that declares none. | | `dimensions.reportingTagDimensions` | array of object | no | Every dimension record referenced by a reportingTags entry on any line, named once however many lines reference it. One list for every dimension: department, business unit, value center, employee, customer, vendor, team, location, region and the custom dimensions all resolve here, so adding a dimension adds entries rather than a collection. A record that has since been deactivated is still named, because a posted line legitimately references one. | | `dimensions.reportingTagDimensions[].dimensionCode` | string | yes | The reporting dimension this record is for, matching the dimensionCode on the lines' reportingTags. Possible values: 'department', 'businessUnit', 'team', 'location', 'region', 'employee', 'vendor', 'customer', 'valueCenter', 'custom'. | | `dimensions.reportingTagDimensions[].modelId` | string | yes | The dimension record this entry describes, matching the modelId on the lines' reportingTags. The pair (dimensionCode, modelId) is the key. | | `dimensions.reportingTagDimensions[].code` | string | no | The record's own business code, such as a department number, a customer number or a value center code. Absent for a dimension that keeps no code. | | `dimensions.reportingTagDimensions[].name` | string | yes | The display name of the dimension record. | | `dimensions.reportingTagDimensions[].customDimensionId` | string | no | The custom dimension this record belongs to. Present only for a 'custom' value, where several dimensions share one model and are told apart only by this. See id on GET base/custom-dimensions. | | `consolidationCurrency` | string | no | The consolidating parent's currency, the currency of every consolidated amount in this response. Stated once because it is the same for every line. Format ISO 4217, for example 'USD'. Present only when consolidated is true. | ## Errors | Code | Status | | --- | --- | | `RESULT_TOO_LARGE` | 400 | | `WINDOW_TOO_LONG` | 400 | | `DATE_RANGE_REQUIRED` | 400 | | `AMBIGUOUS_DATE_FILTER` | 400 | | `ENTITY_NOT_FOUND` | 400 | | `BOOK_NOT_FOUND` | 400 | | `PERIOD_NOT_FOUND` | 400 | | `CONSOLIDATION_ACCOUNTS_NOT_CONFIGURED` | 400 | | `CONSOLIDATION_ACCOUNT_MAPPING_MISSING` | 400 | | `CONSOLIDATION_RATES_NOT_CONFIGURED` | 400 | | `EXCHANGE_RATE_NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: RESULT_TOO_LARGE, WINDOW_TOO_LONG, DATE_RANGE_REQUIRED, AMBIGUOUS_DATE_FILTER, ENTITY_NOT_FOUND, BOOK_NOT_FOUND, PERIOD_NOT_FOUND, CONSOLIDATION_ACCOUNTS_NOT_CONFIGURED, CONSOLIDATION_ACCOUNT_MAPPING_MISSING, CONSOLIDATION_RATES_NOT_CONFIGURED, EXCHANGE_RATE_NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the trial balance for one entity, accounting book and window. `POST /accounting/reports/trial-balance/search` Group: Accounting › Reports › Trial Balances · Operation ID: `searchTrialBalance` Returns one row per account that has a balance or a movement in the window, for one entity and one accounting book. The rows are the whole chart of accounts, so they include the leaf accounts that hold postings and the grouping accounts that roll them up. A grouping row repeats the sums of its descendants, so add up only the rows where isGroupingAccount is false, or use totals, which is summed from the leaves alone. When total debits do not equal total credits, the response still comes back in full, with a TRIAL_BALANCE_DOES_NOT_TIE_OUT warning. The report is not paginated: it is bounded by the chart of accounts, and a partial page would not tie out. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchTrialBalance) ## 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`. | ## Request body Required, `application/json`: searchTrialBalanceInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `entityId` | string | yes | The entity to report on. When consolidated is false, only this entity's own postings are included. When true, this is the consolidating parent and the report covers its whole hierarchy. Exactly one entity per request. See id on GET base/entities. | | `bookId` | string | yes | The accounting book to report on. It must belong to entityId. A book that belongs to another entity is reported as BOOK_NOT_FOUND. Exactly one book per request, because balances from different books are not comparable. See id on GET accounting/books/:id. | | `consolidated` | boolean | yes | Whether to return the consolidated trial balance for entityId and its hierarchy. When true, subsidiary balances are translated into the parent's currency and intercompany eliminations are applied. When false, only this entity's own trial balance is returned, with no translation and no elimination. | | `startDate` | string (date) | yes | The inclusive start of the reporting window. Format YYYY-MM-DD. functionalStartingBalance is the position immediately before this date. Interpreted on the axis named by dateAxis. | | `endDate` | string (date) | yes | The inclusive end of the reporting window. Format YYYY-MM-DD. functionalEndingBalance is the position at the close of this date. Interpreted on the axis named by dateAxis. | | `dateAxis` | string, one of `"postingDate"`, `"accountingPeriod"` | yes | Which axis the reporting window is measured on. 'postingDate' counts a movement on the date it was booked, the axis audit and reconciliation want. 'accountingPeriod' counts it in the period it was booked into, where budget comparisons belong. There is no default, because the two axes genuinely disagree for a line booked into an earlier period. | Example: ```json { "entityId": "ebbe16c9-2c44-4a8e-a813-6ed5a78ffab3", "bookId": "8d534b26-2998-40dd-8727-9fe3f89ff503", "consolidated": false, "startDate": "2025-01-01", "endDate": "2025-12-31", "dateAxis": "postingDate" } ``` ## Response `200` Success Under `data` (searchTrialBalanceOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | One row per account that has a balance or a movement in scope, ordered by account number. The rows are the whole chart of accounts, so read isGroupingAccount before summing. An empty array when no account has a balance or movement in the window. | | `items[].accountId` | string | yes | The account this row reports on. See id on GET accounting/accounts/:id. | | `items[].accountNumber` | string | yes | The account number, as defined by the chart of accounts. Rows are ordered by it. | | `items[].accountName` | string | no | The name of the account. Absent when the account has none recorded. | | `items[].accountType` | string | no | The type of the account. Possible values: 'asset', 'liability', 'equity', 'income', 'costOfGoodsSold', 'expense', 'otherIncome', 'otherExpense'. | | `items[].isGroupingAccount` | boolean | yes | Whether this is a grouping (rollup) account. Its four figures are the sums of its descendants and are already counted in its children's rows, so sum only the rows where this is false, or use totals. | | `items[].parentAccountId` | string | no | The grouping account this row rolls up into. Absent at the root of the chart of accounts. See id on GET accounting/accounts/:id. | | `items[].functionalStartingBalance` | string (decimal) | yes | The account's balance immediately before startDate, in reportingCurrency. Positive is a debit balance, negative a credit balance. Zero at the start of a fiscal year for an income or expense account, because the year-end rollover moves the prior year's result into retained earnings. | | `items[].functionalDebit` | string (decimal) | yes | Total debit movements within the window, in reportingCurrency. Always present, and zero when there was no movement. Never negative, because a reversal is booked as a credit. | | `items[].functionalCredit` | string (decimal) | yes | Total credit movements within the window, in reportingCurrency. Always present, and zero when there was no movement. Never negative. | | `items[].functionalEndingBalance` | string (decimal) | yes | The account's balance at the close of endDate, in reportingCurrency. functionalEndingBalance = functionalStartingBalance + functionalDebit - functionalCredit. | | `totals` | object | yes | The report's column totals, in reportingCurrency, summed over the leaf rows only. functionalDebit and functionalCredit here are the two figures a trial balance compares. | | `totals.functionalStartingBalance` | string (decimal) | yes | The sum of functionalStartingBalance over the leaf rows. Zero in a balanced ledger. | | `totals.functionalDebit` | string (decimal) | yes | The sum of functionalDebit over the leaf rows. Equals functionalCredit when the report ties out. | | `totals.functionalCredit` | string (decimal) | yes | The sum of functionalCredit over the leaf rows. | | `totals.functionalEndingBalance` | string (decimal) | yes | The sum of functionalEndingBalance over the leaf rows. Zero in a balanced ledger. | | `tiesOut` | boolean | yes | Whether totals.functionalDebit equals totals.functionalCredit, computed from the figures in this response. When false, the response is still returned in full, with a TRIAL_BALANCE_DOES_NOT_TIE_OUT warning. | | `reportingCurrency` | string | yes | The currency every amount in this response is expressed in. Format ISO 4217, for example 'USD'. When consolidated is false this is the entity's own functional currency, and when true it is the consolidating parent's. | ## Errors | Code | Status | | --- | --- | | `INVALID_DATE_RANGE` | 400 | | `ENTITY_NOT_FOUND` | 400 | | `BOOK_NOT_FOUND` | 400 | | `CONSOLIDATION_ACCOUNT_MAPPING_MISSING` | 400 | | `CONSOLIDATION_RATES_NOT_CONFIGURED` | 400 | | `EXCHANGE_RATE_NOT_FOUND` | 400 | | `TRIAL_BALANCE_CALCULATION_FAILED` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_DATE_RANGE, ENTITY_NOT_FOUND, BOOK_NOT_FOUND, CONSOLIDATION_ACCOUNT_MAPPING_MISSING, CONSOLIDATION_RATES_NOT_CONFIGURED, EXCHANGE_RATE_NOT_FOUND, TRIAL_BALANCE_CALCULATION_FAILED - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Revenue Contracts `GET /accounting/revenue-contracts` Group: Accounting › Revenue Contracts · Operation ID: `revenueContracts` Returns a list of revenue contracts. Each entry has header fields only. For the revenue lines and revenue plans, or to filter by customer, legal entity or sales order, use GET accounting/revenue-contracts/:id or POST accounting/revenue-contracts/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=revenueContracts) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `approvalStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `generalStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `revenueStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isContractClosed` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isContractCombined` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `migrationDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `revenueStartDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `revenueEndDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, approvalStatus, generalStatus, revenueStatus, isContractClosed, isContractCombined, currency, migrationDate, revenueStartDate, revenueEndDate, totalContractValue, deferredRevenue, revenueRecognized, revenueRemaining, lastModifiedDate - Selectable: id, revenueContractNumber, approvalStatus, generalStatus, revenueStatus, isContractClosed, isContractCombined, isLongTermContract, currency, functionalCurrency, exchangeRate, entityName, customerName, migrationDate, revenueStartDate, revenueEndDate, totalContractValue, deferredRevenue, revenueRecognized, revenueRemaining, lastModifiedDate ## Response `200` Success Under `data`, an array of revenueContractsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the revenue contract. | | `revenueContractNumber` | string or null | no | The revenue contract number, unique within the tenant. | | `approvalStatus` | string | no | The approval status of the contract. Possible values: 'draft', 'pendingApproval', 'revise', 'approved', 'rejected'. | | `generalStatus` | string or null | no | The overall status of the contract, combining approval and any active revenue hold. Possible values: 'approved', 'approvedOnHold', 'pendingApproval', 'pendingApprovalOnHold'. | | `revenueStatus` | string or null | no | The revenue-recognition progress across the contract's lines. Possible values: 'notStarted', 'notStartedOnHold', 'inProgress', 'inProgressOnHold', 'complete', 'completeModified'. | | `isContractClosed` | boolean | no | Whether the contract has been closed, with its open revenue moved to a new combined revenue contract. | | `isContractCombined` | boolean | no | Whether the contract is the combination target that absorbed a closed contract's open revenue. | | `isLongTermContract` | boolean or null | no | Whether the recognition window from revenueStartDate to revenueEndDate exceeds 366 days. | | `currency` | string | no | The currency the contract is denominated in. Every amount on the contract is in this currency. Format ISO 4217, for example 'USD'. | | `functionalCurrency` | string or null | no | The owning entity's functional currency. Present only when it differs from the contract currency. Format ISO 4217, for example 'USD'. | | `exchangeRate` | string (decimal) | no | The rate used to convert the contract currency to the entity's functional currency. A ratio, not an amount, with no currency of its own. | | `entityName` | string or null | no | The name of the legal entity that owns this contract. | | `customerName` | string or null | no | The display name of the customer this contract is with. | | `migrationDate` | string (date) or null | no | The date this contract's opening balances were migrated from a prior system. Format YYYY-MM-DD. Absent on a contract created natively. | | `revenueStartDate` | string (date) or null | no | The earliest revenue-recognition start date across the contract's lines. Format YYYY-MM-DD. | | `revenueEndDate` | string (date) or null | no | The latest revenue-recognition end date across the contract's lines. Format YYYY-MM-DD. | | `totalContractValue` | string (decimal) or null | no | The total contract value, in the contract currency. It is the total price of the sales order this contract was created from, less the contract's cost-of-contract-value. | | `deferredRevenue` | string (decimal) or null | no | The deferred revenue not yet recognized, in the contract currency. Zero once the contract is closed. | | `revenueRecognized` | string (decimal) or null | no | The cumulative revenue recognized to date, in the contract currency. | | `revenueRemaining` | string (decimal) or null | no | The revenue remaining to be recognized, in the contract currency. | | `lastModifiedDate` | string (date-time) | no | The date and time the revenue contract last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Get Revenue Contract `GET /accounting/revenue-contracts/{id}` Group: Accounting › Revenue Contracts · Operation ID: `getRevenueContract` Returns the revenue contract with the given identifier, including its revenue lines, each line's revenue plan and that plan's period rows. Use the contract's id, not its revenueContractNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getRevenueContract) ## 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`. | | `id` | path | string | yes | The unique identifier for the revenue contract to return. | ## Response `200` Success Under `data` (getRevenueContractOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the revenue contract. | | `revenueContractNumber` | string | no | The revenue contract number, unique within the tenant. | | `approvalStatus` | string | yes | The approval status of the contract. Possible values: 'draft', 'pendingApproval', 'revise', 'approved', 'rejected'. | | `generalStatus` | string | no | The overall status of the contract, combining approval and any active revenue hold. Possible values: 'approved', 'approvedOnHold', 'pendingApproval', 'pendingApprovalOnHold'. | | `revenueStatus` | string | no | The revenue-recognition progress across the contract's lines. Possible values: 'notStarted', 'notStartedOnHold', 'inProgress', 'inProgressOnHold', 'complete', 'completeModified'. | | `isContractClosed` | boolean | yes | Whether the contract has been closed, with its open revenue moved to a new combined revenue contract. | | `isContractCombined` | boolean | yes | Whether the contract is the combination target that absorbed a closed contract's open revenue. | | `isLongTermContract` | boolean | no | Whether the recognition window from revenueStartDate to revenueEndDate exceeds 366 days. | | `sspCompliant` | boolean | no | Whether every line not excluded from allocation is standalone-selling-price compliant. Absent when every line is excluded from allocation. | | `currency` | string | yes | The currency the contract is denominated in. Every amount on the contract is in this currency. Format ISO 4217, for example 'USD'. | | `functionalCurrency` | string | no | The owning entity's functional currency. Present only when it differs from the contract currency. Format ISO 4217, for example 'USD'. | | `exchangeRate` | string (decimal) | yes | The rate used to convert the contract currency to the entity's functional currency. A ratio, not an amount, with no currency of its own. | | `entityId` | string | no | The legal entity that owns this contract. See id on GET base/entities. | | `entityName` | string | no | The name of the legal entity that owns this contract. | | `customerId` | string | no | The customer this contract is with. See id on GET accounting/customers/:id. | | `customerName` | string | no | The display name of the customer this contract is with. | | `salesOrderId` | string | no | The sales order this contract was created from. See id on GET accounting/sales-orders/:id. | | `salesOrderNumber` | string | no | The number of the sales order this contract was created from. | | `modifiedRevenueContractId` | string | no | The revenue contract this one was combined or modified into. Absent when there is no such successor. See id on GET accounting/revenue-contracts/:id. | | `closeContractDate` | string (date-time) | no | The date and time the contract was closed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Absent on an open contract. | | `migrationDate` | string (date) | no | The date this contract's opening balances were migrated from a prior system. Format YYYY-MM-DD. Absent on a contract created natively. | | `revenueStartDate` | string (date) | no | The earliest revenue-recognition start date across the contract's lines. Format YYYY-MM-DD. | | `revenueEndDate` | string (date) | no | The latest revenue-recognition end date across the contract's lines. Format YYYY-MM-DD. | | `salesEffectiveDate` | string (date) | no | The book date of the sales order this contract was created from. Format YYYY-MM-DD. | | `totalContractValue` | string (decimal) | no | The total contract value, in the contract currency. It is the total price of the sales order this contract was created from, less the contract's cost-of-contract-value. | | `totalContractAmount` | string (decimal) | no | The total contract amount across the contract's open lines, in the contract currency. | | `totalCostOfContractValue` | string (decimal) | no | The total cost-of-contract-value excluded from the revenue calculation, in the contract currency. | | `totalCostOfRevenue` | string (decimal) | no | The total cost of revenue recognized, in the contract currency. | | `totalEstimatedTransactionPrice` | string (decimal) | no | The total estimated transaction price used in allocation, in the contract currency. | | `totalFairValueAmount` | string (decimal) | no | The total fair value used in allocation, in the contract currency. | | `totalVariableConsideration` | string (decimal) | no | The total variable consideration across the contract's lines, in the contract currency. | | `deferredRevenue` | string (decimal) | no | The deferred revenue not yet recognized, in the contract currency. Zero once the contract is closed. | | `revenueAmount` | string (decimal) | no | The total revenue across the contract's lines, in the contract currency. | | `revenueRecognized` | string (decimal) | no | The cumulative revenue recognized to date, in the contract currency. | | `revenueRemaining` | string (decimal) | no | The revenue remaining to be recognized, in the contract currency. | | `remainingPerformanceObligation` | string (decimal) | no | The remaining performance obligation, in the contract currency. It is contract value plus returns and variable consideration, less committed recognized revenue. Zero once the contract is closed. | | `unbilledReceivables` | string (decimal) | no | The sum of unbilled journal entries for this contract, in the contract currency. | | `returnedAmount` | string (decimal) | no | The amount returned against the contract's revenue plan, in the contract currency. | | `oneTimeDiscountAmount` | string (decimal) | no | The one-time discount allocated across the contract's lines, in the contract currency. | | `pctRevenueRecognized` | string (decimal) | no | The fraction of total contract value recognized to date, from 0 to 1. 1 once the contract is closed. A ratio, not an amount, with no currency of its own. | | `externalIdentifiers` | array of object | no | The contract's identifiers in other systems, one per configured field name. An empty array or absent when the contract has none. | | `externalIdentifiers[].fieldName` | string | yes | The configured field name of the identifier, for example 'netsuiteRevenueContractId'. | | `externalIdentifiers[].value` | string | yes | The identifier in the other system. | | `externalIdentifiers[].externalUrl` | string | no | A link to the record in the other system. Absent when none is stored. | | `lines` | array of object | no | The contract's revenue lines, in line order, each with its revenue plan nested. An empty array when the contract has no lines. | | `lines[].id` | string | yes | The unique identifier for the revenue contract line. | | `lines[].revenueContractLineNumber` | string | no | The revenue contract line number, unique within the contract. | | `lines[].type` | string | yes | The kind of line. Possible values: 'standard', 'thresholdIncrement', 'overage'. | | `lines[].currency` | string | yes | The currency the line is denominated in. Every amount on the line and its revenue plan is in this currency. Format ISO 4217, for example 'USD'. | | `lines[].functionalCurrency` | string | no | The owning entity's functional currency. Present only when it differs from the line currency. Format ISO 4217, for example 'USD'. | | `lines[].exchangeRate` | string (decimal) | no | The rate used to convert the line currency to the functional currency. A ratio, not an amount, with no currency of its own. | | `lines[].generalStatus` | string | no | The overall status of the line, combining approval and any active revenue hold. Possible values: 'notStarted', 'notStartedOnHold', 'inProgress', 'inProgressOnHold', 'complete', 'completeModified'. | | `lines[].revenueStatus` | string | no | The revenue-recognition progress for the line. Possible values: 'notStarted', 'notStartedOnHold', 'inProgress', 'inProgressOnHold', 'complete', 'completeModified'. | | `lines[].isClosed` | boolean | yes | Whether the line has been closed, with its open revenue moved to a combined contract. | | `lines[].isLongTerm` | boolean | no | Whether the line's recognition window exceeds 366 days. | | `lines[].isEvergreen` | boolean | no | Whether the line has no fixed end date and recognizes indefinitely. | | `lines[].sspCompliant` | boolean | no | Whether the line is standalone-selling-price compliant. | | `lines[].isAllocationExcluded` | boolean | no | Whether the line is excluded from the contract's revenue allocation. | | `lines[].productCode` | string | no | The code of the product the line covers, as it stood when the line was created. | | `lines[].unitOfMeasure` | string | no | The unit the line's quantity is measured in. Possible values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `lines[].performanceObligationName` | string | no | The name of the performance obligation the line satisfies. | | `lines[].operationalPerformanceObligationNumber` | string | no | The number of the operational performance obligation the line is tracked under. | | `lines[].revenueSchedulingTemplateName` | string | no | The name of the revenue scheduling template that generated the line's plan. | | `lines[].revenueRecognitionPattern` | string | no | How revenue is recognized over the line's term. Possible values: 'upfront', 'overTime', 'asDelivered', 'asConsumed'. | | `lines[].amortizationMethod` | string | no | How the line's amount is spread across periods. Possible values: 'exactDays', 'pointInTime', 'straightLineProRateFirstLast', 'straightLine', 'usageBased'. | | `lines[].revenueSchedulingEvent` | string | yes | The event that starts recognition on the line. Possible values: 'revenueArrangementCreation', 'uponInvoicing', 'uponUsageConsumption', 'manualRelease', 'uponInvoicingPercentInvoiced', 'uponInvoicingFullAmount', 'serviceActivation'. | | `lines[].revenueAccount` | string | no | The name of the revenue account the line recognizes into. | | `lines[].deferredRevenueAccount` | string | no | The name of the deferred-revenue account the line defers into. | | `lines[].revRecStartDate` | string (date) | no | The first day of the line's revenue-recognition window. Format YYYY-MM-DD. | | `lines[].revRecEndDate` | string (date) | no | The last day of the line's revenue-recognition window, inclusive. Format YYYY-MM-DD. | | `lines[].salesOrderBookingDate` | string (date) | no | The book date of the sales order line this line was created from. Format YYYY-MM-DD. | | `lines[].revenueTerm` | string (decimal) | no | The length of the line's revenue term, in the line's scheduling unit. A count, not an amount. | | `lines[].salesOrderProductLine` | string | no | The number of the sales order product line this line was created from. | | `lines[].contractAmount` | string (decimal) | no | The contract amount booked on the line, in the line currency. | | `lines[].adjustedContractAmount` | string (decimal) | no | The contract amount after modifications, in the line currency. | | `lines[].listPrice` | string (decimal) | no | The undiscounted list price of the line, in the line currency. | | `lines[].totalContractValue` | string (decimal) | no | The total contract value attributed to the line, in the line currency. | | `lines[].totalCostOfContractValue` | string (decimal) | no | The cost-of-contract-value excluded from the line's revenue calculation, in the line currency. | | `lines[].costOfRevenueAmount` | string (decimal) | no | The cost of revenue booked on the line, in the line currency. | | `lines[].costOfRevenueRecognized` | string (decimal) | no | The cost of revenue recognized to date on the line, in the line currency. | | `lines[].estimatedTransactionPrice` | string (decimal) | no | The estimated transaction price used for the line in allocation, in the line currency. | | `lines[].fairValueAmount` | string (decimal) | no | The fair value used for the line in allocation, in the line currency. | | `lines[].variableConsideration` | string (decimal) | no | The variable consideration on the line, in the line currency. | | `lines[].extendedSSP` | string (decimal) | no | The extended standalone selling price of the line, unit standalone selling price times quantity, in the line currency. | | `lines[].revenueAmount` | string (decimal) | no | The revenue allocated to the line, in the line currency. | | `lines[].revenueRecognized` | string (decimal) | no | The revenue recognized to date on the line, in the line currency. | | `lines[].revenueRemaining` | string (decimal) | no | The revenue still to be recognized on the line, in the line currency. | | `lines[].deferredRevenueAmount` | string (decimal) | no | The deferred revenue outstanding on the line, in the line currency. | | `lines[].remainingPerformanceObligation` | string (decimal) | no | The remaining performance obligation on the line, in the line currency. | | `lines[].unbilledReceivables` | string (decimal) | no | The unbilled receivables on the line, in the line currency. | | `lines[].unscheduledRevenue` | string (decimal) | no | The revenue on the line not yet scheduled into a plan period, in the line currency. | | `lines[].totalInvoicedAmount` | string (decimal) | no | The total invoiced against the line to date, in the line currency. | | `lines[].oneTimeDiscountAllocationAmount` | string (decimal) | no | The one-time discount allocated to the line, in the line currency. | | `lines[].fvAllocationRatio` | string (decimal) | no | The share of the contract's fair value allocated to the line, from 0 to 1. A ratio, not an amount, with no currency of its own. | | `lines[].revenuePlanHeader` | object | no | The line's revenue plan, the schedule revenue is recognized against. At most one per line. Absent when no plan has been generated for the line yet. | | `lines[].revenuePlanHeader.id` | string | yes | The unique identifier for the revenue plan header. | | `lines[].revenuePlanHeader.revenuePlanHeaderNumber` | string | no | The revenue plan header number. | | `lines[].revenuePlanHeader.status` | string | no | The status of the plan. Possible values: 'notStarted', 'pendingRecognition', 'inProgress', 'complete', 'completeModified'. | | `lines[].revenuePlanHeader.currency` | string | yes | The currency the plan is denominated in. Every amount on the plan and its plan lines is in this currency. Format ISO 4217, for example 'USD'. | | `lines[].revenuePlanHeader.amortizationMethod` | string | no | How the plan spreads its amount across periods. Possible values: 'exactDays', 'pointInTime', 'straightLineProRateFirstLast', 'straightLine', 'usageBased'. | | `lines[].revenuePlanHeader.revenueSchedulingEvent` | string | yes | The event that starts recognition on the plan. Possible values: 'revenueArrangementCreation', 'uponInvoicing', 'uponUsageConsumption', 'manualRelease', 'uponInvoicingPercentInvoiced', 'uponInvoicingFullAmount', 'serviceActivation'. | | `lines[].revenuePlanHeader.isContractClosed` | boolean | yes | Whether the contract behind the plan has been closed. | | `lines[].revenuePlanHeader.revRecStartDate` | string (date) | no | The first day of the plan's recognition window. Format YYYY-MM-DD. | | `lines[].revenuePlanHeader.revRecEndDate` | string (date) | no | The last day of the plan's recognition window, inclusive. Format YYYY-MM-DD. | | `lines[].revenuePlanHeader.comments` | string | no | Free-form comments recorded on the plan. | | `lines[].revenuePlanHeader.amount` | string (decimal) | no | The total amount the plan schedules, in the plan currency. | | `lines[].revenuePlanHeader.totalRecognized` | string (decimal) | no | The total recognized across the plan's lines to date, in the plan currency. | | `lines[].revenuePlanHeader.remainingDeferred` | string (decimal) | no | The amount still deferred on the plan, in the plan currency. | | `lines[].revenuePlanHeader.returnedAmount` | string (decimal) | no | The amount returned against the plan, in the plan currency. | | `lines[].revenuePlanHeader.unscheduledRevenue` | string (decimal) | no | The plan amount not yet scheduled into a period, in the plan currency. | | `lines[].revenuePlanHeader.revenuePlanLines` | array of object | no | The plan's period rows, the recognition schedule, one entry per planned period, earliest planned period first. An empty array when the plan has not been scheduled yet. | | `lines[].revenuePlanHeader.revenuePlanLines[].id` | string | yes | The unique identifier for the revenue plan line. | | `lines[].revenuePlanHeader.revenuePlanLines[].lineId` | string | no | The line identifier within the plan. | | `lines[].revenuePlanHeader.revenuePlanLines[].revenuePlanType` | string | yes | What the plan line schedules. Possible values: 'revenueRecognition', 'returnRevenueRecognition', 'unCommittedRevenueRecognition', 'modificationDeferredRevenueAdjustment', 'allowance', 'overage', 'usageExpiration', 'costOfRevenue', 'unholdRevenuePeriod', 'retrospectiveAdjustment'. | | `lines[].revenuePlanHeader.revenuePlanLines[].currency` | string | yes | The currency the plan line is denominated in. Format ISO 4217, for example 'USD'. | | `lines[].revenuePlanHeader.revenuePlanLines[].plannedPeriod` | string | no | The name of the accounting period this row is planned for. | | `lines[].revenuePlanHeader.revenuePlanLines[].plannedPeriodDate` | string (date) | no | The start date of the planned accounting period. Format YYYY-MM-DD. Plan lines are returned in ascending order of this date. | | `lines[].revenuePlanHeader.revenuePlanLines[].postingPeriod` | string | no | The name of the accounting period the row was posted in, when it differs from the planned one. | | `lines[].revenuePlanHeader.revenuePlanLines[].isPosted` | boolean | no | Whether the row has been posted to the general ledger. | | `lines[].revenuePlanHeader.revenuePlanLines[].holdStatus` | string | no | Whether revenue on the row is held back. Possible values: 'notHold', 'holdPlaced', 'holdRemoved'. | | `lines[].revenuePlanHeader.revenuePlanLines[].releaseUnit` | string | no | The unit the release amount is expressed in. Possible values: 'quantity', 'percent', 'amount'. | | `lines[].revenuePlanHeader.revenuePlanLines[].releaseAmount` | string (decimal) | no | The amount or quantity released in this period, in the unit given by releaseUnit. | | `lines[].revenuePlanHeader.revenuePlanLines[].pctRecognizedInPeriod` | string (decimal) | no | The fraction of the plan recognized in this period, from 0 to 1. A ratio, not an amount, with no currency of its own. | | `lines[].revenuePlanHeader.revenuePlanLines[].cumulativePercentage` | string (decimal) | no | The fraction of the plan recognized cumulatively through this period, from 0 to 1. A ratio, not an amount, with no currency of its own. | | `lines[].revenuePlanHeader.revenuePlanLines[].revenueAccount` | string | no | The name of the revenue account this row recognizes into. | | `lines[].revenuePlanHeader.revenuePlanLines[].deferredRevenueAccount` | string | no | The name of the deferred-revenue account this row releases from. | | `lines[].revenuePlanHeader.revenuePlanLines[].journalEntry` | string | no | The number of the journal entry this row posted through, once posted. | | `lines[].revenuePlanHeader.revenuePlanLines[].amount` | string (decimal) | no | The amount scheduled for this period, in the plan line currency. | | `lines[].revenuePlanHeader.revenuePlanLines[].revenueAmount` | string (decimal) | no | The revenue scheduled for this period, in the plan line currency. | | `lines[].revenuePlanHeader.revenuePlanLines[].revenueRecognized` | string (decimal) | no | The revenue recognized in this period, in the plan line currency. | | `lines[].revenuePlanHeader.revenuePlanLines[].cumulativeRecognition` | string (decimal) | no | The revenue recognized cumulatively through this period, in the plan line currency. | | `lines[].revenuePlanHeader.revenuePlanLines[].unscheduledRevenue` | string (decimal) | no | The amount on this row not yet scheduled, in the plan line currency. | | `lines[].revenuePlanHeader.revenuePlanLines[].postedAmount` | string (decimal) | no | The amount actually posted to the ledger for this row, in the plan line currency. | | `lastModifiedDate` | string (date-time) | yes | The date and time the revenue contract last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Search Revenue Contract `POST /accounting/revenue-contracts/search` Group: Accounting › Revenue Contracts · Operation ID: `searchRevenueContract` Returns the revenue contracts that match the given filters. Each match is the full contract, including its revenue lines, revenue plans and period rows. For a cheaper header-only list, use GET accounting/revenue-contracts. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchRevenueContract) ## 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`. | ## Request body Required, `application/json`: searchRevenueContractInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A revenue contract must match every filter you send. Omit to return all revenue contracts. | | `filters.id` | string | no | Limits results to the revenue contract with this id. | | `filters.revenueContractNumber` | string | no | Limits results to the revenue contract with this number. The match is exact. | | `filters.approvalStatus` | string | no | Limits results to contracts in this approval status. Possible values: 'draft', 'pendingApproval', 'revise', 'approved', 'rejected'. Any other value matches no contract. | | `filters.generalStatus` | string | no | Limits results to contracts in this general status. Possible values: 'approved', 'approvedOnHold', 'pendingApproval', 'pendingApprovalOnHold'. Any other value matches no contract. | | `filters.revenueStatus` | string | no | Limits results to contracts in this revenue-recognition status. Possible values: 'notStarted', 'notStartedOnHold', 'inProgress', 'inProgressOnHold', 'complete', 'completeModified'. Any other value matches no contract. | | `filters.currency` | string | no | Limits results to contracts in this currency. Format ISO 4217, for example 'USD'. | | `filters.isContractClosed` | boolean | no | Limits results to closed contracts when true, or open contracts when false. | | `filters.isContractCombined` | boolean | no | Limits results to combination-target contracts when true, or ordinary contracts when false. | | `filters.customerId` | string | no | Limits results to contracts with this customer. See id on GET accounting/customers/:id. An id that matches no customer returns no contracts. | | `filters.entityId` | string | no | Limits results to contracts owned by this legal entity. See id on GET base/entities. An id that matches no entity returns no contracts. | | `filters.salesOrderId` | string | no | Limits results to contracts created from this sales order. See id on GET accounting/sales-orders/:id. An id that matches no sales order returns no contracts. | | `filters.externalIdentifier` | object | no | Limits results to the contract with this external identifier. Give both fieldName and value. A fieldName alone would match every system's value for that key. | | `filters.externalIdentifier.fieldName` | string | yes | The configured field name of the identifier, for example 'netsuiteRevenueContractId'. | | `filters.externalIdentifier.value` | string | yes | The identifier value to match, exactly as the other system stores it. | | `filters.revenueStartDate` | object | no | Limits results to contracts whose revenueStartDate falls in this range. Omit either bound to leave that side open. Both bounds are inclusive. | | `filters.revenueStartDate.start` | string (date) | no | The earliest revenueStartDate to include, inclusive. Format YYYY-MM-DD. | | `filters.revenueStartDate.end` | string (date) | no | The latest revenueStartDate to include, inclusive. Format YYYY-MM-DD. | | `filters.revenueEndDate` | object | no | Limits results to contracts whose revenueEndDate falls in this range. Omit either bound to leave that side open. Both bounds are inclusive. | | `filters.revenueEndDate.start` | string (date) | no | The earliest revenueEndDate to include, inclusive. Format YYYY-MM-DD. | | `filters.revenueEndDate.end` | string (date) | no | The latest revenueEndDate to include, inclusive. Format YYYY-MM-DD. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of revenue contracts per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "revenueContractNumber": "", "approvalStatus": "", "generalStatus": "", "revenueStatus": "", "currency": "", "isContractClosed": true, "isContractCombined": true, "customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "salesOrderId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "externalIdentifier": { "fieldName": "", "value": "" }, "revenueStartDate": { "start": "2026-01-15", "end": "2026-01-15" }, "revenueEndDate": { "start": "2026-01-15", "end": "2026-01-15" } }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchRevenueContractOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The revenue contracts on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the revenue contract. | | `items[].revenueContractNumber` | string | no | The revenue contract number, unique within the tenant. | | `items[].approvalStatus` | string | yes | The approval status of the contract. Possible values: 'draft', 'pendingApproval', 'revise', 'approved', 'rejected'. | | `items[].generalStatus` | string | no | The overall status of the contract, combining approval and any active revenue hold. Possible values: 'approved', 'approvedOnHold', 'pendingApproval', 'pendingApprovalOnHold'. | | `items[].revenueStatus` | string | no | The revenue-recognition progress across the contract's lines. Possible values: 'notStarted', 'notStartedOnHold', 'inProgress', 'inProgressOnHold', 'complete', 'completeModified'. | | `items[].isContractClosed` | boolean | yes | Whether the contract has been closed, with its open revenue moved to a new combined revenue contract. | | `items[].isContractCombined` | boolean | yes | Whether the contract is the combination target that absorbed a closed contract's open revenue. | | `items[].isLongTermContract` | boolean | no | Whether the recognition window from revenueStartDate to revenueEndDate exceeds 366 days. | | `items[].sspCompliant` | boolean | no | Whether every line not excluded from allocation is standalone-selling-price compliant. Absent when every line is excluded from allocation. | | `items[].currency` | string | yes | The currency the contract is denominated in. Every amount on the contract is in this currency. Format ISO 4217, for example 'USD'. | | `items[].functionalCurrency` | string | no | The owning entity's functional currency. Present only when it differs from the contract currency. Format ISO 4217, for example 'USD'. | | `items[].exchangeRate` | string (decimal) | yes | The rate used to convert the contract currency to the entity's functional currency. A ratio, not an amount, with no currency of its own. | | `items[].entityId` | string | no | The legal entity that owns this contract. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the legal entity that owns this contract. | | `items[].customerId` | string | no | The customer this contract is with. See id on GET accounting/customers/:id. | | `items[].customerName` | string | no | The display name of the customer this contract is with. | | `items[].salesOrderId` | string | no | The sales order this contract was created from. See id on GET accounting/sales-orders/:id. | | `items[].salesOrderNumber` | string | no | The number of the sales order this contract was created from. | | `items[].modifiedRevenueContractId` | string | no | The revenue contract this one was combined or modified into. Absent when there is no such successor. See id on GET accounting/revenue-contracts/:id. | | `items[].closeContractDate` | string (date-time) | no | The date and time the contract was closed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Absent on an open contract. | | `items[].migrationDate` | string (date) | no | The date this contract's opening balances were migrated from a prior system. Format YYYY-MM-DD. Absent on a contract created natively. | | `items[].revenueStartDate` | string (date) | no | The earliest revenue-recognition start date across the contract's lines. Format YYYY-MM-DD. | | `items[].revenueEndDate` | string (date) | no | The latest revenue-recognition end date across the contract's lines. Format YYYY-MM-DD. | | `items[].salesEffectiveDate` | string (date) | no | The book date of the sales order this contract was created from. Format YYYY-MM-DD. | | `items[].totalContractValue` | string (decimal) | no | The total contract value, in the contract currency. It is the total price of the sales order this contract was created from, less the contract's cost-of-contract-value. | | `items[].totalContractAmount` | string (decimal) | no | The total contract amount across the contract's open lines, in the contract currency. | | `items[].totalCostOfContractValue` | string (decimal) | no | The total cost-of-contract-value excluded from the revenue calculation, in the contract currency. | | `items[].totalCostOfRevenue` | string (decimal) | no | The total cost of revenue recognized, in the contract currency. | | `items[].totalEstimatedTransactionPrice` | string (decimal) | no | The total estimated transaction price used in allocation, in the contract currency. | | `items[].totalFairValueAmount` | string (decimal) | no | The total fair value used in allocation, in the contract currency. | | `items[].totalVariableConsideration` | string (decimal) | no | The total variable consideration across the contract's lines, in the contract currency. | | `items[].deferredRevenue` | string (decimal) | no | The deferred revenue not yet recognized, in the contract currency. Zero once the contract is closed. | | `items[].revenueAmount` | string (decimal) | no | The total revenue across the contract's lines, in the contract currency. | | `items[].revenueRecognized` | string (decimal) | no | The cumulative revenue recognized to date, in the contract currency. | | `items[].revenueRemaining` | string (decimal) | no | The revenue remaining to be recognized, in the contract currency. | | `items[].remainingPerformanceObligation` | string (decimal) | no | The remaining performance obligation, in the contract currency. It is contract value plus returns and variable consideration, less committed recognized revenue. Zero once the contract is closed. | | `items[].unbilledReceivables` | string (decimal) | no | The sum of unbilled journal entries for this contract, in the contract currency. | | `items[].returnedAmount` | string (decimal) | no | The amount returned against the contract's revenue plan, in the contract currency. | | `items[].oneTimeDiscountAmount` | string (decimal) | no | The one-time discount allocated across the contract's lines, in the contract currency. | | `items[].pctRevenueRecognized` | string (decimal) | no | The fraction of total contract value recognized to date, from 0 to 1. 1 once the contract is closed. A ratio, not an amount, with no currency of its own. | | `items[].externalIdentifiers` | array of object | no | The contract's identifiers in other systems, one per configured field name. An empty array or absent when the contract has none. | | `items[].externalIdentifiers[].fieldName` | string | yes | The configured field name of the identifier, for example 'netsuiteRevenueContractId'. | | `items[].externalIdentifiers[].value` | string | yes | The identifier in the other system. | | `items[].externalIdentifiers[].externalUrl` | string | no | A link to the record in the other system. Absent when none is stored. | | `items[].lines` | array of object | no | The contract's revenue lines, in line order, each with its revenue plan nested. An empty array when the contract has no lines. | | `items[].lines[].id` | string | yes | The unique identifier for the revenue contract line. | | `items[].lines[].revenueContractLineNumber` | string | no | The revenue contract line number, unique within the contract. | | `items[].lines[].type` | string | yes | The kind of line. Possible values: 'standard', 'thresholdIncrement', 'overage'. | | `items[].lines[].currency` | string | yes | The currency the line is denominated in. Every amount on the line and its revenue plan is in this currency. Format ISO 4217, for example 'USD'. | | `items[].lines[].functionalCurrency` | string | no | The owning entity's functional currency. Present only when it differs from the line currency. Format ISO 4217, for example 'USD'. | | `items[].lines[].exchangeRate` | string (decimal) | no | The rate used to convert the line currency to the functional currency. A ratio, not an amount, with no currency of its own. | | `items[].lines[].generalStatus` | string | no | The overall status of the line, combining approval and any active revenue hold. Possible values: 'notStarted', 'notStartedOnHold', 'inProgress', 'inProgressOnHold', 'complete', 'completeModified'. | | `items[].lines[].revenueStatus` | string | no | The revenue-recognition progress for the line. Possible values: 'notStarted', 'notStartedOnHold', 'inProgress', 'inProgressOnHold', 'complete', 'completeModified'. | | `items[].lines[].isClosed` | boolean | yes | Whether the line has been closed, with its open revenue moved to a combined contract. | | `items[].lines[].isLongTerm` | boolean | no | Whether the line's recognition window exceeds 366 days. | | `items[].lines[].isEvergreen` | boolean | no | Whether the line has no fixed end date and recognizes indefinitely. | | `items[].lines[].sspCompliant` | boolean | no | Whether the line is standalone-selling-price compliant. | | `items[].lines[].isAllocationExcluded` | boolean | no | Whether the line is excluded from the contract's revenue allocation. | | `items[].lines[].productCode` | string | no | The code of the product the line covers, as it stood when the line was created. | | `items[].lines[].unitOfMeasure` | string | no | The unit the line's quantity is measured in. Possible values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `items[].lines[].performanceObligationName` | string | no | The name of the performance obligation the line satisfies. | | `items[].lines[].operationalPerformanceObligationNumber` | string | no | The number of the operational performance obligation the line is tracked under. | | `items[].lines[].revenueSchedulingTemplateName` | string | no | The name of the revenue scheduling template that generated the line's plan. | | `items[].lines[].revenueRecognitionPattern` | string | no | How revenue is recognized over the line's term. Possible values: 'upfront', 'overTime', 'asDelivered', 'asConsumed'. | | `items[].lines[].amortizationMethod` | string | no | How the line's amount is spread across periods. Possible values: 'exactDays', 'pointInTime', 'straightLineProRateFirstLast', 'straightLine', 'usageBased'. | | `items[].lines[].revenueSchedulingEvent` | string | yes | The event that starts recognition on the line. Possible values: 'revenueArrangementCreation', 'uponInvoicing', 'uponUsageConsumption', 'manualRelease', 'uponInvoicingPercentInvoiced', 'uponInvoicingFullAmount', 'serviceActivation'. | | `items[].lines[].revenueAccount` | string | no | The name of the revenue account the line recognizes into. | | `items[].lines[].deferredRevenueAccount` | string | no | The name of the deferred-revenue account the line defers into. | | `items[].lines[].revRecStartDate` | string (date) | no | The first day of the line's revenue-recognition window. Format YYYY-MM-DD. | | `items[].lines[].revRecEndDate` | string (date) | no | The last day of the line's revenue-recognition window, inclusive. Format YYYY-MM-DD. | | `items[].lines[].salesOrderBookingDate` | string (date) | no | The book date of the sales order line this line was created from. Format YYYY-MM-DD. | | `items[].lines[].revenueTerm` | string (decimal) | no | The length of the line's revenue term, in the line's scheduling unit. A count, not an amount. | | `items[].lines[].salesOrderProductLine` | string | no | The number of the sales order product line this line was created from. | | `items[].lines[].contractAmount` | string (decimal) | no | The contract amount booked on the line, in the line currency. | | `items[].lines[].adjustedContractAmount` | string (decimal) | no | The contract amount after modifications, in the line currency. | | `items[].lines[].listPrice` | string (decimal) | no | The undiscounted list price of the line, in the line currency. | | `items[].lines[].totalContractValue` | string (decimal) | no | The total contract value attributed to the line, in the line currency. | | `items[].lines[].totalCostOfContractValue` | string (decimal) | no | The cost-of-contract-value excluded from the line's revenue calculation, in the line currency. | | `items[].lines[].costOfRevenueAmount` | string (decimal) | no | The cost of revenue booked on the line, in the line currency. | | `items[].lines[].costOfRevenueRecognized` | string (decimal) | no | The cost of revenue recognized to date on the line, in the line currency. | | `items[].lines[].estimatedTransactionPrice` | string (decimal) | no | The estimated transaction price used for the line in allocation, in the line currency. | | `items[].lines[].fairValueAmount` | string (decimal) | no | The fair value used for the line in allocation, in the line currency. | | `items[].lines[].variableConsideration` | string (decimal) | no | The variable consideration on the line, in the line currency. | | `items[].lines[].extendedSSP` | string (decimal) | no | The extended standalone selling price of the line, unit standalone selling price times quantity, in the line currency. | | `items[].lines[].revenueAmount` | string (decimal) | no | The revenue allocated to the line, in the line currency. | | `items[].lines[].revenueRecognized` | string (decimal) | no | The revenue recognized to date on the line, in the line currency. | | `items[].lines[].revenueRemaining` | string (decimal) | no | The revenue still to be recognized on the line, in the line currency. | | `items[].lines[].deferredRevenueAmount` | string (decimal) | no | The deferred revenue outstanding on the line, in the line currency. | | `items[].lines[].remainingPerformanceObligation` | string (decimal) | no | The remaining performance obligation on the line, in the line currency. | | `items[].lines[].unbilledReceivables` | string (decimal) | no | The unbilled receivables on the line, in the line currency. | | `items[].lines[].unscheduledRevenue` | string (decimal) | no | The revenue on the line not yet scheduled into a plan period, in the line currency. | | `items[].lines[].totalInvoicedAmount` | string (decimal) | no | The total invoiced against the line to date, in the line currency. | | `items[].lines[].oneTimeDiscountAllocationAmount` | string (decimal) | no | The one-time discount allocated to the line, in the line currency. | | `items[].lines[].fvAllocationRatio` | string (decimal) | no | The share of the contract's fair value allocated to the line, from 0 to 1. A ratio, not an amount, with no currency of its own. | | `items[].lines[].revenuePlanHeader` | object | no | The line's revenue plan, the schedule revenue is recognized against. At most one per line. Absent when no plan has been generated for the line yet. | | `items[].lines[].revenuePlanHeader.id` | string | yes | The unique identifier for the revenue plan header. | | `items[].lines[].revenuePlanHeader.revenuePlanHeaderNumber` | string | no | The revenue plan header number. | | `items[].lines[].revenuePlanHeader.status` | string | no | The status of the plan. Possible values: 'notStarted', 'pendingRecognition', 'inProgress', 'complete', 'completeModified'. | | `items[].lines[].revenuePlanHeader.currency` | string | yes | The currency the plan is denominated in. Every amount on the plan and its plan lines is in this currency. Format ISO 4217, for example 'USD'. | | `items[].lines[].revenuePlanHeader.amortizationMethod` | string | no | How the plan spreads its amount across periods. Possible values: 'exactDays', 'pointInTime', 'straightLineProRateFirstLast', 'straightLine', 'usageBased'. | | `items[].lines[].revenuePlanHeader.revenueSchedulingEvent` | string | yes | The event that starts recognition on the plan. Possible values: 'revenueArrangementCreation', 'uponInvoicing', 'uponUsageConsumption', 'manualRelease', 'uponInvoicingPercentInvoiced', 'uponInvoicingFullAmount', 'serviceActivation'. | | `items[].lines[].revenuePlanHeader.isContractClosed` | boolean | yes | Whether the contract behind the plan has been closed. | | `items[].lines[].revenuePlanHeader.revRecStartDate` | string (date) | no | The first day of the plan's recognition window. Format YYYY-MM-DD. | | `items[].lines[].revenuePlanHeader.revRecEndDate` | string (date) | no | The last day of the plan's recognition window, inclusive. Format YYYY-MM-DD. | | `items[].lines[].revenuePlanHeader.comments` | string | no | Free-form comments recorded on the plan. | | `items[].lines[].revenuePlanHeader.amount` | string (decimal) | no | The total amount the plan schedules, in the plan currency. | | `items[].lines[].revenuePlanHeader.totalRecognized` | string (decimal) | no | The total recognized across the plan's lines to date, in the plan currency. | | `items[].lines[].revenuePlanHeader.remainingDeferred` | string (decimal) | no | The amount still deferred on the plan, in the plan currency. | | `items[].lines[].revenuePlanHeader.returnedAmount` | string (decimal) | no | The amount returned against the plan, in the plan currency. | | `items[].lines[].revenuePlanHeader.unscheduledRevenue` | string (decimal) | no | The plan amount not yet scheduled into a period, in the plan currency. | | `items[].lines[].revenuePlanHeader.revenuePlanLines` | array of object | no | The plan's period rows, the recognition schedule, one entry per planned period, earliest planned period first. An empty array when the plan has not been scheduled yet. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].id` | string | yes | The unique identifier for the revenue plan line. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].lineId` | string | no | The line identifier within the plan. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].revenuePlanType` | string | yes | What the plan line schedules. Possible values: 'revenueRecognition', 'returnRevenueRecognition', 'unCommittedRevenueRecognition', 'modificationDeferredRevenueAdjustment', 'allowance', 'overage', 'usageExpiration', 'costOfRevenue', 'unholdRevenuePeriod', 'retrospectiveAdjustment'. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].currency` | string | yes | The currency the plan line is denominated in. Format ISO 4217, for example 'USD'. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].plannedPeriod` | string | no | The name of the accounting period this row is planned for. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].plannedPeriodDate` | string (date) | no | The start date of the planned accounting period. Format YYYY-MM-DD. Plan lines are returned in ascending order of this date. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].postingPeriod` | string | no | The name of the accounting period the row was posted in, when it differs from the planned one. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].isPosted` | boolean | no | Whether the row has been posted to the general ledger. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].holdStatus` | string | no | Whether revenue on the row is held back. Possible values: 'notHold', 'holdPlaced', 'holdRemoved'. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].releaseUnit` | string | no | The unit the release amount is expressed in. Possible values: 'quantity', 'percent', 'amount'. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].releaseAmount` | string (decimal) | no | The amount or quantity released in this period, in the unit given by releaseUnit. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].pctRecognizedInPeriod` | string (decimal) | no | The fraction of the plan recognized in this period, from 0 to 1. A ratio, not an amount, with no currency of its own. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].cumulativePercentage` | string (decimal) | no | The fraction of the plan recognized cumulatively through this period, from 0 to 1. A ratio, not an amount, with no currency of its own. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].revenueAccount` | string | no | The name of the revenue account this row recognizes into. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].deferredRevenueAccount` | string | no | The name of the deferred-revenue account this row releases from. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].journalEntry` | string | no | The number of the journal entry this row posted through, once posted. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].amount` | string (decimal) | no | The amount scheduled for this period, in the plan line currency. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].revenueAmount` | string (decimal) | no | The revenue scheduled for this period, in the plan line currency. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].revenueRecognized` | string (decimal) | no | The revenue recognized in this period, in the plan line currency. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].cumulativeRecognition` | string (decimal) | no | The revenue recognized cumulatively through this period, in the plan line currency. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].unscheduledRevenue` | string (decimal) | no | The amount on this row not yet scheduled, in the plan line currency. | | `items[].lines[].revenuePlanHeader.revenuePlanLines[].postedAmount` | string (decimal) | no | The amount actually posted to the ledger for this row, in the plan line currency. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the revenue contract last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more revenue contracts remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Revenue Scheduling Templates `GET /accounting/revenue-scheduling-templates` Group: Accounting › Revenue Scheduling Templates · Operation ID: `revenueSchedulingTemplates` Returns a list of revenue scheduling templates. To match on name, use POST accounting/revenue-scheduling-templates/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=revenueSchedulingTemplates) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `revenueSchedulingTemplateNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `revenueRecognitionPattern` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `revenueSchedulingEvent` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `amortizationMethod` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, revenueSchedulingTemplateNumber, revenueSchedulingTemplateName, revenueRecognitionPattern, revenueSchedulingEvent, amortizationMethod, lastModifiedDate - Selectable: id, revenueSchedulingTemplateNumber, revenueSchedulingTemplateName, revenueRecognitionPattern, revenueSchedulingEvent, amortizationMethod, lastModifiedDate ## Response `200` Success Under `data`, an array of revenueSchedulingTemplatesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the revenue scheduling template. | | `revenueSchedulingTemplateNumber` | string or null | no | The template number, unique within the tenant, for example 'RST-0007'. Absent while none has been assigned. | | `revenueSchedulingTemplateName` | string | no | The name of the revenue scheduling template. | | `revenueRecognitionPattern` | string | no | How revenue is recognized under this template. Possible values: 'upfront', 'overTime', 'asDelivered', 'asConsumed'. | | `revenueSchedulingEvent` | string | no | The event that creates a revenue plan under this template. Possible values: 'revenueArrangementCreation', 'uponInvoicing', 'uponUsageConsumption', 'manualRelease', 'uponInvoicingPercentInvoiced', 'uponInvoicingFullAmount', 'serviceActivation'. | | `amortizationMethod` | string or null | no | How the scheduled amount is spread over the plan's periods. Possible values: 'exactDays', 'pointInTime', 'straightLineProRateFirstLast', 'straightLine', 'usageBased'. Absent when the recognition pattern does not spread revenue over time. | | `lastModifiedDate` | string (date-time) | no | The date and time the revenue scheduling template last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Get Revenue Scheduling Template `GET /accounting/revenue-scheduling-templates/{id}` Group: Accounting › Revenue Scheduling Templates · Operation ID: `getRevenueSchedulingTemplate` Returns the revenue scheduling template with the given identifier. Use the template's id, not its revenueSchedulingTemplateNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getRevenueSchedulingTemplate) ## 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`. | | `id` | path | string | yes | The unique identifier for the revenue scheduling template to return. | ## Response `200` Success Under `data` (getRevenueSchedulingTemplateOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the revenue scheduling template. | | `revenueSchedulingTemplateNumber` | string | no | The template number, unique within the tenant, for example 'RST-0007'. Absent while none has been assigned. | | `revenueSchedulingTemplateName` | string | yes | The name of the revenue scheduling template. | | `revenueRecognitionPattern` | string | yes | How revenue is recognized under this template. Possible values: 'upfront', 'overTime', 'asDelivered', 'asConsumed'. | | `revenueSchedulingEvent` | string | yes | The event that creates a revenue plan under this template. Possible values: 'revenueArrangementCreation', 'uponInvoicing', 'uponUsageConsumption', 'manualRelease', 'uponInvoicingPercentInvoiced', 'uponInvoicingFullAmount', 'serviceActivation'. | | `amortizationMethod` | string | no | How the scheduled amount is spread over the plan's periods. Possible values: 'exactDays', 'pointInTime', 'straightLineProRateFirstLast', 'straightLine', 'usageBased'. Absent when the recognition pattern does not spread revenue over time. | | `lastModifiedDate` | string (date-time) | yes | The date and time the revenue scheduling template last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the revenue scheduling templates that match the given filters. `POST /accounting/revenue-scheduling-templates/search` Group: Accounting › Revenue Scheduling Templates · Operation ID: `searchRevenueSchedulingTemplate` [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchRevenueSchedulingTemplate) ## 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`. | ## Request body Required, `application/json`: searchRevenueSchedulingTemplateInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A revenue scheduling template must match every filter you send. Omit to return all revenue scheduling templates. | | `filters.id` | string | no | Limits results to the revenue scheduling template with this identifier. The match is exact. | | `filters.revenueSchedulingTemplateNumber` | string | no | Limits results to the template with this template number. The match is exact. | | `filters.revenueSchedulingTemplateName` | string | no | Limits results to the template with this exact name. The match is case-sensitive. Takes precedence over nameContains when both are sent. | | `filters.nameContains` | string | no | Limits results to templates whose name contains this text. The match is case-insensitive. Ignored when revenueSchedulingTemplateName is also sent. | | `filters.revenueRecognitionPattern` | string | no | Limits results to templates with this recognition pattern. Possible values: 'upfront', 'overTime', 'asDelivered', 'asConsumed'. Any other value matches nothing. | | `filters.revenueSchedulingEvent` | string | no | Limits results to templates triggered by this event. Possible values: 'revenueArrangementCreation', 'uponInvoicing', 'uponUsageConsumption', 'manualRelease', 'uponInvoicingPercentInvoiced', 'uponInvoicingFullAmount', 'serviceActivation'. Any other value matches nothing. | | `filters.amortizationMethod` | string | no | Limits results to templates with this amortization method. Possible values: 'exactDays', 'pointInTime', 'straightLineProRateFirstLast', 'straightLine', 'usageBased'. Any other value matches nothing. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of templates per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "revenueSchedulingTemplateNumber": "", "revenueSchedulingTemplateName": "", "nameContains": "", "revenueRecognitionPattern": "", "revenueSchedulingEvent": "", "amortizationMethod": "" }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchRevenueSchedulingTemplateOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The revenue scheduling templates that match, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the revenue scheduling template. | | `items[].revenueSchedulingTemplateNumber` | string | no | The template number, unique within the tenant, for example 'RST-0007'. Absent while none has been assigned. | | `items[].revenueSchedulingTemplateName` | string | yes | The name of the revenue scheduling template. | | `items[].revenueRecognitionPattern` | string | yes | How revenue is recognized under this template. Possible values: 'upfront', 'overTime', 'asDelivered', 'asConsumed'. | | `items[].revenueSchedulingEvent` | string | yes | The event that creates a revenue plan under this template. Possible values: 'revenueArrangementCreation', 'uponInvoicing', 'uponUsageConsumption', 'manualRelease', 'uponInvoicingPercentInvoiced', 'uponInvoicingFullAmount', 'serviceActivation'. | | `items[].amortizationMethod` | string | no | How the scheduled amount is spread over the plan's periods. Possible values: 'exactDays', 'pointInTime', 'straightLineProRateFirstLast', 'straightLine', 'usageBased'. Absent when the recognition pattern does not spread revenue over time. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the revenue scheduling template last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # List Sales Order Shipments `GET /accounting/sales-order-shipments` Group: Accounting › Sales Order Shipments · Operation ID: `listSalesOrderShipments` Returns a list of sales order shipments. To match on carrier, tracking id, shipment number, incoterm or the shipping contact email, or by several statuses at once, use POST accounting/sales-order-shipments/search. This endpoint takes only filters that are safe in a URL: ids, the status value and the date bounds. To match on carrier, tracking id, shipment number, incoterm or the shipping contact email, to filter by several statuses at once, or to filter by the customer's requested date or a shipped sales order product item, use POST accounting/sales-order-shipments/search. The date and instant bounds are inclusive, so a row exactly on a boundary is returned again. Deduplicate on id when you page an incremental sync with modifiedFrom. [Open in the interactive reference](https://docs.api.lensing.ai/?op=listSalesOrderShipments) ## 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`. | | `salesOrderId` | query | string | no | Limits results to shipments that fulfill this sales order. See id on GET accounting/sales-orders/:id. | | `inventoryShipmentId` | query | number | no | Limits results to the shipment mirroring this inventory shipment, given as its numeric inventory shipment id. At most one shipment matches. | | `warehouseId` | query | string | no | Limits results to shipments dispatched from this warehouse location. See id on GET inventory/warehouses/:id. | | `status` | query | string | no | Limits results to shipments in this state. Accepted values: 'pendingShipment', 'shipped', 'cancelled'. Any other value matches no shipment. Use search to match several states at once. | | `shipDateFrom` | query | string | no | Limits results to shipments dispatched on or after this date, inclusive. Format YYYY-MM-DD. Shipments with no ship date are excluded once this bound is given. | | `shipDateTo` | query | string | no | Limits results to shipments dispatched on or before this date, inclusive. Format YYYY-MM-DD. | | `deliveryDateFrom` | query | string | no | Limits results to shipments delivered, or expected to be delivered, on or after this date, inclusive. Format YYYY-MM-DD. | | `deliveryDateTo` | query | string | no | Limits results to shipments delivered, or expected to be delivered, on or before this date, inclusive. Format YYYY-MM-DD. | | `modifiedFrom` | query | string | no | Limits results to shipments changed at or after this time, inclusive. Format YYYY-MM-DDTHH:mm:ss.sssZ in UTC. | | `modifiedTo` | query | string | no | Limits results to shipments changed at or before this time, inclusive. Format YYYY-MM-DDTHH:mm:ss.sssZ in UTC. | | `cursor` | query | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `pageSize` | query | number | no | The number of shipments per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | ## Response `200` Success Under `data` (listSalesOrderShipmentsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The shipments on this page, ordered by ship date with the most recent first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the sales order shipment. | | `items[].salesOrderId` | string | yes | The sales order this shipment fulfills. See id on GET accounting/sales-orders/:id. | | `items[].inventoryShipmentId` | number | no | The numeric identifier of the shipment document in inventory, which owns the shipment. Absent only on legacy rows written before the mirroring existed. | | `items[].shipmentNumber` | string | no | The shipment document number, assigned by inventory. Absent until inventory has issued one. | | `items[].status` | string | no | The lifecycle status of the shipment. Possible values: 'pendingShipment', 'shipped', 'cancelled'. 'pendingShipment' is drafted in inventory but not yet dispatched, 'shipped' is dispatched, and 'cancelled' means the inventory shipment was deleted and this mirror was soft-cancelled. | | `items[].trackingId` | string | no | The carrier tracking number or tracking URL, when one has been recorded. | | `items[].carrier` | string | no | The name of the carrier handling the shipment. | | `items[].incoterm` | string | no | The incoterm governing shipping responsibility for this shipment. For example 'DAP' or 'EXW'. | | `items[].warehouseId` | string | no | The warehouse location the shipment is dispatched from. See id on GET inventory/warehouses/:id. | | `items[].shipToContactEmail` | string | no | The email of the contact who receives this shipment. | | `items[].customerRequestDate` | string (date) | no | The date the customer asked for delivery. Format YYYY-MM-DD. | | `items[].shipDate` | string (date) | no | The date the shipment was dispatched. Format YYYY-MM-DD. Absent while the shipment is still pending. | | `items[].deliveryDate` | string (date) | no | The date the shipment was, or is expected to be, delivered. Format YYYY-MM-DD. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the shipment last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].items` | array of object | yes | The shipped lines, oldest first. An empty array when the shipment has no lines. | | `items[].items[].id` | string | yes | The unique identifier for the shipment line. PATCH matches updateItems and deleteItems on it. | | `items[].items[].salesOrderProductItemId` | string | yes | The sales order product item this line ships. There is no endpoint for it. | | `items[].items[].shippedQty` | string (decimal) | yes | The quantity shipped on this line, as a count of units. | | `items[].items[].orderedQty` | string (decimal) | yes | The total quantity ordered for the underlying sales order product item, as a count of units. | | `items[].items[].unshippedQty` | string (decimal) | yes | The quantity still outstanding on this line, as a count of units: orderedQty minus shippedQty. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as the cursor query parameter. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more shipments remain beyond this page. | ## Errors | Code | Status | | --- | --- | | `INVALID_FILTER` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_FILTER - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Sales Order Shipment `POST /accounting/sales-order-shipments` Group: Accounting › Sales Order Shipments · Operation ID: `createSalesOrderShipment` Ships an approved sales order to its customer and returns the id of the new shipment. The shipment is drafted in inventory, and this record mirrors it. The lines come from the order's inventory products and cannot be supplied here. Ships an approved sales order to its customer and returns only the created shipment's id. Read the shipment number and inventoryShipmentId with GET accounting/sales-order-shipments/:id. Inventory owns the shipment. The call drafts the real shipment in inventory and moves stock, and the record this endpoint returns is the accounting-side mirror, linked by inventoryShipmentId. A shipment cannot be created in accounting alone. The new shipment starts at 'pendingShipment' with every line at shippedQty 0, and inventory fills the quantities in when it commits the shipment. The lines come from the sales order's inventory products and their items and cannot be supplied here. The logistics fields carrier, trackingId, incoterm, warehouseId, shipToContactEmail, customerRequestDate and deliveryDate are accounting-side detail and can also be set later through PATCH. Beyond SALES_ORDER_NOT_FOUND and WAREHOUSE_NOT_FOUND, the call is rejected with a localized message when the sales order is not approved, has no inventory products or no inventory product items, or when no inventory implementation is registered. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createSalesOrderShipment) ## 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`: createSalesOrderShipmentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `salesOrderId` | string | yes | The approved sales order to ship. Its inventory products and their items define what goes on the shipment. See id on GET accounting/sales-orders/:id. | | `trackingId` | string | no | The carrier tracking number or tracking URL to record on the shipment. Omit to leave it unset. | | `carrier` | string | no | The name of the carrier handling the shipment. Omit to leave it unset. | | `incoterm` | string | no | The incoterm governing shipping responsibility. For example 'DAP' or 'EXW'. Omit to leave it unset. | | `warehouseId` | string | no | The warehouse location the shipment is dispatched from. Omit to leave it unset. See id on GET inventory/warehouses/:id. | | `shipToContactEmail` | string | no | The email of the contact who receives this shipment. Omit to leave it unset. | | `customerRequestDate` | string (date) | no | The date the customer asked for delivery. Use format YYYY-MM-DD. Omit to leave it unset. | | `deliveryDate` | string (date) | no | The date the shipment is expected to be delivered. Use format YYYY-MM-DD. Omit to leave it unset. | Example: ```json { "salesOrderId": "9c2a6a10-7d34-4c8e-9b21-5a0f3e6d81b7", "trackingId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "carrier": "DHL", "incoterm": "DAP", "warehouseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "shipToContactEmail": "", "customerRequestDate": "2026-01-15", "deliveryDate": "2026-01-15" } ``` ## Response `200` Success Under `data` (createSalesOrderShipmentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created sales order shipment. | ## 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 | | `WAREHOUSE_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: SALES_ORDER_NOT_FOUND, WAREHOUSE_NOT_FOUND - `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 --- # Get Sales Order Shipment `GET /accounting/sales-order-shipments/{id}` Group: Accounting › Sales Order Shipments · Operation ID: `getSalesOrderShipment` Returns the sales order shipment with the given identifier, including its shipped lines. inventoryShipmentId links to the shipment document in inventory, which owns it. items[].unshippedQty is orderedQty minus shippedQty. While a shipment is still 'pendingShipment', every shippedQty is 0, because inventory fills the quantities in when it commits the shipment. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getSalesOrderShipment) ## 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`. | | `id` | path | string | yes | The unique identifier for the sales order shipment to return. | ## Response `200` Success Under `data` (getSalesOrderShipmentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the sales order shipment. | | `salesOrderId` | string | yes | The sales order this shipment fulfills. See id on GET accounting/sales-orders/:id. | | `inventoryShipmentId` | number | no | The numeric identifier of the shipment document in inventory, which owns the shipment. Absent only on legacy rows written before the mirroring existed. | | `shipmentNumber` | string | no | The shipment document number, assigned by inventory. Absent until inventory has issued one. | | `status` | string | no | The lifecycle status of the shipment. Possible values: 'pendingShipment', 'shipped', 'cancelled'. 'pendingShipment' is drafted in inventory but not yet dispatched, 'shipped' is dispatched, and 'cancelled' means the inventory shipment was deleted and this mirror was soft-cancelled. | | `trackingId` | string | no | The carrier tracking number or tracking URL, when one has been recorded. | | `carrier` | string | no | The name of the carrier handling the shipment. | | `incoterm` | string | no | The incoterm governing shipping responsibility for this shipment. For example 'DAP' or 'EXW'. | | `warehouseId` | string | no | The warehouse location the shipment is dispatched from. See id on GET inventory/warehouses/:id. | | `shipToContactEmail` | string | no | The email of the contact who receives this shipment. | | `customerRequestDate` | string (date) | no | The date the customer asked for delivery. Format YYYY-MM-DD. | | `shipDate` | string (date) | no | The date the shipment was dispatched. Format YYYY-MM-DD. Absent while the shipment is still pending. | | `deliveryDate` | string (date) | no | The date the shipment was, or is expected to be, delivered. Format YYYY-MM-DD. | | `lastModifiedDate` | string (date-time) | yes | The date and time the shipment last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items` | array of object | yes | The shipped lines, oldest first. An empty array when the shipment has no lines. | | `items[].id` | string | yes | The unique identifier for the shipment line. PATCH matches updateItems and deleteItems on it. | | `items[].salesOrderProductItemId` | string | yes | The sales order product item this line ships. There is no endpoint for it. | | `items[].shippedQty` | string (decimal) | yes | The quantity shipped on this line, as a count of units. | | `items[].orderedQty` | string (decimal) | yes | The total quantity ordered for the underlying sales order product item, as a count of units. | | `items[].unshippedQty` | string (decimal) | yes | The quantity still outstanding on this line, as a count of units: orderedQty minus shippedQty. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Sales Order Shipment `PATCH /accounting/sales-order-shipments/{id}` Group: Accounting › Sales Order Shipments · Operation ID: `updateSalesOrderShipment` Updates the sales order shipment with the given identifier and its lines. Only the fields in the request body change. carrier, tracking id, ship date and shipped quantities can also be written from inventory when the shipment is committed, so a value set here can be overwritten by that sync. Every field is optional and omitting it leaves the value unchanged. The response is empty. Read the result with GET accounting/sales-order-shipments/:id. The lines are a child collection, edited through the three arrays createItems, updateItems and deleteItems rather than by resending the whole array. Omit an array for no operations of that kind, and omit all three to leave the lines untouched. createItems entries carry no id because the row does not exist yet. updateItems and deleteItems match on the line's own id, and a line belonging to another shipment is rejected. The change is all-or-nothing, so a failure at any point leaves the shipment exactly as it was. Changing any line re-derives the shipped-product rollup on the sales order products those lines belong to. carrier, trackingId, shipDate and the shipped quantities can also be written from the inventory side when the mirrored shipment is committed, so a value set here can later be replaced by that sync. Setting status to 'cancelled' soft-cancels the accounting mirror only, and the inventory shipment is untouched. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateSalesOrderShipment) ## 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. | | `id` | path | string | yes | The unique identifier for the sales order shipment to update. | ## Request body Required, `application/json`: updateSalesOrderShipmentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `trackingId` | string | no | The carrier tracking number or tracking URL. Omit to leave unchanged. | | `shipmentNumber` | string | no | The shipment document number. Omit to leave unchanged. | | `carrier` | string | no | The name of the carrier handling the shipment. Omit to leave unchanged. | | `incoterm` | string | no | The incoterm. For example 'DAP' or 'EXW'. Omit to leave unchanged. | | `status` | string, one of `"pendingShipment"`, `"shipped"`, `"cancelled"` | no | The lifecycle status. Omit to leave unchanged. Setting 'cancelled' soft-cancels the accounting mirror only and leaves the inventory shipment untouched. | | `warehouseId` | string | no | The warehouse location the shipment is dispatched from. Omit to leave unchanged. See id on GET inventory/warehouses/:id. | | `shipToContactEmail` | string | no | The shipping contact email. Omit to leave unchanged. | | `customerRequestDate` | string (date) | no | The date the customer requested delivery. Use format YYYY-MM-DD. Omit to leave unchanged. | | `shipDate` | string (date) | no | The date the shipment was dispatched. Use format YYYY-MM-DD. Omit to leave unchanged. | | `deliveryDate` | string (date) | no | The date the shipment is delivered. Use format YYYY-MM-DD. Omit to leave unchanged. | | `createItems` | array of object | no | Lines to add to the shipment. Each has no id because the row does not exist yet, and its orderedQty is copied from the referenced sales order product item. Omit for no additions. | | `createItems[].salesOrderProductItemId` | string | yes | The sales order product item the new line ships. Fails with SALES_ORDER_PRODUCT_ITEMS_NOT_FOUND when it matches nothing. There is no endpoint for it. | | `createItems[].shippedQty` | string (decimal) | yes | The quantity shipped on the new line, as a count of units. | | `updateItems` | array of object | no | Lines to change, matched by the line's own id. Omit for no changes. | | `updateItems[].id` | string | yes | The unique identifier for the shipment line to change. It must belong to this shipment, or the call fails with SHIPMENT_ITEMS_NOT_FOUND. | | `updateItems[].shippedQty` | string (decimal) | no | The new shipped quantity for the line, as a count of units. Omit to leave unchanged. | | `deleteItems` | array of string | no | The identifiers of the shipment lines to remove. Each must belong to this shipment, or the call fails with SHIPMENT_ITEMS_NOT_FOUND. Omit for no removals. | Example: ```json { "trackingId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "shipmentNumber": "", "carrier": "DHL", "incoterm": "", "status": "shipped", "warehouseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "shipToContactEmail": "", "customerRequestDate": "2026-01-15", "shipDate": "2026-01-15", "deliveryDate": "2026-01-15", "createItems": [ { "salesOrderProductItemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "shippedQty": "19.99" } ], "updateItems": [ { "id": "5a0f3e6d-81b7-4c8e-9b21-9c2a6a107d34", "shippedQty": "5" } ], "deleteItems": [ "" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WAREHOUSE_NOT_FOUND` | 400 | | `SHIPMENT_ITEMS_NOT_FOUND` | 400 | | `SALES_ORDER_PRODUCT_ITEMS_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WAREHOUSE_NOT_FOUND, SHIPMENT_ITEMS_NOT_FOUND, SALES_ORDER_PRODUCT_ITEMS_NOT_FOUND - `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 --- # Delete Sales Order Shipment `DELETE /accounting/sales-order-shipments/{id}` Group: Accounting › Sales Order Shipments · Operation ID: `deleteSalesOrderShipment` Deletes the accounting-side sales order shipment record only. Inventory owns the shipment document and is not called, so a shipment that still exists there is left without its accounting counterpart. To retire a real shipment, delete it in inventory instead, which soft-cancels this record and keeps it for fulfillment history. Deletes one sales order shipment and its lines, then re-derives the shipped-product rollup on the sales order products those lines belonged to. This removes the accounting-side record only. Inventory owns the shipment document and is not called, so a shipment that still exists there is left without its accounting counterpart. To retire a real shipment, delete it in inventory instead. That soft-cancels this record, so its status becomes 'cancelled' and the row and its lines are kept for fulfillment history. Reach for this endpoint only for records that should never have existed. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteSalesOrderShipment) ## 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. | | `id` | path | string | yes | The unique identifier for the sales order shipment to delete. | ## Response `200` Success Under `data` (deleteSalesOrderShipmentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted sales order shipment. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Returns the sales order shipments that match the given filters. `POST /accounting/sales-order-shipments/search` Group: Accounting › Sales Order Shipments · Operation ID: `searchSalesOrderShipments` This endpoint takes the criteria a query string cannot: free-text matching on carrier, tracking id, shipment number and incoterm, the shipping contact email, several statuses at once, the customer's requested delivery date, and the shipped sales order product item. The text filters match a case-insensitive substring. Every filter is optional and they are combined with AND. Omit them all to match every shipment. The id, warehouse and date-range filters that GET accounting/sales-order-shipments also serves are available here too. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchSalesOrderShipments) ## 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`. | ## Request body Required, `application/json`: searchSalesOrderShipmentsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A shipment must match every filter you send. Omit to return all shipments. | | `filters.salesOrderId` | string | no | Limits results to shipments that fulfill this sales order. See id on GET accounting/sales-orders/:id. | | `filters.inventoryShipmentId` | number | no | Limits results to the shipment mirroring this inventory shipment, given as its numeric inventory shipment id. At most one shipment matches. | | `filters.warehouseId` | string | no | Limits results to shipments dispatched from this warehouse location. See id on GET inventory/warehouses/:id. | | `filters.status` | array of string, one of `"pendingShipment"`, `"shipped"`, `"cancelled"` | no | Limits results to shipments in any of these states. Omit or pass an empty array to match every state. | | `filters.salesOrderProductItemId` | string | no | Limits results to shipments that have a line for this sales order product item. This is how you find the shipments covering one ordered item. There is no endpoint for it. | | `filters.shipmentNumber` | string | no | Limits results to shipments whose shipment number contains this text. The match is case-insensitive. | | `filters.trackingId` | string | no | Limits results to shipments whose tracking number or URL contains this text. The match is case-insensitive. | | `filters.carrier` | string | no | Limits results to shipments whose carrier name contains this text. The match is case-insensitive. | | `filters.incoterm` | string | no | Limits results to shipments whose incoterm contains this text. The match is case-insensitive. | | `filters.shipToContactEmail` | string | no | Limits results to shipments whose shipping contact email contains this text. The match is case-insensitive. | | `filters.shipDate` | object | no | Limits results by dispatch date. Both bounds are inclusive. Omit one to leave that side open. | | `filters.shipDate.start` | string (date) | no | The earliest dispatch date to include, inclusive. Format YYYY-MM-DD. | | `filters.shipDate.end` | string (date) | no | The latest dispatch date to include, inclusive. Format YYYY-MM-DD. | | `filters.deliveryDate` | object | no | Limits results by delivery date. Both bounds are inclusive. Omit one to leave that side open. | | `filters.deliveryDate.start` | string (date) | no | The earliest delivery date to include, inclusive. Format YYYY-MM-DD. | | `filters.deliveryDate.end` | string (date) | no | The latest delivery date to include, inclusive. Format YYYY-MM-DD. | | `filters.customerRequestDate` | object | no | Limits results by the date the customer requested delivery. Both bounds are inclusive. Omit one to leave that side open. | | `filters.customerRequestDate.start` | string (date) | no | The earliest requested delivery date to include, inclusive. Format YYYY-MM-DD. | | `filters.customerRequestDate.end` | string (date) | no | The latest requested delivery date to include, inclusive. Format YYYY-MM-DD. | | `filters.lastModifiedDate` | object | no | Limits results by last change. Both bounds are inclusive. Format YYYY-MM-DDTHH:mm:ss.sssZ in UTC. Omit one to leave that side open. | | `filters.lastModifiedDate.start` | string (date-time) | no | The earliest change time to include, inclusive. Format YYYY-MM-DDTHH:mm:ss.sssZ in UTC. | | `filters.lastModifiedDate.end` | string (date-time) | no | The latest change time to include, inclusive. Format YYYY-MM-DDTHH:mm:ss.sssZ in UTC. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of shipments per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "carrier": "dhl", "status": [ "pendingShipment", "shipped" ] }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchSalesOrderShipmentsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The shipments on this page, ordered by ship date with the most recent first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the sales order shipment. | | `items[].salesOrderId` | string | yes | The sales order this shipment fulfills. See id on GET accounting/sales-orders/:id. | | `items[].inventoryShipmentId` | number | no | The numeric identifier of the shipment document in inventory, which owns the shipment. Absent only on legacy rows written before the mirroring existed. | | `items[].shipmentNumber` | string | no | The shipment document number, assigned by inventory. Absent until inventory has issued one. | | `items[].status` | string | no | The lifecycle status of the shipment. Possible values: 'pendingShipment', 'shipped', 'cancelled'. 'pendingShipment' is drafted in inventory but not yet dispatched, 'shipped' is dispatched, and 'cancelled' means the inventory shipment was deleted and this mirror was soft-cancelled. | | `items[].trackingId` | string | no | The carrier tracking number or tracking URL, when one has been recorded. | | `items[].carrier` | string | no | The name of the carrier handling the shipment. | | `items[].incoterm` | string | no | The incoterm governing shipping responsibility for this shipment. For example 'DAP' or 'EXW'. | | `items[].warehouseId` | string | no | The warehouse location the shipment is dispatched from. See id on GET inventory/warehouses/:id. | | `items[].shipToContactEmail` | string | no | The email of the contact who receives this shipment. | | `items[].customerRequestDate` | string (date) | no | The date the customer asked for delivery. Format YYYY-MM-DD. | | `items[].shipDate` | string (date) | no | The date the shipment was dispatched. Format YYYY-MM-DD. Absent while the shipment is still pending. | | `items[].deliveryDate` | string (date) | no | The date the shipment was, or is expected to be, delivered. Format YYYY-MM-DD. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the shipment last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].items` | array of object | yes | The shipped lines, oldest first. An empty array when the shipment has no lines. | | `items[].items[].id` | string | yes | The unique identifier for the shipment line. PATCH matches updateItems and deleteItems on it. | | `items[].items[].salesOrderProductItemId` | string | yes | The sales order product item this line ships. There is no endpoint for it. | | `items[].items[].shippedQty` | string (decimal) | yes | The quantity shipped on this line, as a count of units. | | `items[].items[].orderedQty` | string (decimal) | yes | The total quantity ordered for the underlying sales order product item, as a count of units. | | `items[].items[].unshippedQty` | string (decimal) | yes | The quantity still outstanding on this line, as a count of units: orderedQty minus shippedQty. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more shipments remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Sales Orders `GET /accounting/sales-orders` Group: Accounting › Sales Orders · Operation ID: `salesOrders` List sales orders with structured filters. GET data-set exposing filter[field][op]= (eq/ne/gt/gte/lt/lte/in/notIn/between/like), sort=-bookDate,orderNumber, sparse fields=, and page[size]/page[cursor] cursor pagination. Returns the collection header of each order; the full header with totals, customer and external identifiers comes from GET accounting/sales-orders/:id or POST accounting/sales-orders/search. Every request must carry the Api-Version header. [Open in the interactive reference](https://docs.api.lensing.ai/?op=salesOrders) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `orderNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `orderType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `approvalStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `bookDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `serviceStartDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `serviceEndDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isEvergreen` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, orderNumber, orderType, approvalStatus, bookDate, serviceStartDate, serviceEndDate, currency, isEvergreen, term, lastModifiedDate - Selectable: id, orderNumber, orderType, approvalStatus, bookDate, serviceStartDate, serviceEndDate, currency, isEvergreen, term, customerName, customerNumber, entityName, entityNumber, salesArrangement, lastModifiedDate ## Response `200` Success Under `data`, an array of salesOrdersRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | Stable, platform-assigned id (a UUID) of the sales order — the public identifier used by GET accounting/sales-orders/:id. | | `orderNumber` | string or null | no | System-assigned sales-order number. Filter target: filter[orderNumber][eq]=SO-1042. | | `orderType` | string or null | no | Order type. An `OrderType` member. Filter target: filter[orderType][eq]=renewal. | | `approvalStatus` | string or null | no | Approval state. An `ApprovalStatus` member. Filter target: filter[approvalStatus][eq]=approved. | | `bookDate` | string (date) | no | Date the order was booked (YYYY-MM-DD). Filter target: filter[bookDate][gte]=2026-01-01. | | `serviceStartDate` | string (date) | no | First day of the service period (YYYY-MM-DD). | | `serviceEndDate` | string (date) or null | no | Last day of the service period (YYYY-MM-DD). Absent on an evergreen order. | | `currency` | string | no | Transaction currency as a 3-letter ISO 4217 code. Filter target: filter[currency][in]=USD,EUR. | | `isEvergreen` | boolean | no | True when the order renews indefinitely. Filter target: filter[isEvergreen][eq]=false. | | `term` | string (decimal) or null | no | Contract term in months, when the order is not evergreen. | | `customerName` | string or null | no | Customer display name, denormalised onto the order. Returned only — in neither filterableFields nor orderableFields, because it identifies the counterparty. | | `customerNumber` | string or null | no | System-generated customer number, denormalised onto the order. Returned only — in neither filterableFields nor orderableFields, because it identifies the counterparty. | | `entityName` | string or null | no | Name of the owning legal entity, denormalised onto the order. Returned only — in neither filterableFields nor orderableFields. To scope a query by entity, use POST accounting/sales-orders/search, which filters on entityId. | | `entityNumber` | string or null | no | System-assigned number of the owning legal entity. Returned only — in neither filterableFields nor orderableFields. To scope a query by entity, use POST accounting/sales-orders/search, which filters on entityId. | | `salesArrangement` | string or null | no | Number of the sales arrangement this order belongs to. Returned only — in neither filterableFields nor orderableFields, because it exposes internal commercial structure. | | `lastModifiedDate` | string (date-time) | no | Instant the order last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Filter and sort on this to sync incrementally: filter[lastModifiedDate][gte]=2026-08-01T00:00:00.000Z with sort=lastModifiedDate. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create a sales order `POST /accounting/sales-orders` Group: Accounting › Sales Orders · Operation ID: `createSalesOrder` Creates a sales order with its products, subscriptions and pricing in one transaction. Attach to an existing customer with `customerId`, or create one inline with `customer`. Products are grouped as `subscriptions[]` (recurring, each with its own product lines) and `nonSubscriptionProducts[]` (one-time). Line pricing is reconciled from listPrice: supply `unitPrice`, or `discountAmount`, or `discountPercentage` (precedence in that order) and the rest is derived; tiered/volume/stairstep/rateCard lines carry their prices on `tiers`/`rateCardEntries` instead. Rolled back atomically on any validation failure. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createSalesOrder) ## 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`: createSalesOrderInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `approvalStatus` | string | no | Send `approved` to have the order approved as soon as it is created. The order is always written as `pendingApproval` first and only then transitioned, so every approval side-effect — invoice-plan generation above all — runs against a complete order. Any other value leaves it pending approval. | | `bookDate` | string (date) | no | Day the order is booked. Defaults to today, and is the date the entity's active invoice policy is resolved for. | | `currency` | string | yes | ISO 4217 code every price on the order is denominated in — header, lines, tiers and rate-card rows alike. | | `customer` | InlineCustomer | no | A new customer to create alongside the order. Provide exactly one of `customerId` and `customer`. It is created through the same path as the Customer create endpoint, so anything that path rejects fails the whole request and leaves no order behind. | | `customer.additionalContactEmail` | string | no | Secondary address copied on customer correspondence. | | `customer.addresses` | array of CustomerAddress | no | The customer's addresses, each naming the slot it fills. Both a `billTo` and a `soldTo` address are needed before an order can be raised, so supply both here. | | `customer.addresses[].attention` | string | no | Person or department correspondence to this address is directed to. | | `customer.addresses[].city` | string | yes | City. | | `customer.addresses[].country` | string | yes | ISO 3166-1 alpha-2 country code. | | `customer.addresses[].isDefault` | boolean | no | Marks this entry as the default for its `type`. At most one address per type may set it; when no entry of a type is marked, the first one of that type becomes its default. | | `customer.addresses[].line1` | string | yes | Street and number. | | `customer.addresses[].line2` | string | no | Second address line, where the address needs one. | | `customer.addresses[].stateProvince` | string | no | State, province or region, where the country uses one. | | `customer.addresses[].type` | string | yes | Which slot this address fills: `billTo` for the address invoices go to, `soldTo` for the contracting party. An order can only be raised once the customer has one of each. | | `customer.addresses[].zipCode` | string | yes | Postal code. | | `customer.billableTravelExpenseInvoiceDetail` | string | no | How billable travel expenses appear on the invoice: `detailed` line by line, or `consolidated` into one figure. | | `customer.businessIdentificationNumber` | string | no | The customer's business registration number. | | `customer.businessType` | string | no | Whether the customer is a `company` or an `individual`. | | `customer.customerType` | string | no | A CustomerPartnerResellerType member: `default` or `partner`. `endCustomer` is rejected here, because it requires a partner link this contract does not carry. | | `customer.defaultCurrency` | string | no | ISO 4217 code the customer is billed in unless an order says otherwise. | | `customer.entityId` | string | yes | UUID of the legal Entity that owns the customer. Fixed at creation — a customer cannot later be moved to another entity. | | `customer.exemptionNumber` | string | no | The exemption certificate number. Only accepted while `taxExempt` is true. | | `customer.externalIdentifiers` | array of ExternalIdentifier | no | Identifiers the customer carries in other systems, upserted by `fieldName`. A `fieldName` may appear only once. | | `customer.externalIdentifiers[].externalUrl` | string | no | Deep link to the record in the owning system, shown beside the id. | | `customer.externalIdentifiers[].fieldName` | string | yes | Names the ExternalIdentifierConfiguration this id belongs to, as that configuration spells it. It must already be configured for the tenant; an unknown name is rejected. Identifiers are upserted by this name, so sending it again overwrites the stored value. | | `customer.externalIdentifiers[].value` | string | yes | The identifier as the owning system spells it. | | `customer.invoiceConsolidation` | string | no | What invoices are grouped by: `customer`, `purchaseOrderNumber` or `salesArrangement`. | | `customer.invoiceDeliveryMode` | string | no | Whether invoices go out `automated` or are sent `manual`ly. | | `customer.invoiceEmail` | string | no | Address invoices are sent to. | | `customer.isExemptFromCreditCardFee` | boolean | no | Whether credit-card surcharges are waived for this customer. | | `customer.mainContactName` | string | no | Name of the primary contact person. | | `customer.mainEmail` | string | no | Primary contact address for the customer. | | `customer.name` | string | yes | Legal or trading name of the customer. | | `customer.parentId` | string | no | UUID of the customer above this one in the business-partner hierarchy. | | `customer.partnerId` | string | no | UUID of the partner this customer buys through. | | `customer.partnerType` | string | no | How a `partner` customer acts: `reseller` or `thirdPartyPayer`. | | `customer.payment` | CustomerPayment | no | Payment-provider link registered for the customer once it has been created. | | `customer.payment.externalCustomerId` | string | no | The customer's id in the provider's system. | | `customer.payment.externalPaymentMethodId` | string | no | The payment method's id in the provider's system. Requires `externalCustomerId` — a method with no customer to hang it on is rejected. | | `customer.payment.provider` | string | no | The payment provider, named exactly as the tenant's provider configuration spells it. Required as soon as either external id is supplied, and an unconfigured name is rejected. | | `customer.paymentTermCode` | string | no | The customer's payment term, named by its code as configured for `entityId`. A code that entity does not have fails the request, and no order is created. | | `customer.preferredLanguage` | string | no | A PreferredLanguage member; `english` is the only one today. | | `customer.salesChannel` | string | no | A SalesChannel member: `salesAssisted` or `selfServe`. | | `customer.segment` | string | no | A CustomerSegment member: `SMB`, `MidMarket` or `Enterprise`. | | `customer.status` | string | no | Whether the customer is `active` or `inactive`. | | `customer.taxExempt` | boolean | no | Whether the customer is exempt from tax. Must be true before `exemptionNumber` is accepted. | | `customer.taxExemptCode` | string | no | The code behind the exemption — the reason category the authority uses. | | `customer.taxRegistrations` | array of TaxRegistration | no | Tax registrations to record, upserted by (`country`, `type`). | | `customer.taxRegistrations[].country` | string | yes | ISO 3166-1 alpha-2 code of the jurisdiction the customer is registered in. Together with `type` it is what a registration is upserted by. | | `customer.taxRegistrations[].effectiveDate` | string (date) | no | Day the registration takes effect. | | `customer.taxRegistrations[].isActive` | boolean | no | Whether the registration is currently in force. | | `customer.taxRegistrations[].note` | string | no | Free-text note kept with the registration. | | `customer.taxRegistrations[].taxNumber` | string | yes | The registration number as the authority issues it. | | `customer.taxRegistrations[].type` | string | yes | A TaxRegistrationType member: `tax`, `salesTax`, `VAT`, `GST`, `EIN` or `TIN`. Together with `country` it is what a registration is upserted by. | | `customerId` | string | no | UUID of the existing Customer the order is raised for. Provide exactly one of `customerId` and `customer`; sending both, or neither, is rejected. | | `dataSource` | string | no | The system this order came from, as an ExtractionProviders member (`Salesforce`, `NetSuite`, `Everest` and the rest). Omit it for an order raised directly through this API. | | `effectiveDate` | string (date) | no | Day the order becomes commercially effective. | | `entityId` | string | yes | UUID of the legal Entity the order belongs to. | | `excludeFromTaxEngine` | boolean | no | Whether the order is kept out of the tax engine, so no tax is calculated for it. | | `externalIdentifiers` | array of ExternalIdentifier | no | Identifiers the order carries in other systems, upserted by `fieldName`. | | `externalIdentifiers[].externalUrl` | string | no | Deep link to the record in the owning system, shown beside the id. | | `externalIdentifiers[].fieldName` | string | yes | Names the ExternalIdentifierConfiguration this id belongs to, as that configuration spells it. It must already be configured for the tenant; an unknown name is rejected. Identifiers are upserted by this name, so sending it again overwrites the stored value. | | `externalIdentifiers[].value` | string | yes | The identifier as the owning system spells it. | | `externalizedInvoicing` | boolean | no | Whether invoicing for this order is handled in another system. Defaults to false. | | `nonSubscriptionProducts` | array of ProductLine | no | One-time product lines, sitting on the order itself rather than under a subscription. Line types that are not subscription-based belong here — putting one under a subscription is rejected with `INVALID_SUBSCRIPTION_PRODUCT`. | | `nonSubscriptionProducts[].allowance` | LineAllowance | no | The Allowance & Overage terms of a Commit with Overage line. Required for that line type and rejected on any other; omitted fields default from the product catalog. | | `nonSubscriptionProducts[].allowance.allowance` | string (decimal) | yes | Committed quantity per allowance term, in the catalog Allowance's unit of measure. | | `nonSubscriptionProducts[].allowance.allowanceTerm` | string | no | The period the allowance covers: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually` or `orderTerm`. Defaults to the product catalog's Allowance term. | | `nonSubscriptionProducts[].allowance.maximumRolloverLimit` | string (decimal) | no | Cap on the quantity that may roll over. Accepted only when the catalog Allowance has rollover enabled, and defaults to its setting. | | `nonSubscriptionProducts[].allowance.overage` | LineOverage | no | Discount and invoicing overrides for the line's overage. Omit it to take everything from the catalog Overage and its price-book entry. | | `nonSubscriptionProducts[].allowance.overage.discountAmount` | string (decimal) | no | Discount per unit off the overage price, in the order currency. Takes precedence over `discountPercentage` when both are sent; omit both to price the overage exactly as its price-book entry has it. | | `nonSubscriptionProducts[].allowance.overage.discountPercentage` | string (decimal) | no | Discount per unit off the overage price on a 0–100 scale, so `10` means ten percent. Used only when `discountAmount` is not given. | | `nonSubscriptionProducts[].allowance.overage.invoiceTiming` | string | no | Whether the overage invoices `advance` (at the start of its period) or `arrears` (after it). Defaults from the entity's active invoice policy for overage lines, then from the parent line. | | `nonSubscriptionProducts[].allowance.overage.lineInvoiceDay` | string | no | Which day of the period the overage invoices on: `anniversary`, `first`, `last` or `specific`. Defaults from the entity's active invoice policy for overage lines, then from the parent line. | | `nonSubscriptionProducts[].allowance.rolloverDuration` | string (decimal) | no | How many allowance terms unused allowance rolls over for. Accepted only when the catalog Allowance has rollover enabled, and defaults to its setting. | | `nonSubscriptionProducts[].discountAmount` | string (decimal) | no | Discount per unit off `listPrice`, in the order currency. Used when no `unitPrice` is given, and then drives the other two. | | `nonSubscriptionProducts[].discountPercentage` | string (decimal) | no | Discount per unit off `listPrice` on a 0–100 scale, so `10` means ten percent. Stored as a 0–1 fraction. The lowest precedence of the three — used only when neither `unitPrice` nor `discountAmount` is given. | | `nonSubscriptionProducts[].externalIdentifiers` | array of ExternalIdentifier | no | Identifiers this line carries in other systems, upserted by `fieldName`. | | `nonSubscriptionProducts[].externalIdentifiers[].externalUrl` | string | no | Deep link to the record in the owning system, shown beside the id. | | `nonSubscriptionProducts[].externalIdentifiers[].fieldName` | string | yes | Names the ExternalIdentifierConfiguration this id belongs to, as that configuration spells it. It must already be configured for the tenant; an unknown name is rejected. Identifiers are upserted by this name, so sending it again overwrites the stored value. | | `nonSubscriptionProducts[].externalIdentifiers[].value` | string | yes | The identifier as the owning system spells it. | | `nonSubscriptionProducts[].invoiceFrequency` | string | no | How often the line invoices — an InvoiceFrequency member: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually`, `once`, `custom`, `template`, `milestone`, `allAtOnce` or `termEnd`. Defaults to the product line's own setting. | | `nonSubscriptionProducts[].invoiceFrequencyTemplateId` | string | no | UUID of the invoice frequency template. | | `nonSubscriptionProducts[].invoiceTiming` | string | no | Whether the line invoices `advance` (at the start of the service period) or `arrears` (after it). Defaults from the entity's active invoice policy for this line type. | | `nonSubscriptionProducts[].lineInvoiceDay` | string | no | Which day of the period the line invoices on: `anniversary`, `first`, `last` or `specific`. Defaults from the entity's active invoice policy. | | `nonSubscriptionProducts[].listPrice` | string (decimal) | no | Undiscounted price per unit, in the order currency. Defaults to the list price the order's price book holds for this product line, and is the figure the other pricing fields are reconciled against. | | `nonSubscriptionProducts[].priceModel` | string | no | How the line is priced: `flat`, `perUnit`, `tiered`, `volume`, `stairstep` or `rateCard`. Defaults to the product line's own model. `tiered`, `volume` and `stairstep` take their prices from `tiers` and `rateCard` from `rateCardEntries`, so for those the line-level discount fields are not reconciled. `thresholdBased` is rejected: it needs a base line plus a metered usage child, which this endpoint does not build. | | `nonSubscriptionProducts[].pricingTerm` | string | no | The period the price is quoted for: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually` or `once`. Defaults to the product line's own term. | | `nonSubscriptionProducts[].productLineId` | string | yes | UUID of the ProductLine being ordered. It settles the product, the line type and the billing rules every omitted field defaults from. It must exist, and it must not be threshold-based or prepaid: those need a base line plus a metered usage child under one product, which this endpoint does not build. | | `nonSubscriptionProducts[].quantity` | string (decimal) | no | Number of units ordered. Required unless the line takes its quantity from somewhere else: usage-metered line types (PayAsYouGo, Usage, Billable*) meter it from usage records and tier-priced models (`tiered`, `volume`, `stairstep`, `thresholdBased`) derive it from the tier structure, so for those it defaults to zero. Anywhere else, omitting it fails the request. | | `nonSubscriptionProducts[].rateCardEntries` | array of RateCardEntry | no | Per-unit rates for a `rateCard`-priced line. Supplying any entry moves pricing onto the rate card, so the line-level discount fields stop being reconciled. | | `nonSubscriptionProducts[].rateCardEntries[].billableComponentId` | string | no | UUID of the BillableComponent this row meters. Optional — a row may price a unit of measure without naming a component — but a UUID that does not resolve rejects the whole request. | | `nonSubscriptionProducts[].rateCardEntries[].conversion` | string (decimal) | no | Factor converting the billable unit into the reporting unit — 1000 when usage is metered per call but reported per thousand. | | `nonSubscriptionProducts[].rateCardEntries[].description` | string | no | Free-text label for the row, carried through to the stored rate-card entry. | | `nonSubscriptionProducts[].rateCardEntries[].listPrice` | string (decimal) | no | Undiscounted rate per `unitOfMeasure`, in the order currency. Honoured only while the product line's `enforceListPrice` is false; otherwise the product line's own list price wins. | | `nonSubscriptionProducts[].rateCardEntries[].unitOfMeasure` | string | yes | The unit this row is priced in. | | `nonSubscriptionProducts[].rateCardEntries[].unitPrice` | string (decimal) | yes | Negotiated rate per `unitOfMeasure`, in the order currency. | | `nonSubscriptionProducts[].serviceEndDate` | string (date) | no | Last day of the line's service period. The order's own service end is the latest across all its lines, and is left open on an evergreen order. | | `nonSubscriptionProducts[].serviceStartDate` | string (date) | no | First day of the line's service period. The order's own service start is the earliest across all its lines. | | `nonSubscriptionProducts[].tags` | array of LineTag | no | Value-accounting tags to allocate this line by. Requires value-allocation tagging to be enabled for sales orders. | | `nonSubscriptionProducts[].tags[].dimension` | string | yes | Name of the value-accounting dimension, as configured for the tenant. Resolved by name, not by id. | | `nonSubscriptionProducts[].tags[].value` | string | yes | Name of the value within that dimension. The pair must resolve to an existing tag and the dimension must be enabled for sales orders; an unknown pair rejects the whole request. | | `nonSubscriptionProducts[].tiers` | array of LineTier | no | The pricing tiers of a `tiered`, `volume` or `stairstep` line. The ranges must be contiguous, and the last tier is left open-ended by omitting its `toQuantity`. | | `nonSubscriptionProducts[].tiers[].fromQuantity` | string (decimal) | yes | Lower bound of the tier's quantity range, inclusive. | | `nonSubscriptionProducts[].tiers[].listPrice` | string (decimal) | no | Undiscounted price for this tier, in the order currency. Defaults to `unitPrice`, i.e. no discount on the tier. | | `nonSubscriptionProducts[].tiers[].priceModel` | string | no | How `unitPrice` applies within the tier: `perUnit` (the default) charges it for every unit in the range, `flat` charges it once for the whole tier. | | `nonSubscriptionProducts[].tiers[].toQuantity` | string (decimal) | no | Upper bound of the tier's quantity range. Omit it on the last tier to leave the range open-ended. | | `nonSubscriptionProducts[].tiers[].unitPrice` | string (decimal) | yes | Negotiated price for this tier, in the order currency. Charged per unit in the range when `priceModel` is `perUnit`, or once for the whole tier when it is `flat`. | | `nonSubscriptionProducts[].totalPrice` | string (decimal) | no | Total for the line, in the order currency. Persisted exactly as supplied rather than recomputed — omit it and the line's own pricing drives the total. | | `nonSubscriptionProducts[].unitOfMeasure` | string | no | The unit the line is priced and metered in. Defaults to the product line's own unit. | | `nonSubscriptionProducts[].unitPrice` | string (decimal) | no | Negotiated price per unit, in the order currency. The highest precedence of the three pricing inputs — supply it and `discountAmount` and `discountPercentage` are recomputed from it (discountAmount = listPrice − unitPrice). | | `oneTimeDiscountAllocation` | string | no | How the one-time discount is spread across the order's invoices: `firstInvoice` puts all of it on the first, `alignToInvoiceFrequency` spreads it over the invoicing schedule, `custom` leaves the allocation to be set by hand. | | `oneTimeDiscountAmount` | string (decimal) | no | A one-off discount on the order as a whole, in the order currency. Independent of the per-line discount fields. | | `opportunityId` | string | no | Id of the originating opportunity in the CRM, kept as text. | | `opportunityName` | string | no | Name of the opportunity the order came out of. | | `paymentTermCode` | string | no | The order's payment term, named by its code. Defaults to the term on the entity's invoice policy active at `bookDate`. | | `priceBookId` | string | no | UUID of the PriceBook the lines take their list prices from. Defaults to the standard price book. | | `purchaseOrder` | string | no | The customer's own purchase-order number for this order. | | `salesArrangementId` | string | no | UUID of an existing SalesArrangement to file the order under. Omit it and a new arrangement is created for the customer in the order currency. | | `serviceStartDate` | string (date) | yes | First day of the order's service period. Once the lines are built the order takes the earliest line start instead, so this is what governs an order whose lines carry no dates of their own. | | `signDate` | string (date) | no | Day the order was signed. | | `subscriptions` | array of Subscription | no | The recurring part of the order. Each subscription carries its own term and its own product lines. | | `subscriptions[].autoRenew` | boolean | no | Whether the subscription renews itself at the end of its term. | | `subscriptions[].externalSubscriptionId` | string | no | The subscription's id in the system that owns it, kept for reconciliation. | | `subscriptions[].products` | array of SubscriptionProductLine | yes | The recurring product lines of this subscription. Every one must be a subscription-based product line — a one-time line placed here is rejected with `INVALID_SUBSCRIPTION_PRODUCT` and belongs in the order's `nonSubscriptionProducts` instead. | | `subscriptions[].products[].allowance` | LineAllowance | no | The Allowance & Overage terms of a Commit with Overage line. Required for that line type and rejected on any other; omitted fields default from the product catalog. | | `subscriptions[].products[].allowance.allowance` | string (decimal) | yes | Committed quantity per allowance term, in the catalog Allowance's unit of measure. | | `subscriptions[].products[].allowance.allowanceTerm` | string | no | The period the allowance covers: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually` or `orderTerm`. Defaults to the product catalog's Allowance term. | | `subscriptions[].products[].allowance.maximumRolloverLimit` | string (decimal) | no | Cap on the quantity that may roll over. Accepted only when the catalog Allowance has rollover enabled, and defaults to its setting. | | `subscriptions[].products[].allowance.overage` | LineOverage | no | Discount and invoicing overrides for the line's overage. Omit it to take everything from the catalog Overage and its price-book entry. | | `subscriptions[].products[].allowance.overage.discountAmount` | string (decimal) | no | Discount per unit off the overage price, in the order currency. Takes precedence over `discountPercentage` when both are sent; omit both to price the overage exactly as its price-book entry has it. | | `subscriptions[].products[].allowance.overage.discountPercentage` | string (decimal) | no | Discount per unit off the overage price on a 0–100 scale, so `10` means ten percent. Used only when `discountAmount` is not given. | | `subscriptions[].products[].allowance.overage.invoiceTiming` | string | no | Whether the overage invoices `advance` (at the start of its period) or `arrears` (after it). Defaults from the entity's active invoice policy for overage lines, then from the parent line. | | `subscriptions[].products[].allowance.overage.lineInvoiceDay` | string | no | Which day of the period the overage invoices on: `anniversary`, `first`, `last` or `specific`. Defaults from the entity's active invoice policy for overage lines, then from the parent line. | | `subscriptions[].products[].allowance.rolloverDuration` | string (decimal) | no | How many allowance terms unused allowance rolls over for. Accepted only when the catalog Allowance has rollover enabled, and defaults to its setting. | | `subscriptions[].products[].discountAmount` | string (decimal) | no | Discount per unit off `listPrice`, in the order currency. Used when no `unitPrice` is given, and then drives the other two. | | `subscriptions[].products[].discountPercentage` | string (decimal) | no | Discount per unit off `listPrice` on a 0–100 scale, so `10` means ten percent. Stored as a 0–1 fraction. The lowest precedence of the three — used only when neither `unitPrice` nor `discountAmount` is given. | | `subscriptions[].products[].externalIdentifiers` | array of ExternalIdentifier | no | Identifiers this line carries in other systems, upserted by `fieldName`. | | `subscriptions[].products[].externalIdentifiers[].externalUrl` | string | no | Deep link to the record in the owning system, shown beside the id. | | `subscriptions[].products[].externalIdentifiers[].fieldName` | string | yes | Names the ExternalIdentifierConfiguration this id belongs to, as that configuration spells it. It must already be configured for the tenant; an unknown name is rejected. Identifiers are upserted by this name, so sending it again overwrites the stored value. | | `subscriptions[].products[].externalIdentifiers[].value` | string | yes | The identifier as the owning system spells it. | | `subscriptions[].products[].invoiceFrequency` | string | no | How often the line invoices — an InvoiceFrequency member: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually`, `once`, `custom`, `template`, `milestone`, `allAtOnce` or `termEnd`. Defaults to the product line's own setting. | | `subscriptions[].products[].invoiceFrequencyTemplateId` | string | no | UUID of the invoice frequency template | | `subscriptions[].products[].invoiceTiming` | string | no | Whether the line invoices `advance` (at the start of the service period) or `arrears` (after it). Defaults from the entity's active invoice policy for this line type. | | `subscriptions[].products[].lineInvoiceDay` | string | no | Which day of the period the line invoices on: `anniversary`, `first`, `last` or `specific`. Defaults from the entity's active invoice policy. | | `subscriptions[].products[].listPrice` | string (decimal) | no | Undiscounted price per unit, in the order currency. Defaults to the list price the order's price book holds for this product line, and is the figure the other pricing fields are reconciled against. | | `subscriptions[].products[].priceModel` | string | no | How the line is priced: `flat`, `perUnit`, `tiered`, `volume`, `stairstep` or `rateCard`. Defaults to the product line's own model. `tiered`, `volume` and `stairstep` take their prices from `tiers` and `rateCard` from `rateCardEntries`, so for those the line-level discount fields are not reconciled. `thresholdBased` is rejected: it needs a base line plus a metered usage child, which this endpoint does not build. | | `subscriptions[].products[].pricingTerm` | string | no | The period the price is quoted for: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually` or `once`. Defaults to the product line's own term. | | `subscriptions[].products[].productLineId` | string | yes | UUID of the ProductLine being ordered. It settles the product, the line type and the billing rules every omitted field defaults from. It must exist, and it must not be threshold-based or prepaid: those need a base line plus a metered usage child under one product, which this endpoint does not build. | | `subscriptions[].products[].quantity` | string (decimal) | no | Number of units ordered. Required unless the line takes its quantity from somewhere else: usage-metered line types (PayAsYouGo, Usage, Billable*) meter it from usage records and tier-priced models (`tiered`, `volume`, `stairstep`, `thresholdBased`) derive it from the tier structure, so for those it defaults to zero. Anywhere else, omitting it fails the request. | | `subscriptions[].products[].rampDeal` | RampDeal | no | Scheduled changes to quantity and price across the line's life. Subscription lines only. | | `subscriptions[].products[].rampDeal.interval` | string | yes | The granularity the deal steps at: `monthly`, `quarterly` or `annually`. | | `subscriptions[].products[].rampDeal.invoiceTiming` | string | no | Whether each segment invoices `advance` (at the start of its period) or `arrears` (after it). | | `subscriptions[].products[].rampDeal.ramps` | array of Ramp | yes | The segments of the deal in order, earliest first. Together they span the subscription line's service period. | | `subscriptions[].products[].rampDeal.ramps[].invoiceFrequency` | string | no | How often this segment invoices. Defaults to the subscription line's own setting. | | `subscriptions[].products[].rampDeal.ramps[].invoiceFrequencyTemplateId` | string | no | UUID of the invoice frequency template. | | `subscriptions[].products[].rampDeal.ramps[].listPrice` | string (decimal) | no | Undiscounted price per unit for this segment, in the order currency. Defaults to `unitPrice`. | | `subscriptions[].products[].rampDeal.ramps[].quantity` | string (decimal) | yes | Number of units in force for this segment. | | `subscriptions[].products[].rampDeal.ramps[].serviceEndDate` | string (date) | no | Last day of this segment. Defaults so that the segments run contiguously up to the next one. | | `subscriptions[].products[].rampDeal.ramps[].serviceStartDate` | string (date) | no | First day of this segment. Defaults so that the segments run contiguously from the subscription line's own start. | | `subscriptions[].products[].rampDeal.ramps[].unitPrice` | string (decimal) | yes | Negotiated price per unit for this segment, in the order currency. | | `subscriptions[].products[].rateCardEntries` | array of RateCardEntry | no | Per-unit rates for a `rateCard`-priced line. Supplying any entry moves pricing onto the rate card, so the line-level discount fields stop being reconciled. | | `subscriptions[].products[].rateCardEntries[].billableComponentId` | string | no | UUID of the BillableComponent this row meters. Optional — a row may price a unit of measure without naming a component — but a UUID that does not resolve rejects the whole request. | | `subscriptions[].products[].rateCardEntries[].conversion` | string (decimal) | no | Factor converting the billable unit into the reporting unit — 1000 when usage is metered per call but reported per thousand. | | `subscriptions[].products[].rateCardEntries[].description` | string | no | Free-text label for the row, carried through to the stored rate-card entry. | | `subscriptions[].products[].rateCardEntries[].listPrice` | string (decimal) | no | Undiscounted rate per `unitOfMeasure`, in the order currency. Honoured only while the product line's `enforceListPrice` is false; otherwise the product line's own list price wins. | | `subscriptions[].products[].rateCardEntries[].unitOfMeasure` | string | yes | The unit this row is priced in. | | `subscriptions[].products[].rateCardEntries[].unitPrice` | string (decimal) | yes | Negotiated rate per `unitOfMeasure`, in the order currency. | | `subscriptions[].products[].serviceEndDate` | string (date) | no | Last day of the line's service period. The order's own service end is the latest across all its lines, and is left open on an evergreen order. | | `subscriptions[].products[].serviceStartDate` | string (date) | no | First day of the line's service period. The order's own service start is the earliest across all its lines. | | `subscriptions[].products[].tags` | array of LineTag | no | Value-accounting tags to allocate this line by. Requires value-allocation tagging to be enabled for sales orders. | | `subscriptions[].products[].tags[].dimension` | string | yes | Name of the value-accounting dimension, as configured for the tenant. Resolved by name, not by id. | | `subscriptions[].products[].tags[].value` | string | yes | Name of the value within that dimension. The pair must resolve to an existing tag and the dimension must be enabled for sales orders; an unknown pair rejects the whole request. | | `subscriptions[].products[].tiers` | array of LineTier | no | The pricing tiers of a `tiered`, `volume` or `stairstep` line. The ranges must be contiguous, and the last tier is left open-ended by omitting its `toQuantity`. | | `subscriptions[].products[].tiers[].fromQuantity` | string (decimal) | yes | Lower bound of the tier's quantity range, inclusive. | | `subscriptions[].products[].tiers[].listPrice` | string (decimal) | no | Undiscounted price for this tier, in the order currency. Defaults to `unitPrice`, i.e. no discount on the tier. | | `subscriptions[].products[].tiers[].priceModel` | string | no | How `unitPrice` applies within the tier: `perUnit` (the default) charges it for every unit in the range, `flat` charges it once for the whole tier. | | `subscriptions[].products[].tiers[].toQuantity` | string (decimal) | no | Upper bound of the tier's quantity range. Omit it on the last tier to leave the range open-ended. | | `subscriptions[].products[].tiers[].unitPrice` | string (decimal) | yes | Negotiated price for this tier, in the order currency. Charged per unit in the range when `priceModel` is `perUnit`, or once for the whole tier when it is `flat`. | | `subscriptions[].products[].totalPrice` | string (decimal) | no | Total for the line, in the order currency. Persisted exactly as supplied rather than recomputed — omit it and the line's own pricing drives the total. | | `subscriptions[].products[].unitOfMeasure` | string | no | The unit the line is priced and metered in. Defaults to the product line's own unit. | | `subscriptions[].products[].unitPrice` | string (decimal) | no | Negotiated price per unit, in the order currency. The highest precedence of the three pricing inputs — supply it and `discountAmount` and `discountPercentage` are recomputed from it (discountAmount = listPrice − unitPrice). | | `subscriptions[].serviceEndDate` | string (date) | no | Last day the subscription is in service. Omit it on an evergreen subscription. | | `subscriptions[].serviceStartDate` | string (date) | yes | First day the subscription is in service. | | `subscriptions[].term` | string (decimal) | no | Intended length of a `termed` subscription in months. What actually bounds the subscription is `serviceStartDate` and `serviceEndDate`, so supply the end date to fix when it ends. | | `subscriptions[].termType` | string | yes | `termed` for a subscription of fixed length, `evergreen` for an open-ended one that keeps renewing. A single evergreen subscription makes the whole order evergreen, which leaves the order's service end date open. | Example: ```json { "approvalStatus": "approved", "bookDate": "2026-01-15", "currency": "EUR", "customer": { "additionalContactEmail": "finance@contoso.example", "addresses": [ { "attention": "Accounts Payable", "city": "München", "country": "DE", "isDefault": true, "line1": "Prinzregentenstraße 22", "line2": "4. OG", "stateProvince": "Bayern", "type": "billTo", "zipCode": "80538" } ], "billableTravelExpenseInvoiceDetail": "detailed", "businessIdentificationNumber": "HRB 123456", "businessType": "company", "customerType": "default", "defaultCurrency": "EUR", "entityId": "1c9e5f38-7b04-4a26-8d31-5f60e2ab7c95", "exemptionNumber": "EX-2026-0042", "externalIdentifiers": [ { "externalUrl": "https://netsuite.example.com/app/accounting/transactions/salesord.nl?id=10042", "fieldName": "netsuiteInternalId", "value": "SO-10042" } ], "invoiceConsolidation": "customer", "invoiceDeliveryMode": "automated", "invoiceEmail": "ap@contoso.example", "isExemptFromCreditCardFee": true, "mainContactName": "A. Meyer", "mainEmail": "hello@contoso.example", "name": "Contoso GmbH", "parentId": "4d2b8e70-91a3-4c6f-b5d8-1e7a03f9c284", "partnerId": "4d2b8e70-91a3-4c6f-b5d8-1e7a03f9c284", "partnerType": "reseller", "payment": { "externalCustomerId": "cus_NffrFeUfNV2Hib", "externalPaymentMethodId": "pm_1NGY3s2eZvKYlo2CQmRz9k4T", "provider": "Stripe" }, "paymentTermCode": "NET30", "preferredLanguage": "english", "salesChannel": "salesAssisted", "segment": "Enterprise", "status": "active", "taxExempt": true, "taxExemptCode": "RESALE", "taxRegistrations": [ { "country": "DE", "effectiveDate": "2026-01-01", "isActive": true, "note": "Filed with the Munich tax office", "taxNumber": "DE123456789", "type": "VAT" } ] }, "customerId": "4d2b8e70-91a3-4c6f-b5d8-1e7a03f9c284", "dataSource": "Salesforce", "effectiveDate": "2026-02-01", "entityId": "1c9e5f38-7b04-4a26-8d31-5f60e2ab7c95", "excludeFromTaxEngine": true, "externalIdentifiers": [ { "externalUrl": "https://netsuite.example.com/app/accounting/transactions/salesord.nl?id=10042", "fieldName": "netsuiteInternalId", "value": "SO-10042" } ], "externalizedInvoicing": true, "nonSubscriptionProducts": [ { "allowance": { "allowance": "1000", "allowanceTerm": "monthly", "maximumRolloverLimit": "500", "overage": { "discountAmount": "1.50", "discountPercentage": "10", "invoiceTiming": "arrears", "lineInvoiceDay": "anniversary" }, "rolloverDuration": "2" }, "discountAmount": "120.00", "discountPercentage": "10", "externalIdentifiers": [ { "externalUrl": "https://netsuite.example.com/app/accounting/transactions/salesord.nl?id=10042", "fieldName": "netsuiteInternalId", "value": "SO-10042" } ], "invoiceFrequency": "monthly", "invoiceFrequencyTemplateId": "9f1c2d34-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "invoiceTiming": "advance", "lineInvoiceDay": "anniversary", "listPrice": "1200.00", "priceModel": "perUnit", "pricingTerm": "annually", "productLineId": "6a1f2c94-3d58-4b17-9e02-7c8ab4d5f610", "quantity": "10", "rateCardEntries": [ { "billableComponentId": "9f1c2d34-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "conversion": "1000", "description": "API calls beyond the included volume", "listPrice": "0.005", "unitOfMeasure": "apiCall", "unitPrice": "0.004" } ], "serviceEndDate": "2026-12-31", "serviceStartDate": "2026-01-01", "tags": [ { "dimension": "Department", "value": "Engineering" } ], "tiers": [ { "fromQuantity": "0", "listPrice": "15.00", "priceModel": "perUnit", "toQuantity": "100", "unitPrice": "12.50" } ], "totalPrice": "12960.00", "unitOfMeasure": "seat", "unitPrice": "1080.00" } ], "oneTimeDiscountAllocation": "firstInvoice", "oneTimeDiscountAmount": "500.00", "opportunityId": "0065f00000ABCDEfg", "opportunityName": "Contoso — 2026 renewal", "paymentTermCode": "NET30", "priceBookId": "72fd1a08-6c39-4e5b-83a0-9d4127be6f5c", "purchaseOrder": "PO-2026-0042", "salesArrangementId": "e30a76b5-c284-4d91-a7f3-2b508c6de194", "serviceStartDate": "2026-01-01", "signDate": "2026-01-10", "subscriptions": [ { "autoRenew": true, "externalSubscriptionId": "sub_1NGY3s2eZvKYlo2C", "products": [ { "allowance": { "allowance": "1000", "allowanceTerm": "monthly", "maximumRolloverLimit": "500", "overage": { "discountAmount": null, "discountPercentage": null, "invoiceTiming": null, "lineInvoiceDay": null }, "rolloverDuration": "2" }, "discountAmount": "120.00", "discountPercentage": "10", "externalIdentifiers": [ { "externalUrl": null, "fieldName": null, "value": null } ], "invoiceFrequency": "monthly", "invoiceFrequencyTemplateId": "9f1c2d34-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "invoiceTiming": "advance", "lineInvoiceDay": "anniversary", "listPrice": "1200.00", "priceModel": "perUnit", "pricingTerm": "annually", "productLineId": "6a1f2c94-3d58-4b17-9e02-7c8ab4d5f610", "quantity": "10", "rampDeal": { "interval": "annually", "invoiceTiming": "advance", "ramps": [ null ] }, "rateCardEntries": [ { "billableComponentId": null, "conversion": null, "description": null, "listPrice": null, "unitOfMeasure": null, "unitPrice": null } ], "serviceEndDate": "2026-12-31", "serviceStartDate": "2026-01-01", "tags": [ { "dimension": null, "value": null } ], "tiers": [ { "fromQuantity": null, "listPrice": null, "priceModel": null, "toQuantity": null, "unitPrice": null } ], "totalPrice": "12960.00", "unitOfMeasure": "seat", "unitPrice": "1080.00" } ], "serviceEndDate": "2026-12-31", "serviceStartDate": "2026-01-01", "term": "12", "termType": "termed" } ] } ``` ## Response `200` Success Under `data` (createSalesOrderOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `salesOrderId` | string | yes | UUID of the new sales order — what every other sales-order route addresses it by. | ## 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 | | --- | --- | | `INVALID_PRODUCT_LINE` | 400 | | `INVALID_SUBSCRIPTION_PRODUCT` | 400 | | `VALIDATION_FAILED` | 400 | | `NOT_FOUND` | 404 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_PRODUCT_LINE, INVALID_SUBSCRIPTION_PRODUCT, VALIDATION_FAILED - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found. Codes: 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 --- # Get Sales Order `GET /accounting/sales-orders/{id}` Group: Accounting › Sales Orders · Operation ID: `getSalesOrder` Fetch a single sales order by its id (a UUID), bound from the :id path segment. Returns the order header with its customer, entity and external identifiers — the same item schema as POST accounting/sales-orders/search. Read-only. Raises NOT_FOUND when no order has that id (should map to 404). Every request must carry the Api-Version header. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getSalesOrder) ## 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`. | | `id` | path | string | yes | Id of the sales order to fetch (a UUID); bound from the :id path segment. | ## Response `200` Success Under `data` (getSalesOrderOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Id of the sales order (a UUID) — the public identifier used by GET accounting/sales-orders/:id. | | `orderNumber` | string | yes | System-assigned sales-order number. Human-readable reference used on invoices and proposals. | | `orderType` | string | no | Type of sales order, e.g. new business, renewal, expansion, downgrade. | | `approvalStatus` | string | no | Approval state of the order. An `ApprovalStatus` member, e.g. 'draft', 'pendingApproval', 'approved'. | | `bookDate` | string (date) | yes | Date the order was booked (YYYY-MM-DD). Drives revenue-recognition period assignment. | | `serviceStartDate` | string (date) | no | First date services or products under this order are delivered (YYYY-MM-DD). | | `serviceEndDate` | string (date) | no | Last date services or products under this order are delivered (YYYY-MM-DD). Absent for evergreen orders. | | `signDate` | string (date) | no | Date the order was signed (YYYY-MM-DD), when recorded. | | `salesEffectiveDate` | string (date) | no | Date the order becomes effective for sales reporting (YYYY-MM-DD), when recorded. | | `isEvergreen` | boolean | no | True when the order renews indefinitely rather than ending on serviceEndDate. | | `excludeFromTaxEngine` | boolean | no | When true, invoices, credit memos and debit memos from this order are never submitted to the tax engine. | | `term` | string (decimal) | no | Contract term length in months. Absent for evergreen (open-ended) orders. | | `currency` | string | yes | ISO 4217 three-letter currency code (e.g. USD, EUR, GBP) the order is denominated in. | | `totalContract` | string (decimal) | no | Total contract value across all line items, in the order currency (see `currency`). | | `invoicedAmount` | string (decimal) | no | Amount already invoiced against the contract value, in the order currency (see `currency`). The remaining backlog is totalContract − invoicedAmount. | | `oneTimeDiscountAmount` | string (decimal) | no | Lump-sum discount applied to the entire order, in the order currency (see `currency`). | | `oneTimeDiscountAllocation` | string | no | How the lump-sum discount is allocated across products. One of 'firstInvoice', 'alignToInvoiceFrequency'. | | `paymentTerm` | string | no | Payment-term identifier or display name applied to invoices from this order. | | `purchaseOrder` | string | no | The customer's own purchase-order reference for this order, when supplied. | | `memo` | string | no | Free-text memo recorded on the order. | | `crmContractId` | string | no | Identifier of the originating contract in the CRM, when the order came from one. | | `salesArrangement` | string | no | Number of the sales arrangement this order belongs to. | | `salesArrangementId` | string | no | Id of that sales arrangement (a UUID) — the form a write action accepts. | | `paymentTermId` | string | no | Id of the payment term (a UUID). paymentTerm carries its code; this is the form a write action accepts. | | `opportunityId` | string | no | Identifier of the opportunity this order came from, as issued by the CRM — not an id from this API. | | `opportunityName` | string | no | Name of that CRM opportunity. | | `dataSource` | string | no | System the order originated in when it was integrated rather than entered here, e.g. 'Salesforce'. | | `priceBookId` | string | no | Id of the price book this order's prices were taken from (a UUID). | | `priceBookName` | string | no | Name of that price book, denormalised onto the order. | | `externalizedInvoicing` | boolean | no | True when invoicing for this order happens in another system, so no invoices are generated from it here. | | `customer` | object | yes | Customer denormalised from the linked Customer record (key fields only). Fetch the full record from GET accounting/customers/:id. | | `customer.id` | string | yes | Id of the customer (a UUID). | | `customer.customerNumber` | string | no | System-assigned customer reference number. | | `customer.name` | string | yes | Legal or business name of the customer. | | `customer.invoiceEmail` | string | no | Email that receives invoices for this customer. | | `customer.mainEmail` | string | no | Primary business contact email for the customer. | | `customer.segment` | string | no | Market-segment classification. One of 'SMB', 'MidMarket', 'Enterprise'. | | `customer.taxExempt` | boolean | no | True if the customer is exempt from tax on all transactions. | | `entityId` | string | yes | Id of the legal entity that owns this sales order (a UUID). | | `entityName` | string | no | Name of the owning legal entity. | | `entityNumber` | string | yes | System-assigned number of the legal entity that owns this order. | | `soldToAddress` | object | yes | Sold-to address used for tax determination on this order. | | `soldToAddress.addressId` | string | no | Id of the address record (a UUID), when one is stored. | | `soldToAddress.line1` | string | yes | Street address line 1: number, street name, or PO box. | | `soldToAddress.line2` | string | no | Apartment, suite, unit, building, floor, etc. | | `soldToAddress.city` | string | yes | City or locality. | | `soldToAddress.stateProvince` | string | no | State, province, or region. | | `soldToAddress.country` | string | yes | Country name or ISO 3166-1 country code. | | `soldToAddress.zipCode` | string | yes | Postal code or ZIP code as required by the country postal system. | | `billToAddress` | object | yes | Bill-to address where invoices from this order are sent. | | `billToAddress.addressId` | string | no | Id of the address record (a UUID), when one is stored. | | `billToAddress.line1` | string | yes | Street address line 1: number, street name, or PO box. | | `billToAddress.line2` | string | no | Apartment, suite, unit, building, floor, etc. | | `billToAddress.city` | string | yes | City or locality. | | `billToAddress.stateProvince` | string | no | State, province, or region. | | `billToAddress.country` | string | yes | Country name or ISO 3166-1 country code. | | `billToAddress.zipCode` | string | yes | Postal code or ZIP code as required by the country postal system. | | `subscriptions` | array of object | yes | Subscriptions on the order, one entry per subscription, each with the product lines that belong to it. Mirrors the subscriptions[] the create action takes. | | `subscriptions[].id` | string | no | Id of the subscription (a UUID). Absent when the line records only a subscription number. | | `subscriptions[].subscriptionNumber` | string (decimal) | no | System-assigned subscription number. | | `subscriptions[].externalSubscriptionId` | string | no | The subscribing system's own id for this subscription, when the order was integrated from one. | | `subscriptions[].termType` | string | no | Whether the subscription runs to a fixed end date or renews indefinitely. One of 'termed', 'evergreen'. | | `subscriptions[].autoRenew` | boolean | no | True when the subscription renews automatically at the end of its term. | | `subscriptions[].serviceStartDate` | string (date) | no | First date the subscription delivers service (YYYY-MM-DD). | | `subscriptions[].serviceEndDate` | string (date) | no | Last date the subscription delivers service (YYYY-MM-DD). Absent while evergreen. Create takes a term in months instead; this is what that term resolved to. | | `subscriptions[].products` | array of object | yes | Product lines in this subscription. Each entry is a SalesOrderProductLine and may carry tiered pricing, rate-card pricing, ramp deals, or per-item breakdowns. | | `subscriptions[].products[].productCode` | string | yes | Company-specific code of the product. | | `subscriptions[].products[].productId` | string | yes | Id of the product (a UUID). | | `subscriptions[].products[].productLineId` | string | yes | Id of the product line under the product (a UUID). | | `subscriptions[].products[].productName` | string | no | Product name as recorded on this line. May differ from the product's current name — the line keeps the name it was sold under. | | `subscriptions[].products[].salesOrderProductLineId` | string | no | Id of this sales-order line (a UUID). | | `subscriptions[].products[].quantity` | string (decimal) | yes | Quantity of the product on this line. For ramp deals the per-period quantity is on each ramp instead. | | `subscriptions[].products[].listPrice` | string (decimal) | no | Per-unit list price before discounts. | | `subscriptions[].products[].unitPrice` | string (decimal) | no | Per-unit price after discounts. Equals listPrice − discountAmount, or listPrice * (1 − discountPercentage/100). | | `subscriptions[].products[].totalPrice` | string (decimal) | no | Total price for this line (unitPrice * quantity). | | `subscriptions[].products[].discountAmount` | string (decimal) | no | Per-unit discount in the order currency. | | `subscriptions[].products[].discountPercentage` | string (decimal) | no | Per-unit discount as a percentage (e.g. 10 for 10%). | | `subscriptions[].products[].invoiceFrequency` | string | no | How often invoices are generated for this line, e.g. monthly, quarterly, annually. | | `subscriptions[].products[].invoiceTiming` | string | no | Whether each invoice for this line is raised at the start or the end of the period it covers. One of 'inAdvance', 'inArrears'. | | `subscriptions[].products[].lineInvoiceDay` | string | no | Which day of the period the invoice is dated, e.g. 'firstDayOfMonth', 'specificDay', 'serviceStartDate'. | | `subscriptions[].products[].priceModel` | string | no | How the price is derived from quantity, e.g. 'flatFee', 'perUnit', 'tiered', 'volume'. Tiered and volume models carry their bands in tiers. | | `subscriptions[].products[].unitOfMeasure` | string | no | Unit the quantity is counted in, e.g. 'each', 'user', 'gigabyte'. | | `subscriptions[].products[].pricingTerm` | string | no | Period the unit price is quoted for, e.g. 'monthly', 'annual' — the basis prices are normalised to before invoicing. | | `subscriptions[].products[].serviceStartDate` | string (date) | no | First date services for this product are delivered (YYYY-MM-DD). Defaults to the order-level serviceStartDate. | | `subscriptions[].products[].serviceEndDate` | string (date) | no | Last date services for this product are delivered (YYYY-MM-DD). Defaults to the order-level serviceEndDate. | | `subscriptions[].products[].externalIdentifiers` | array of object | yes | External identifiers recorded against this line, one per configured field. Empty when none are. | | `subscriptions[].products[].externalIdentifiers[].fieldName` | string | yes | Configured field the identifier belongs to, e.g. 'salesforceLineId'. | | `subscriptions[].products[].externalIdentifiers[].value` | string | yes | The identifier's value in that external system. | | `subscriptions[].products[].externalIdentifiers[].externalUrl` | string | no | Deep link to the record in that system, when the configuration defines one. | | `subscriptions[].products[].overage` | object | no | Overage pricing for usage above the line's allowance. Present only when the product line has an allowance configured. | | `subscriptions[].products[].overage.listPrice` | string (decimal) | no | Per-unit list price for overage usage. | | `subscriptions[].products[].overage.unitPrice` | string (decimal) | no | Per-unit price for overage usage after discounts. | | `subscriptions[].products[].overage.discountAmount` | string (decimal) | no | Per-unit overage discount in the order currency. | | `subscriptions[].products[].overage.discountPercentage` | string (decimal) | no | Per-unit overage discount as a percentage. | | `subscriptions[].products[].rateCardEntries` | array of object | no | Rate-card rows for this line. Present when the product line uses rate-card pricing. | | `subscriptions[].products[].rateCardEntries[].id` | string | yes | Id of this rate-card entry (a UUID). | | `subscriptions[].products[].rateCardEntries[].identifier` | string | no | BillableComponent identifier this rate-card row meters. | | `subscriptions[].products[].rateCardEntries[].label` | string | no | Free-text label for this rate-card row, used as an additional discriminator alongside the identifier. | | `subscriptions[].products[].rateCardEntries[].unitOfMeasure` | string | yes | Unit in which this rate-card row is priced. | | `subscriptions[].products[].rateCardEntries[].unitPrice` | string (decimal) | yes | Per-unit rate negotiated for this order. | | `subscriptions[].products[].rateCardEntries[].currency` | string | yes | ISO 4217 currency code for this rate-card row. | | `subscriptions[].products[].tiers` | array of object | no | Pricing tiers for this product line. Present only for tiered, volume or stairstep price models. | | `subscriptions[].products[].tiers[].fromQuantity` | string (decimal) | yes | Lower bound of the quantity range (inclusive). | | `subscriptions[].products[].tiers[].toQuantity` | string (decimal) | no | Upper bound of the quantity range (inclusive). Absent on an open-ended last tier. | | `subscriptions[].products[].tiers[].unitPrice` | string (decimal) | yes | Per-unit price for quantities in this tier. | | `subscriptions[].products[].tiers[].listPrice` | string (decimal) | no | Per-unit list price before discount. | | `subscriptions[].products[].tiers[].priceModel` | string | no | Per-tier price model. One of 'flat', 'perUnit'. | | `subscriptions[].products[].tags` | array of object | no | Value-accounting tags applied to this product, as dimension/value pairs. Tags propagate to journal-entry lines for dimensional reporting. | | `subscriptions[].products[].tags[].dimension` | string | yes | Tag dimension name, e.g. 'Department', 'Project'. | | `subscriptions[].products[].tags[].value` | string | yes | Value within the dimension, e.g. 'Engineering', 'Alpha-2026'. | | `subscriptions[].products[].items` | array of object | no | Per-item breakdown: one entry per SalesOrderProductItem (product x inventory item) under this sales-order product. Present only for inventory-tracked products. | | `subscriptions[].products[].items[].id` | string | yes | Id of this SalesOrderProductItem junction record (a UUID). | | `subscriptions[].products[].items[].itemId` | string | no | Id of the linked inventory item (a UUID). Absent when the record has no item set. | | `subscriptions[].products[].items[].itemCode` | string | no | Item code from ItemMaster. | | `subscriptions[].products[].items[].itemName` | string | no | Item name from ItemMaster. | | `subscriptions[].products[].items[].productName` | string | no | Product name denormalised from the parent SalesOrderProduct. | | `subscriptions[].products[].items[].itemOrdered` | string (decimal) | yes | Ordered quantity for this item, after explosion: line quantity x product-item quantity. | | `subscriptions[].products[].items[].totalShipped` | string (decimal) | no | Sum of shipped quantity across all linked SalesOrderShipmentItems. | | `subscriptions[].products[].rampDeal` | object | no | Ramp-deal definition. Present only when the product is a ramp deal; ramps echo the persisted SalesOrderProductLine records. | | `subscriptions[].products[].rampDeal.interval` | string | yes | Ramp interval mode. One of 'annually', 'specificDates'. | | `subscriptions[].products[].rampDeal.invoiceTiming` | string | yes | Ramp invoice timing. One of 'perInterval', 'allAtOnce'. | | `subscriptions[].products[].rampDeal.ramps` | array of object | yes | Persisted ramps, ordered by start date. At least two. | | `subscriptions[].products[].rampDeal.ramps[].serviceStartDate` | string (date) | yes | First date this ramp's services are delivered (YYYY-MM-DD). | | `subscriptions[].products[].rampDeal.ramps[].serviceEndDate` | string (date) | yes | Last date this ramp's services are delivered (YYYY-MM-DD). | | `subscriptions[].products[].rampDeal.ramps[].quantity` | string (decimal) | yes | Quantity for this ramp period. | | `subscriptions[].products[].rampDeal.ramps[].listPrice` | string (decimal) | no | Per-unit list price for this ramp before discount. | | `subscriptions[].products[].rampDeal.ramps[].unitPrice` | string (decimal) | no | Per-unit price for this ramp after discount. | | `subscriptions[].products[].rampDeal.ramps[].invoiceFrequency` | string | no | How often this ramp is invoiced. | | `subscriptions[].products[].prepaidConfiguration` | object | no | Prepaid balance details for this product. Absent when the product is not configured as prepaid. | | `subscriptions[].products[].prepaidConfiguration.overConsumptionBehavior` | string | yes | Behaviour when consumption exceeds the remaining prepaid balance. One of 'allowNegative' (balance may go negative and is reconciled at renewal) or 'overage' (usage beyond the balance switches to pay-as-you-go invoicing). | | `subscriptions[].products[].prepaidConfiguration.rolloverAtRenewal` | boolean | yes | Whether any unused balance carries forward when the subscription renews. | | `subscriptions[].products[].prepaidConfiguration.balance` | string (decimal) | no | Remaining prepaid quantity for this product. Decreases as consumption is applied. | | `subscriptions[].products[].prepaidConfiguration.consumedAmount` | string (decimal) | no | Total quantity consumed from the prepaid balance to date. | | `nonSubscriptionProducts` | array of object | yes | One-time and other non-subscription product lines on the order, e.g. one-off services or perpetual licences. Same line schema as a subscription's products. | | `nonSubscriptionProducts[].productCode` | string | yes | Company-specific code of the product. | | `nonSubscriptionProducts[].productId` | string | yes | Id of the product (a UUID). | | `nonSubscriptionProducts[].productLineId` | string | yes | Id of the product line under the product (a UUID). | | `nonSubscriptionProducts[].productName` | string | no | Product name as recorded on this line. May differ from the product's current name — the line keeps the name it was sold under. | | `nonSubscriptionProducts[].salesOrderProductLineId` | string | no | Id of this sales-order line (a UUID). | | `nonSubscriptionProducts[].quantity` | string (decimal) | yes | Quantity of the product on this line. For ramp deals the per-period quantity is on each ramp instead. | | `nonSubscriptionProducts[].listPrice` | string (decimal) | no | Per-unit list price before discounts. | | `nonSubscriptionProducts[].unitPrice` | string (decimal) | no | Per-unit price after discounts. Equals listPrice − discountAmount, or listPrice * (1 − discountPercentage/100). | | `nonSubscriptionProducts[].totalPrice` | string (decimal) | no | Total price for this line (unitPrice * quantity). | | `nonSubscriptionProducts[].discountAmount` | string (decimal) | no | Per-unit discount in the order currency. | | `nonSubscriptionProducts[].discountPercentage` | string (decimal) | no | Per-unit discount as a percentage (e.g. 10 for 10%). | | `nonSubscriptionProducts[].invoiceFrequency` | string | no | How often invoices are generated for this line, e.g. monthly, quarterly, annually. | | `nonSubscriptionProducts[].invoiceTiming` | string | no | Whether each invoice for this line is raised at the start or the end of the period it covers. One of 'inAdvance', 'inArrears'. | | `nonSubscriptionProducts[].lineInvoiceDay` | string | no | Which day of the period the invoice is dated, e.g. 'firstDayOfMonth', 'specificDay', 'serviceStartDate'. | | `nonSubscriptionProducts[].priceModel` | string | no | How the price is derived from quantity, e.g. 'flatFee', 'perUnit', 'tiered', 'volume'. Tiered and volume models carry their bands in tiers. | | `nonSubscriptionProducts[].unitOfMeasure` | string | no | Unit the quantity is counted in, e.g. 'each', 'user', 'gigabyte'. | | `nonSubscriptionProducts[].pricingTerm` | string | no | Period the unit price is quoted for, e.g. 'monthly', 'annual' — the basis prices are normalised to before invoicing. | | `nonSubscriptionProducts[].serviceStartDate` | string (date) | no | First date services for this product are delivered (YYYY-MM-DD). Defaults to the order-level serviceStartDate. | | `nonSubscriptionProducts[].serviceEndDate` | string (date) | no | Last date services for this product are delivered (YYYY-MM-DD). Defaults to the order-level serviceEndDate. | | `nonSubscriptionProducts[].externalIdentifiers` | array of object | yes | External identifiers recorded against this line, one per configured field. Empty when none are. | | `nonSubscriptionProducts[].externalIdentifiers[].fieldName` | string | yes | Configured field the identifier belongs to, e.g. 'salesforceLineId'. | | `nonSubscriptionProducts[].externalIdentifiers[].value` | string | yes | The identifier's value in that external system. | | `nonSubscriptionProducts[].externalIdentifiers[].externalUrl` | string | no | Deep link to the record in that system, when the configuration defines one. | | `nonSubscriptionProducts[].overage` | object | no | Overage pricing for usage above the line's allowance. Present only when the product line has an allowance configured. | | `nonSubscriptionProducts[].overage.listPrice` | string (decimal) | no | Per-unit list price for overage usage. | | `nonSubscriptionProducts[].overage.unitPrice` | string (decimal) | no | Per-unit price for overage usage after discounts. | | `nonSubscriptionProducts[].overage.discountAmount` | string (decimal) | no | Per-unit overage discount in the order currency. | | `nonSubscriptionProducts[].overage.discountPercentage` | string (decimal) | no | Per-unit overage discount as a percentage. | | `nonSubscriptionProducts[].rateCardEntries` | array of object | no | Rate-card rows for this line. Present when the product line uses rate-card pricing. | | `nonSubscriptionProducts[].rateCardEntries[].id` | string | yes | Id of this rate-card entry (a UUID). | | `nonSubscriptionProducts[].rateCardEntries[].identifier` | string | no | BillableComponent identifier this rate-card row meters. | | `nonSubscriptionProducts[].rateCardEntries[].label` | string | no | Free-text label for this rate-card row, used as an additional discriminator alongside the identifier. | | `nonSubscriptionProducts[].rateCardEntries[].unitOfMeasure` | string | yes | Unit in which this rate-card row is priced. | | `nonSubscriptionProducts[].rateCardEntries[].unitPrice` | string (decimal) | yes | Per-unit rate negotiated for this order. | | `nonSubscriptionProducts[].rateCardEntries[].currency` | string | yes | ISO 4217 currency code for this rate-card row. | | `nonSubscriptionProducts[].tiers` | array of object | no | Pricing tiers for this product line. Present only for tiered, volume or stairstep price models. | | `nonSubscriptionProducts[].tiers[].fromQuantity` | string (decimal) | yes | Lower bound of the quantity range (inclusive). | | `nonSubscriptionProducts[].tiers[].toQuantity` | string (decimal) | no | Upper bound of the quantity range (inclusive). Absent on an open-ended last tier. | | `nonSubscriptionProducts[].tiers[].unitPrice` | string (decimal) | yes | Per-unit price for quantities in this tier. | | `nonSubscriptionProducts[].tiers[].listPrice` | string (decimal) | no | Per-unit list price before discount. | | `nonSubscriptionProducts[].tiers[].priceModel` | string | no | Per-tier price model. One of 'flat', 'perUnit'. | | `nonSubscriptionProducts[].tags` | array of object | no | Value-accounting tags applied to this product, as dimension/value pairs. Tags propagate to journal-entry lines for dimensional reporting. | | `nonSubscriptionProducts[].tags[].dimension` | string | yes | Tag dimension name, e.g. 'Department', 'Project'. | | `nonSubscriptionProducts[].tags[].value` | string | yes | Value within the dimension, e.g. 'Engineering', 'Alpha-2026'. | | `nonSubscriptionProducts[].items` | array of object | no | Per-item breakdown: one entry per SalesOrderProductItem (product x inventory item) under this sales-order product. Present only for inventory-tracked products. | | `nonSubscriptionProducts[].items[].id` | string | yes | Id of this SalesOrderProductItem junction record (a UUID). | | `nonSubscriptionProducts[].items[].itemId` | string | no | Id of the linked inventory item (a UUID). Absent when the record has no item set. | | `nonSubscriptionProducts[].items[].itemCode` | string | no | Item code from ItemMaster. | | `nonSubscriptionProducts[].items[].itemName` | string | no | Item name from ItemMaster. | | `nonSubscriptionProducts[].items[].productName` | string | no | Product name denormalised from the parent SalesOrderProduct. | | `nonSubscriptionProducts[].items[].itemOrdered` | string (decimal) | yes | Ordered quantity for this item, after explosion: line quantity x product-item quantity. | | `nonSubscriptionProducts[].items[].totalShipped` | string (decimal) | no | Sum of shipped quantity across all linked SalesOrderShipmentItems. | | `nonSubscriptionProducts[].rampDeal` | object | no | Ramp-deal definition. Present only when the product is a ramp deal; ramps echo the persisted SalesOrderProductLine records. | | `nonSubscriptionProducts[].rampDeal.interval` | string | yes | Ramp interval mode. One of 'annually', 'specificDates'. | | `nonSubscriptionProducts[].rampDeal.invoiceTiming` | string | yes | Ramp invoice timing. One of 'perInterval', 'allAtOnce'. | | `nonSubscriptionProducts[].rampDeal.ramps` | array of object | yes | Persisted ramps, ordered by start date. At least two. | | `nonSubscriptionProducts[].rampDeal.ramps[].serviceStartDate` | string (date) | yes | First date this ramp's services are delivered (YYYY-MM-DD). | | `nonSubscriptionProducts[].rampDeal.ramps[].serviceEndDate` | string (date) | yes | Last date this ramp's services are delivered (YYYY-MM-DD). | | `nonSubscriptionProducts[].rampDeal.ramps[].quantity` | string (decimal) | yes | Quantity for this ramp period. | | `nonSubscriptionProducts[].rampDeal.ramps[].listPrice` | string (decimal) | no | Per-unit list price for this ramp before discount. | | `nonSubscriptionProducts[].rampDeal.ramps[].unitPrice` | string (decimal) | no | Per-unit price for this ramp after discount. | | `nonSubscriptionProducts[].rampDeal.ramps[].invoiceFrequency` | string | no | How often this ramp is invoiced. | | `nonSubscriptionProducts[].prepaidConfiguration` | object | no | Prepaid balance details for this product. Absent when the product is not configured as prepaid. | | `nonSubscriptionProducts[].prepaidConfiguration.overConsumptionBehavior` | string | yes | Behaviour when consumption exceeds the remaining prepaid balance. One of 'allowNegative' (balance may go negative and is reconciled at renewal) or 'overage' (usage beyond the balance switches to pay-as-you-go invoicing). | | `nonSubscriptionProducts[].prepaidConfiguration.rolloverAtRenewal` | boolean | yes | Whether any unused balance carries forward when the subscription renews. | | `nonSubscriptionProducts[].prepaidConfiguration.balance` | string (decimal) | no | Remaining prepaid quantity for this product. Decreases as consumption is applied. | | `nonSubscriptionProducts[].prepaidConfiguration.consumedAmount` | string (decimal) | no | Total quantity consumed from the prepaid balance to date. | | `lastModifiedDate` | string (date-time) | yes | Instant the order last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Poll on this to sync incrementally rather than re-reading the whole collection. | | `externalIdentifiers` | array of object | no | External identifiers owned by other systems. At most one entry per fieldName. Absent when the order has none; an omitted field and [] are equivalent. | | `externalIdentifiers[].fieldName` | string | yes | Configured key of the external identifier — a stable camelCase slug such as 'netsuiteSoNumber', not the human-readable label. Must match an active SalesOrderExternalIdentifierConfiguration. | | `externalIdentifiers[].value` | string | yes | The identifier as the source system holds it. | | `externalIdentifiers[].externalUrl` | string | no | Absolute http(s) link to the object in the originating system, when one is stored. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Partially update a sales order `PATCH /accounting/sales-orders/{id}` Group: Accounting › Sales Orders · Operation ID: `updateSalesOrder` Writes only the fields the request body carries. Everything omitted is left exactly as it was. Product lines are edited through three parallel arrays: `createProducts` adds new lines, `updateProducts` rewrites existing ones (matched by `salesOrderProductLineId`, omitted fields left unchanged), and `deleteProducts` removes them by id; `createSubscriptions` adds new subscriptions with their own lines. A new Commit with Overage line requires `allowance` (its Allowance & Overage terms, defaulting from the product catalog); other line types reject it, and `updateProducts` cannot change an existing line's allowance. While an order is pending approval the whole header and its lines are editable. Once approved only a small set of header fields may change, and `updateProducts` entries may carry nothing but `salesOrderProductLineId` and `tags`; any other field or product operation fails with `APPROVED_FIELD_LOCKED`. An unknown id fails with `NOT_FOUND`; a product line id that does not belong to the order fails with `INVALID_LINE`. Runs in a transaction, so a failure leaves the order untouched. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateSalesOrder) ## 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. | | `id` | path | string | yes | UUID of the sales order to update. The selector, never itself changed. | ## Request body Required, `application/json`: updateSalesOrderInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `billToAddressId` | string | no | UUID of the Address invoices for this order go to. An address that does not exist fails with `INVALID_ADDRESS`. Stays editable after the order is approved. | | `createProducts` | array of object | no | Product lines to add to the order. Locked once the order is approved: sending it then fails with `APPROVED_FIELD_LOCKED`. | | `createProducts[].allowance` | LineAllowance | no | The Allowance & Overage terms of a Commit with Overage line. Required for that line type and rejected on any other; omitted fields default from the product catalog. | | `createProducts[].allowance.allowance` | string (decimal) | yes | Committed quantity per allowance term, in the catalog Allowance's unit of measure. | | `createProducts[].allowance.allowanceTerm` | string | no | The period the allowance covers: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually` or `orderTerm`. Defaults to the product catalog's Allowance term. | | `createProducts[].allowance.maximumRolloverLimit` | string (decimal) | no | Cap on the quantity that may roll over. Accepted only when the catalog Allowance has rollover enabled, and defaults to its setting. | | `createProducts[].allowance.overage` | LineOverage | no | Discount and invoicing overrides for the line's overage. Omit it to take everything from the catalog Overage and its price-book entry. | | `createProducts[].allowance.overage.discountAmount` | string (decimal) | no | Discount per unit off the overage price, in the order currency. Takes precedence over `discountPercentage` when both are sent; omit both to price the overage exactly as its price-book entry has it. | | `createProducts[].allowance.overage.discountPercentage` | string (decimal) | no | Discount per unit off the overage price on a 0–100 scale, so `10` means ten percent. Used only when `discountAmount` is not given. | | `createProducts[].allowance.overage.invoiceTiming` | string | no | Whether the overage invoices `advance` (at the start of its period) or `arrears` (after it). Defaults from the entity's active invoice policy for overage lines, then from the parent line. | | `createProducts[].allowance.overage.lineInvoiceDay` | string | no | Which day of the period the overage invoices on: `anniversary`, `first`, `last` or `specific`. Defaults from the entity's active invoice policy for overage lines, then from the parent line. | | `createProducts[].allowance.rolloverDuration` | string (decimal) | no | How many allowance terms unused allowance rolls over for. Accepted only when the catalog Allowance has rollover enabled, and defaults to its setting. | | `createProducts[].discountAmount` | string (decimal) | no | Discount per unit off `listPrice`, in the order currency. Used when no `unitPrice` is given, and then drives the other two. | | `createProducts[].discountPercentage` | string (decimal) | no | Discount per unit off `listPrice` on a 0–100 scale, so `10` means ten percent. Stored as a 0–1 fraction. The lowest precedence of the three — used only when neither `unitPrice` nor `discountAmount` is given. | | `createProducts[].externalIdentifiers` | array of object | no | Identifiers this line carries in other systems, upserted by `fieldName`. | | `createProducts[].externalIdentifiers[].externalUrl` | string | no | Deep link to the record in the owning system, shown beside the id. | | `createProducts[].externalIdentifiers[].fieldName` | string | yes | Names the ExternalIdentifierConfiguration this id belongs to, as that configuration spells it. It must already be configured for the tenant; an unknown name is rejected. Identifiers are upserted by this name, so sending it again overwrites the stored value. | | `createProducts[].externalIdentifiers[].value` | string | yes | The identifier as the owning system spells it. | | `createProducts[].invoiceFrequency` | string | no | How often the line invoices — an InvoiceFrequency member: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually`, `once`, `custom`, `template`, `milestone`, `allAtOnce` or `termEnd`. Defaults to the product line's own setting. | | `createProducts[].invoiceTiming` | string | no | Whether the line invoices `advance` (at the start of the service period) or `arrears` (after it). Defaults from the entity's active invoice policy for this line type. | | `createProducts[].lineInvoiceDay` | string | no | Which day of the period the line invoices on: `anniversary`, `first`, `last` or `specific`. Defaults from the entity's active invoice policy. | | `createProducts[].listPrice` | string (decimal) | no | Undiscounted price per unit, in the order currency. Defaults to the list price the order's price book holds for this product line, and is the figure the other pricing fields are reconciled against. | | `createProducts[].priceModel` | string | no | How the line is priced: `flat`, `perUnit`, `tiered`, `volume`, `stairstep` or `rateCard`. Defaults to the product line's own model. `tiered`, `volume` and `stairstep` take their prices from `tiers` and `rateCard` from `rateCardEntries`, so for those the line-level discount fields are not reconciled. `thresholdBased` is rejected: it needs a base line plus a metered usage child, which this endpoint does not build. | | `createProducts[].pricingTerm` | string | no | The period the price is quoted for: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually` or `once`. Defaults to the product line's own term. | | `createProducts[].productLineId` | string | yes | UUID of the ProductLine being added. It settles the product, the line type and the billing rules every omitted field defaults from. | | `createProducts[].quantity` | string (decimal) | no | Number of units ordered. Required unless the line takes its quantity from somewhere else: usage-metered line types (PayAsYouGo, Usage, Billable*) meter it from usage records and tier-priced models (`tiered`, `volume`, `stairstep`, `thresholdBased`) derive it from the tier structure, so for those it defaults to zero. Anywhere else, omitting it fails the request. | | `createProducts[].rateCardEntries` | array of object | no | Per-unit rates for a `rateCard`-priced line. Supplying any entry moves pricing onto the rate card, so the line-level discount fields stop being reconciled. | | `createProducts[].rateCardEntries[].billableComponentId` | string | no | UUID of the BillableComponent this row meters. Optional — a row may price a unit of measure without naming a component — but a UUID that does not resolve rejects the whole request. | | `createProducts[].rateCardEntries[].conversion` | string (decimal) | no | Factor converting the billable unit into the reporting unit — 1000 when usage is metered per call but reported per thousand. | | `createProducts[].rateCardEntries[].description` | string | no | Free-text label for the row, carried through to the stored rate-card entry. | | `createProducts[].rateCardEntries[].listPrice` | string (decimal) | no | Undiscounted rate per `unitOfMeasure`, in the order currency. Honoured only while the product line's `enforceListPrice` is false; otherwise the product line's own list price wins. | | `createProducts[].rateCardEntries[].unitOfMeasure` | string | yes | The unit this row is priced in. | | `createProducts[].rateCardEntries[].unitPrice` | string (decimal) | yes | Negotiated rate per `unitOfMeasure`, in the order currency. | | `createProducts[].serviceEndDate` | string (date) | no | Last day of the line's service period. The order's own service end is the latest across all its lines, and is left open on an evergreen order. | | `createProducts[].serviceStartDate` | string (date) | no | First day of the line's service period. The order's own service start is the earliest across all its lines. | | `createProducts[].subscriptionId` | string | no | UUID of a subscription already on this order to hang the line under. Omit it to add the line to the order itself as a one-time line. A subscription that is not on the order fails with `INVALID_SUBSCRIPTION`. | | `createProducts[].tags` | array of object | no | Value-accounting tags to allocate this line by. Requires value-allocation tagging to be enabled for sales orders. | | `createProducts[].tags[].dimension` | string | yes | Name of the value-accounting dimension, as configured for the tenant. Resolved by name, not by id. | | `createProducts[].tags[].value` | string | yes | Name of the value within that dimension. The pair must resolve to an existing tag and the dimension must be enabled for sales orders; an unknown pair rejects the whole request. | | `createProducts[].tiers` | array of object | no | The pricing tiers of a `tiered`, `volume` or `stairstep` line. The ranges must be contiguous, and the last tier is left open-ended by omitting its `toQuantity`. | | `createProducts[].tiers[].fromQuantity` | string (decimal) | yes | Lower bound of the tier's quantity range, inclusive. | | `createProducts[].tiers[].listPrice` | string (decimal) | no | Undiscounted price for this tier, in the order currency. Defaults to `unitPrice`, i.e. no discount on the tier. | | `createProducts[].tiers[].priceModel` | string | no | How `unitPrice` applies within the tier: `perUnit` (the default) charges it for every unit in the range, `flat` charges it once for the whole tier. | | `createProducts[].tiers[].toQuantity` | string (decimal) | no | Upper bound of the tier's quantity range. Omit it on the last tier to leave the range open-ended. | | `createProducts[].tiers[].unitPrice` | string (decimal) | yes | Negotiated price for this tier, in the order currency. Charged per unit in the range when `priceModel` is `perUnit`, or once for the whole tier when it is `flat`. | | `createProducts[].totalPrice` | string (decimal) | no | Total for the line, in the order currency. Persisted exactly as supplied rather than recomputed — omit it and the line's own pricing drives the total. | | `createProducts[].unitOfMeasure` | string | no | The unit the line is priced and metered in. Defaults to the product line's own unit. | | `createProducts[].unitPrice` | string (decimal) | no | Negotiated price per unit, in the order currency. The highest precedence of the three pricing inputs — supply it and `discountAmount` and `discountPercentage` are recomputed from it (discountAmount = listPrice − unitPrice). | | `createSubscriptions` | array of object | no | New subscriptions to add to the order, each bringing its own product lines. Locked once the order is approved: sending it then fails with `APPROVED_FIELD_LOCKED`. | | `createSubscriptions[].autoRenew` | boolean | no | Whether the subscription renews itself at the end of its term. | | `createSubscriptions[].products` | array of ProductLine | yes | The recurring product lines of the new subscription. Every one must be a subscription-based product line, or the request fails with `INVALID_SUBSCRIPTION_PRODUCT`. | | `createSubscriptions[].products[].allowance` | LineAllowance | no | The Allowance & Overage terms of a Commit with Overage line. Required for that line type and rejected on any other; omitted fields default from the product catalog. | | `createSubscriptions[].products[].allowance.allowance` | string (decimal) | yes | Committed quantity per allowance term, in the catalog Allowance's unit of measure. | | `createSubscriptions[].products[].allowance.allowanceTerm` | string | no | The period the allowance covers: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually` or `orderTerm`. Defaults to the product catalog's Allowance term. | | `createSubscriptions[].products[].allowance.maximumRolloverLimit` | string (decimal) | no | Cap on the quantity that may roll over. Accepted only when the catalog Allowance has rollover enabled, and defaults to its setting. | | `createSubscriptions[].products[].allowance.overage` | LineOverage | no | Discount and invoicing overrides for the line's overage. Omit it to take everything from the catalog Overage and its price-book entry. | | `createSubscriptions[].products[].allowance.overage.discountAmount` | string (decimal) | no | Discount per unit off the overage price, in the order currency. Takes precedence over `discountPercentage` when both are sent; omit both to price the overage exactly as its price-book entry has it. | | `createSubscriptions[].products[].allowance.overage.discountPercentage` | string (decimal) | no | Discount per unit off the overage price on a 0–100 scale, so `10` means ten percent. Used only when `discountAmount` is not given. | | `createSubscriptions[].products[].allowance.overage.invoiceTiming` | string | no | Whether the overage invoices `advance` (at the start of its period) or `arrears` (after it). Defaults from the entity's active invoice policy for overage lines, then from the parent line. | | `createSubscriptions[].products[].allowance.overage.lineInvoiceDay` | string | no | Which day of the period the overage invoices on: `anniversary`, `first`, `last` or `specific`. Defaults from the entity's active invoice policy for overage lines, then from the parent line. | | `createSubscriptions[].products[].allowance.rolloverDuration` | string (decimal) | no | How many allowance terms unused allowance rolls over for. Accepted only when the catalog Allowance has rollover enabled, and defaults to its setting. | | `createSubscriptions[].products[].discountAmount` | string (decimal) | no | Discount per unit off `listPrice`, in the order currency. Used when no `unitPrice` is given, and then drives the other two. | | `createSubscriptions[].products[].discountPercentage` | string (decimal) | no | Discount per unit off `listPrice` on a 0–100 scale, so `10` means ten percent. Stored as a 0–1 fraction. The lowest precedence of the three — used only when neither `unitPrice` nor `discountAmount` is given. | | `createSubscriptions[].products[].externalIdentifiers` | array of ExternalIdentifier | no | Identifiers this line carries in other systems, upserted by `fieldName`. | | `createSubscriptions[].products[].externalIdentifiers[].externalUrl` | string | no | Deep link to the record in the owning system, shown beside the id. | | `createSubscriptions[].products[].externalIdentifiers[].fieldName` | string | yes | Names the ExternalIdentifierConfiguration this id belongs to, as that configuration spells it. It must already be configured for the tenant; an unknown name is rejected. Identifiers are upserted by this name, so sending it again overwrites the stored value. | | `createSubscriptions[].products[].externalIdentifiers[].value` | string | yes | The identifier as the owning system spells it. | | `createSubscriptions[].products[].invoiceFrequency` | string | no | How often the line invoices — an InvoiceFrequency member: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually`, `once`, `custom`, `template`, `milestone`, `allAtOnce` or `termEnd`. Defaults to the product line's own setting. | | `createSubscriptions[].products[].invoiceFrequencyTemplateId` | string | no | UUID of the invoice frequency template. | | `createSubscriptions[].products[].invoiceTiming` | string | no | Whether the line invoices `advance` (at the start of the service period) or `arrears` (after it). Defaults from the entity's active invoice policy for this line type. | | `createSubscriptions[].products[].lineInvoiceDay` | string | no | Which day of the period the line invoices on: `anniversary`, `first`, `last` or `specific`. Defaults from the entity's active invoice policy. | | `createSubscriptions[].products[].listPrice` | string (decimal) | no | Undiscounted price per unit, in the order currency. Defaults to the list price the order's price book holds for this product line, and is the figure the other pricing fields are reconciled against. | | `createSubscriptions[].products[].priceModel` | string | no | How the line is priced: `flat`, `perUnit`, `tiered`, `volume`, `stairstep` or `rateCard`. Defaults to the product line's own model. `tiered`, `volume` and `stairstep` take their prices from `tiers` and `rateCard` from `rateCardEntries`, so for those the line-level discount fields are not reconciled. `thresholdBased` is rejected: it needs a base line plus a metered usage child, which this endpoint does not build. | | `createSubscriptions[].products[].pricingTerm` | string | no | The period the price is quoted for: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually` or `once`. Defaults to the product line's own term. | | `createSubscriptions[].products[].productLineId` | string | yes | UUID of the ProductLine being ordered. It settles the product, the line type and the billing rules every omitted field defaults from. It must exist, and it must not be threshold-based or prepaid: those need a base line plus a metered usage child under one product, which this endpoint does not build. | | `createSubscriptions[].products[].quantity` | string (decimal) | no | Number of units ordered. Required unless the line takes its quantity from somewhere else: usage-metered line types (PayAsYouGo, Usage, Billable*) meter it from usage records and tier-priced models (`tiered`, `volume`, `stairstep`, `thresholdBased`) derive it from the tier structure, so for those it defaults to zero. Anywhere else, omitting it fails the request. | | `createSubscriptions[].products[].rateCardEntries` | array of RateCardEntry | no | Per-unit rates for a `rateCard`-priced line. Supplying any entry moves pricing onto the rate card, so the line-level discount fields stop being reconciled. | | `createSubscriptions[].products[].rateCardEntries[].billableComponentId` | string | no | UUID of the BillableComponent this row meters. Optional — a row may price a unit of measure without naming a component — but a UUID that does not resolve rejects the whole request. | | `createSubscriptions[].products[].rateCardEntries[].conversion` | string (decimal) | no | Factor converting the billable unit into the reporting unit — 1000 when usage is metered per call but reported per thousand. | | `createSubscriptions[].products[].rateCardEntries[].description` | string | no | Free-text label for the row, carried through to the stored rate-card entry. | | `createSubscriptions[].products[].rateCardEntries[].listPrice` | string (decimal) | no | Undiscounted rate per `unitOfMeasure`, in the order currency. Honoured only while the product line's `enforceListPrice` is false; otherwise the product line's own list price wins. | | `createSubscriptions[].products[].rateCardEntries[].unitOfMeasure` | string | yes | The unit this row is priced in. | | `createSubscriptions[].products[].rateCardEntries[].unitPrice` | string (decimal) | yes | Negotiated rate per `unitOfMeasure`, in the order currency. | | `createSubscriptions[].products[].serviceEndDate` | string (date) | no | Last day of the line's service period. The order's own service end is the latest across all its lines, and is left open on an evergreen order. | | `createSubscriptions[].products[].serviceStartDate` | string (date) | no | First day of the line's service period. The order's own service start is the earliest across all its lines. | | `createSubscriptions[].products[].tags` | array of LineTag | no | Value-accounting tags to allocate this line by. Requires value-allocation tagging to be enabled for sales orders. | | `createSubscriptions[].products[].tags[].dimension` | string | yes | Name of the value-accounting dimension, as configured for the tenant. Resolved by name, not by id. | | `createSubscriptions[].products[].tags[].value` | string | yes | Name of the value within that dimension. The pair must resolve to an existing tag and the dimension must be enabled for sales orders; an unknown pair rejects the whole request. | | `createSubscriptions[].products[].tiers` | array of LineTier | no | The pricing tiers of a `tiered`, `volume` or `stairstep` line. The ranges must be contiguous, and the last tier is left open-ended by omitting its `toQuantity`. | | `createSubscriptions[].products[].tiers[].fromQuantity` | string (decimal) | yes | Lower bound of the tier's quantity range, inclusive. | | `createSubscriptions[].products[].tiers[].listPrice` | string (decimal) | no | Undiscounted price for this tier, in the order currency. Defaults to `unitPrice`, i.e. no discount on the tier. | | `createSubscriptions[].products[].tiers[].priceModel` | string | no | How `unitPrice` applies within the tier: `perUnit` (the default) charges it for every unit in the range, `flat` charges it once for the whole tier. | | `createSubscriptions[].products[].tiers[].toQuantity` | string (decimal) | no | Upper bound of the tier's quantity range. Omit it on the last tier to leave the range open-ended. | | `createSubscriptions[].products[].tiers[].unitPrice` | string (decimal) | yes | Negotiated price for this tier, in the order currency. Charged per unit in the range when `priceModel` is `perUnit`, or once for the whole tier when it is `flat`. | | `createSubscriptions[].products[].totalPrice` | string (decimal) | no | Total for the line, in the order currency. Persisted exactly as supplied rather than recomputed — omit it and the line's own pricing drives the total. | | `createSubscriptions[].products[].unitOfMeasure` | string | no | The unit the line is priced and metered in. Defaults to the product line's own unit. | | `createSubscriptions[].products[].unitPrice` | string (decimal) | no | Negotiated price per unit, in the order currency. The highest precedence of the three pricing inputs — supply it and `discountAmount` and `discountPercentage` are recomputed from it (discountAmount = listPrice − unitPrice). | | `createSubscriptions[].serviceEndDate` | string (date) | no | Last day the subscription is in service. Omit it on an evergreen subscription. | | `createSubscriptions[].serviceStartDate` | string (date) | yes | First day the subscription is in service. | | `createSubscriptions[].term` | string (decimal) | no | Intended length of a `termed` subscription in months. What actually bounds the subscription is `serviceStartDate` and `serviceEndDate`, so supply the end date to fix when it ends. | | `createSubscriptions[].termType` | string | yes | `termed` for a subscription of fixed length, `evergreen` for an open-ended one that keeps renewing. | | `deleteProducts` | array of string | no | UUIDs of product lines to remove from the order. Each must belong to it, or the request fails with `INVALID_LINE`. Locked once the order is approved: sending it then fails with `APPROVED_FIELD_LOCKED`. | | `effectiveDate` | string (date) | no | Day the order becomes commercially effective. Stays editable after the order is approved. | | `excludeFromTaxEngine` | boolean | no | Whether the order is kept out of the tax engine, so no tax is calculated for it. Locked once the order is approved: sending it then fails with `APPROVED_FIELD_LOCKED`. | | `oneTimeDiscountAllocation` | string | no | How the one-time discount is spread across the order's invoices: `firstInvoice` puts all of it on the first, `alignToInvoiceFrequency` spreads it over the invoicing schedule, `custom` leaves the allocation to be set by hand. Locked once the order is approved: sending it then fails with `APPROVED_FIELD_LOCKED`. | | `oneTimeDiscountAmount` | string (decimal) | no | A one-off discount on the order as a whole, in the order currency. Independent of the per-line discount fields. Locked once the order is approved: sending it then fails with `APPROVED_FIELD_LOCKED`. | | `opportunityName` | string | no | Name of the opportunity the order came out of. Stays editable after the order is approved. | | `paymentTermCode` | string | no | The order's payment term, named by its code. Stays editable after the order is approved. | | `purchaseOrder` | string | no | The customer's own purchase-order number for this order. Stays editable after the order is approved. | | `serviceEndDate` | string (date) | no | Last day of the order's service period. Locked once the order is approved: sending it then fails with `APPROVED_FIELD_LOCKED`. | | `serviceStartDate` | string (date) | no | First day of the order's service period. Locked once the order is approved: sending it then fails with `APPROVED_FIELD_LOCKED`. | | `signDate` | string (date) | no | Day the order was signed. Stays editable after the order is approved. | | `soldToAddressId` | string | no | UUID of the Address of the contracting party. An address that does not exist fails with `INVALID_ADDRESS`. Stays editable after the order is approved. | | `updateProducts` | array of object | no | Changes to product lines already on the order, each matched by its `salesOrderProductLineId`. Fields left out of an entry keep their current values. Once the order is approved an entry may carry only `salesOrderProductLineId` and `tags`; any other field then fails with `APPROVED_FIELD_LOCKED`. | | `updateProducts[].discountAmount` | string (decimal) | no | Discount per unit off `listPrice`, in the order currency. This endpoint has no `unitPrice` field, so the discount is what drives it: unitPrice = listPrice − discountAmount. Takes precedence over `discountPercentage`. | | `updateProducts[].discountPercentage` | string (decimal) | no | Discount per unit off `listPrice` on a 0–100 scale, so `10` means ten percent. Stored as a 0–1 fraction. Used only when `discountAmount` is absent. | | `updateProducts[].externalIdentifiers` | array of object | no | Identifiers to set on the line, upserted by `fieldName`. Names not listed are left as they are — there is no way to remove one. | | `updateProducts[].externalIdentifiers[].externalUrl` | string | no | Deep link to the record in the owning system, shown beside the id. | | `updateProducts[].externalIdentifiers[].fieldName` | string | yes | Names the ExternalIdentifierConfiguration this id belongs to, as that configuration spells it. It must already be configured for the tenant; an unknown name is rejected. Identifiers are upserted by this name, so sending it again overwrites the stored value. | | `updateProducts[].externalIdentifiers[].value` | string | yes | The identifier as the owning system spells it. | | `updateProducts[].invoiceFrequency` | string | no | New invoicing frequency for the line — an InvoiceFrequency member: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually`, `once`, `custom`, `template`, `milestone`, `allAtOnce` or `termEnd`. Omit it to leave the frequency as it stands. | | `updateProducts[].invoiceTiming` | string | no | Whether the line invoices `advance` (at the start of the service period) or `arrears` (after it). Omit it to leave the timing as it stands. | | `updateProducts[].lineInvoiceDay` | string | no | Which day of the period the line invoices on: `anniversary`, `first`, `last` or `specific`. Omit it to leave the day as it stands. | | `updateProducts[].listPrice` | string (decimal) | no | New undiscounted price per unit, in the order currency. Changing it on its own still re-reconciles the line: the existing discount amount is held fixed and `unitPrice` and `discountPercentage` are re-derived against the new list price. | | `updateProducts[].quantity` | string (decimal) | no | New number of units for the line. Omit it to leave the quantity as it stands. | | `updateProducts[].rateCardEntries` | array of object | no | Replaces the line's rate-card rows: a row carrying an `id` is updated, one without is added, and any existing row whose id is not in the list is deleted. Omit the field entirely to leave the line's rows untouched. | | `updateProducts[].rateCardEntries[].billableComponentId` | string | no | UUID of the BillableComponent this row meters. Optional — a row may price a unit of measure without naming a component — but a UUID that does not resolve rejects the whole request. | | `updateProducts[].rateCardEntries[].conversion` | string (decimal) | no | Factor converting the billable unit into the reporting unit — 1000 when usage is metered per call but reported per thousand. | | `updateProducts[].rateCardEntries[].description` | string | no | Free-text label for the row, carried through to the stored rate-card entry. | | `updateProducts[].rateCardEntries[].id` | string | no | UUID of a rate-card row already on this line, to change it in place. Leave it out to add a new row. An id that is not on the line fails the request and nothing is applied. | | `updateProducts[].rateCardEntries[].listPrice` | string (decimal) | no | Undiscounted rate per `unitOfMeasure`, in the order currency. Honoured only while the product line's `enforceListPrice` is false; otherwise the product line's own list price wins. | | `updateProducts[].rateCardEntries[].unitOfMeasure` | string | yes | The unit this row is priced in. | | `updateProducts[].rateCardEntries[].unitPrice` | string (decimal) | yes | Negotiated rate per `unitOfMeasure`, in the order currency. | | `updateProducts[].salesOrderProductLineId` | string | yes | UUID of the order's product line to change. It must belong to this order, or the request fails with `INVALID_LINE`. | | `updateProducts[].serviceEndDate` | string (date) | no | New last day of the line's service period. | | `updateProducts[].serviceStartDate` | string (date) | no | New first day of the line's service period. | | `updateProducts[].tags` | array of object | no | Replaces the line's value-accounting tags wholesale. Send `[]` to clear them; omit the field to leave them untouched. Lines that share one product (ramp deals) share their tags, so all of them must be sent the same set. Stays editable after the order is approved. | | `updateProducts[].tags[].dimension` | string | yes | Name of the value-accounting dimension, as configured for the tenant. Resolved by name, not by id. | | `updateProducts[].tags[].value` | string | yes | Name of the value within that dimension. The pair must resolve to an existing tag and the dimension must be enabled for sales orders; an unknown pair rejects the whole request. | | `updateProducts[].tiers` | array of object | no | Replaces the line's pricing tiers wholesale. Omit the field to leave them untouched. | | `updateProducts[].tiers[].fromQuantity` | string (decimal) | yes | Lower bound of the tier's quantity range, inclusive. | | `updateProducts[].tiers[].listPrice` | string (decimal) | no | Undiscounted price for this tier, in the order currency. Defaults to `unitPrice`, i.e. no discount on the tier. | | `updateProducts[].tiers[].priceModel` | string | no | How `unitPrice` applies within the tier: `perUnit` (the default) charges it for every unit in the range, `flat` charges it once for the whole tier. | | `updateProducts[].tiers[].toQuantity` | string (decimal) | no | Upper bound of the tier's quantity range. Omit it on the last tier to leave the range open-ended. | | `updateProducts[].tiers[].unitPrice` | string (decimal) | yes | Negotiated price for this tier, in the order currency. Charged per unit in the range when `priceModel` is `perUnit`, or once for the whole tier when it is `flat`. | | `updateProducts[].totalPrice` | string (decimal) | no | New total for the line, in the order currency. Persisted exactly as supplied rather than recomputed. | Example: ```json { "billToAddressId": "5b8c31f6-04ad-472e-9f81-63a0d7e295cb", "createProducts": [ { "allowance": { "allowance": "1000", "allowanceTerm": "monthly", "maximumRolloverLimit": "500", "overage": { "discountAmount": "1.50", "discountPercentage": "10", "invoiceTiming": "arrears", "lineInvoiceDay": "anniversary" }, "rolloverDuration": "2" }, "discountAmount": "120.00", "discountPercentage": "10", "externalIdentifiers": [ { "externalUrl": "https://netsuite.example.com/app/accounting/transactions/salesord.nl?id=10042", "fieldName": "netsuiteInternalId", "value": "SO-10042" } ], "invoiceFrequency": "monthly", "invoiceTiming": "advance", "lineInvoiceDay": "anniversary", "listPrice": "1200.00", "priceModel": "perUnit", "pricingTerm": "annually", "productLineId": "6a1f2c94-3d58-4b17-9e02-7c8ab4d5f610", "quantity": "10", "rateCardEntries": [ { "billableComponentId": "9f1c2d34-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "conversion": "1000", "description": "API calls beyond the included volume", "listPrice": "0.005", "unitOfMeasure": "apiCall", "unitPrice": "0.004" } ], "serviceEndDate": "2026-12-31", "serviceStartDate": "2026-01-01", "subscriptionId": "c8402e17-95bf-4a63-8d20-71ec3f6a0b48", "tags": [ { "dimension": "Department", "value": "Engineering" } ], "tiers": [ { "fromQuantity": "0", "listPrice": "15.00", "priceModel": "perUnit", "toQuantity": "100", "unitPrice": "12.50" } ], "totalPrice": "12960.00", "unitOfMeasure": "seat", "unitPrice": "1080.00" } ], "createSubscriptions": [ { "autoRenew": true, "products": [ { "allowance": { "allowance": "1000", "allowanceTerm": "monthly", "maximumRolloverLimit": "500", "overage": { "discountAmount": null, "discountPercentage": null, "invoiceTiming": null, "lineInvoiceDay": null }, "rolloverDuration": "2" }, "discountAmount": "120.00", "discountPercentage": "10", "externalIdentifiers": [ { "externalUrl": null, "fieldName": null, "value": null } ], "invoiceFrequency": "monthly", "invoiceFrequencyTemplateId": "9f1c2d34-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "invoiceTiming": "advance", "lineInvoiceDay": "anniversary", "listPrice": "1200.00", "priceModel": "perUnit", "pricingTerm": "annually", "productLineId": "6a1f2c94-3d58-4b17-9e02-7c8ab4d5f610", "quantity": "10", "rateCardEntries": [ { "billableComponentId": null, "conversion": null, "description": null, "listPrice": null, "unitOfMeasure": null, "unitPrice": null } ], "serviceEndDate": "2026-12-31", "serviceStartDate": "2026-01-01", "tags": [ { "dimension": null, "value": null } ], "tiers": [ { "fromQuantity": null, "listPrice": null, "priceModel": null, "toQuantity": null, "unitPrice": null } ], "totalPrice": "12960.00", "unitOfMeasure": "seat", "unitPrice": "1080.00" } ], "serviceEndDate": "2026-12-31", "serviceStartDate": "2026-01-01", "term": "12", "termType": "termed" } ], "deleteProducts": [ "" ], "effectiveDate": "2026-02-01", "excludeFromTaxEngine": true, "oneTimeDiscountAllocation": "firstInvoice", "oneTimeDiscountAmount": "500.00", "opportunityName": "Contoso — 2026 renewal", "paymentTermCode": "NET30", "purchaseOrder": "PO-2026-0042", "serviceEndDate": "2026-12-31", "serviceStartDate": "2026-01-01", "signDate": "2026-01-10", "soldToAddressId": "5b8c31f6-04ad-472e-9f81-63a0d7e295cb", "updateProducts": [ { "discountAmount": "120.00", "discountPercentage": "10", "externalIdentifiers": [ { "externalUrl": "https://netsuite.example.com/app/accounting/transactions/salesord.nl?id=10042", "fieldName": "netsuiteInternalId", "value": "SO-10042" } ], "invoiceFrequency": "monthly", "invoiceTiming": "advance", "lineInvoiceDay": "anniversary", "listPrice": "1200.00", "quantity": "10", "rateCardEntries": [ { "billableComponentId": "9f1c2d34-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "conversion": "1000", "description": "API calls beyond the included volume", "id": "3f7b9a52-0c1d-48e6-b934-a5d720e8146f", "listPrice": "0.005", "unitOfMeasure": "apiCall", "unitPrice": "0.004" } ], "salesOrderProductLineId": "a05e7d13-8f62-4b90-bc47-1e63d0592a8f", "serviceEndDate": "2026-12-31", "serviceStartDate": "2026-01-01", "tags": [ { "dimension": "Department", "value": "Engineering" } ], "tiers": [ { "fromQuantity": "0", "listPrice": "15.00", "priceModel": "perUnit", "toQuantity": "100", "unitPrice": "12.50" } ], "totalPrice": "12960.00" } ] } ``` ## Response `200` Success Under `data` (updateSalesOrderOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `approvalStatus` | string | yes | The order's approval status after the update: `draft`, `pendingApproval`, `revise` or `approved`. | | `id` | string | yes | UUID of the sales order that was updated. | ## 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 | | --- | --- | | `APPROVED_FIELD_LOCKED` | 400 | | `INVALID_ADDRESS` | 400 | | `INVALID_LINE` | 400 | | `INVALID_SUBSCRIPTION` | 400 | | `INVALID_SUBSCRIPTION_PRODUCT` | 400 | | `VALIDATION_FAILED` | 400 | | `NOT_FOUND` | 404 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: APPROVED_FIELD_LOCKED, INVALID_ADDRESS, INVALID_LINE, INVALID_SUBSCRIPTION, INVALID_SUBSCRIPTION_PRODUCT, VALIDATION_FAILED - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found. Codes: 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 --- # Delete Sales Order `DELETE /accounting/sales-orders/{id}` Group: Accounting › Sales Orders · Operation ID: `deleteSalesOrder` Permanently delete one sales order and every record hanging off it. Refuses an order that is past pending approval, amends another order, or has already produced journal entries, invoices or recognized revenue. Returns an empty output. Every request must carry the Api-Version header. Permanently deletes one sales order and everything hanging off it — its products and product lines, the subscription lines, invoice-plan lines, revenue records and taxation those lines drive, metrics, attachments, external identifiers, and the sales arrangement when this was its last order. ``` DELETE accounting/sales-orders/3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45 ``` **The response is empty** — what was deleted is what the caller named. Read the order with `GET accounting/sales-orders/{id}` first if you need its number for a log. **Only an order that has not yet taken effect may be deleted.** The call is refused, and nothing is written, when the order is past pending approval (`NOT_PENDING_APPROVAL`), amends an existing order rather than being a new one (`ORDER_IS_MODIFICATION`), already has journal entries (`HAS_JOURNAL_ENTRIES`), has been invoiced (`ALREADY_INVOICED`), or has revenue recognized against it (`REVENUE_RECOGNIZED`). These guard the records the order has already produced downstream; an order that has gone further than this is retired through its own business process, not removed. Deleting an id that does not exist is `NOT_FOUND`, not a silent success. The call runs in a transaction, so a failure at any point leaves the order and its records untouched. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteSalesOrder) ## 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. | | `id` | path | string | yes | Id (a UUID) of the sales order to delete; bound from the :id path segment. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `NOT_PENDING_APPROVAL` | 400 | | `ORDER_IS_MODIFICATION` | 400 | | `HAS_JOURNAL_ENTRIES` | 400 | | `ALREADY_INVOICED` | 400 | | `REVENUE_RECOGNIZED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, NOT_PENDING_APPROVAL, ORDER_IS_MODIFICATION, HAS_JOURNAL_ENTRIES, ALREADY_INVOICED, REVENUE_RECOGNIZED - `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 --- # Search Sales Order `POST /accounting/sales-orders/search` Group: Accounting › Sales Orders · Operation ID: `searchSalesOrder` Search sales orders by id, order number, type, approval status, purchase order, legal entity or external identifier, and page through the matches. POST so the filters travel in the JSON body rather than the URL. Read-only. Returns the full order header for each match; matching nothing is an empty items array with HTTP 200, not a 404. Every request must carry the Api-Version header. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchSalesOrder) ## 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`. | ## Request body Required, `application/json`: searchSalesOrderInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | Search filters, nested as one object so they map onto the POST body. All are optional and are ANDed; omitting every one of them matches all sales orders. | | `filters.id` | string | no | Exact match on a sales order's id (a UUID). This is how a single known order is fetched. | | `filters.orderNumber` | string | no | Exact match on the system-assigned sales-order number. | | `filters.orderType` | string | no | Restrict to orders of this type. An `OrderType` member; any other value yields an empty page. | | `filters.approvalStatus` | string | no | Restrict to orders in this approval state. An `ApprovalStatus` member; any other value yields an empty page. | | `filters.purchaseOrder` | string | no | Exact match on the customer's purchase-order reference. In the body because it is a customer-supplied string, not a system identifier. | | `filters.entityId` | string | no | Restrict to orders owned by this legal entity, given as the entity id (a UUID). An id that matches no entity yields an empty page, not an error. | | `filters.externalIdentifier` | object | no | Match one identifier owned by another system. Both members are required: a fieldName on its own would match every value stored under that key. Not unique across orders, so this returns the ordinary items array. | | `filters.externalIdentifier.fieldName` | string | yes | Configured key of the external identifier — a stable camelCase slug such as 'netsuiteSoNumber'. Must match an active SalesOrderExternalIdentifierConfiguration or the call fails with UNKNOWN_EXTERNAL_IDENTIFIER. | | `filters.externalIdentifier.value` | string | yes | The identifier as the source system holds it. Matched exactly, case-sensitively. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | Opaque forward cursor from a previous response's pagination.nextCursor; omit for the first page. Do not construct manually. | | `page.size` | number | no | Maximum number of orders to return. Defaults to 50, capped at 200; the server may return fewer, but never more. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "orderNumber": "", "orderType": "", "approvalStatus": "", "purchaseOrder": "", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "externalIdentifier": { "fieldName": "", "value": "" } }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchSalesOrderOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | Matching sales orders for this page, newest first. Empty when nothing matches. Same item schema as get/v1. | | `items[].id` | string | yes | Id of the sales order (a UUID) — the public identifier used by GET accounting/sales-orders/:id. | | `items[].orderNumber` | string | yes | System-assigned sales-order number. Human-readable reference used on invoices and proposals. | | `items[].orderType` | string | no | Type of sales order, e.g. new business, renewal, expansion, downgrade. | | `items[].approvalStatus` | string | no | Approval state of the order. An `ApprovalStatus` member, e.g. 'draft', 'pendingApproval', 'approved'. | | `items[].bookDate` | string (date) | yes | Date the order was booked (YYYY-MM-DD). Drives revenue-recognition period assignment. | | `items[].serviceStartDate` | string (date) | no | First date services or products under this order are delivered (YYYY-MM-DD). | | `items[].serviceEndDate` | string (date) | no | Last date services or products under this order are delivered (YYYY-MM-DD). Absent for evergreen orders. | | `items[].signDate` | string (date) | no | Date the order was signed (YYYY-MM-DD), when recorded. | | `items[].salesEffectiveDate` | string (date) | no | Date the order becomes effective for sales reporting (YYYY-MM-DD), when recorded. | | `items[].isEvergreen` | boolean | no | True when the order renews indefinitely rather than ending on serviceEndDate. | | `items[].excludeFromTaxEngine` | boolean | no | When true, invoices, credit memos and debit memos from this order are never submitted to the tax engine. | | `items[].term` | string (decimal) | no | Contract term length in months. Absent for evergreen (open-ended) orders. | | `items[].currency` | string | yes | ISO 4217 three-letter currency code (e.g. USD, EUR, GBP) the order is denominated in. | | `items[].totalContract` | string (decimal) | no | Total contract value across all line items, in the order currency (see `currency`). | | `items[].invoicedAmount` | string (decimal) | no | Amount already invoiced against the contract value, in the order currency (see `currency`). The remaining backlog is totalContract − invoicedAmount. | | `items[].oneTimeDiscountAmount` | string (decimal) | no | Lump-sum discount applied to the entire order, in the order currency (see `currency`). | | `items[].oneTimeDiscountAllocation` | string | no | How the lump-sum discount is allocated across products. One of 'firstInvoice', 'alignToInvoiceFrequency'. | | `items[].paymentTerm` | string | no | Payment-term identifier or display name applied to invoices from this order. | | `items[].purchaseOrder` | string | no | The customer's own purchase-order reference for this order, when supplied. | | `items[].memo` | string | no | Free-text memo recorded on the order. | | `items[].crmContractId` | string | no | Identifier of the originating contract in the CRM, when the order came from one. | | `items[].salesArrangement` | string | no | Number of the sales arrangement this order belongs to. | | `items[].salesArrangementId` | string | no | Id of that sales arrangement (a UUID) — the form a write action accepts. | | `items[].paymentTermId` | string | no | Id of the payment term (a UUID). paymentTerm carries its code; this is the form a write action accepts. | | `items[].opportunityId` | string | no | Identifier of the opportunity this order came from, as issued by the CRM — not an id from this API. | | `items[].opportunityName` | string | no | Name of that CRM opportunity. | | `items[].dataSource` | string | no | System the order originated in when it was integrated rather than entered here, e.g. 'Salesforce'. | | `items[].priceBookId` | string | no | Id of the price book this order's prices were taken from (a UUID). | | `items[].priceBookName` | string | no | Name of that price book, denormalised onto the order. | | `items[].externalizedInvoicing` | boolean | no | True when invoicing for this order happens in another system, so no invoices are generated from it here. | | `items[].customer` | object | yes | Customer denormalised from the linked Customer record (key fields only). Fetch the full record from GET accounting/customers/:id. | | `items[].customer.id` | string | yes | Id of the customer (a UUID). | | `items[].customer.customerNumber` | string | no | System-assigned customer reference number. | | `items[].customer.name` | string | yes | Legal or business name of the customer. | | `items[].customer.invoiceEmail` | string | no | Email that receives invoices for this customer. | | `items[].customer.mainEmail` | string | no | Primary business contact email for the customer. | | `items[].customer.segment` | string | no | Market-segment classification. One of 'SMB', 'MidMarket', 'Enterprise'. | | `items[].customer.taxExempt` | boolean | no | True if the customer is exempt from tax on all transactions. | | `items[].entityId` | string | yes | Id of the legal entity that owns this sales order (a UUID). | | `items[].entityName` | string | no | Name of the owning legal entity. | | `items[].entityNumber` | string | yes | System-assigned number of the legal entity that owns this order. | | `items[].soldToAddress` | object | yes | Sold-to address used for tax determination on this order. | | `items[].soldToAddress.addressId` | string | no | Id of the address record (a UUID), when one is stored. | | `items[].soldToAddress.line1` | string | yes | Street address line 1: number, street name, or PO box. | | `items[].soldToAddress.line2` | string | no | Apartment, suite, unit, building, floor, etc. | | `items[].soldToAddress.city` | string | yes | City or locality. | | `items[].soldToAddress.stateProvince` | string | no | State, province, or region. | | `items[].soldToAddress.country` | string | yes | Country name or ISO 3166-1 country code. | | `items[].soldToAddress.zipCode` | string | yes | Postal code or ZIP code as required by the country postal system. | | `items[].billToAddress` | object | yes | Bill-to address where invoices from this order are sent. | | `items[].billToAddress.addressId` | string | no | Id of the address record (a UUID), when one is stored. | | `items[].billToAddress.line1` | string | yes | Street address line 1: number, street name, or PO box. | | `items[].billToAddress.line2` | string | no | Apartment, suite, unit, building, floor, etc. | | `items[].billToAddress.city` | string | yes | City or locality. | | `items[].billToAddress.stateProvince` | string | no | State, province, or region. | | `items[].billToAddress.country` | string | yes | Country name or ISO 3166-1 country code. | | `items[].billToAddress.zipCode` | string | yes | Postal code or ZIP code as required by the country postal system. | | `items[].subscriptions` | array of object | yes | Subscriptions on the order, one entry per subscription, each with the product lines that belong to it. Mirrors the subscriptions[] the create action takes. | | `items[].subscriptions[].id` | string | no | Id of the subscription (a UUID). Absent when the line records only a subscription number. | | `items[].subscriptions[].subscriptionNumber` | string (decimal) | no | System-assigned subscription number. | | `items[].subscriptions[].externalSubscriptionId` | string | no | The subscribing system's own id for this subscription, when the order was integrated from one. | | `items[].subscriptions[].termType` | string | no | Whether the subscription runs to a fixed end date or renews indefinitely. One of 'termed', 'evergreen'. | | `items[].subscriptions[].autoRenew` | boolean | no | True when the subscription renews automatically at the end of its term. | | `items[].subscriptions[].serviceStartDate` | string (date) | no | First date the subscription delivers service (YYYY-MM-DD). | | `items[].subscriptions[].serviceEndDate` | string (date) | no | Last date the subscription delivers service (YYYY-MM-DD). Absent while evergreen. Create takes a term in months instead; this is what that term resolved to. | | `items[].subscriptions[].products` | array of object | yes | Product lines in this subscription. Each entry is a SalesOrderProductLine and may carry tiered pricing, rate-card pricing, ramp deals, or per-item breakdowns. | | `items[].subscriptions[].products[].productCode` | string | yes | Company-specific code of the product. | | `items[].subscriptions[].products[].productId` | string | yes | Id of the product (a UUID). | | `items[].subscriptions[].products[].productLineId` | string | yes | Id of the product line under the product (a UUID). | | `items[].subscriptions[].products[].productName` | string | no | Product name as recorded on this line. May differ from the product's current name — the line keeps the name it was sold under. | | `items[].subscriptions[].products[].salesOrderProductLineId` | string | no | Id of this sales-order line (a UUID). | | `items[].subscriptions[].products[].quantity` | string (decimal) | yes | Quantity of the product on this line. For ramp deals the per-period quantity is on each ramp instead. | | `items[].subscriptions[].products[].listPrice` | string (decimal) | no | Per-unit list price before discounts. | | `items[].subscriptions[].products[].unitPrice` | string (decimal) | no | Per-unit price after discounts. Equals listPrice − discountAmount, or listPrice * (1 − discountPercentage/100). | | `items[].subscriptions[].products[].totalPrice` | string (decimal) | no | Total price for this line (unitPrice * quantity). | | `items[].subscriptions[].products[].discountAmount` | string (decimal) | no | Per-unit discount in the order currency. | | `items[].subscriptions[].products[].discountPercentage` | string (decimal) | no | Per-unit discount as a percentage (e.g. 10 for 10%). | | `items[].subscriptions[].products[].invoiceFrequency` | string | no | How often invoices are generated for this line, e.g. monthly, quarterly, annually. | | `items[].subscriptions[].products[].invoiceTiming` | string | no | Whether each invoice for this line is raised at the start or the end of the period it covers. One of 'inAdvance', 'inArrears'. | | `items[].subscriptions[].products[].lineInvoiceDay` | string | no | Which day of the period the invoice is dated, e.g. 'firstDayOfMonth', 'specificDay', 'serviceStartDate'. | | `items[].subscriptions[].products[].priceModel` | string | no | How the price is derived from quantity, e.g. 'flatFee', 'perUnit', 'tiered', 'volume'. Tiered and volume models carry their bands in tiers. | | `items[].subscriptions[].products[].unitOfMeasure` | string | no | Unit the quantity is counted in, e.g. 'each', 'user', 'gigabyte'. | | `items[].subscriptions[].products[].pricingTerm` | string | no | Period the unit price is quoted for, e.g. 'monthly', 'annual' — the basis prices are normalised to before invoicing. | | `items[].subscriptions[].products[].serviceStartDate` | string (date) | no | First date services for this product are delivered (YYYY-MM-DD). Defaults to the order-level serviceStartDate. | | `items[].subscriptions[].products[].serviceEndDate` | string (date) | no | Last date services for this product are delivered (YYYY-MM-DD). Defaults to the order-level serviceEndDate. | | `items[].subscriptions[].products[].externalIdentifiers` | array of object | yes | External identifiers recorded against this line, one per configured field. Empty when none are. | | `items[].subscriptions[].products[].externalIdentifiers[].fieldName` | string | yes | Configured field the identifier belongs to, e.g. 'salesforceLineId'. | | `items[].subscriptions[].products[].externalIdentifiers[].value` | string | yes | The identifier's value in that external system. | | `items[].subscriptions[].products[].externalIdentifiers[].externalUrl` | string | no | Deep link to the record in that system, when the configuration defines one. | | `items[].subscriptions[].products[].overage` | object | no | Overage pricing for usage above the line's allowance. Present only when the product line has an allowance configured. | | `items[].subscriptions[].products[].overage.listPrice` | string (decimal) | no | Per-unit list price for overage usage. | | `items[].subscriptions[].products[].overage.unitPrice` | string (decimal) | no | Per-unit price for overage usage after discounts. | | `items[].subscriptions[].products[].overage.discountAmount` | string (decimal) | no | Per-unit overage discount in the order currency. | | `items[].subscriptions[].products[].overage.discountPercentage` | string (decimal) | no | Per-unit overage discount as a percentage. | | `items[].subscriptions[].products[].rateCardEntries` | array of object | no | Rate-card rows for this line. Present when the product line uses rate-card pricing. | | `items[].subscriptions[].products[].rateCardEntries[].id` | string | yes | Id of this rate-card entry (a UUID). | | `items[].subscriptions[].products[].rateCardEntries[].identifier` | string | no | BillableComponent identifier this rate-card row meters. | | `items[].subscriptions[].products[].rateCardEntries[].label` | string | no | Free-text label for this rate-card row, used as an additional discriminator alongside the identifier. | | `items[].subscriptions[].products[].rateCardEntries[].unitOfMeasure` | string | yes | Unit in which this rate-card row is priced. | | `items[].subscriptions[].products[].rateCardEntries[].unitPrice` | string (decimal) | yes | Per-unit rate negotiated for this order. | | `items[].subscriptions[].products[].rateCardEntries[].currency` | string | yes | ISO 4217 currency code for this rate-card row. | | `items[].subscriptions[].products[].tiers` | array of object | no | Pricing tiers for this product line. Present only for tiered, volume or stairstep price models. | | `items[].subscriptions[].products[].tiers[].fromQuantity` | string (decimal) | yes | Lower bound of the quantity range (inclusive). | | `items[].subscriptions[].products[].tiers[].toQuantity` | string (decimal) | no | Upper bound of the quantity range (inclusive). Absent on an open-ended last tier. | | `items[].subscriptions[].products[].tiers[].unitPrice` | string (decimal) | yes | Per-unit price for quantities in this tier. | | `items[].subscriptions[].products[].tiers[].listPrice` | string (decimal) | no | Per-unit list price before discount. | | `items[].subscriptions[].products[].tiers[].priceModel` | string | no | Per-tier price model. One of 'flat', 'perUnit'. | | `items[].subscriptions[].products[].tags` | array of object | no | Value-accounting tags applied to this product, as dimension/value pairs. Tags propagate to journal-entry lines for dimensional reporting. | | `items[].subscriptions[].products[].tags[].dimension` | string | yes | Tag dimension name, e.g. 'Department', 'Project'. | | `items[].subscriptions[].products[].tags[].value` | string | yes | Value within the dimension, e.g. 'Engineering', 'Alpha-2026'. | | `items[].subscriptions[].products[].items` | array of object | no | Per-item breakdown: one entry per SalesOrderProductItem (product x inventory item) under this sales-order product. Present only for inventory-tracked products. | | `items[].subscriptions[].products[].items[].id` | string | yes | Id of this SalesOrderProductItem junction record (a UUID). | | `items[].subscriptions[].products[].items[].itemId` | string | no | Id of the linked inventory item (a UUID). Absent when the record has no item set. | | `items[].subscriptions[].products[].items[].itemCode` | string | no | Item code from ItemMaster. | | `items[].subscriptions[].products[].items[].itemName` | string | no | Item name from ItemMaster. | | `items[].subscriptions[].products[].items[].productName` | string | no | Product name denormalised from the parent SalesOrderProduct. | | `items[].subscriptions[].products[].items[].itemOrdered` | string (decimal) | yes | Ordered quantity for this item, after explosion: line quantity x product-item quantity. | | `items[].subscriptions[].products[].items[].totalShipped` | string (decimal) | no | Sum of shipped quantity across all linked SalesOrderShipmentItems. | | `items[].subscriptions[].products[].rampDeal` | object | no | Ramp-deal definition. Present only when the product is a ramp deal; ramps echo the persisted SalesOrderProductLine records. | | `items[].subscriptions[].products[].rampDeal.interval` | string | yes | Ramp interval mode. One of 'annually', 'specificDates'. | | `items[].subscriptions[].products[].rampDeal.invoiceTiming` | string | yes | Ramp invoice timing. One of 'perInterval', 'allAtOnce'. | | `items[].subscriptions[].products[].rampDeal.ramps` | array of object | yes | Persisted ramps, ordered by start date. At least two. | | `items[].subscriptions[].products[].rampDeal.ramps[].serviceStartDate` | string (date) | yes | First date this ramp's services are delivered (YYYY-MM-DD). | | `items[].subscriptions[].products[].rampDeal.ramps[].serviceEndDate` | string (date) | yes | Last date this ramp's services are delivered (YYYY-MM-DD). | | `items[].subscriptions[].products[].rampDeal.ramps[].quantity` | string (decimal) | yes | Quantity for this ramp period. | | `items[].subscriptions[].products[].rampDeal.ramps[].listPrice` | string (decimal) | no | Per-unit list price for this ramp before discount. | | `items[].subscriptions[].products[].rampDeal.ramps[].unitPrice` | string (decimal) | no | Per-unit price for this ramp after discount. | | `items[].subscriptions[].products[].rampDeal.ramps[].invoiceFrequency` | string | no | How often this ramp is invoiced. | | `items[].subscriptions[].products[].prepaidConfiguration` | object | no | Prepaid balance details for this product. Absent when the product is not configured as prepaid. | | `items[].subscriptions[].products[].prepaidConfiguration.overConsumptionBehavior` | string | yes | Behaviour when consumption exceeds the remaining prepaid balance. One of 'allowNegative' (balance may go negative and is reconciled at renewal) or 'overage' (usage beyond the balance switches to pay-as-you-go invoicing). | | `items[].subscriptions[].products[].prepaidConfiguration.rolloverAtRenewal` | boolean | yes | Whether any unused balance carries forward when the subscription renews. | | `items[].subscriptions[].products[].prepaidConfiguration.balance` | string (decimal) | no | Remaining prepaid quantity for this product. Decreases as consumption is applied. | | `items[].subscriptions[].products[].prepaidConfiguration.consumedAmount` | string (decimal) | no | Total quantity consumed from the prepaid balance to date. | | `items[].nonSubscriptionProducts` | array of object | yes | One-time and other non-subscription product lines on the order, e.g. one-off services or perpetual licences. Same line schema as a subscription's products. | | `items[].nonSubscriptionProducts[].productCode` | string | yes | Company-specific code of the product. | | `items[].nonSubscriptionProducts[].productId` | string | yes | Id of the product (a UUID). | | `items[].nonSubscriptionProducts[].productLineId` | string | yes | Id of the product line under the product (a UUID). | | `items[].nonSubscriptionProducts[].productName` | string | no | Product name as recorded on this line. May differ from the product's current name — the line keeps the name it was sold under. | | `items[].nonSubscriptionProducts[].salesOrderProductLineId` | string | no | Id of this sales-order line (a UUID). | | `items[].nonSubscriptionProducts[].quantity` | string (decimal) | yes | Quantity of the product on this line. For ramp deals the per-period quantity is on each ramp instead. | | `items[].nonSubscriptionProducts[].listPrice` | string (decimal) | no | Per-unit list price before discounts. | | `items[].nonSubscriptionProducts[].unitPrice` | string (decimal) | no | Per-unit price after discounts. Equals listPrice − discountAmount, or listPrice * (1 − discountPercentage/100). | | `items[].nonSubscriptionProducts[].totalPrice` | string (decimal) | no | Total price for this line (unitPrice * quantity). | | `items[].nonSubscriptionProducts[].discountAmount` | string (decimal) | no | Per-unit discount in the order currency. | | `items[].nonSubscriptionProducts[].discountPercentage` | string (decimal) | no | Per-unit discount as a percentage (e.g. 10 for 10%). | | `items[].nonSubscriptionProducts[].invoiceFrequency` | string | no | How often invoices are generated for this line, e.g. monthly, quarterly, annually. | | `items[].nonSubscriptionProducts[].invoiceTiming` | string | no | Whether each invoice for this line is raised at the start or the end of the period it covers. One of 'inAdvance', 'inArrears'. | | `items[].nonSubscriptionProducts[].lineInvoiceDay` | string | no | Which day of the period the invoice is dated, e.g. 'firstDayOfMonth', 'specificDay', 'serviceStartDate'. | | `items[].nonSubscriptionProducts[].priceModel` | string | no | How the price is derived from quantity, e.g. 'flatFee', 'perUnit', 'tiered', 'volume'. Tiered and volume models carry their bands in tiers. | | `items[].nonSubscriptionProducts[].unitOfMeasure` | string | no | Unit the quantity is counted in, e.g. 'each', 'user', 'gigabyte'. | | `items[].nonSubscriptionProducts[].pricingTerm` | string | no | Period the unit price is quoted for, e.g. 'monthly', 'annual' — the basis prices are normalised to before invoicing. | | `items[].nonSubscriptionProducts[].serviceStartDate` | string (date) | no | First date services for this product are delivered (YYYY-MM-DD). Defaults to the order-level serviceStartDate. | | `items[].nonSubscriptionProducts[].serviceEndDate` | string (date) | no | Last date services for this product are delivered (YYYY-MM-DD). Defaults to the order-level serviceEndDate. | | `items[].nonSubscriptionProducts[].externalIdentifiers` | array of object | yes | External identifiers recorded against this line, one per configured field. Empty when none are. | | `items[].nonSubscriptionProducts[].externalIdentifiers[].fieldName` | string | yes | Configured field the identifier belongs to, e.g. 'salesforceLineId'. | | `items[].nonSubscriptionProducts[].externalIdentifiers[].value` | string | yes | The identifier's value in that external system. | | `items[].nonSubscriptionProducts[].externalIdentifiers[].externalUrl` | string | no | Deep link to the record in that system, when the configuration defines one. | | `items[].nonSubscriptionProducts[].overage` | object | no | Overage pricing for usage above the line's allowance. Present only when the product line has an allowance configured. | | `items[].nonSubscriptionProducts[].overage.listPrice` | string (decimal) | no | Per-unit list price for overage usage. | | `items[].nonSubscriptionProducts[].overage.unitPrice` | string (decimal) | no | Per-unit price for overage usage after discounts. | | `items[].nonSubscriptionProducts[].overage.discountAmount` | string (decimal) | no | Per-unit overage discount in the order currency. | | `items[].nonSubscriptionProducts[].overage.discountPercentage` | string (decimal) | no | Per-unit overage discount as a percentage. | | `items[].nonSubscriptionProducts[].rateCardEntries` | array of object | no | Rate-card rows for this line. Present when the product line uses rate-card pricing. | | `items[].nonSubscriptionProducts[].rateCardEntries[].id` | string | yes | Id of this rate-card entry (a UUID). | | `items[].nonSubscriptionProducts[].rateCardEntries[].identifier` | string | no | BillableComponent identifier this rate-card row meters. | | `items[].nonSubscriptionProducts[].rateCardEntries[].label` | string | no | Free-text label for this rate-card row, used as an additional discriminator alongside the identifier. | | `items[].nonSubscriptionProducts[].rateCardEntries[].unitOfMeasure` | string | yes | Unit in which this rate-card row is priced. | | `items[].nonSubscriptionProducts[].rateCardEntries[].unitPrice` | string (decimal) | yes | Per-unit rate negotiated for this order. | | `items[].nonSubscriptionProducts[].rateCardEntries[].currency` | string | yes | ISO 4217 currency code for this rate-card row. | | `items[].nonSubscriptionProducts[].tiers` | array of object | no | Pricing tiers for this product line. Present only for tiered, volume or stairstep price models. | | `items[].nonSubscriptionProducts[].tiers[].fromQuantity` | string (decimal) | yes | Lower bound of the quantity range (inclusive). | | `items[].nonSubscriptionProducts[].tiers[].toQuantity` | string (decimal) | no | Upper bound of the quantity range (inclusive). Absent on an open-ended last tier. | | `items[].nonSubscriptionProducts[].tiers[].unitPrice` | string (decimal) | yes | Per-unit price for quantities in this tier. | | `items[].nonSubscriptionProducts[].tiers[].listPrice` | string (decimal) | no | Per-unit list price before discount. | | `items[].nonSubscriptionProducts[].tiers[].priceModel` | string | no | Per-tier price model. One of 'flat', 'perUnit'. | | `items[].nonSubscriptionProducts[].tags` | array of object | no | Value-accounting tags applied to this product, as dimension/value pairs. Tags propagate to journal-entry lines for dimensional reporting. | | `items[].nonSubscriptionProducts[].tags[].dimension` | string | yes | Tag dimension name, e.g. 'Department', 'Project'. | | `items[].nonSubscriptionProducts[].tags[].value` | string | yes | Value within the dimension, e.g. 'Engineering', 'Alpha-2026'. | | `items[].nonSubscriptionProducts[].items` | array of object | no | Per-item breakdown: one entry per SalesOrderProductItem (product x inventory item) under this sales-order product. Present only for inventory-tracked products. | | `items[].nonSubscriptionProducts[].items[].id` | string | yes | Id of this SalesOrderProductItem junction record (a UUID). | | `items[].nonSubscriptionProducts[].items[].itemId` | string | no | Id of the linked inventory item (a UUID). Absent when the record has no item set. | | `items[].nonSubscriptionProducts[].items[].itemCode` | string | no | Item code from ItemMaster. | | `items[].nonSubscriptionProducts[].items[].itemName` | string | no | Item name from ItemMaster. | | `items[].nonSubscriptionProducts[].items[].productName` | string | no | Product name denormalised from the parent SalesOrderProduct. | | `items[].nonSubscriptionProducts[].items[].itemOrdered` | string (decimal) | yes | Ordered quantity for this item, after explosion: line quantity x product-item quantity. | | `items[].nonSubscriptionProducts[].items[].totalShipped` | string (decimal) | no | Sum of shipped quantity across all linked SalesOrderShipmentItems. | | `items[].nonSubscriptionProducts[].rampDeal` | object | no | Ramp-deal definition. Present only when the product is a ramp deal; ramps echo the persisted SalesOrderProductLine records. | | `items[].nonSubscriptionProducts[].rampDeal.interval` | string | yes | Ramp interval mode. One of 'annually', 'specificDates'. | | `items[].nonSubscriptionProducts[].rampDeal.invoiceTiming` | string | yes | Ramp invoice timing. One of 'perInterval', 'allAtOnce'. | | `items[].nonSubscriptionProducts[].rampDeal.ramps` | array of object | yes | Persisted ramps, ordered by start date. At least two. | | `items[].nonSubscriptionProducts[].rampDeal.ramps[].serviceStartDate` | string (date) | yes | First date this ramp's services are delivered (YYYY-MM-DD). | | `items[].nonSubscriptionProducts[].rampDeal.ramps[].serviceEndDate` | string (date) | yes | Last date this ramp's services are delivered (YYYY-MM-DD). | | `items[].nonSubscriptionProducts[].rampDeal.ramps[].quantity` | string (decimal) | yes | Quantity for this ramp period. | | `items[].nonSubscriptionProducts[].rampDeal.ramps[].listPrice` | string (decimal) | no | Per-unit list price for this ramp before discount. | | `items[].nonSubscriptionProducts[].rampDeal.ramps[].unitPrice` | string (decimal) | no | Per-unit price for this ramp after discount. | | `items[].nonSubscriptionProducts[].rampDeal.ramps[].invoiceFrequency` | string | no | How often this ramp is invoiced. | | `items[].nonSubscriptionProducts[].prepaidConfiguration` | object | no | Prepaid balance details for this product. Absent when the product is not configured as prepaid. | | `items[].nonSubscriptionProducts[].prepaidConfiguration.overConsumptionBehavior` | string | yes | Behaviour when consumption exceeds the remaining prepaid balance. One of 'allowNegative' (balance may go negative and is reconciled at renewal) or 'overage' (usage beyond the balance switches to pay-as-you-go invoicing). | | `items[].nonSubscriptionProducts[].prepaidConfiguration.rolloverAtRenewal` | boolean | yes | Whether any unused balance carries forward when the subscription renews. | | `items[].nonSubscriptionProducts[].prepaidConfiguration.balance` | string (decimal) | no | Remaining prepaid quantity for this product. Decreases as consumption is applied. | | `items[].nonSubscriptionProducts[].prepaidConfiguration.consumedAmount` | string (decimal) | no | Total quantity consumed from the prepaid balance to date. | | `items[].lastModifiedDate` | string (date-time) | yes | Instant the order last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Poll on this to sync incrementally rather than re-reading the whole collection. | | `items[].externalIdentifiers` | array of object | no | External identifiers owned by other systems. At most one entry per fieldName. Absent when the order has none; an omitted field and [] are equivalent. | | `items[].externalIdentifiers[].fieldName` | string | yes | Configured key of the external identifier — a stable camelCase slug such as 'netsuiteSoNumber', not the human-readable label. Must match an active SalesOrderExternalIdentifierConfiguration. | | `items[].externalIdentifiers[].value` | string | yes | The identifier as the source system holds it. | | `items[].externalIdentifiers[].externalUrl` | string | no | Absolute http(s) link to the object in the originating system, when one is stored. | | `pagination` | object | yes | Pagination cursor for this page. | | `pagination.nextCursor` | string | no | Opaque cursor to pass as page.cursor for the next page; absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Errors | Code | Status | | --- | --- | | `UNKNOWN_EXTERNAL_IDENTIFIER` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: UNKNOWN_EXTERNAL_IDENTIFIER - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # List Accounting Sales Returns `GET /accounting/sales-returns` Group: Accounting › Sales Returns · Operation ID: `listAccountingSalesReturns` Returns a list of sales returns. Each entry is a header. The full record, with lines, comes from GET accounting/sales-returns/:id or POST accounting/sales-returns/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=listAccountingSalesReturns) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `returnType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `salesOrderId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, status, returnType, returnDate, salesOrderId, currency, lastModifiedDate - Selectable: id, salesReturnNumber, status, returnType, returnDate, salesOrderId, currency, lastModifiedDate ## Response `200` Success Under `data`, an array of listAccountingSalesReturnsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the sales return. | | `salesReturnNumber` | string or null | no | The RMA number, for example 'SR-1'. Absent while none has been assigned. | | `status` | string or null | no | The lifecycle status of the sales return. Possible values: 'draft', 'submitted', 'received', 'approved', 'completed', 'cancelled', 'rejected'. | | `returnType` | string | no | The resolution path for the return. Possible values: 'credit', 'replacement'. It cannot be changed once status moves past 'draft'. | | `returnDate` | string (date) | no | The transaction date for the return. Format YYYY-MM-DD. | | `salesOrderId` | string or null | no | The sales order this return is against. A sales return is always against exactly one sales order. See id on GET accounting/sales-orders/:id. | | `currency` | string | no | The currency of every amount on this record and its lines. Format ISO 4217, for example 'USD'. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the sales return last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a sales return against a sales order. `POST /accounting/sales-returns` Group: Accounting › Sales Returns · Operation ID: `createAccountingSalesReturn` [Open in the interactive reference](https://docs.api.lensing.ai/?op=createAccountingSalesReturn) ## 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`: createAccountingSalesReturnInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `salesOrderId` | string | yes | The sales order to return against. The order must be partially or fully invoiced. See id on GET accounting/sales-orders/:id. | | `returnType` | string | yes | The resolution path for the return. Accepted values: 'credit', 'replacement'. It cannot be changed once the return moves past 'draft'. | | `returnDate` | string | yes | The transaction date for the return. Use format YYYY-MM-DD. | | `reasonCode` | string | no | The header-level reason for the return. Accepted values: 'defectiveItem', 'wrongItemShipped', 'damagedInTransit', 'customerRequest', 'duplicateOrder', 'notAsDescribed', 'other'. | | `lines` | array of object | yes | The return lines. At least one line with a quantity above 0 is required. | | `lines[].salesOrderProductLineId` | string | yes | The sales order product line being returned. | | `lines[].quantity` | string (decimal) | yes | The quantity to return. It must be above 0 and not exceed what remains returnable on the line. | | `lines[].reasonCode` | string | no | The reason for this line. Accepted values: 'defectiveItem', 'wrongItemShipped', 'damagedInTransit', 'customerRequest', 'duplicateOrder', 'notAsDescribed', 'other'. | | `lines[].returnFeeAmount` | string (decimal) | no | The fee deducted from the customer refund for this line, such as a restocking fee, in the sales order's currency. Credit returns only. | | `notes` | string | no | Free-form notes for the return. | | `shippingRefundAmount` | string (decimal) | no | The shipping charge to refund to the customer, in the sales order's currency. Credit returns only. Requires shippingSalesOrderProductLineId. | | `shippingSalesOrderProductLineId` | string | no | The shipping sales order product line whose charge is refunded. Its product has product type 'shipping'. | Example: ```json { "salesOrderId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "returnType": "", "returnDate": "", "reasonCode": "", "lines": [ { "salesOrderProductLineId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "reasonCode": "", "returnFeeAmount": "19.99" } ], "notes": "", "shippingRefundAmount": "19.99", "shippingSalesOrderProductLineId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` ## Response `200` Success Under `data` (createAccountingSalesReturnOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created sales return. | ## 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_PRODUCT_LINE_NOT_FOUND` | 400 | | `LINES_REQUIRED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: SALES_ORDER_NOT_FOUND, SALES_ORDER_PRODUCT_LINE_NOT_FOUND, LINES_REQUIRED - `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 --- # Get Accounting Sales Return `GET /accounting/sales-returns/{id}` Group: Accounting › Sales Returns · Operation ID: `getAccountingSalesReturn` Returns the sales return with the given identifier, including its lines. Use the sales return's id, not its salesReturnNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getAccountingSalesReturn) ## 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`. | | `id` | path | string | yes | The unique identifier for the sales return to return. | ## Response `200` Success Under `data` (getAccountingSalesReturnOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the sales return. | | `salesReturnNumber` | string | no | The RMA number, for example 'SR-1'. Absent while none has been assigned. | | `status` | string | yes | The lifecycle status of the sales return. Possible values: 'draft', 'submitted', 'received', 'approved', 'completed', 'cancelled', 'rejected'. | | `returnType` | string | yes | The resolution path for the return. Possible values: 'credit', 'replacement'. It cannot be changed once status moves past 'draft'. | | `returnDate` | string | yes | The transaction date for the return. Format YYYY-MM-DD. | | `salesOrderId` | string | yes | The sales order this return is against. A sales return is always against exactly one sales order. See id on GET accounting/sales-orders/:id. | | `currency` | string | yes | The currency of every amount on this record and its lines. Format ISO 4217, for example 'USD'. | | `reasonCode` | string | no | The customer-facing reason for the return. Possible values: 'defectiveItem', 'wrongItemShipped', 'damagedInTransit', 'customerRequest', 'duplicateOrder', 'notAsDescribed', 'other'. It is for reporting and does not affect how the return is resolved. | | `notes` | string | no | Free-form notes for the return. | | `returnTrackingNumber` | string | no | The carrier tracking number for the inbound return shipment. | | `totalReturnAmount` | string (decimal) | no | The total return amount, the sum of every line's totalPrice, in the return currency. | | `returnFeeAmount` | string (decimal) | no | The fee deducted from the customer refund, such as a restocking fee, in the return currency. Credit returns only. It is set either here or on the lines, never both. | | `shippingRefundAmount` | string (decimal) | no | The shipping charge refunded to the customer, in the return currency. Credit returns only. | | `totalRevenueImpact` | string (decimal) | no | The revenue reversed to the customer, the product-line revenue impact plus the shipping refund, in the return currency. Always 0 for the replacement path. | | `revenueStatus` | string | no | The revenue reversal status. Possible values: 'notApplicable', 'notStarted', 'reversed'. 'notApplicable' is the replacement path, and 'reversed' is a completed credit return. | | `inventoryReturnNumber` | string | no | The document number of the linked inventory sales return. Absent until one has been created. | | `lastModifiedDate` | string (date-time) | yes | The date and time the sales return last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The product lines being returned. | | `lines[].id` | string | yes | The unique identifier for this return line. | | `lines[].salesOrderProductLineId` | string | yes | The sales order product line this line returns. There is no endpoint for it. | | `lines[].lineType` | string | yes | The product line type, copied from the sales order line at creation. | | `lines[].quantity` | string (decimal) | yes | The quantity being returned. | | `lines[].unitOfMeasure` | string | yes | The unit of measure, copied from the sales order line. | | `lines[].currency` | string | yes | The currency of unitPrice, totalPrice and returnFeeAmount on this line, copied from the sales return. Format ISO 4217, for example 'USD'. | | `lines[].unitPrice` | string (decimal) | yes | The unit price, copied from the sales order line at creation, in this line's currency. | | `lines[].totalPrice` | string (decimal) | no | The total price for the line, quantity times unitPrice, in this line's currency. | | `lines[].returnFeeAmount` | string (decimal) | no | The fee deducted from the customer refund for this line, in this line's currency. Credit returns only. Set only when fees are tracked per line rather than on the header. | | `lines[].revenueImpactAmount` | string (decimal) | no | The revenue reversed for this line, in this line's currency. 0 for the replacement path. | | `lines[].lineStatus` | string | yes | The lifecycle status of this line. Possible values: 'pending', 'received', 'approved', 'completed', 'cancelled', 'rejected'. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Accounting Sales Return `PATCH /accounting/sales-returns/{id}` Group: Accounting › Sales Returns · Operation ID: `updateAccountingSalesReturn` Updates the sales return with the given identifier. Only the fields in the request body change. Use the sales return's id, not its salesReturnNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateAccountingSalesReturn) ## 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. | | `id` | path | string | yes | The unique identifier for the sales return to update. | ## Request body Required, `application/json`: updateAccountingSalesReturnInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `notes` | string or null | no | New free-form notes for the return. | | `reasonCode` | string or null | no | The new header-level reason for the return. Accepted values: 'defectiveItem', 'wrongItemShipped', 'damagedInTransit', 'customerRequest', 'duplicateOrder', 'notAsDescribed', 'other'. | | `returnTrackingNumber` | string or null | no | The new carrier tracking number for the inbound return shipment. | Example: ```json { "notes": "", "reasonCode": "", "returnTrackingNumber": "" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Cancels the sales return with the given identifier. `POST /accounting/sales-returns/{id}/cancel` Group: Accounting › Sales Returns · Operation ID: `cancelAccountingSalesReturn` [Open in the interactive reference](https://docs.api.lensing.ai/?op=cancelAccountingSalesReturn) ## 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. | | `id` | path | string | yes | The unique identifier for the sales return to cancel. | ## Response `200` Success Under `data` (cancelAccountingSalesReturnOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the sales return. | | `status` | string | yes | The lifecycle status after the call. Always 'cancelled' on success. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `INVALID_STATUS` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, INVALID_STATUS - `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 --- # Search Accounting Sales Return `POST /accounting/sales-returns/search` Group: Accounting › Sales Returns · Operation ID: `searchAccountingSalesReturn` Returns the sales returns that match the given filters, each with its lines. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchAccountingSalesReturn) ## 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`. | ## Request body Required, `application/json`: searchAccountingSalesReturnInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A sales return must match every filter you send. Omit to return all sales returns. | | `filters.id` | string | no | Limits results to the sales return with this identifier. | | `filters.salesReturnNumber` | string | no | Limits results to the sales return with this RMA number, for example 'SR-104'. | | `filters.status` | array of string | no | Limits results to sales returns in any of these statuses. Accepted values: 'draft', 'submitted', 'received', 'approved', 'completed', 'cancelled', 'rejected'. An unknown value matches nothing. An empty array does not constrain. | | `filters.returnType` | array of string | no | Limits results to sales returns of any of these resolution paths. Accepted values: 'credit', 'replacement'. An unknown value matches nothing. An empty array does not constrain. | | `filters.reasonCode` | array of string | no | Limits results to sales returns with any of these header-level reasons. Accepted values: 'defectiveItem', 'wrongItemShipped', 'damagedInTransit', 'customerRequest', 'duplicateOrder', 'notAsDescribed', 'other'. An empty array does not constrain. | | `filters.salesOrderId` | string | no | Limits results to sales returns raised against the sales order with this identifier. An identifier matching no sales order returns an empty page. | | `filters.currency` | string | no | Limits results to sales returns in this currency. Format ISO 4217, for example 'USD'. | | `filters.notes` | string | no | Limits results to sales returns whose notes contain this text. The match is case-insensitive. | | `filters.returnTrackingNumber` | string | no | Limits results to the sales return with this carrier tracking number. | | `filters.returnDate` | object | no | Limits results to sales returns whose returnDate falls in this range. Omit either bound to leave that side open. | | `filters.returnDate.start` | string (date) | no | The earliest date to include. Format YYYY-MM-DD. | | `filters.returnDate.end` | string (date) | no | The latest date to include. Format YYYY-MM-DD. | | `filters.lastModifiedDate` | object | no | Limits results to sales returns changed in this range, for polling an incremental sync. Omit either bound to leave that side open. | | `filters.lastModifiedDate.start` | string | no | The earliest time to include, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `filters.lastModifiedDate.end` | string | no | The latest time to include, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of sales returns per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "salesReturnNumber": "", "status": [ "" ], "returnType": [ "" ], "reasonCode": [ "" ], "salesOrderId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "currency": "", "notes": "", "returnTrackingNumber": "", "returnDate": { "start": "2026-01-15", "end": "2026-01-15" }, "lastModifiedDate": { "start": "", "end": "" } }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchAccountingSalesReturnOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The sales returns on this page, oldest first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the sales return. | | `items[].salesReturnNumber` | string | no | The RMA number, for example 'SR-1'. Absent while none has been assigned. | | `items[].status` | string | yes | The lifecycle status of the sales return. Possible values: 'draft', 'submitted', 'received', 'approved', 'completed', 'cancelled', 'rejected'. | | `items[].returnType` | string | yes | The resolution path for the return. Possible values: 'credit', 'replacement'. It cannot be changed once status moves past 'draft'. | | `items[].returnDate` | string | yes | The transaction date for the return. Format YYYY-MM-DD. | | `items[].salesOrderId` | string | yes | The sales order this return is against. A sales return is always against exactly one sales order. See id on GET accounting/sales-orders/:id. | | `items[].currency` | string | yes | The currency of every amount on this record and its lines. Format ISO 4217, for example 'USD'. | | `items[].reasonCode` | string | no | The customer-facing reason for the return. Possible values: 'defectiveItem', 'wrongItemShipped', 'damagedInTransit', 'customerRequest', 'duplicateOrder', 'notAsDescribed', 'other'. It is for reporting and does not affect how the return is resolved. | | `items[].notes` | string | no | Free-form notes for the return. | | `items[].returnTrackingNumber` | string | no | The carrier tracking number for the inbound return shipment. | | `items[].totalReturnAmount` | string (decimal) | no | The total return amount, the sum of every line's totalPrice, in the return currency. | | `items[].returnFeeAmount` | string (decimal) | no | The fee deducted from the customer refund, such as a restocking fee, in the return currency. Credit returns only. It is set either here or on the lines, never both. | | `items[].shippingRefundAmount` | string (decimal) | no | The shipping charge refunded to the customer, in the return currency. Credit returns only. | | `items[].totalRevenueImpact` | string (decimal) | no | The revenue reversed to the customer, the product-line revenue impact plus the shipping refund, in the return currency. Always 0 for the replacement path. | | `items[].revenueStatus` | string | no | The revenue reversal status. Possible values: 'notApplicable', 'notStarted', 'reversed'. 'notApplicable' is the replacement path, and 'reversed' is a completed credit return. | | `items[].inventoryReturnNumber` | string | no | The document number of the linked inventory sales return. Absent until one has been created. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the sales return last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | yes | The product lines being returned. | | `items[].lines[].id` | string | yes | The unique identifier for this return line. | | `items[].lines[].salesOrderProductLineId` | string | yes | The sales order product line this line returns. There is no endpoint for it. | | `items[].lines[].lineType` | string | yes | The product line type, copied from the sales order line at creation. | | `items[].lines[].quantity` | string (decimal) | yes | The quantity being returned. | | `items[].lines[].unitOfMeasure` | string | yes | The unit of measure, copied from the sales order line. | | `items[].lines[].currency` | string | yes | The currency of unitPrice, totalPrice and returnFeeAmount on this line, copied from the sales return. Format ISO 4217, for example 'USD'. | | `items[].lines[].unitPrice` | string (decimal) | yes | The unit price, copied from the sales order line at creation, in this line's currency. | | `items[].lines[].totalPrice` | string (decimal) | no | The total price for the line, quantity times unitPrice, in this line's currency. | | `items[].lines[].returnFeeAmount` | string (decimal) | no | The fee deducted from the customer refund for this line, in this line's currency. Credit returns only. Set only when fees are tracked per line rather than on the header. | | `items[].lines[].revenueImpactAmount` | string (decimal) | no | The revenue reversed for this line, in this line's currency. 0 for the replacement path. | | `items[].lines[].lineStatus` | string | yes | The lifecycle status of this line. Possible values: 'pending', 'received', 'approved', 'completed', 'cancelled', 'rejected'. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more sales returns remain beyond this page. | ## Errors | Code | Status | | --- | --- | | `INVALID_TIMESTAMP` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_TIMESTAMP - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Subscriptions `GET /accounting/subscriptions` Group: Accounting › Subscriptions · Operation ID: `subscriptions` Returns a list of subscriptions. There is no get-one endpoint. For the full record, including the customer and legal entity, use POST accounting/subscriptions/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=subscriptions) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `autoRenew` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `renewalTerm` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `renewalPriceChangePercentage` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `termType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `subscriptionStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `serviceStartDateBasis` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `serviceActivationDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `serviceStartDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `serviceEndDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `submissionDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `cancellationDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `cancellationReason` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isCancellationEnabled` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `suspensionDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `suspensionReason` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isSuspensionEnabled` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, currency, autoRenew, renewalTerm, renewalPriceChangePercentage, termType, subscriptionStatus, serviceStartDateBasis, serviceActivationDate, serviceStartDate, serviceEndDate, submissionDate, cancellationDate, cancellationReason, isCancellationEnabled, suspensionDate, suspensionReason, isSuspensionEnabled, annualRecurringRevenue, monthlyRecurringRevenue, totalUserCount, prepaidBalance, lastModifiedDate - Selectable: id, subscriptionNumber, customerName, currency, autoRenew, renewalTerm, renewalPriceChangePercentage, termType, subscriptionStatus, serviceStartDateBasis, serviceActivationDate, serviceStartDate, serviceEndDate, submissionDate, cancellationDate, cancellationReason, isCancellationEnabled, suspensionDate, suspensionReason, isSuspensionEnabled, externalSubscriptionId, annualRecurringRevenue, monthlyRecurringRevenue, totalUserCount, prepaidBalance, lastModifiedDate ## Response `200` Success Under `data`, an array of subscriptionsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the subscription. | | `subscriptionNumber` | string or null | no | The subscription number, for example 'SUB-1', unique within the tenant. Absent until one has been assigned. | | `customerName` | string or null | no | The display name of the customer this subscription belongs to. | | `currency` | string | no | The currency of annualRecurringRevenue and monthlyRecurringRevenue. Format ISO 4217, for example 'USD'. | | `autoRenew` | boolean or null | no | Whether the subscription renews automatically at the end of its term. | | `renewalTerm` | number or null | no | The length of the auto-renewal term, in the subscription's term unit. | | `renewalPriceChangePercentage` | string (decimal) or null | no | The percentage the price is adjusted by on renewal. It is a percentage, not an amount, so it has no currency. | | `termType` | string | no | The contract term structure. Possible values: 'termed', 'evergreen'. | | `subscriptionStatus` | string or null | no | The subscription's lifecycle status, derived from the status of its lines. Possible values: 'pendingActivation', 'notStarted', 'active', 'pendingOrderApproval', 'canceled', 'suspended'. | | `serviceStartDateBasis` | string or null | no | How the service start date is determined. Possible values: 'fixedDate', 'serviceActivation'. | | `serviceActivationDate` | string (date) or null | no | The date service was activated, when serviceStartDateBasis is 'serviceActivation'. Format YYYY-MM-DD. | | `serviceStartDate` | string (date) or null | no | The earliest service start date across the subscription's lines. Format YYYY-MM-DD. | | `serviceEndDate` | string (date) or null | no | The latest service end date across the subscription's lines. Format YYYY-MM-DD. | | `submissionDate` | string (date) or null | no | The date the subscription's originating sales order was submitted. Format YYYY-MM-DD. | | `cancellationDate` | string (date) or null | no | The date from which the subscription is or will be canceled. Absent when no cancellation has been recorded. Format YYYY-MM-DD. | | `cancellationReason` | string or null | no | The reason code for the cancellation. Possible values: 'noReasonGiven', 'budgetary', 'movingToCompetitors', 'lowCustomerSatisfaction', 'bankruptcy', 'acquisitionMerger'. Absent when the subscription has not been canceled. | | `isCancellationEnabled` | boolean or null | no | Whether the cancellation procedure has been applied to this subscription. | | `suspensionDate` | string (date) or null | no | The date from which the subscription is or will be suspended. Absent when no suspension has been recorded. Format YYYY-MM-DD. | | `suspensionReason` | string or null | no | The reason code for the suspension. Possible values: 'unpaidBalance', 'invalidPaymentMethod'. Absent when the subscription has not been suspended. | | `isSuspensionEnabled` | boolean or null | no | Whether the suspension process is enabled for this subscription. | | `externalSubscriptionId` | string or null | no | The subscription's identifier in an external system. Absent when none was recorded at creation. | | `annualRecurringRevenue` | string (decimal) or null | no | The annualized recurring revenue, in the subscription's currency: the sum of each active line's annual recurring revenue. | | `monthlyRecurringRevenue` | string (decimal) or null | no | The monthly recurring revenue, in the subscription's currency: the sum of each active line's monthly recurring revenue. | | `totalUserCount` | string (decimal) or null | no | The total active user or seat count across the subscription's active lines. It is a count, not a currency amount. | | `prepaidBalance` | string (decimal) or null | no | The remaining prepaid balance available to this subscription, summed across its prepaid balance accesses. It is a drawdown counter, not a currency amount. | | `lastModifiedDate` | string (date-time) | no | The date and time the subscription record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Get Subscription `GET /accounting/subscriptions/{id}` Group: Accounting › Subscriptions · Operation ID: `getSubscription` Fetch a single subscription by its id (a UUID), bound from the :id path segment. Returns the subscription with its customer and legal entity references, renewal terms, derived service dates and lifecycle status, cancellation/suspension state and notes, recognised recurring revenue, and its line detail: every subscription line with its product, quantity, service dates and own recurring revenue. The full item schema — header fields and the nested `lines` collection alike — matches POST accounting/subscriptions/search (R2.4.1). GET accounting/subscriptions is a data-set and returns header fields only. Read-only. Raises NOT_FOUND when no subscription has that id (should map to 404). Every request must carry the Api-Version header. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getSubscription) ## 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`. | | `id` | path | string | yes | Id of the subscription to fetch (a UUID); bound from the :id path segment. | ## Response `200` Success Under `data` (getSubscriptionOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Id of the subscription (a UUID) — the public identifier used by GET accounting/subscriptions/:id. | | `subscriptionNumber` | string | no | System-generated, human-readable subscription number (e.g. SUB-1), unique per tenant. Absent until one has been assigned. | | `customerId` | string | no | Id of the customer this subscription belongs to (a UUID). | | `customerName` | string | no | Display name of that customer, denormalised onto the subscription. | | `entityId` | string | no | Id of the legal entity that owns this subscription (a UUID). Absent when the entity's own uuid column is unset — a pre-existing data-quality gap on some entity records, not an error (see qtc/subscription/core/search/searchSubscription.ts's toViews()). | | `currency` | string | yes | ISO 4217 three-letter currency code (e.g. USD, EUR, GBP) governing annualRecurringRevenue and monthlyRecurringRevenue below. | | `autoRenew` | boolean | no | Whether the subscription renews automatically at term end. | | `renewalTerm` | number | no | Length of the auto-renewal term, in the subscription's term unit. | | `renewalPriceChangePercentage` | string (decimal) | no | Percentage to adjust the price by on renewal. A percentage, not an amount — it carries no currency (R9.4c). | | `termType` | string | yes | Contract term structure. A TermType member: 'termed' or 'evergreen'. | | `subscriptionStatus` | string | no | Derived lifecycle status, computed from the status of the subscription's lines. A SubscriptionStatus member: 'pendingActivation', 'notStarted', 'active', 'pendingOrderApproval', 'canceled', 'suspended'. | | `serviceStartDateBasis` | string | no | Basis on which the service start date is determined. A ServiceStartDateBasis member: 'fixedDate' or 'serviceActivation'. | | `serviceActivationDate` | string (date) | no | Date service was activated, when serviceStartDateBasis is 'serviceActivation' (YYYY-MM-DD). | | `serviceStartDate` | string (date) | no | Earliest service start date across the subscription's lines (YYYY-MM-DD), derived. | | `serviceEndDate` | string (date) | no | Latest service end date across the subscription's lines (YYYY-MM-DD), derived. | | `submissionDate` | string (date) | no | Date the subscription's originating sales order was submitted (YYYY-MM-DD). | | `cancellationDate` | string (date) | no | Date from which the subscription is or will be canceled, when a cancellation has been recorded (YYYY-MM-DD). | | `cancellationNote` | string | no | Free-text note recorded about the cancellation. Carried here and on search/v1; the list data-set omits it as free text (R7.2). | | `cancellationReason` | string | no | Short reason code for the cancellation. A SubscriptionCancellationReasons member. Absent when the subscription has not been canceled. | | `isCancellationEnabled` | boolean | no | Whether the cancellation procedure has been applied to this subscription. | | `suspensionDate` | string (date) | no | Date from which the subscription is or will be suspended, when a suspension has been recorded (YYYY-MM-DD). | | `suspensionNote` | string | no | Free-text note recorded about the suspension. Carried here and on search/v1; the list data-set omits it as free text (R7.2). | | `suspensionReason` | string | no | Short reason code for the suspension. A SubscriptionSuspensionReasons member. Absent when the subscription has not been suspended. | | `isSuspensionEnabled` | boolean | no | Whether the suspension process is enabled for this subscription. | | `externalSubscriptionId` | string | no | Identifier of this subscription in an external system, when one was recorded at creation. | | `annualRecurringRevenue` | string (decimal) | no | Annualized recurring revenue, in the subscription currency (see `currency`): the sum of each active line's own annual recurring revenue. | | `monthlyRecurringRevenue` | string (decimal) | no | Monthly recurring revenue, in the subscription currency (see `currency`): the sum of each active line's own monthly recurring revenue. | | `totalUserCount` | string (decimal) | no | Total active user/seat count across the subscription's active lines. A count, not a currency amount (R9.4c). | | `prepaidBalance` | string (decimal) | no | Remaining prepaid balance available to this subscription, summed across its prepaid balance accesses. A drawdown counter, not a currency amount (R9.4c). | | `lines` | array of object | yes | The subscription's lines — one per product on the subscription — oldest first. Empty when the subscription has none. POST accounting/subscriptions/search carries the identical collection (R2.4.1); GET accounting/subscriptions cannot, being a scalar-only data-set (R2.3). | | `lines[].id` | string | yes | Id of the subscription line (a UUID) — its public identifier. | | `lines[].subscriptionLineNumber` | string | no | Unique, human-readable number of this subscription line. Absent until one has been assigned. | | `lines[].productName` | string | no | Name of the product this line covers. | | `lines[].quantity` | string (decimal) | no | Quantity across the line's associated sales order product lines. A count, not a currency amount (R9.4c). | | `lines[].currency` | string | yes | ISO 4217 three-letter currency code (e.g. USD, EUR, GBP) governing this line's annualRecurringRevenue and monthlyRecurringRevenue. | | `lines[].subscriptionLineStatus` | string | no | Lifecycle status of this line. A SubscriptionLineStatus member. | | `lines[].serviceStartDate` | string (date) | no | Date service starts for this line (YYYY-MM-DD). | | `lines[].serviceEndDate` | string (date) | no | Date service ends for this line (YYYY-MM-DD). | | `lines[].customServiceEndDate` | string (date) | no | Service end date recorded against this line by a subscription update, when one has overridden the derived date (YYYY-MM-DD). | | `lines[].maxSalesOrderProductLineServiceEndDate` | string (date) | no | Latest service end date across this line's sales order product lines (YYYY-MM-DD). | | `lines[].annualRecurringRevenue` | string (decimal) | no | Annualized recurring revenue of this line, in the line currency (see this line's `currency`). | | `lines[].monthlyRecurringRevenue` | string (decimal) | no | Monthly recurring revenue of this line, in the line currency (see this line's `currency`). | | `lastModifiedDate` | string (date-time) | yes | Instant the subscription record last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Poll on this to sync incrementally rather than re-reading the whole collection. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Search Subscription `POST /accounting/subscriptions/search` Group: Accounting › Subscriptions · Operation ID: `searchSubscription` Returns the subscriptions that match the given filters. Each match is the full record, including the customer and legal entity and the cancellation and suspension notes the collection omits. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchSubscription) ## 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`. | ## Request body Required, `application/json`: searchSubscriptionInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A subscription must match every filter you send. Omit to return all subscriptions you may read. | | `filters.id` | string | no | Limits results to the subscription with this identifier. The match is exact. | | `filters.subscriptionNumber` | string | no | Limits results to the subscription with this subscription number, for example 'SUB-1'. The match is exact. | | `filters.customerId` | string | no | Limits results to subscriptions of this customer. An id that matches no customer returns an empty page. See id on GET accounting/customers/:id. | | `filters.entityId` | string | no | Limits results to subscriptions owned by this legal entity. An id that matches no entity returns an empty page. See id on GET base/entities. | | `filters.termType` | string | no | Limits results to subscriptions with this term structure. Possible values: 'termed', 'evergreen'. Any other value matches nothing. | | `filters.subscriptionStatus` | string | no | Limits results to subscriptions in this status. Possible values: 'pendingActivation', 'notStarted', 'active', 'pendingOrderApproval', 'canceled', 'suspended'. Any other value matches nothing. | | `filters.autoRenew` | boolean | no | Limits results to subscriptions set to auto-renew when true, or not when false. | | `filters.cancellationReason` | string | no | Limits results to subscriptions canceled for this reason. Possible values: 'noReasonGiven', 'budgetary', 'movingToCompetitors', 'lowCustomerSatisfaction', 'bankruptcy', 'acquisitionMerger'. Any other value matches nothing. | | `filters.suspensionReason` | string | no | Limits results to subscriptions suspended for this reason. Possible values: 'unpaidBalance', 'invalidPaymentMethod'. Any other value matches nothing. | | `filters.externalSubscriptionId` | string | no | Limits results to the subscription with this external identifier. The match is exact. | | `filters.serviceStartDate` | object | no | Limits results to subscriptions whose derived serviceStartDate falls in this range. Both bounds are inclusive, and omitting one leaves that side open. | | `filters.serviceStartDate.start` | string (date) | no | The earliest serviceStartDate to include. Format YYYY-MM-DD. | | `filters.serviceStartDate.end` | string (date) | no | The latest serviceStartDate to include. Format YYYY-MM-DD. | | `filters.serviceEndDate` | object | no | Limits results to subscriptions whose derived serviceEndDate falls in this range. Both bounds are inclusive, and omitting one leaves that side open. | | `filters.serviceEndDate.start` | string (date) | no | The earliest serviceEndDate to include. Format YYYY-MM-DD. | | `filters.serviceEndDate.end` | string (date) | no | The latest serviceEndDate to include. Format YYYY-MM-DD. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of subscriptions per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "subscriptionNumber": "", "customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "termType": "", "subscriptionStatus": "", "autoRenew": true, "cancellationReason": "", "suspensionReason": "", "externalSubscriptionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "serviceStartDate": { "start": "2026-01-15", "end": "2026-01-15" }, "serviceEndDate": { "start": "2026-01-15", "end": "2026-01-15" } }, "page": { "cursor": "", "size": 0 } } ``` ## Response `200` Success Under `data` (searchSubscriptionOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The subscriptions that match, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the subscription. | | `items[].subscriptionNumber` | string | no | The subscription number, for example 'SUB-1', unique within the tenant. Absent until one has been assigned. | | `items[].customerId` | string | no | The customer this subscription belongs to. See id on GET accounting/customers/:id. | | `items[].customerName` | string | no | The display name of the customer this subscription belongs to. | | `items[].entityId` | string | no | The legal entity that owns this subscription. Absent when the owning entity has no recorded identifier. See id on GET base/entities. | | `items[].currency` | string | yes | The currency of annualRecurringRevenue and monthlyRecurringRevenue. Format ISO 4217, for example 'USD'. | | `items[].autoRenew` | boolean | no | Whether the subscription renews automatically at the end of its term. | | `items[].renewalTerm` | number | no | The length of the auto-renewal term, in the subscription's term unit. | | `items[].renewalPriceChangePercentage` | string (decimal) | no | The percentage the price is adjusted by on renewal. It is a percentage, not an amount, so it has no currency. | | `items[].termType` | string | yes | The contract term structure. Possible values: 'termed', 'evergreen'. | | `items[].subscriptionStatus` | string | no | The subscription's lifecycle status, derived from the status of its lines. Possible values: 'pendingActivation', 'notStarted', 'active', 'pendingOrderApproval', 'canceled', 'suspended'. | | `items[].serviceStartDateBasis` | string | no | How the service start date is determined. Possible values: 'fixedDate', 'serviceActivation'. | | `items[].serviceActivationDate` | string (date) | no | The date service was activated, when serviceStartDateBasis is 'serviceActivation'. Format YYYY-MM-DD. | | `items[].serviceStartDate` | string (date) | no | The earliest service start date across the subscription's lines. Format YYYY-MM-DD. | | `items[].serviceEndDate` | string (date) | no | The latest service end date across the subscription's lines. Format YYYY-MM-DD. | | `items[].submissionDate` | string (date) | no | The date the subscription's originating sales order was submitted. Format YYYY-MM-DD. | | `items[].cancellationDate` | string (date) | no | The date from which the subscription is or will be canceled. Absent when no cancellation has been recorded. Format YYYY-MM-DD. | | `items[].cancellationNote` | string | no | A free-text note recorded about the cancellation. | | `items[].cancellationReason` | string | no | The reason code for the cancellation. Possible values: 'noReasonGiven', 'budgetary', 'movingToCompetitors', 'lowCustomerSatisfaction', 'bankruptcy', 'acquisitionMerger'. Absent when the subscription has not been canceled. | | `items[].isCancellationEnabled` | boolean | no | Whether the cancellation procedure has been applied to this subscription. | | `items[].suspensionDate` | string (date) | no | The date from which the subscription is or will be suspended. Absent when no suspension has been recorded. Format YYYY-MM-DD. | | `items[].suspensionNote` | string | no | A free-text note recorded about the suspension. | | `items[].suspensionReason` | string | no | The reason code for the suspension. Possible values: 'unpaidBalance', 'invalidPaymentMethod'. Absent when the subscription has not been suspended. | | `items[].isSuspensionEnabled` | boolean | no | Whether the suspension process is enabled for this subscription. | | `items[].externalSubscriptionId` | string | no | The subscription's identifier in an external system. Absent when none was recorded at creation. | | `items[].annualRecurringRevenue` | string (decimal) | no | The annualized recurring revenue, in the subscription's currency: the sum of each active line's annual recurring revenue. | | `items[].monthlyRecurringRevenue` | string (decimal) | no | The monthly recurring revenue, in the subscription's currency: the sum of each active line's monthly recurring revenue. | | `items[].totalUserCount` | string (decimal) | no | The total active user or seat count across the subscription's active lines. It is a count, not a currency amount. | | `items[].prepaidBalance` | string (decimal) | no | The remaining prepaid balance available to this subscription, summed across its prepaid balance accesses. It is a drawdown counter, not a currency amount. | | `items[].lines` | array of object | yes | The subscription's lines — one per product on the subscription — oldest first. Empty when the subscription has none. GET accounting/subscriptions/:id carries the identical collection (R2.4.1); GET accounting/subscriptions cannot, being a scalar-only data-set (R2.3). Read for the whole page in one query, but a large page still fans out into every line of every subscription on it. | | `items[].lines[].id` | string | yes | Id of the subscription line (a UUID) — its public identifier. | | `items[].lines[].subscriptionLineNumber` | string | no | Unique, human-readable number of this subscription line. Absent until one has been assigned. | | `items[].lines[].productName` | string | no | Name of the product this line covers. | | `items[].lines[].quantity` | string (decimal) | no | Quantity across the line's associated sales order product lines. A count, not a currency amount (R9.4c). | | `items[].lines[].currency` | string | yes | ISO 4217 three-letter currency code (e.g. USD, EUR, GBP) governing this line's annualRecurringRevenue and monthlyRecurringRevenue. | | `items[].lines[].subscriptionLineStatus` | string | no | Lifecycle status of this line. A SubscriptionLineStatus member. | | `items[].lines[].serviceStartDate` | string (date) | no | Date service starts for this line (YYYY-MM-DD). | | `items[].lines[].serviceEndDate` | string (date) | no | Date service ends for this line (YYYY-MM-DD). | | `items[].lines[].customServiceEndDate` | string (date) | no | Service end date recorded against this line by a subscription update, when one has overridden the derived date (YYYY-MM-DD). | | `items[].lines[].maxSalesOrderProductLineServiceEndDate` | string (date) | no | Latest service end date across this line's sales order product lines (YYYY-MM-DD). | | `items[].lines[].annualRecurringRevenue` | string (decimal) | no | Annualized recurring revenue of this line, in the line currency (see this line's `currency`). | | `items[].lines[].monthlyRecurringRevenue` | string (decimal) | no | Monthly recurring revenue of this line, in the line currency (see this line's `currency`). | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the subscription record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Tax Configurations `GET /accounting/tax-configurations` Group: Accounting › Tax Configurations · Operation ID: `taxConfigurations` Returns a list of tax configurations, each with its header fields only. For a configuration's tax codes and GL accounts, use GET accounting/tax-configurations/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=taxConfigurations) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `taxRegime` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, entityId, status, taxRegime, lastModifiedDate - Selectable: id, entityId, status, taxRegime, lastModifiedDate ## Response `200` Success Under `data`, an array of taxConfigurationsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the tax configuration. | | `entityId` | string | no | The legal entity this configuration belongs to. An entity has at most one tax configuration. See id on GET base/entities. | | `status` | string | no | The status of the tax configuration. The tax codes of an inactive configuration are not offered on documents. Possible values: 'Active', 'Inactive'. | | `taxRegime` | string | no | The tax system that determines which tax components are valid. Possible values: 'EU_VAT', 'India_GST', 'Canada_GST', 'Ghana_VAT', 'SingleRate_GST'. | | `lastModifiedDate` | string (date-time) | no | The date and time the tax configuration record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a tax configuration for a legal entity. `POST /accounting/tax-configurations` Group: Accounting › Tax Configurations · Operation ID: `createTaxConfiguration` An entity has at most one tax configuration. A second create for the same entityId fails with ENTITY_ALREADY_CONFIGURED, which names the existing configuration's id. The response contains only the new configuration's id. Read the record with GET accounting/tax-configurations/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createTaxConfiguration) ## 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`: createTaxConfigurationInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `entityId` | string | yes | The legal entity this configuration belongs to. An entity has at most one tax configuration. See id on GET base/entities. | | `status` | string, one of `"Active"`, `"Inactive"` | yes | The status of the tax configuration. The tax codes of an inactive configuration are not offered on documents. | | `taxRegime` | string, one of `"EU_VAT"`, `"India_GST"`, `"Canada_GST"`, `"Ghana_VAT"`, `"SingleRate_GST"` | no | The tax system that determines which tax components are valid. Omit to derive it from the entity's country. An unrecognized country derives EU VAT. | | `lines` | array of object | yes | The complete set of tax codes for this configuration. Must not be empty. Each line's title, and any taxCode supplied, must be unique within the array. | | `lines[].taxCode` | string | no | The business tax code as printed on source documents, for example 'V19EUG' or 'U00EU'. Supply it when migrating from another system. | | `lines[].title` | string | yes | The display title of the tax code. Must be unique within the configuration. | | `lines[].comment` | string | no | A free-form note on when to use this tax code. | | `lines[].vatDeclarationField` | string | no | The box or boxes on the VAT return that this code feeds, taken verbatim from the tax authority form, for example '66' or '46+47+67'. Stored as free text and not evaluated. | | `lines[].percentage` | string (decimal) | yes | The tax rate, as a decimal fraction. For example, 0.19 is 19%. Send 0 for a zero-rated or exempt code. | | `lines[].rateType` | string, one of `"Standard"`, `"Reduced"`, `"SuperReduced"`, `"Zero"`, `"Exempt"` | yes | The legal rate classification of the tax code. | | `lines[].scope` | string, one of `"Domestic"`, `"IntraEU_B2B"`, `"IntraEU_B2C"`, `"ExtraEU_Goods"`, `"ExtraEU_Services"` | yes | The territorial scope of the transaction. | | `lines[].direction` | string, one of `"Input"`, `"Output"` | yes | Which side the tax sits on. 'Input' is tax paid on purchases, 'Output' is tax charged on sales. | | `lines[].isReverseCharge` | boolean | yes | Whether the recipient rather than the supplier accounts for the tax, for example under §13b UStG. When true, every account on the line must set reverseChargeCreditAccountId. | | `lines[].isActive` | boolean | yes | Whether the tax code is offered for selection on new documents. | | `lines[].accounts` | array of object | yes | The GL accounts this tax code books to, one entry per chart of accounts the entity books in. Must not be empty, including for exempt and zero-rated codes, which book a tax line at zero. | | `lines[].accounts[].chartOfAccountsId` | string | yes | The chart of accounts this mapping applies to. See id on GET accounting/chart-of-accounts/:id. | | `lines[].accounts[].accountId` | string | yes | The account the tax amount is booked to. See id on GET accounting/accounts/:id. | | `lines[].accounts[].reverseChargeCreditAccountId` | string | no | The account credited with the offsetting tax amount. Required on reverse-charge lines, ignored otherwise. See id on GET accounting/accounts/:id. | Example: ```json { "entityId": "b6b3a6ce-6c66-4d43-9c8e-6c6c9b8f4a11", "status": "Active", "taxRegime": "EU_VAT", "lines": [ { "title": "Standard rate", "taxCode": "V19EUG", "percentage": "0.19", "rateType": "Standard", "scope": "Domestic", "direction": "Output", "isReverseCharge": false, "isActive": true, "accounts": [ { "chartOfAccountsId": "1b9c1f2e-4a6d-4c31-9f5e-2d7a08b6c410", "accountId": "7e4d3c21-9b08-4a5f-8c62-1f0e5d9a3b74" } ] } ] } ``` ## Response `200` Success Under `data` (createTaxConfigurationOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created tax configuration. | ## 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 | | --- | --- | | `ENTITY_NOT_FOUND` | 400 | | `ENTITY_ALREADY_CONFIGURED` | 400 | | `NO_LINES` | 400 | | `DUPLICATE_LINE_TITLE` | 400 | | `DUPLICATE_TAX_CODE` | 400 | | `LINE_WITHOUT_ACCOUNTS` | 400 | | `MISSING_REVERSE_CHARGE_ACCOUNT` | 400 | | `CHART_OF_ACCOUNTS_NOT_FOUND` | 400 | | `ACCOUNT_NOT_FOUND` | 400 | | `REVERSE_CHARGE_ACCOUNT_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: ENTITY_NOT_FOUND, ENTITY_ALREADY_CONFIGURED, NO_LINES, DUPLICATE_LINE_TITLE, DUPLICATE_TAX_CODE, LINE_WITHOUT_ACCOUNTS, MISSING_REVERSE_CHARGE_ACCOUNT, CHART_OF_ACCOUNTS_NOT_FOUND, ACCOUNT_NOT_FOUND, REVERSE_CHARGE_ACCOUNT_NOT_FOUND - `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 --- # Returns the tax configuration with the given identifier. `GET /accounting/tax-configurations/{id}` Group: Accounting › Tax Configurations · Operation ID: `getTaxConfiguration` [Open in the interactive reference](https://docs.api.lensing.ai/?op=getTaxConfiguration) ## 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`. | | `id` | path | string | yes | The unique identifier for the tax configuration to return. | ## Response `200` Success Under `data` (getTaxConfigurationOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the tax configuration. | | `entityId` | string | yes | The legal entity this configuration belongs to. An entity has at most one tax configuration. See id on GET base/entities. | | `entityName` | string | no | The name of the owning legal entity. | | `status` | string | yes | The status of the tax configuration. The tax codes of an inactive configuration are not offered on documents. Possible values: 'Active', 'Inactive'. | | `taxRegime` | string | yes | The tax system that determines which tax components are valid. Possible values: 'EU_VAT', 'India_GST', 'Canada_GST', 'Ghana_VAT', 'SingleRate_GST'. | | `externalId` | string | no | An external system's identifier for this configuration, set when it was migrated from another system. | | `lastModifiedDate` | string (date-time) | yes | The date and time the tax configuration record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The tax codes of this configuration, in display order. Every configuration has at least one. | | `lines[].id` | string | yes | The unique identifier for the tax code line. | | `lines[].taxCode` | string | no | The business tax code as printed on source documents, for example 'V19EUG' or 'U00EU'. Absent when none was supplied. | | `lines[].title` | string | yes | The display title of the tax code. Unique within the configuration. | | `lines[].comment` | string | no | A free-form note on when to use this tax code. Absent when none was set. | | `lines[].vatDeclarationField` | string | no | The box or boxes on the VAT return that this code feeds, taken verbatim from the tax authority form, for example '66' or '46+47+67'. Free text, not evaluated. Absent when none was set. | | `lines[].percentage` | string (decimal) | yes | The tax rate, as a decimal fraction. For example, 0.19 is 19%. 0 for a zero-rated or exempt code. | | `lines[].rateType` | string | yes | The legal rate classification of the tax code. Possible values: 'Standard', 'Reduced', 'SuperReduced', 'Zero', 'Exempt'. | | `lines[].scope` | string | yes | The territorial scope of the transaction. Possible values: 'Domestic', 'IntraEU_B2B', 'IntraEU_B2C', 'ExtraEU_Goods', 'ExtraEU_Services'. | | `lines[].direction` | string | yes | Which side the tax sits on. 'Input' is tax paid on purchases, 'Output' is tax charged on sales. Possible values: 'Input', 'Output'. | | `lines[].isReverseCharge` | boolean | yes | Whether the recipient rather than the supplier accounts for the tax, for example under §13b UStG. A reverse-charge line books a second, crediting entry to reverseChargeCreditAccountId on each of its accounts. | | `lines[].isActive` | boolean | yes | Whether the tax code is offered for selection on new documents. Inactive codes stay readable on existing documents. | | `lines[].index` | number | yes | The position of the line within its configuration, used for display order. | | `lines[].accounts` | array of object | yes | The GL accounts this tax code books to, one per chart of accounts the entity books in. Every tax code has at least one, including exempt and zero-rated codes, which book a tax line at zero. | | `lines[].accounts[].id` | string | yes | The unique identifier for the account mapping. | | `lines[].accounts[].chartOfAccountsId` | string | yes | The chart of accounts this mapping applies to. See id on GET accounting/chart-of-accounts/:id. | | `lines[].accounts[].accountId` | string | yes | The account the tax amount is booked to. See id on GET accounting/accounts/:id. | | `lines[].accounts[].reverseChargeCreditAccountId` | string | no | The account credited with the offsetting tax amount on a reverse-charge line. Absent when isReverseCharge is false. See id on GET accounting/accounts/:id. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Tax Configuration `PATCH /accounting/tax-configurations/{id}` Group: Accounting › Tax Configurations · Operation ID: `updateTaxConfiguration` Updates the tax configuration with the given identifier. Only the fields in the request body change. Only the fields in the request body change. Omitting lines leaves every existing tax code unchanged. When lines is supplied it replaces the whole set: a line with an id updates that tax code, a line without one creates a tax code, and any existing line not in the array is deleted with its accounts. An id that names no configuration fails with NOT_FOUND. The response is empty. Read the updated record with GET accounting/tax-configurations/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateTaxConfiguration) ## 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. | | `id` | path | string | yes | The unique identifier for the tax configuration to update. | ## Request body Required, `application/json`: updateTaxConfigurationInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string, one of `"Active"`, `"Inactive"` | no | The new status. Omit to leave it unchanged. | | `taxRegime` | string, one of `"EU_VAT"`, `"India_GST"`, `"Canada_GST"`, `"Ghana_VAT"`, `"SingleRate_GST"` | no | The new tax regime. Omit to leave it unchanged. | | `lines` | array of object | no | The replacement set of tax codes. Omit to leave every existing tax code unchanged. When supplied it replaces the whole set: a line with an id updates that tax code, a line without one creates a tax code, and any existing line not in the array is deleted with its accounts. Must not be empty when supplied. | | `lines[].id` | string | no | The unique identifier for an existing tax code line to update. Omit to create a new line. An id that does not belong to this configuration fails with LINE_NOT_FOUND. | | `lines[].taxCode` | string | no | The business tax code as printed on source documents, for example 'V19EUG' or 'U00EU'. | | `lines[].title` | string | yes | The display title of the tax code. Must be unique within the configuration. | | `lines[].comment` | string | no | A free-form note on when to use this tax code. | | `lines[].vatDeclarationField` | string | no | The box or boxes on the VAT return that this code feeds, taken verbatim from the tax authority form, for example '66' or '46+47+67'. Stored as free text and not evaluated. | | `lines[].percentage` | string (decimal) | yes | The tax rate, as a decimal fraction. For example, 0.19 is 19%. Send 0 for a zero-rated or exempt code. | | `lines[].rateType` | string, one of `"Standard"`, `"Reduced"`, `"SuperReduced"`, `"Zero"`, `"Exempt"` | yes | The legal rate classification of the tax code. | | `lines[].scope` | string, one of `"Domestic"`, `"IntraEU_B2B"`, `"IntraEU_B2C"`, `"ExtraEU_Goods"`, `"ExtraEU_Services"` | yes | The territorial scope of the transaction. | | `lines[].direction` | string, one of `"Input"`, `"Output"` | yes | Which side the tax sits on. 'Input' is tax paid on purchases, 'Output' is tax charged on sales. | | `lines[].isReverseCharge` | boolean | yes | Whether the recipient rather than the supplier accounts for the tax. When true, every account on the line must set reverseChargeCreditAccountId. | | `lines[].isActive` | boolean | yes | Whether the tax code is offered for selection on new documents. | | `lines[].accounts` | array of object | yes | The GL accounts this tax code books to, one entry per chart of accounts the entity books in. Must not be empty, including for exempt and zero-rated codes, which book a tax line at zero. | | `lines[].accounts[].chartOfAccountsId` | string | yes | The chart of accounts this mapping applies to. See id on GET accounting/chart-of-accounts/:id. | | `lines[].accounts[].accountId` | string | yes | The account the tax amount is booked to. See id on GET accounting/accounts/:id. | | `lines[].accounts[].reverseChargeCreditAccountId` | string | no | The account credited with the offsetting tax amount. Required on reverse-charge lines, ignored otherwise. See id on GET accounting/accounts/:id. | Example: ```json { "status": "Inactive", "taxRegime": "EU_VAT", "lines": [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "taxCode": "", "title": "", "comment": "<comment>", "vatDeclarationField": "<vatDeclarationField>", "percentage": "19.99", "rateType": "Standard", "scope": "Domestic", "direction": "Input", "isReverseCharge": true, "isActive": true, "accounts": [ { "chartOfAccountsId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "reverseChargeCreditAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ] } ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `LINE_NOT_FOUND` | 400 | | `DUPLICATE_LINE_TITLE` | 400 | | `DUPLICATE_TAX_CODE` | 400 | | `LINE_WITHOUT_ACCOUNTS` | 400 | | `MISSING_REVERSE_CHARGE_ACCOUNT` | 400 | | `CHART_OF_ACCOUNTS_NOT_FOUND` | 400 | | `ACCOUNT_NOT_FOUND` | 400 | | `REVERSE_CHARGE_ACCOUNT_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, NO_LINES, LINE_NOT_FOUND, DUPLICATE_LINE_TITLE, DUPLICATE_TAX_CODE, LINE_WITHOUT_ACCOUNTS, MISSING_REVERSE_CHARGE_ACCOUNT, CHART_OF_ACCOUNTS_NOT_FOUND, ACCOUNT_NOT_FOUND, REVERSE_CHARGE_ACCOUNT_NOT_FOUND - `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 --- # Search Tax Configuration `POST /accounting/tax-configurations/search` Group: Accounting › Tax Configurations · Operation ID: `searchTaxConfiguration` Returns the tax configurations that match the given filters. Each match is the full record, with its tax codes and GL account mappings. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchTaxConfiguration) ## 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`. | ## Request body Required, `application/json`: searchTaxConfigurationInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A tax configuration must match every filter you send. Omit to return all tax configurations. | | `filters.id` | string | no | Limits results to the tax configuration with this identifier. | | `filters.entityId` | string | no | Limits results to the configuration of this legal entity. An entity has at most one, so this returns at most one match. See id on GET base/entities. | | `filters.status` | string | no | Limits results to configurations of this status. Possible values: 'Active', 'Inactive'. Any other value matches no configuration. | | `filters.taxRegime` | string | no | Limits results to configurations of this tax regime. Possible values: 'EU_VAT', 'India_GST', 'Canada_GST', 'Ghana_VAT', 'SingleRate_GST'. Any other value matches no configuration. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of tax configurations per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "status": "<status>", "taxRegime": "<taxRegime>" }, "page": { "cursor": "<cursor>", "size": 0 } } ``` ## Response `200` Success Under `data` (searchTaxConfigurationOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The matching tax configurations for this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the tax configuration. | | `items[].entityId` | string | yes | The legal entity this configuration belongs to. An entity has at most one tax configuration. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the owning legal entity. | | `items[].status` | string | yes | The status of the tax configuration. The tax codes of an inactive configuration are not offered on documents. Possible values: 'Active', 'Inactive'. | | `items[].taxRegime` | string | yes | The tax system that determines which tax components are valid. Possible values: 'EU_VAT', 'India_GST', 'Canada_GST', 'Ghana_VAT', 'SingleRate_GST'. | | `items[].externalId` | string | no | An external system's identifier for this configuration, set when it was migrated from another system. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the tax configuration record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | yes | The tax codes of this configuration, in display order. Every configuration has at least one. | | `items[].lines[].id` | string | yes | The unique identifier for the tax code line. | | `items[].lines[].taxCode` | string | no | The business tax code as printed on source documents, for example 'V19EUG' or 'U00EU'. Absent when none was supplied. | | `items[].lines[].title` | string | yes | The display title of the tax code. Unique within the configuration. | | `items[].lines[].comment` | string | no | A free-form note on when to use this tax code. Absent when none was set. | | `items[].lines[].vatDeclarationField` | string | no | The box or boxes on the VAT return that this code feeds, taken verbatim from the tax authority form, for example '66' or '46+47+67'. Free text, not evaluated. Absent when none was set. | | `items[].lines[].percentage` | string (decimal) | yes | The tax rate, as a decimal fraction. For example, 0.19 is 19%. 0 for a zero-rated or exempt code. | | `items[].lines[].rateType` | string | yes | The legal rate classification of the tax code. Possible values: 'Standard', 'Reduced', 'SuperReduced', 'Zero', 'Exempt'. | | `items[].lines[].scope` | string | yes | The territorial scope of the transaction. Possible values: 'Domestic', 'IntraEU_B2B', 'IntraEU_B2C', 'ExtraEU_Goods', 'ExtraEU_Services'. | | `items[].lines[].direction` | string | yes | Which side the tax sits on. 'Input' is tax paid on purchases, 'Output' is tax charged on sales. Possible values: 'Input', 'Output'. | | `items[].lines[].isReverseCharge` | boolean | yes | Whether the recipient rather than the supplier accounts for the tax, for example under §13b UStG. A reverse-charge line books a second, crediting entry to reverseChargeCreditAccountId on each of its accounts. | | `items[].lines[].isActive` | boolean | yes | Whether the tax code is offered for selection on new documents. Inactive codes stay readable on existing documents. | | `items[].lines[].index` | number | yes | The position of the line within its configuration, used for display order. | | `items[].lines[].accounts` | array of object | yes | The GL accounts this tax code books to, one per chart of accounts the entity books in. Every tax code has at least one, including exempt and zero-rated codes, which book a tax line at zero. | | `items[].lines[].accounts[].id` | string | yes | The unique identifier for the account mapping. | | `items[].lines[].accounts[].chartOfAccountsId` | string | yes | The chart of accounts this mapping applies to. See id on GET accounting/chart-of-accounts/:id. | | `items[].lines[].accounts[].accountId` | string | yes | The account the tax amount is booked to. See id on GET accounting/accounts/:id. | | `items[].lines[].accounts[].reverseChargeCreditAccountId` | string | no | The account credited with the offsetting tax amount on a reverse-charge line. Absent when isReverseCharge is false. See id on GET accounting/accounts/:id. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # List Usage `GET /accounting/usages` Group: Accounting › Usage · Operation ID: `listUsage` Returns a list of usage records. Filter by status, unit of measure, usage period, billable component, and date ranges. [Open in the interactive reference](https://docs.api.lensing.ai/?op=listUsage) ## 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`. | | `status` | query | string | no | Limits results to usage records with this status. Accepted values: 'pending', 'processed', 'error', 'invoiced', 'pendingRebill', 'rebilled', 'staged', 'skipped'. Omit for every status. | | `unitOfMeasure` | query | string | no | Limits results to usage records in this unit, for example 'Hour', 'GB' or 'Call'. Omit for every unit. | | `usagePeriodId` | query | string | no | Limits results to usage records in the usage period with this identifier. An identifier matching no usage period returns an empty page. | | `billableComponentId` | query | string | no | Limits results to usage records attributed to the billable component with this identifier. An identifier matching no billable component returns an empty page. | | `dateFrom` | query | string | no | Limits results to usage records whose date is on or after this bound. Use format YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss.sssZ. A time zone is converted to UTC before matching. | | `dateTo` | query | string | no | Limits results to usage records whose date is on or before this bound. Use format YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss.sssZ. A time zone is converted to UTC before matching. | | `lastModifiedFrom` | query | string | no | Limits results to usage records changed on or after this time, in UTC. Use format YYYY-MM-DDTHH:mm:ss.sssZ. | | `cursor` | query | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `pageSize` | query | number | no | The number of usage records per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | ## Response `200` Success Under `data` (listUsageOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The usage records on this page, newest first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the usage record. | | `items[].usageNumber` | string | no | The usage number, unique within the tenant. Absent while none has been assigned. | | `items[].usageIdentifier` | string | no | The external identifier for this usage record, as supplied at ingestion. | | `items[].date` | string (date-time) | yes | The date and time the usage occurred, without a time zone. Format YYYY-MM-DDTHH:mm:ss. | | `items[].description` | string | no | A free-form description of the usage record. Absent when none is set. | | `items[].productIdentifier` | string | no | The product code or identifier this usage is attributed to, as supplied at ingestion. | | `items[].billableComponentId` | string | no | The billable component this usage is attributed to. Absent until it is resolved. There is no endpoint for it. | | `items[].usageComponent` | string | no | The billable component's name or identifier, included so you do not need a second read. | | `items[].quantity` | string (decimal) | yes | The quantity of usage recorded, in unitOfMeasure. | | `items[].unitOfMeasure` | string | no | The unit the quantity is measured in, for example 'Hour', 'GB' or 'Call'. | | `items[].unitPrice` | string (decimal) | yes | The price per unit for this usage. This record has no currency field. The currency is on the subscription or contract this usage bills against. | | `items[].status` | string | yes | The lifecycle status of the usage record. Possible values: 'pending', 'processed', 'error', 'invoiced', 'pendingRebill', 'rebilled', 'staged', 'skipped'. | | `items[].usagePeriodId` | string | yes | The unique identifier for the usage period this record belongs to. There is no endpoint for it. | | `items[].customFields` | UnknownObject | no | Custom data recorded on this usage record, as a JSON object. Absent when none was set. | | `items[].orderIndex` | number | no | The position of this usage record within the batch it was created in. Absent when not known. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the usage record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as the cursor query parameter. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more usage records remain beyond this page. | ## Errors | Code | Status | | --- | --- | | `INVALID_DATE_FILTER` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_DATE_FILTER - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the usage record with the given identifier. `GET /accounting/usages/{id}` Group: Accounting › Usage · Operation ID: `getUsage` [Open in the interactive reference](https://docs.api.lensing.ai/?op=getUsage) ## 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`. | | `id` | path | string | yes | The unique identifier for the usage record to return. | ## Response `200` Success Under `data` (getUsageOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the usage record. | | `usageNumber` | string | no | The usage number, unique within the tenant. Absent while none has been assigned. | | `usageIdentifier` | string | no | The external identifier for this usage record, as supplied at ingestion. | | `date` | string (date-time) | yes | The date and time the usage occurred, without a time zone. Format YYYY-MM-DDTHH:mm:ss. | | `description` | string | no | A free-form description of the usage record. Absent when none is set. | | `productIdentifier` | string | no | The product code or identifier this usage is attributed to, as supplied at ingestion. | | `billableComponentId` | string | no | The billable component this usage is attributed to. Absent until it is resolved. There is no endpoint for it. | | `usageComponent` | string | no | The billable component's name or identifier, included so you do not need a second read. | | `quantity` | string (decimal) | yes | The quantity of usage recorded, in unitOfMeasure. | | `unitOfMeasure` | string | no | The unit the quantity is measured in, for example 'Hour', 'GB' or 'Call'. | | `unitPrice` | string (decimal) | yes | The price per unit for this usage. This record has no currency field. The currency is on the subscription or contract this usage bills against. | | `status` | string | yes | The lifecycle status of the usage record. Possible values: 'pending', 'processed', 'error', 'invoiced', 'pendingRebill', 'rebilled', 'staged', 'skipped'. | | `usagePeriodId` | string | yes | The unique identifier for the usage period this record belongs to. There is no endpoint for it. | | `customFields` | UnknownObject | no | Custom data recorded on this usage record, as a JSON object. Absent when none was set. | | `orderIndex` | number | no | The position of this usage record within the batch it was created in. Absent when not known. | | `lastModifiedDate` | string (date-time) | yes | The date and time the usage record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Ingest Usage `POST /accounting/usages/ingest` Group: Accounting › Usage · Operation ID: `ingestUsage` Ingests one or more usage records through the staging pipeline. Returns a dataExtractionId to poll with GET accounting/usages/ingestions. Processing is asynchronous unless you set sync to true. [Open in the interactive reference](https://docs.api.lensing.ai/?op=ingestUsage) ## 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`: ingestUsageInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The usage records to ingest, in the default usage ingestion schema. | | `items[].usageIdentifier` | string | yes | A stable external identifier for this usage record. It is used to identify re-ingested duplicates. | | `items[].dateTime` | string | yes | The date or date and time the usage was recorded against. Use format YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].unitOfMeasure` | string | yes | The unit the quantity is measured in, for example 'Hour', 'GB' or 'Call'. | | `items[].quantity` | string | yes | The quantity of usage, as a decimal string to keep precision. Send 0 to reset the usage recorded for the period. | | `items[].usageComponent` | string | no | The billable component this usage maps to, by name or identifier. Omit to let Everest resolve it from productIdentifier and unitOfMeasure. | | `items[].productIdentifier` | string | no | The product code or identifier this usage is attributed to. It is used to resolve the billable component when usageComponent is omitted. | | `items[].trackId` | string | no | An identifier you supply to track this row through GET accounting/usages/ingestions. | | `mapping` | string | no | The column mapping to apply. Defaults to 'Usage API Mapping' when omitted. | | `sync` | boolean | no | Whether to wait until ingestion finishes before responding. Defaults to false, which returns at once with a dataExtractionId to poll with GET accounting/usages/ingestions. | Example: ```json { "items": [ { "usageIdentifier": "<usageIdentifier>", "dateTime": "<dateTime>", "unitOfMeasure": "<unitOfMeasure>", "quantity": "<quantity>", "usageComponent": "<usageComponent>", "productIdentifier": "<productIdentifier>", "trackId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ], "mapping": "<mapping>", "sync": true } ``` ## Response `200` Success Under `data` (ingestUsageOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object | yes | The accepted ingestion job. | | `data.dataExtractionId` | number | yes | The identifier of the ingestion job. Pass it to GET accounting/usages/ingestions to poll per-record results. | ## 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 | | --- | --- | | `MAPPING_NOT_FOUND` | 400 | | `PROCESSING_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: MAPPING_NOT_FOUND, PROCESSING_FAILED - `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 --- # Get Ingestion Status `GET /accounting/usages/ingestions` Group: Accounting › Usage · Operation ID: `getIngestionStatus` Returns the processing status of the usage ingestion job with the given dataExtractionId. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getIngestionStatus) ## 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`. | | `dataExtractionId` | query | number | yes | The identifier of the ingestion job to check, returned by POST accounting/usages/ingest. | ## Response `200` Success Under `data` (getIngestionStatusOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | object or null | no | The status of the ingestion job. Null when no job matches dataExtractionId. | | `data.dataExtractionId` | number | yes | The identifier of the ingestion job, returned unchanged. | | `data.name` | string | no | The name recorded for this ingestion, usually the original file name. Absent when none is set. | | `data.mapping` | string | yes | The column mapping applied to this ingestion. An empty string when the default schema was used. | | `data.extractedData` | array of object | yes | The per-record outcomes, one entry for each row of the ingested payload. | | `data.extractedData[].everestId` | number | no | The internal identifier assigned to the row once it is processed. Absent until then. | | `data.extractedData[].status` | string | yes | The processing status of this row, for example 'PENDING', 'Staged', 'Integrated' or 'FAILED'. | | `data.extractedData[].isActive` | boolean | yes | Whether this row is currently active. An inactive row stays visible for audit but is left out of downstream billing. | | `data.extractedData[].error` | string | no | The reason the row failed. Present only when status is a failure. | | `data.extractedData[].trackId` | string | no | The trackId you supplied for this row at ingestion. Absent when you supplied none. | ## Errors | Code | Status | | --- | --- | | `MULTIPLE_UPLOADS_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: MULTIPLE_UPLOADS_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the usage records that match the given filters. `POST /accounting/usages/search` Group: Accounting › Usage · Operation ID: `searchUsage` [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchUsage) ## 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`. | ## Request body Required, `application/json`: searchUsageInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A usage record must match every filter you send. Omit to return all usage records. | | `filters.id` | string | no | Limits results to the usage record with this identifier. | | `filters.usageNumber` | string | no | Limits results to the usage record with this usage number. | | `filters.usageIdentifier` | string | no | Limits results to usage records with this external identifier. | | `filters.productIdentifier` | string | no | Limits results to usage records with this product code or identifier. | | `filters.status` | string | no | Limits results to usage records with this status. Accepted values: 'pending', 'processed', 'error', 'invoiced', 'pendingRebill', 'rebilled', 'staged', 'skipped'. Any other value matches nothing. | | `filters.unitOfMeasure` | string | no | Limits results to usage records in this unit, for example 'Hour', 'GB' or 'Call'. | | `filters.usagePeriodId` | string | no | Limits results to usage records in the usage period with this identifier. An identifier matching no usage period returns an empty page. | | `filters.billableComponentId` | string | no | Limits results to usage records attributed to the billable component with this identifier. An identifier matching no billable component returns an empty page. | | `filters.date` | object | no | Limits results to usage records whose date falls in this range. Omit either bound to leave that side open. Both bounds are inclusive. | | `filters.date.start` | string | no | The earliest date to include. Use format YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss.sssZ. A time zone is converted to UTC before matching. | | `filters.date.end` | string | no | The latest date to include. Use format YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss.sssZ. A time zone is converted to UTC before matching. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of usage records per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "usageNumber": "<usageNumber>", "usageIdentifier": "<usageIdentifier>", "productIdentifier": "<productIdentifier>", "status": "<status>", "unitOfMeasure": "<unitOfMeasure>", "usagePeriodId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "billableComponentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "date": { "start": "<start>", "end": "<end>" } }, "page": { "cursor": "<cursor>", "size": 0 } } ``` ## Response `200` Success Under `data` (searchUsageOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The usage records on this page, newest first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the usage record. | | `items[].usageNumber` | string | no | The usage number, unique within the tenant. Absent while none has been assigned. | | `items[].usageIdentifier` | string | no | The external identifier for this usage record, as supplied at ingestion. | | `items[].date` | string (date-time) | yes | The date and time the usage occurred, without a time zone. Format YYYY-MM-DDTHH:mm:ss. | | `items[].description` | string | no | A free-form description of the usage record. Absent when none is set. | | `items[].productIdentifier` | string | no | The product code or identifier this usage is attributed to, as supplied at ingestion. | | `items[].billableComponentId` | string | no | The billable component this usage is attributed to. Absent until it is resolved. There is no endpoint for it. | | `items[].usageComponent` | string | no | The billable component's name or identifier, included so you do not need a second read. | | `items[].quantity` | string (decimal) | yes | The quantity of usage recorded, in unitOfMeasure. | | `items[].unitOfMeasure` | string | no | The unit the quantity is measured in, for example 'Hour', 'GB' or 'Call'. | | `items[].unitPrice` | string (decimal) | yes | The price per unit for this usage. This record has no currency field. The currency is on the subscription or contract this usage bills against. | | `items[].status` | string | yes | The lifecycle status of the usage record. Possible values: 'pending', 'processed', 'error', 'invoiced', 'pendingRebill', 'rebilled', 'staged', 'skipped'. | | `items[].usagePeriodId` | string | yes | The unique identifier for the usage period this record belongs to. There is no endpoint for it. | | `items[].customFields` | UnknownObject | no | Custom data recorded on this usage record, as a JSON object. Absent when none was set. | | `items[].orderIndex` | number | no | The position of this usage record within the batch it was created in. Absent when not known. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the usage record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more usage records remain beyond this page. | ## Errors | Code | Status | | --- | --- | | `INVALID_DATE_FILTER` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_DATE_FILTER - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Vendor Bills `GET /accounting/vendor-bills` Group: Accounting › Vendor Bills · Operation ID: `vendorBills` Returns a list of vendor bills. Each entry is the bill header. Read one bill with its line items from GET accounting/vendor-bills/:id or POST accounting/vendor-bills/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=vendorBills) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `vendorBillNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `billNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `billDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `serviceDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `dueDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `postingStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentTerms` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `externalVendorBillId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, vendorBillNumber, billNumber, billDate, serviceDate, dueDate, currency, status, paymentStatus, postingStatus, paymentTerms, total, amountPaid, openAmount, lastModifiedDate - Selectable: id, vendorBillNumber, billNumber, billDate, serviceDate, dueDate, currency, status, paymentStatus, postingStatus, paymentTerms, total, amountPaid, openAmount, externalVendorBillId, vendorName, entityName, lastModifiedDate ## Response `200` Success Under `data`, an array of vendorBillsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the vendor bill. | | `vendorBillNumber` | string or null | no | The vendor bill number. Stable across edits. | | `billNumber` | string or null | no | The vendor-issued invoice number, as printed on the bill. It is not unique across vendors. | | `billDate` | string (date) or null | no | The date printed on the vendor's invoice. Format YYYY-MM-DD. | | `serviceDate` | string (date) or null | no | The date the goods or services were delivered. Format YYYY-MM-DD. Absent when not recorded. | | `dueDate` | string (date) or null | no | The date payment is due. Format YYYY-MM-DD. | | `currency` | string or null | no | The currency the bill is denominated in. Format ISO 4217, for example 'USD'. Governs total, amountPaid and openAmount. | | `status` | string or null | no | The approval status. Values include 'pendingApproval', 'approved', 'rejected', 'voided' and 'reversed'. | | `paymentStatus` | string or null | no | The payment status. Values include 'unpaid', 'partiallyPaid', 'fullyPaid', 'pending', 'notApplicable' and 'voided'. | | `postingStatus` | string or null | no | The accounting posting status, defined by the accounting subsystem, for example 'notPosted', 'posted' or 'postingFailed'. | | `paymentTerms` | string or null | no | The payment-terms key applied to this bill, for example 'net30', 'net60' or 'dueOnReceipt'. | | `total` | string (decimal) or null | no | The total bill amount, gross, in the bill currency. | | `amountPaid` | string (decimal) or null | no | The cumulative amount paid against this bill so far, in the bill currency. | | `openAmount` | string (decimal) or null | no | The outstanding amount still owed, total minus amountPaid, in the bill currency. | | `externalVendorBillId` | string or null | no | The identifier this bill has in the upstream system that produced it. It is not unique. | | `vendorName` | string or null | no | The name of the vendor this bill is from. | | `entityName` | string or null | no | The name of the legal entity this bill is posted under. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the vendor bill last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a vendor bill. `POST /accounting/vendor-bills` Group: Accounting › Vendor Bills · Operation ID: `createVendorBill` The response contains only the new bill's id. Read the record with GET accounting/vendor-bills/:id if you need its stored state, where vendorBillNumber appears once assigned. The bill is always created pendingApproval, so a status cannot be supplied here. currency governs every line amount and vatAmount, which are decimals with no currency of their own. vatPercentage is a percentage, so 19 means 19%, not 0.19. A bill with more lines than your tenant runs in one call is refused with TOO_MANY_LINES, so split it or load it through the integration platform. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createVendorBill) ## 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`. | ## Request body Required, `application/json`: createVendorBillInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `vendorId` | string | yes | The vendor the bill is from. See id on GET accounting/vendors/:id. | | `entityId` | string | yes | The legal entity the bill is posted under. See id on GET base/entities. | | `billNumber` | string | yes | The vendor-issued invoice number, as printed on the bill. It is not unique across vendors. | | `billDate` | string (date) | yes | The date printed on the vendor's invoice. Use format YYYY-MM-DD. | | `dueDate` | string (date) | yes | The date payment is due. Use format YYYY-MM-DD. | | `currency` | string | yes | The currency of the bill. Format ISO 4217, for example 'USD'. Governs every line amount and vatAmount, which are decimals. | | `serviceDate` | string (date) | no | The date the goods or services were delivered. Use format YYYY-MM-DD. Omit to leave it unset. For an entity whose country requires a service date it then defaults to billDate. | | `paymentTerms` | string | no | The payment-terms key, for example 'net30', 'net60' or 'dueOnReceipt'. The API does not check it against a list. Omit to inherit 'custom'. | | `externalVendorBillId` | string | no | The identifier this bill has in the upstream system that produced it. It is not unique. | | `markAsPaid` | boolean | no | Whether to mark the bill paid on creation. Defaults to false. | | `origin` | string | no | The source system this record came from, as free-form text, for example 'Ramp'. Omit it and it stays unset. The server does not set a value. | | `lines` | array of object | yes | The line items of the bill, in the order they should appear. At least one is expected. A bill with no lines asks for no expense. | | `lines[].amount` | string (decimal) | yes | The total line amount, gross, including VAT where applicable, as a decimal in the bill currency. | | `lines[].description` | string | no | A description of the line item, typically copied from the vendor's invoice. | | `lines[].accountId` | string | no | The GL expense account this line posts to. See id on GET accounting/accounts/:id. | | `lines[].departmentId` | string | no | The department this line is charged to. See id on GET departments/:id. | | `lines[].businessUnitId` | string | no | The business unit this line is charged to. There is no endpoint for it. | | `lines[].customerId` | string | no | The customer this expense is rebilled to. See id on GET accounting/customers/:id. It is only meaningful alongside billableExpense. | | `lines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. 19 means 19%, not 0.19. | | `lines[].vatAccountId` | string | no | The GL account the VAT posts to. See id on GET accounting/accounts/:id. | | `lines[].reverseVatAccountId` | string | no | The GL account reverse-charge VAT posts to. See id on GET accounting/accounts/:id. | | `lines[].vatAmount` | string (decimal) | no | The explicit VAT amount, as a decimal in the bill currency. Omit to derive it from vatPercentage. | | `lines[].valueAllocationId` | string | no | The value-allocation rule distributing this line across dimensions. There is no endpoint for it. | | `lines[].purchaseOrderLineId` | string | no | The purchase-order line this line is matched against. There is no endpoint for it. | | `lines[].billableExpense` | string | no | The billable-expense status for this line. Values include 'yes', 'no' and 'skipped'. | | `lines[].lineType` | string | no | The kind of line. Values include 'expense' (a one-time cost) and 'prepaidExpense' (an upfront cost spread over several periods). Defaults to 'expense'. | | `attachments` | array of object | no | Files to attach to the bill. Each names an already-uploaded file by its id. | | `attachments[].fileName` | string | yes | The display name of the attached file. | | `attachments[].fileId` | string | yes | The identifier of the already-uploaded file. There is no endpoint for it. | Example: ```json { "vendorId": "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "entityId": "8f2c1d40-5a6b-4c7d-9e80-1f2a3b4c5d6e", "billNumber": "INV-88213", "billDate": "2026-08-01", "dueDate": "2026-08-31", "currency": "EUR", "serviceDate": "2026-01-15", "paymentTerms": "<paymentTerms>", "externalVendorBillId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "markAsPaid": true, "origin": "<origin>", "lines": [ { "amount": "1200.00", "description": "Consulting, August", "vatPercentage": "19.00" } ], "attachments": [ { "fileName": "<fileName>", "fileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ] } ``` ## Response `200` Success Under `data` (createVendorBillOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created vendor bill. | ## Errors | Code | Status | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `ACCOUNT_ENTITY_MISMATCH` | 400 | | `TOO_MANY_LINES` | 400 | | `IDEMPOTENCY_NOT_SUPPORTED` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, ACCOUNT_ENTITY_MISMATCH, TOO_MANY_LINES. Sending an Idempotency-Key is answered with `IDEMPOTENCY_NOT_SUPPORTED`: this action does not run in its own transaction, so the replay guarantee cannot be offered and the header is refused rather than ignored. - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the vendor bill with the given identifier. `GET /accounting/vendor-bills/{id}` Group: Accounting › Vendor Bills · Operation ID: `getVendorBill` A vendor bill is an invoice received from a vendor, with its header and line items. It shares its read with POST accounting/vendor-bills/search, which returns the same record. The list at GET accounting/vendor-bills is header-only, so the line items come back here and on search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getVendorBill) ## 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`. | | `id` | path | string | yes | The unique identifier for the vendor bill to return. | ## Response `200` Success Under `data` (getVendorBillOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the vendor bill. | | `vendorBillNumber` | string | yes | The vendor bill number. Stable across edits. | | `billNumber` | string | yes | The vendor-issued invoice number, as printed on the bill. It is not unique across vendors. | | `billDate` | string (date) | yes | The date printed on the vendor's invoice. Format YYYY-MM-DD. | | `serviceDate` | string (date) | no | The date the goods or services were delivered. Format YYYY-MM-DD. Absent when not recorded. | | `dueDate` | string (date) | yes | The date payment is due. Format YYYY-MM-DD. | | `currency` | string | yes | The currency the bill is denominated in. Format ISO 4217, for example 'USD'. Governs total, amountPaid, openAmount and every line amount. | | `status` | string | yes | The approval status. Values include 'pendingApproval', 'approved', 'rejected', 'voided' and 'reversed'. | | `paymentStatus` | string | no | The payment status. Values include 'unpaid', 'partiallyPaid', 'fullyPaid', 'pending', 'notApplicable' and 'voided'. | | `postingStatus` | string | no | The accounting posting status, defined by the accounting subsystem, for example 'notPosted', 'posted' or 'postingFailed'. | | `paymentTerms` | string | no | The payment-terms key applied to this bill, for example 'net30', 'net60' or 'dueOnReceipt'. Inherited from the vendor unless overridden. | | `total` | string (decimal) | no | The total bill amount, gross, in the bill currency. | | `amountPaid` | string (decimal) | no | The cumulative amount paid against this bill so far, in the bill currency. | | `openAmount` | string (decimal) | no | The outstanding amount still owed, total minus amountPaid, in the bill currency. | | `externalVendorBillId` | string | no | The identifier this bill has in the upstream system that produced it. It is not unique. | | `vendorId` | string | no | The vendor this bill is from. See id on GET accounting/vendors/:id. | | `vendorName` | string | no | The name of the vendor this bill is from. | | `entityId` | string | no | The legal entity this bill is posted under. See id on GET base/entities. | | `entityName` | string | no | The name of the legal entity this bill is posted under. | | `purchaseOrderId` | string | no | The purchase order this bill is matched to. See id on GET accounting/purchase-orders/:id. The match is line-level, so it is absent when the bill is matched to no purchase order. | | `purchaseOrderNumber` | string | no | The number of the purchase order this bill is matched to. Absent when the bill is matched to no purchase order. | | `externalPurchaseOrderNumber` | string | no | The external reference of the purchase order this bill is matched to. Absent when the bill is matched to no purchase order, or when that purchase order has no external reference. | | `lastModifiedDate` | string (date-time) | yes | The date and time the vendor bill last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `approvals` | array of object | no | The approval decisions on this bill, oldest first. An empty array when no approval policy has matched the bill. | | `approvals[].decision` | string | yes | What was decided. Values include 'approve' and 'reject'. | | `approvals[].employeeName` | string | no | The name of the employee who decided. | | `approvals[].message` | string | no | The reviewer's note, where one was given. | | `approvals[].decidedAt` | string (date-time) | yes | The date and time the decision was recorded, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `origin` | string | no | The source system this record came from, as free-form text. Absent when none was supplied. | | `lines` | array of object | no | The bill line items, in their order on the bill. An empty array when the bill has none. | | `lines[].id` | string | yes | The unique identifier for the line item. | | `lines[].lineNumber` | string | no | The line item number. | | `lines[].description` | string | no | A description of the line item, typically copied from the vendor's invoice. | | `lines[].lineType` | string | no | The type of line item. Values include 'expense' and 'prepaidExpense'. | | `lines[].amount` | string (decimal) | no | The total line amount, gross, in the bill currency. | | `lines[].currency` | string | no | The currency for the line. It always matches the bill currency. Format ISO 4217, for example 'USD'. | | `lines[].netAmount` | string (decimal) | no | The line amount excluding VAT, in the bill currency. | | `lines[].vatAmount` | string (decimal) | no | The computed VAT amount on the line, in the bill currency. | | `lines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. 21 means 21%, not 0.21. | | `lines[].billableExpense` | string | no | The billable-expense status for this line. Values include 'yes', 'no' and 'skipped'. | | `lines[].externalVendorBillItemId` | string | no | The identifier this line has in the upstream system that produced it. | | `lines[].accountId` | string | no | The GL expense account this line posts to. See id on GET accounting/accounts/:id. | | `lines[].departmentId` | string | no | The department this line is charged to. See id on GET departments/:id. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Vendor Bill `PATCH /accounting/vendor-bills/{id}` Group: Accounting › Vendor Bills · Operation ID: `updateVendorBill` Updates the vendor bill with the given identifier. Only the fields in the request body change. Only the fields in the request body change. Lines change through three arrays, applied in the order delete, then update, then create in one transaction, so one call can retire a line and add its replacement. updateLines and deleteLines address lines by the id the bill publishes, and a line id belonging to another bill is refused with LINE_NOT_FOUND. Omit all three to leave the lines untouched. currency governs the amounts on any line the call writes, and the stored currency is used when it is omitted. The response is empty. Read the record with GET accounting/vendor-bills/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateVendorBill) ## 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. | | `id` | path | string | yes | The unique identifier for the vendor bill to update. | ## Request body Required, `application/json`: updateVendorBillInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `billNumber` | string | no | The vendor-issued invoice number, as printed on the bill. It is not unique across vendors. Omit to leave unchanged. | | `billDate` | string (date) | no | The date printed on the vendor's invoice. Use format YYYY-MM-DD. Omit to leave unchanged. | | `dueDate` | string (date) | no | The date payment is due. Use format YYYY-MM-DD. Omit to leave unchanged. | | `serviceDate` | string (date) | no | The date the goods or services were delivered. Use format YYYY-MM-DD. Omit to leave unchanged. | | `currency` | string | no | The currency of the bill. Format ISO 4217, for example 'USD'. Omit to leave unchanged. | | `paymentTerms` | string | no | The payment-terms key, for example 'net30', 'net60' or 'dueOnReceipt'. Omit to leave unchanged. | | `vendorId` | string | no | The vendor the bill is from. See id on GET accounting/vendors/:id. Omit to leave unchanged. | | `entityId` | string | no | The legal entity the bill is posted under. See id on GET base/entities. Omit to leave unchanged. | | `markAsPaid` | boolean | no | Whether the bill is marked paid. Omit to leave unchanged. | | `origin` | string | no | The source system this record came from, as free-form text, for example 'Ramp'. Omit it and it stays unset. The server does not set a value. | | `createLines` | array of object | no | Lines to add to the bill, each with the same shape as on create and no id. Omit for no additions. | | `createLines[].amount` | string (decimal) | yes | The total line amount, gross, including VAT where applicable, as a decimal in the bill currency. | | `createLines[].description` | string | no | A description of the line item, typically copied from the vendor's invoice. | | `createLines[].accountId` | string | no | The GL expense account this line posts to. See id on GET accounting/accounts/:id. | | `createLines[].departmentId` | string | no | The department this line is charged to. See id on GET departments/:id. | | `createLines[].businessUnitId` | string | no | The business unit this line is charged to. There is no endpoint for it. | | `createLines[].customerId` | string | no | The customer this expense is rebilled to. See id on GET accounting/customers/:id. It is only meaningful alongside billableExpense. | | `createLines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. 19 means 19%, not 0.19. | | `createLines[].vatAccountId` | string | no | The GL account the VAT posts to. See id on GET accounting/accounts/:id. | | `createLines[].reverseVatAccountId` | string | no | The GL account reverse-charge VAT posts to. See id on GET accounting/accounts/:id. | | `createLines[].vatAmount` | string (decimal) | no | The explicit VAT amount, as a decimal in the bill currency. Omit to derive it from vatPercentage. | | `createLines[].valueAllocationId` | string | no | The value-allocation rule distributing this line across dimensions. There is no endpoint for it. | | `createLines[].purchaseOrderLineId` | string | no | The purchase-order line this line is matched against. There is no endpoint for it. | | `createLines[].billableExpense` | string | no | The billable-expense status for this line. Values include 'yes', 'no' and 'skipped'. | | `createLines[].lineType` | string | no | The kind of line. Values include 'expense' (a one-time cost) and 'prepaidExpense' (an upfront cost spread over several periods). Defaults to 'expense'. | | `updateLines` | array of object | no | Lines to change, each addressed by the id the bill publishes. Every field but the id is optional and left unchanged when omitted. Omit for no changes. | | `updateLines[].id` | string | yes | The unique identifier for the line to change. It must belong to this bill. | | `updateLines[].amount` | string (decimal) | no | The total line amount, gross, as a decimal in the bill currency. Omit to leave unchanged. | | `updateLines[].description` | string | no | A description of the line item. Omit to leave unchanged. | | `updateLines[].accountId` | string | no | The GL expense account this line posts to. See id on GET accounting/accounts/:id. Omit to leave unchanged. | | `updateLines[].departmentId` | string | no | The department this line is charged to. See id on GET departments/:id. Omit to leave unchanged. | | `updateLines[].businessUnitId` | string | no | The business unit this line is charged to. There is no endpoint for it. Omit to leave unchanged. | | `updateLines[].customerId` | string | no | The customer this expense is rebilled to. See id on GET accounting/customers/:id. Omit to leave unchanged. | | `updateLines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. 19 means 19%, not 0.19. Omit to leave unchanged. | | `updateLines[].vatAccountId` | string | no | The GL account the VAT posts to. See id on GET accounting/accounts/:id. Omit to leave unchanged. | | `updateLines[].reverseVatAccountId` | string | no | The GL account reverse-charge VAT posts to. See id on GET accounting/accounts/:id. Omit to leave unchanged. | | `updateLines[].vatAmount` | string (decimal) | no | The explicit VAT amount, as a decimal in the bill currency. Omit to leave unchanged. | | `updateLines[].valueAllocationId` | string | no | The value-allocation rule distributing this line across dimensions. There is no endpoint for it. Omit to leave unchanged. | | `updateLines[].purchaseOrderLineId` | string | no | The purchase-order line this line is matched against. There is no endpoint for it. Omit to leave unchanged. | | `updateLines[].billableExpense` | string | no | The billable-expense status for this line. Values include 'yes', 'no' and 'skipped'. Omit to leave unchanged. | | `updateLines[].lineType` | string | no | The kind of line. Values include 'expense' and 'prepaidExpense'. Omit to leave unchanged. | | `deleteLines` | array of string | no | The identifiers of lines to remove, applied before the updates and creates. Each must belong to this bill. | Example: ```json { "billNumber": "<billNumber>", "billDate": "2026-01-15", "dueDate": "2026-09-15", "serviceDate": "2026-01-15", "currency": "<currency>", "paymentTerms": "<paymentTerms>", "vendorId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "markAsPaid": true, "origin": "<origin>", "createLines": [ { "amount": "19.99", "description": "<description>", "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "departmentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "businessUnitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "vatPercentage": "19.99", "vatAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "reverseVatAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "vatAmount": "19.99", "valueAllocationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "purchaseOrderLineId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "billableExpense": "<billableExpense>", "lineType": "<lineType>" } ], "updateLines": [ { "id": "7a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9", "amount": "1350.00" } ], "deleteLines": [ "1f2e3d4c-5b6a-7089-9a8b-7c6d5e4f3a2b" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `LINE_NOT_FOUND` | 400 | | `ACCOUNT_ENTITY_MISMATCH` | 400 | | `TOO_MANY_LINES` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND, LINE_NOT_FOUND, ACCOUNT_ENTITY_MISMATCH, TOO_MANY_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 --- # Deletes a vendor bill. `DELETE /accounting/vendor-bills/{id}` Group: Accounting › Vendor Bills · Operation ID: `deleteVendorBill` The response is empty. Deleting is permanent, and four states block it, each checked before anything is written. A bill paid even in part gives VENDOR_BILL_PAID. A bill whose withheld tax has been remitted to the tax authority gives WITHHOLDING_REMITTED. A bill with posted amortization schedule lines gives POSTED_AMORTIZATION. A bill whose posting period is closed or locked gives POSTING_PERIOD_CLOSED. A draft bill skips all four checks, and a zero-amount bill is exempt from the payment and posting-period checks. Deleting is not voiding: a posted bill that must leave an audit trail should be voided instead, which is not part of this version. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteVendorBill) ## 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. | | `id` | path | string | yes | The unique identifier for the vendor bill to delete. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `VENDOR_BILL_PAID` | 400 | | `WITHHOLDING_REMITTED` | 400 | | `POSTED_AMORTIZATION` | 400 | | `POSTING_PERIOD_CLOSED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, VENDOR_BILL_PAID, WITHHOLDING_REMITTED, POSTED_AMORTIZATION, POSTING_PERIOD_CLOSED - `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 --- # Approves the vendor bill. `POST /accounting/vendor-bills/{id}/approve` Group: Accounting › Vendor Bills · Operation ID: `approveVendorBill` Approves the vendor bill through your tenant's approval policy, which is what makes it payable and posts it to the ledger. With no approvers configured the bill goes straight to approved and posted, and with a multi-step policy it advances one step. The status after the operation is returned. A bill that is already approved is refused with STATUS_CONFLICT. A refusal from the approval framework comes back as APPROVAL_FAILED, with its own message in reason. [Open in the interactive reference](https://docs.api.lensing.ai/?op=approveVendorBill) ## 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. | | `id` | path | string | yes | The unique identifier for the vendor bill to approve. | ## Response `200` Success Under `data` (approveVendorBillOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string | yes | The bill status after the operation. The approval workflow decides it, so it is read back. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `STATUS_CONFLICT` | 400 | | `APPROVAL_FAILED` | 400 | | `BILL_ALREADY_VOIDED` | 400 | | `BILL_ALREADY_REVERSED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, STATUS_CONFLICT, APPROVAL_FAILED, BILL_ALREADY_VOIDED, BILL_ALREADY_REVERSED - `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 --- # Rejects the vendor bill. `POST /accounting/vendor-bills/{id}/reject` Group: Accounting › Vendor Bills · Operation ID: `rejectVendorBill` Rejects the vendor bill, recording an optional reviewMessage against the decision. It resolves through the generic approval framework. A bill that is already rejected is refused with STATUS_CONFLICT, and a refusal from the framework comes back as APPROVAL_FAILED, with its own message in reason. Rejecting is not voiding or deleting: the bill stays unposted, with its number and its audit trail. [Open in the interactive reference](https://docs.api.lensing.ai/?op=rejectVendorBill) ## 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. | | `id` | path | string | yes | The unique identifier for the vendor bill to reject. | ## Request body Required, `application/json`: rejectVendorBillInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `reviewMessage` | string | no | Why the bill is being rejected, recorded against the approval decision. Omit for none. | Example: ```json { "reviewMessage": "Amount does not match the purchase order" } ``` ## Response `200` Success Under `data` (rejectVendorBillOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string | yes | The bill status after the operation. The approval workflow decides it, so it is read back. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `STATUS_CONFLICT` | 400 | | `APPROVAL_FAILED` | 400 | | `BILL_ALREADY_VOIDED` | 400 | | `BILL_ALREADY_REVERSED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, STATUS_CONFLICT, APPROVAL_FAILED, BILL_ALREADY_VOIDED, BILL_ALREADY_REVERSED - `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 --- # Reverses the vendor bill. `POST /accounting/vendor-bills/{id}/reverse` Group: Accounting › Vendor Bills · Operation ID: `reverseVendorBill` Reverses the vendor bill, creating a reversing journal entry dated reversalDate. Reverse differs from void in when the ledger effect lands: a void backs the bill out as of its own posting period, and a reversal lets you choose the date, which a closed period requires. Only an approved bill that is neither voided nor already reversed may be reversed. A bill with applied vendor credits gives BILL_HAS_APPLIED_CREDITS, so unapply them first. A bill with any payment gives BILL_PAID. Remitted withholding gives WITHHOLDING_REMITTED. A reversal date no accounting period covers gives NO_ACCOUNTING_PERIOD, and a closed or locked one gives POSTING_PERIOD_CLOSED. The response is empty. [Open in the interactive reference](https://docs.api.lensing.ai/?op=reverseVendorBill) ## 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. | | `id` | path | string | yes | The unique identifier for the vendor bill to reverse. | ## Request body Required, `application/json`: reverseVendorBillInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `reversalDate` | string (date) | yes | The date the reversing journal entry is posted on. Use format YYYY-MM-DD. Choose a date whose period is open. | Example: ```json { "reversalDate": "2026-09-30" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `BILL_NOT_APPROVED` | 400 | | `BILL_ALREADY_VOIDED` | 400 | | `BILL_PAID` | 400 | | `WITHHOLDING_REMITTED` | 400 | | `BILL_ALREADY_REVERSED` | 400 | | `BILL_HAS_APPLIED_CREDITS` | 400 | | `NO_ACCOUNTING_PERIOD` | 400 | | `POSTING_PERIOD_CLOSED` | 400 | | `JOURNAL_ENTRY_VOIDED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, BILL_NOT_APPROVED, BILL_ALREADY_VOIDED, BILL_PAID, WITHHOLDING_REMITTED, BILL_ALREADY_REVERSED, BILL_HAS_APPLIED_CREDITS, NO_ACCOUNTING_PERIOD, POSTING_PERIOD_CLOSED, JOURNAL_ENTRY_VOIDED - `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 --- # Voids the vendor bill. `POST /accounting/vendor-bills/{id}/void` Group: Accounting › Vendor Bills · Operation ID: `voidVendorBill` Voids the vendor bill and posts the reversing journal entry that backs it out of the ledger. Voiding is not deleting: the bill, its number and its audit trail stay, and the ledger effect is reversed. A voidReason is required and is stored on the bill for an auditor, so an empty or whitespace-only string is refused. Only an approved, posted, unpaid bill may be voided. Voided and reversed are both terminal, so nothing acts on a bill in either state. A bill whose withheld tax was already remitted gives WITHHOLDING_REMITTED, because it still reads as unpaid when payableAmount is net of the withholding. The posting period must be open. The response is empty. [Open in the interactive reference](https://docs.api.lensing.ai/?op=voidVendorBill) ## 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. | | `id` | path | string | yes | The unique identifier for the vendor bill to void. | ## Request body Required, `application/json`: voidVendorBillInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `voidReason` | string | yes | Why the bill is being voided. Stored on the bill and shown in the audit trail. An empty or whitespace-only string is refused. | Example: ```json { "voidReason": "Duplicate of VB-295012" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `BILL_NOT_APPROVED` | 400 | | `BILL_ALREADY_VOIDED` | 400 | | `BILL_PAID` | 400 | | `WITHHOLDING_REMITTED` | 400 | | `BILL_NOT_POSTED` | 400 | | `POSTING_PERIOD_CLOSED` | 400 | | `BILL_ALREADY_REVERSED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, BILL_NOT_APPROVED, BILL_ALREADY_VOIDED, BILL_PAID, WITHHOLDING_REMITTED, BILL_NOT_POSTED, POSTING_PERIOD_CLOSED, BILL_ALREADY_REVERSED - `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 --- # Search Vendor Bill `POST /accounting/vendor-bills/search` Group: Accounting › Vendor Bills · Operation ID: `searchVendorBill` Returns the vendor bills that match the given filters, each with its line items. Searches vendor bills and pages through the matches, returning the full record, header and line items, for each. Matching nothing returns an empty items array, not an error. The purchase-order filters match line-level: a bill matches when any of its lines is linked to a line of that purchase order. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchVendorBill) ## 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`. | ## Request body Required, `application/json`: searchVendorBillInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A vendor bill must match every filter you send. Omit to return all vendor bills. | | `filters.id` | string | no | Limits results to the vendor bill with this identifier. | | `filters.vendorBillNumber` | string | no | Limits results to the vendor bill with this number. | | `filters.billNumber` | string | no | Limits results to bills with this vendor-issued invoice number. It is not unique, so this can match several bills. | | `filters.vendorId` | string | no | Limits results to bills from this vendor. See id on GET accounting/vendors/:id. | | `filters.entityId` | string | no | Limits results to bills posted under this legal entity. See id on GET base/entities. | | `filters.vendorName` | string | no | Limits results to bills from a vendor with this name. | | `filters.entityName` | string | no | Limits results to bills posted under a legal entity with this name. | | `filters.status` | string | no | Limits results to bills in this approval status. Values include 'pendingApproval', 'approved', 'rejected', 'voided' and 'reversed'. Any other value matches no bill. | | `filters.paymentStatus` | string | no | Limits results to bills in this payment status. Values include 'unpaid', 'partiallyPaid', 'fullyPaid', 'pending', 'notApplicable' and 'voided'. Any other value matches no bill. | | `filters.externalVendorBillId` | string | no | Limits results to bills with this upstream identifier. It is not unique, so this can match several bills. | | `filters.purchaseOrderId` | string | no | Limits results to bills matched to this purchase order. See id on GET accounting/purchase-orders/:id. The match is line-level. | | `filters.purchaseOrderNumber` | string | no | Limits results to bills matched to the purchase order with this number. The match is line-level. | | `filters.externalPurchaseOrderNumber` | string | no | Limits results to bills matched to a purchase order with this external reference. It is not unique, and the match is line-level. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first request. | | `page.size` | number | no | The number of vendor bills per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "billNumber": "INV-88213" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchVendorBillOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The vendor bills that match, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the vendor bill. | | `items[].vendorBillNumber` | string | yes | The vendor bill number. Stable across edits. | | `items[].billNumber` | string | yes | The vendor-issued invoice number, as printed on the bill. It is not unique across vendors. | | `items[].billDate` | string (date) | yes | The date printed on the vendor's invoice. Format YYYY-MM-DD. | | `items[].serviceDate` | string (date) | no | The date the goods or services were delivered. Format YYYY-MM-DD. Absent when not recorded. | | `items[].dueDate` | string (date) | yes | The date payment is due. Format YYYY-MM-DD. | | `items[].currency` | string | yes | The currency the bill is denominated in. Format ISO 4217, for example 'USD'. Governs total, amountPaid, openAmount and every line amount. | | `items[].status` | string | yes | The approval status. Values include 'pendingApproval', 'approved', 'rejected', 'voided' and 'reversed'. | | `items[].paymentStatus` | string | no | The payment status. Values include 'unpaid', 'partiallyPaid', 'fullyPaid', 'pending', 'notApplicable' and 'voided'. | | `items[].postingStatus` | string | no | The accounting posting status, defined by the accounting subsystem, for example 'notPosted', 'posted' or 'postingFailed'. | | `items[].paymentTerms` | string | no | The payment-terms key applied to this bill, for example 'net30', 'net60' or 'dueOnReceipt'. Inherited from the vendor unless overridden. | | `items[].total` | string (decimal) | no | The total bill amount, gross, in the bill currency. | | `items[].amountPaid` | string (decimal) | no | The cumulative amount paid against this bill so far, in the bill currency. | | `items[].openAmount` | string (decimal) | no | The outstanding amount still owed, total minus amountPaid, in the bill currency. | | `items[].externalVendorBillId` | string | no | The identifier this bill has in the upstream system that produced it. It is not unique. | | `items[].vendorId` | string | no | The vendor this bill is from. See id on GET accounting/vendors/:id. | | `items[].vendorName` | string | no | The name of the vendor this bill is from. | | `items[].entityId` | string | no | The legal entity this bill is posted under. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the legal entity this bill is posted under. | | `items[].purchaseOrderId` | string | no | The purchase order this bill is matched to. See id on GET accounting/purchase-orders/:id. The match is line-level, so it is absent when the bill is matched to no purchase order. | | `items[].purchaseOrderNumber` | string | no | The number of the purchase order this bill is matched to. Absent when the bill is matched to no purchase order. | | `items[].externalPurchaseOrderNumber` | string | no | The external reference of the purchase order this bill is matched to. Absent when the bill is matched to no purchase order, or when that purchase order has no external reference. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the vendor bill last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].origin` | string | no | The source system this record came from, as free-form text. Absent when none was supplied. | | `items[].lines` | array of object | no | The bill line items, in their order on the bill. An empty array when the bill has none. | | `items[].lines[].id` | string | yes | The unique identifier for the line item. | | `items[].lines[].lineNumber` | string | no | The line item number. | | `items[].lines[].description` | string | no | A description of the line item, typically copied from the vendor's invoice. | | `items[].lines[].lineType` | string | no | The type of line item. Values include 'expense' and 'prepaidExpense'. | | `items[].lines[].amount` | string (decimal) | no | The total line amount, gross, in the bill currency. | | `items[].lines[].currency` | string | no | The currency for the line. It always matches the bill currency. Format ISO 4217, for example 'USD'. | | `items[].lines[].netAmount` | string (decimal) | no | The line amount excluding VAT, in the bill currency. | | `items[].lines[].vatAmount` | string (decimal) | no | The computed VAT amount on the line, in the bill currency. | | `items[].lines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. 21 means 21%, not 0.21. | | `items[].lines[].billableExpense` | string | no | The billable-expense status for this line. Values include 'yes', 'no' and 'skipped'. | | `items[].lines[].externalVendorBillItemId` | string | no | The identifier this line has in the upstream system that produced it. | | `items[].lines[].accountId` | string | no | The GL expense account this line posts to. See id on GET accounting/accounts/:id. | | `items[].lines[].departmentId` | string | no | The department this line is charged to. See id on GET departments/:id. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more bills remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Vendor Credits `GET /accounting/vendor-credits` Group: Accounting › Vendor Credits · Operation ID: `vendorCredits` Returns a list of vendor credits. Each entry is the credit header. Read one credit with its line items from GET accounting/vendor-credits/:id or POST accounting/vendor-credits/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=vendorCredits) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `vendorCreditNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `referenceNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `creditDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `externalId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, vendorCreditNumber, referenceNumber, creditDate, currency, status, totalAmount, lastModifiedDate - Selectable: id, vendorCreditNumber, referenceNumber, creditDate, currency, status, totalAmount, externalId, vendorName, entityName, lastModifiedDate ## Response `200` Success Under `data`, an array of vendorCreditsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the vendor credit. | | `vendorCreditNumber` | string or null | no | The vendor credit number. Absent until a number has been assigned. | | `referenceNumber` | string or null | no | The vendor's credit memo reference number, as printed on the vendor's document. | | `creditDate` | string (date) or null | no | The date on the vendor's credit memo. Format YYYY-MM-DD. | | `currency` | string or null | no | The currency the credit is denominated in. Format ISO 4217, for example 'USD'. | | `status` | string or null | no | The application status, how much of the credit has been set against vendor bills. Values include 'unapplied', 'partiallyApplied' and 'applied'. | | `totalAmount` | string (decimal) or null | no | The total credit amount, in currency. | | `externalId` | string or null | no | The identifier this credit has in the upstream system that produced it. It is not unique. | | `vendorName` | string or null | no | The name of the vendor this credit is from. | | `entityName` | string or null | no | The name of the legal entity this credit is booked under. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the vendor credit last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a vendor credit. `POST /accounting/vendor-credits` Group: Accounting › Vendor Credits · Operation ID: `createVendorCredit` The response contains only the new credit's id. Read the record with GET accounting/vendor-credits/:id if you need its stored state, where vendorCreditNumber appears once assigned. A new credit is always unapplied: the application status is computed from the bills it is applied to and cannot be supplied here. currency governs every line amount and vatAmount, which are decimals with no currency of their own. Applying the credit to a vendor bill is a separate operation, POST accounting/vendor-credits/:id/apply. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createVendorCredit) ## 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`: createVendorCreditInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `vendorId` | string | yes | The vendor being credited. See id on GET accounting/vendors/:id. | | `entityId` | string | yes | The legal entity the credit is booked under. See id on GET base/entities. | | `referenceNumber` | string | yes | The vendor's credit memo reference number, as printed on the document. | | `creditDate` | string (date) | yes | The date on the vendor's credit memo. Use format YYYY-MM-DD. | | `currency` | string | yes | The currency of the credit. Format ISO 4217, for example 'USD'. Governs every line amount and vatAmount, which are decimals. | | `origin` | string | no | The source system this record came from, as free-form text, for example 'Ramp'. Omit it and it stays unset. The server does not set a value. | | `lines` | array of object | yes | The line items of the credit, in the order they should appear. At least one is expected. A credit with no lines credits nothing. | | `lines[].amount` | string (decimal) | yes | The gross credit amount for the line, as a decimal in the credit currency. | | `lines[].description` | string | no | A description of the credit line. | | `lines[].accountId` | string | no | The GL account this line credits. See id on GET accounting/accounts/:id. | | `lines[].departmentId` | string | no | The department this line is allocated to. See id on GET departments/:id. | | `lines[].businessUnitId` | string | no | The business unit this line is allocated to. There is no endpoint for it. | | `lines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. 19 means 19%, not 0.19. | | `lines[].vatAccountId` | string | no | The GL account the VAT posts to. See id on GET accounting/accounts/:id. | | `lines[].reverseVatAccountId` | string | no | The GL account reverse-charge VAT posts to. See id on GET accounting/accounts/:id. | | `lines[].vatAmount` | string (decimal) | no | The explicit VAT amount, as a decimal in the credit currency. Omit to derive it from vatPercentage. | | `lines[].valueAllocationId` | string | no | The value-allocation rule distributing this line across dimensions. There is no endpoint for it. | | `lines[].lineType` | string | no | The kind of line. Accepted values: 'expense' (a one-time credit) or 'prepaid' (a credit spread over several periods). Defaults to 'expense'. | | `attachments` | array of object | no | Files to attach to the credit. Each names an already-uploaded file by its id. | | `attachments[].fileName` | string | yes | The display name of the attached file. | | `attachments[].fileId` | string | yes | The identifier of the already-uploaded file. There is no endpoint for it. | Example: ```json { "vendorId": "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "entityId": "8f2c1d40-5a6b-4c7d-9e80-1f2a3b4c5d6e", "referenceNumber": "CN-4471", "creditDate": "2026-08-01", "currency": "EUR", "origin": "<origin>", "lines": [ { "amount": "300.00", "description": "Returned goods", "vatPercentage": "19.00" } ], "attachments": [ { "fileName": "<fileName>", "fileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ] } ``` ## Response `200` Success Under `data` (createVendorCreditOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created vendor credit. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `TOO_MANY_LINES` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, TOO_MANY_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 --- # Returns the vendor credit with the given identifier. `GET /accounting/vendor-credits/{id}` Group: Accounting › Vendor Credits · Operation ID: `getVendorCredit` A vendor credit is a credit memo from a vendor, with one or more line items, that can be applied against that vendor's bills. It shares its read with POST accounting/vendor-credits/search, which returns the same record. The list at GET accounting/vendor-credits is header-only, so the line items come back here and on search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getVendorCredit) ## 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`. | | `id` | path | string | yes | The unique identifier for the vendor credit to return. | ## Response `200` Success Under `data` (getVendorCreditOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the vendor credit. | | `vendorCreditNumber` | string | no | The vendor credit number. Absent until a number has been assigned. | | `referenceNumber` | string | yes | The vendor's credit memo reference number, as printed on the vendor's document. | | `creditDate` | string (date) | yes | The date on the vendor's credit memo. Format YYYY-MM-DD. | | `currency` | string | yes | The currency the credit is denominated in. Format ISO 4217, for example 'USD'. Governs totalAmount and every line amount. | | `status` | string | no | The application status, how much of the credit has been set against vendor bills. Values include 'unapplied', 'partiallyApplied' and 'applied'. | | `totalAmount` | string (decimal) | no | The total credit amount, in currency. | | `externalId` | string | no | The identifier this credit has in the upstream system that produced it. It is not unique. | | `vendorId` | string | no | The vendor this credit is from. See id on GET accounting/vendors/:id. | | `vendorName` | string | no | The name of the vendor this credit is from. | | `entityId` | string | no | The legal entity this credit is booked under. See id on GET base/entities. | | `entityName` | string | no | The name of the legal entity this credit is booked under. | | `lastModifiedDate` | string (date-time) | yes | The date and time the vendor credit last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `origin` | string | no | The source system this record came from, as free-form text. Absent when none was supplied. | | `lines` | array of object | no | The credit line items, in their order on the credit. An empty array when the credit has none. | | `lines[].id` | string | yes | The unique identifier for the line item. | | `lines[].lineNumber` | string | no | The line item number. | | `lines[].description` | string | no | A description of the credit line item. | | `lines[].lineType` | string | no | The type of line item. Possible values: 'expense', 'prepaid'. | | `lines[].amount` | string (decimal) | no | The line credit amount, in the credit currency. | | `lines[].currency` | string | no | The currency for the line. It always matches the credit currency. Format ISO 4217, for example 'USD'. | | `lines[].netAmount` | string (decimal) | no | The line amount excluding VAT, in the credit currency. | | `lines[].vatAmount` | string (decimal) | no | The computed VAT amount on the line, in the credit currency. | | `lines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. 19 means 19%, not 0.19. | | `lines[].accountId` | string | no | The GL account this line credits. See id on GET accounting/accounts/:id. | | `lines[].departmentId` | string | no | The department this line is allocated to. See id on GET departments/:id. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Vendor Credit `PATCH /accounting/vendor-credits/{id}` Group: Accounting › Vendor Credits · Operation ID: `updateVendorCredit` Updates the vendor credit with the given identifier. Only the fields in the request body change. Only the fields in the request body change. The vendor and the legal entity cannot be changed, because moving a credit to a different vendor or entity changes which bills it can offset, so that is a new credit. Lines change through three arrays, applied in the order delete, then update, then create in one transaction, so one call can retire a line and add its replacement. updateLines and deleteLines address lines by the id the credit publishes, and a line id belonging to another credit is refused with LINE_NOT_FOUND. Omit all three to leave the lines untouched. The response is empty. Read the record with GET accounting/vendor-credits/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateVendorCredit) ## 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. | | `id` | path | string | yes | The unique identifier for the vendor credit to update. | ## Request body Required, `application/json`: updateVendorCreditInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `referenceNumber` | string | no | The vendor's credit memo reference number, as printed on the document. Omit to leave unchanged. | | `creditDate` | string (date) | no | The date on the vendor's credit memo. Use format YYYY-MM-DD. Omit to leave unchanged. | | `currency` | string | no | The currency of the credit. Format ISO 4217, for example 'USD'. Omit to leave unchanged. | | `origin` | string | no | The source system this record came from, as free-form text, for example 'Ramp'. Omit it and it stays unset. The server does not set a value. | | `createLines` | array of object | no | Lines to add to the credit, each with the same shape as on create and no id. Omit for no additions. | | `createLines[].amount` | string (decimal) | yes | The gross credit amount for the line, as a decimal in the credit currency. | | `createLines[].description` | string | no | A description of the credit line. | | `createLines[].accountId` | string | no | The GL account this line credits. See id on GET accounting/accounts/:id. | | `createLines[].departmentId` | string | no | The department this line is allocated to. See id on GET departments/:id. | | `createLines[].businessUnitId` | string | no | The business unit this line is allocated to. There is no endpoint for it. | | `createLines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. 19 means 19%, not 0.19. | | `createLines[].vatAccountId` | string | no | The GL account the VAT posts to. See id on GET accounting/accounts/:id. | | `createLines[].reverseVatAccountId` | string | no | The GL account reverse-charge VAT posts to. See id on GET accounting/accounts/:id. | | `createLines[].vatAmount` | string (decimal) | no | The explicit VAT amount, as a decimal in the credit currency. Omit to derive it from vatPercentage. | | `createLines[].valueAllocationId` | string | no | The value-allocation rule distributing this line across dimensions. There is no endpoint for it. | | `createLines[].lineType` | string | no | The kind of line. Accepted values: 'expense' (a one-time credit) or 'prepaid' (a credit spread over several periods). Defaults to 'expense'. | | `updateLines` | array of object | no | Lines to change, each addressed by the id the credit publishes. Every field but the id is optional and left unchanged when omitted. Omit for no changes. | | `updateLines[].id` | string | yes | The unique identifier for the line to change. It must belong to this credit. | | `updateLines[].amount` | string (decimal) | no | The gross credit amount for the line, as a decimal in the credit currency. Omit to leave unchanged. | | `updateLines[].description` | string | no | A description of the credit line. Omit to leave unchanged. | | `updateLines[].accountId` | string | no | The GL account this line credits. See id on GET accounting/accounts/:id. Omit to leave unchanged. | | `updateLines[].departmentId` | string | no | The department this line is allocated to. See id on GET departments/:id. Omit to leave unchanged. | | `updateLines[].businessUnitId` | string | no | The business unit this line is allocated to. There is no endpoint for it. Omit to leave unchanged. | | `updateLines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. 19 means 19%, not 0.19. Omit to leave unchanged. | | `updateLines[].vatAccountId` | string | no | The GL account the VAT posts to. See id on GET accounting/accounts/:id. Omit to leave unchanged. | | `updateLines[].reverseVatAccountId` | string | no | The GL account reverse-charge VAT posts to. See id on GET accounting/accounts/:id. Omit to leave unchanged. | | `updateLines[].vatAmount` | string (decimal) | no | The explicit VAT amount, as a decimal in the credit currency. Omit to derive it from vatPercentage. | | `updateLines[].valueAllocationId` | string | no | The value-allocation rule distributing this line across dimensions. There is no endpoint for it. Omit to leave unchanged. | | `updateLines[].lineType` | string | no | The kind of line. Accepted values: 'expense' (a one-time credit) or 'prepaid' (a credit spread over several periods). Omit to leave unchanged. | | `deleteLines` | array of string | no | The identifiers of lines to remove, applied before the updates and creates. Each must belong to this credit. | Example: ```json { "referenceNumber": "CN-4471-A", "creditDate": "2026-01-15", "currency": "<currency>", "origin": "<origin>", "createLines": [ { "amount": "19.99", "description": "<description>", "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "departmentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "businessUnitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "vatPercentage": "19.99", "vatAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "reverseVatAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "vatAmount": "19.99", "valueAllocationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "lineType": "<lineType>" } ], "updateLines": [ { "id": "6b2a9f18-7c3d-4e5a-8b19-2d4e6f8a0c31", "amount": "350.00" } ], "deleteLines": [ "<deleteLines>" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `LINE_NOT_FOUND` | 400 | | `TOO_MANY_LINES` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND, LINE_NOT_FOUND, TOO_MANY_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 --- # Deletes a vendor credit. `DELETE /accounting/vendor-credits/{id}` Group: Accounting › Vendor Credits · Operation ID: `deleteVendorCredit` The response is empty. Three states block the delete, each checked before anything is written. A credit already applied to a bill, in whole or in part, gives VENDOR_CREDIT_APPLIED, so unapply it first. A credit whose posting period is closed or locked gives POSTING_PERIOD_CLOSED. A credit whose amortization schedule has already started gives AMORTIZATION_IN_PROGRESS. The posting-period check applies only to a non-draft credit. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteVendorCredit) ## 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. | | `id` | path | string | yes | The unique identifier for the vendor credit to delete. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `VENDOR_CREDIT_APPLIED` | 400 | | `POSTING_PERIOD_CLOSED` | 400 | | `AMORTIZATION_IN_PROGRESS` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, VENDOR_CREDIT_APPLIED, POSTING_PERIOD_CLOSED, AMORTIZATION_IN_PROGRESS - `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 --- # Applies the vendor credit to the given vendor bills. `POST /accounting/vendor-credits/{id}/apply` Group: Accounting › Vendor Credits · Operation ID: `applyVendorCredit` Applies the vendor credit to one or more vendor bills, reducing what each bill still owes. Each application names a bill and an amount, and the amount cannot exceed the bill's open amount or the credit's remaining balance. Applying is reversible with POST accounting/vendor-credits/:id/unapply. A bill with applied credits cannot be reversed until they are detached. [Open in the interactive reference](https://docs.api.lensing.ai/?op=applyVendorCredit) ## 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. | | `id` | path | string | yes | The unique identifier for the vendor credit to apply. | ## Request body Required, `application/json`: applyVendorCreditInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `applications` | array of object | yes | The bills this credit is applied to, and how much goes to each. At least one is required. | | `applications[].vendorBillId` | string | yes | The vendor bill the credit is applied to. See id on GET accounting/vendor-bills/:id. | | `applications[].amount` | string (decimal) | yes | The amount applied to this bill, as a decimal in the bill's currency. It cannot exceed the bill's open amount or the credit's remaining balance. | | `applications[].appliedDate` | string (date) | no | The date the application is recorded on. Use format YYYY-MM-DD. It must be on or after both the bill date and the credit date. Defaults to today in the owning entity's time zone. | Example: ```json { "applications": [ { "vendorBillId": "9b2d5e10-4c78-4a36-8f51-6e0a3c7b2d95", "amount": "300.00", "appliedDate": "2026-08-20" } ] } ``` ## Response `200` Success Under `data` (applyVendorCreditOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `appliedCount` | number | yes | The number of applications recorded. Read the credit with GET accounting/vendor-credits/:id for its remaining balance. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_APPLICATIONS` | 400 | | `APPLICATION_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND, NO_APPLICATIONS, APPLICATION_REJECTED - `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 --- # Detaches the vendor credit from the given vendor bills. `POST /accounting/vendor-credits/{id}/unapply` Group: Accounting › Vendor Credits · Operation ID: `unapplyVendorCredit` Detaches the vendor credit from the bills it was applied to, returning the amount to the credit's available balance. Omit vendorBillIds to detach every application the credit has, or supply it to detach only those bills. A credit with no matching applications gives NO_APPLICATIONS. The response is empty. This is the inverse of POST accounting/vendor-credits/:id/apply, and a bill needs it before it can be reversed. [Open in the interactive reference](https://docs.api.lensing.ai/?op=unapplyVendorCredit) ## 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. | | `id` | path | string | yes | The unique identifier for the vendor credit to detach. | ## Request body Required, `application/json`: unapplyVendorCreditInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `vendorBillIds` | array of string | no | The vendor bills to detach this credit from. See id on GET accounting/vendor-bills/:id. Omit to detach every application the credit has. | Example: ```json { "vendorBillIds": [ "9b2d5e10-4c78-4a36-8f51-6e0a3c7b2d95" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_APPLICATIONS` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND, NO_APPLICATIONS - `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 --- # Search Vendor Credit `POST /accounting/vendor-credits/search` Group: Accounting › Vendor Credits · Operation ID: `searchVendorCredit` Returns the vendor credits that match the given filters, each with its line items. Searches vendor credits and pages through the matches, returning the full record, header and line items, for each. Matching nothing returns an empty items array, not an error. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchVendorCredit) ## 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`. | ## Request body Required, `application/json`: searchVendorCreditInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A vendor credit must match every filter you send. Omit to return all vendor credits. | | `filters.id` | string | no | Limits results to the vendor credit with this identifier. | | `filters.vendorCreditNumber` | string | no | Limits results to the vendor credit with this number. | | `filters.referenceNumber` | string | no | Limits results to credits with this vendor reference number. It is not unique, so this can match several credits. | | `filters.vendorId` | string | no | Limits results to credits from this vendor. See id on GET accounting/vendors/:id. | | `filters.entityId` | string | no | Limits results to credits booked under this legal entity. See id on GET base/entities. | | `filters.vendorName` | string | no | Limits results to credits from a vendor with this name. | | `filters.entityName` | string | no | Limits results to credits booked under a legal entity with this name. | | `filters.status` | string | no | Limits results to credits in this application status. Values include 'unapplied', 'partiallyApplied' and 'applied'. Any other value matches no credit. | | `filters.externalId` | string | no | Limits results to credits with this upstream identifier. It is not unique, so this can match several credits. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first request. | | `page.size` | number | no | The number of vendor credits per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "status": "unapplied" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchVendorCreditOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The vendor credits that match, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the vendor credit. | | `items[].vendorCreditNumber` | string | no | The vendor credit number. Absent until a number has been assigned. | | `items[].referenceNumber` | string | yes | The vendor's credit memo reference number, as printed on the vendor's document. | | `items[].creditDate` | string (date) | yes | The date on the vendor's credit memo. Format YYYY-MM-DD. | | `items[].currency` | string | yes | The currency the credit is denominated in. Format ISO 4217, for example 'USD'. Governs totalAmount and every line amount. | | `items[].status` | string | no | The application status, how much of the credit has been set against vendor bills. Values include 'unapplied', 'partiallyApplied' and 'applied'. | | `items[].totalAmount` | string (decimal) | no | The total credit amount, in currency. | | `items[].externalId` | string | no | The identifier this credit has in the upstream system that produced it. It is not unique. | | `items[].vendorId` | string | no | The vendor this credit is from. See id on GET accounting/vendors/:id. | | `items[].vendorName` | string | no | The name of the vendor this credit is from. | | `items[].entityId` | string | no | The legal entity this credit is booked under. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the legal entity this credit is booked under. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the vendor credit last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].origin` | string | no | The source system this record came from, as free-form text. Absent when none was supplied. | | `items[].lines` | array of object | no | The credit line items, in their order on the credit. An empty array when the credit has none. | | `items[].lines[].id` | string | yes | The unique identifier for the line item. | | `items[].lines[].lineNumber` | string | no | The line item number. | | `items[].lines[].description` | string | no | A description of the credit line item. | | `items[].lines[].lineType` | string | no | The type of line item. Possible values: 'expense', 'prepaid'. | | `items[].lines[].amount` | string (decimal) | no | The line credit amount, in the credit currency. | | `items[].lines[].currency` | string | no | The currency for the line. It always matches the credit currency. Format ISO 4217, for example 'USD'. | | `items[].lines[].netAmount` | string (decimal) | no | The line amount excluding VAT, in the credit currency. | | `items[].lines[].vatAmount` | string (decimal) | no | The computed VAT amount on the line, in the credit currency. | | `items[].lines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. 19 means 19%, not 0.19. | | `items[].lines[].accountId` | string | no | The GL account this line credits. See id on GET accounting/accounts/:id. | | `items[].lines[].departmentId` | string | no | The department this line is allocated to. See id on GET departments/:id. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more credits remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Vendors `GET /accounting/vendors` Group: Accounting › Vendors · Operation ID: `vendors` Returns a list of vendors. To match by name or contact email, use POST accounting/vendors/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=vendors) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `vendorNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `vendorType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `vendorSubType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentTerms` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `preferredCurrency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `independentContractor` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `externalVendorId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `origin` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, vendorNumber, vendorType, vendorSubType, status, paymentTerms, preferredCurrency, lastModifiedDate - Selectable: id, vendorName, vendorNumber, vendorType, vendorSubType, status, paymentTerms, preferredCurrency, independentContractor, externalVendorId, origin, mainContactEmail, mainContactName, lastModifiedDate ## Response `200` Success Under `data`, an array of vendorsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the vendor. | | `vendorName` | string or null | no | The legal or trading name of the vendor. | | `vendorNumber` | string or null | no | The vendor number, for example 'V-2041'. Absent until one has been assigned. | | `vendorType` | string or null | no | The kind of vendor. Possible values: 'supplier', 'contractor', 'tax', 'benefits', 'payrollProvider', 'healthInsurance'. | | `vendorSubType` | string or null | no | A finer classification within the vendor type. Possible values: 'direct', 'indirect'. | | `status` | string or null | no | The lifecycle status of the vendor. Possible values: 'active', 'inactive'. An inactive vendor stays readable so historical documents still resolve, and is not offered for new ones. | | `paymentTerms` | string or null | no | The default payment-terms key applied to this vendor's bills, for example 'net30' or 'dueOnReceipt'. Stored as supplied. | | `preferredCurrency` | string or null | no | The currency the vendor prefers to be paid in. Format ISO 4217, for example 'USD'. | | `independentContractor` | string or null | no | Whether the vendor is an independent contractor. Possible values: 'yes', 'no', 'unsure'. | | `externalVendorId` | string or null | no | The identifier this vendor has in an external system. It is not unique. | | `origin` | string or null | no | The source system that created the vendor, for example 'app'. | | `mainContactEmail` | string or null | no | The email address of the vendor's main contact. | | `mainContactName` | string or null | no | The name of the vendor's main contact. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the vendor last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a vendor. `POST /accounting/vendors` Group: Accounting › Vendors · Operation ID: `createVendor` Creating a vendor also creates the business partner record that the rest of finance posts against, named after the vendor. There is no business-partner field to set. The response contains only the new vendor's id. Read the record with GET accounting/vendors/:id. vendorNumber comes from a gapless number range and appears only on that follow-up read. Bank details and the remittance address are optional and can also be added later with PATCH accounting/vendors/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createVendor) ## 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`: createVendorInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `vendorName` | string | yes | The name of the vendor. | | `vendorType` | string | yes | The kind of vendor. Accepted values: 'supplier', 'contractor', 'tax', 'benefits', 'payrollProvider', 'healthInsurance'. | | `status` | string | yes | The lifecycle status. Accepted values: 'active', 'inactive'. | | `paymentTerms` | string | yes | The default payment-terms key, for example 'net30', 'net60' or 'dueOnReceipt'. Stored as supplied. | | `vendorSubType` | string | no | A finer classification, used when vendorType is 'supplier'. Accepted values: 'direct', 'indirect'. | | `mainContactName` | string | no | The full name of the primary contact. | | `mainContactEmail` | string | no | The email address of the primary contact. | | `mainContactPhone` | string | no | The phone number of the primary contact. | | `billingEmail` | string | no | The email address invoices and billing messages are sent to. | | `preferredCurrency` | string | no | The preferred transaction currency. Format ISO 4217, for example 'USD'. | | `preferredLanguage` | string | no | The language the vendor prefers for messages. | | `independentContractor` | string | no | Whether the vendor is an independent contractor. Accepted values: 'yes', 'no', 'unsure'. | | `externalVendorId` | string | no | The identifier this vendor has in an external system. It is not unique. | | `origin` | string | no | The source system this record came from. | | `defaultAccountId` | string | no | The GL account expenses for this vendor default to. See id on GET accounting/accounts/:id. | | `departmentId` | string | no | The department costs for this vendor default to. See id on GET departments/:id. | | `vendorEntityId` | string | no | The legal entity this vendor belongs to. See id on GET base/entities. | | `bankDetails` | object | no | The bank account the vendor is paid into. Optional. You can add it later with PATCH accounting/vendors/:id. | | `bankDetails.bankAccountNumber` | string | no | The vendor's bank account number, or IBAN. | | `bankDetails.bankAccountType` | string | no | The kind of bank account, as your tenant's account-type list spells it. | | `bankDetails.bankAccountHolderName` | string | no | The name the account is held in. | | `bankDetails.bankAccountNumberType` | string | no | How to read bankAccountNumber. One of 'iban', 'achOrWire' or 'other'. | | `bankDetails.financialInstitutionId` | string | no | The bank's identifier, a BIC, SWIFT or routing number. | | `bankDetails.financialInstitutionIdType` | string | no | How to read financialInstitutionId. | | `remittanceAddress` | object | no | The address remittance advice is sent to. Optional. You can add it later with PATCH accounting/vendors/:id. | | `remittanceAddress.line1` | string | yes | The first address line: street number and name, or PO box. | | `remittanceAddress.line2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `remittanceAddress.city` | string | yes | The city or locality. | | `remittanceAddress.stateProvince` | string | no | The state, province or region. | | `remittanceAddress.zipCode` | string | yes | The postal or ZIP code. | | `remittanceAddress.country` | string | yes | The country, as a name or ISO 3166-1 code. | | `remittanceAddress.attention` | string | no | The person or department that mail to this address is for. | | `attachments` | array of object | no | The files to attach to the vendor. Each names an already-uploaded file by its id. | | `attachments[].fileName` | string | yes | The display name of the attached file. | | `attachments[].fileId` | string | yes | The identifier of the already-uploaded file. | Example: ```json { "vendorName": "Acme GmbH", "vendorType": "supplier", "status": "active", "paymentTerms": "net30", "vendorSubType": "<vendorSubType>", "mainContactName": "Dana Fischer", "mainContactEmail": "<mainContactEmail>", "mainContactPhone": "<mainContactPhone>", "billingEmail": "<billingEmail>", "preferredCurrency": "EUR", "preferredLanguage": "<preferredLanguage>", "independentContractor": "<independentContractor>", "externalVendorId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "origin": "<origin>", "defaultAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "departmentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "vendorEntityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "bankDetails": { "bankAccountNumber": "<bankAccountNumber>", "bankAccountType": "<bankAccountType>", "bankAccountHolderName": "<bankAccountHolderName>", "bankAccountNumberType": "<bankAccountNumberType>", "financialInstitutionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "financialInstitutionIdType": "<financialInstitutionIdType>" }, "remittanceAddress": { "line1": "<line1>", "line2": "<line2>", "city": "<city>", "stateProvince": "<stateProvince>", "zipCode": "<zipCode>", "country": "<country>", "attention": "<attention>" }, "attachments": [ { "fileName": "<fileName>", "fileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ] } ``` ## Response `200` Success Under `data` (createVendorOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created vendor. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND - `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 --- # Returns the vendor with the given identifier. `GET /accounting/vendors/{id}` Group: Accounting › Vendors · Operation ID: `getVendor` To look up a vendor by vendorNumber or externalVendorId, neither of which is unique, use POST accounting/vendors/search. This endpoint does not return the vendor's tax identifiers, bank details, remittance address or custom field values. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getVendor) ## 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`. | | `id` | path | string | yes | The unique identifier for the vendor to return. | ## Response `200` Success Under `data` (getVendorOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the vendor. | | `vendorName` | string | yes | The legal or trading name of the vendor. | | `vendorNumber` | string | no | The vendor number, for example 'V-2041'. Absent until one has been assigned. | | `vendorType` | string | yes | The kind of vendor. Possible values: 'supplier', 'contractor', 'tax', 'benefits', 'payrollProvider', 'healthInsurance'. | | `vendorSubType` | string | no | A finer classification within the vendor type. Possible values: 'direct', 'indirect'. | | `status` | string | yes | The lifecycle status of the vendor. Possible values: 'active', 'inactive'. An inactive vendor stays readable so historical documents still resolve, and is not offered for new ones. | | `paymentTerms` | string | yes | The default payment-terms key applied to this vendor's bills, for example 'net30' or 'dueOnReceipt'. Stored as supplied. | | `preferredCurrency` | string | no | The currency the vendor prefers to be paid in. Format ISO 4217, for example 'USD'. | | `preferredLanguage` | string | no | The language the vendor prefers for messages. | | `independentContractor` | string | no | Whether the vendor is an independent contractor. Possible values: 'yes', 'no', 'unsure'. | | `externalVendorId` | string | no | The identifier this vendor has in an external system. It is not unique. | | `origin` | string | no | The source system that created the vendor, for example 'app'. | | `mainContactName` | string | no | The name of the vendor's main contact. | | `mainContactEmail` | string | no | The email address of the vendor's main contact. | | `mainContactPhone` | string | no | The phone number of the vendor's main contact. | | `billingEmail` | string | no | The email address invoices and remittance advice are sent to. | | `entityId` | string | no | The legal entity the vendor is registered under. See id on GET base/entities. | | `defaultAccountId` | string | no | The GL account this vendor's spend defaults to. See id on GET accounting/accounts/:id. | | `lastModifiedDate` | string (date-time) | yes | The date and time the vendor last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Vendor `PATCH /accounting/vendors/{id}` Group: Accounting › Vendors · Operation ID: `updateVendor` Updates the vendor with the given identifier. Only the fields in the request body change. Only the fields in the request body change. Renaming the vendor also renames the business partner the rest of finance posts against. bankDetails and remittanceAddress are whole sub-objects: sending one replaces its fields, and a vendor that has none has them created on first supply. attachments is tri-state: omit it to keep the current set, send null to remove them all, or send a non-empty array to replace the set, and an empty array is ignored. vendorType and externalVendorId cannot be changed, so this endpoint does not accept them. To retire a vendor with history, set status to 'inactive' rather than deleting it. The response is empty. Read the updated record with GET accounting/vendors/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateVendor) ## 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. | | `id` | path | string | yes | The unique identifier for the vendor to update. | ## Request body Required, `application/json`: updateVendorInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `vendorName` | string | no | The new name of the vendor. Omit to leave it unchanged. | | `status` | string | no | The new lifecycle status. Accepted values: 'active', 'inactive'. Setting 'inactive' retires a vendor with history without deleting it. | | `paymentTerms` | string | no | The new default payment-terms key. Omit to leave it unchanged. Stored as supplied. | | `vendorSubType` | string | no | The new sub-type. Accepted values: 'direct', 'indirect'. Omit to leave it unchanged. | | `mainContactName` | string | no | The new primary contact name. Omit to leave it unchanged. | | `mainContactEmail` | string | no | The new primary contact email. Omit to leave it unchanged. | | `mainContactPhone` | string | no | The new primary contact phone. Omit to leave it unchanged. | | `billingEmail` | string | no | The new billing email. Omit to leave it unchanged. | | `preferredCurrency` | string | no | The new preferred currency. Format ISO 4217, for example 'USD'. Omit to leave it unchanged. | | `preferredLanguage` | string | no | The new preferred language. Omit to leave it unchanged. | | `independentContractor` | string | no | The new independent-contractor value. Accepted values: 'yes', 'no', 'unsure'. Omit to leave it unchanged. | | `origin` | string | no | The new source system. Omit to leave it unchanged. | | `defaultAccountId` | string | no | The new default GL account. Omit to leave it unchanged. See id on GET accounting/accounts/:id. | | `departmentId` | string | no | The new default department. Omit to leave it unchanged. See id on GET departments/:id. | | `vendorEntityId` | string | no | The new legal entity. Omit to leave it unchanged. See id on GET base/entities. | | `bankDetails` | object | no | The bank account the vendor is paid into. Sending this replaces the bank-detail fields. Omit it to leave the bank details unchanged. | | `bankDetails.bankAccountNumber` | string | no | The vendor's bank account number, or IBAN. | | `bankDetails.bankAccountType` | string | no | The kind of bank account, as your tenant's account-type list spells it. | | `bankDetails.bankAccountHolderName` | string | no | The name the account is held in. | | `bankDetails.bankAccountNumberType` | string | no | How to read bankAccountNumber. One of 'iban', 'achOrWire' or 'other'. | | `bankDetails.financialInstitutionId` | string | no | The bank's identifier, a BIC, SWIFT or routing number. | | `bankDetails.financialInstitutionIdType` | string | no | How to read financialInstitutionId. | | `remittanceAddress` | object | no | The address remittance advice is sent to. Sending this replaces the address fields. Omit it to leave the address unchanged. | | `remittanceAddress.line1` | string | yes | The first address line: street number and name, or PO box. | | `remittanceAddress.line2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `remittanceAddress.city` | string | yes | The city or locality. | | `remittanceAddress.stateProvince` | string | no | The state, province or region. | | `remittanceAddress.zipCode` | string | yes | The postal or ZIP code. | | `remittanceAddress.country` | string | yes | The country, as a name or ISO 3166-1 code. | | `remittanceAddress.attention` | string | no | The person or department that mail to this address is for. | | `attachments` | array of object or null | no | The files attached to the vendor. Omit to keep the current set. Send null to remove them all. Send a non-empty array to replace the set. An empty array is ignored, so send null to clear. | | `attachments[].fileName` | string | yes | The display name of the attached file. | | `attachments[].fileId` | string | yes | The identifier of the already-uploaded file. | Example: ```json { "vendorName": "<vendorName>", "status": "<status>", "paymentTerms": "net60", "vendorSubType": "<vendorSubType>", "mainContactName": "Dana Fischer-Weiss", "mainContactEmail": "<mainContactEmail>", "mainContactPhone": "<mainContactPhone>", "billingEmail": "<billingEmail>", "preferredCurrency": "<preferredCurrency>", "preferredLanguage": "<preferredLanguage>", "independentContractor": "<independentContractor>", "origin": "<origin>", "defaultAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "departmentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "vendorEntityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "bankDetails": { "bankAccountNumber": "<bankAccountNumber>", "bankAccountType": "<bankAccountType>", "bankAccountHolderName": "<bankAccountHolderName>", "bankAccountNumberType": "<bankAccountNumberType>", "financialInstitutionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "financialInstitutionIdType": "<financialInstitutionIdType>" }, "remittanceAddress": { "line1": "<line1>", "line2": "<line2>", "city": "<city>", "stateProvince": "<stateProvince>", "zipCode": "<zipCode>", "country": "<country>", "attention": "<attention>" }, "attachments": [ { "fileName": "<fileName>", "fileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND - `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 --- # Deletes the vendor with the given identifier. `DELETE /accounting/vendors/{id}` Group: Accounting › Vendors · Operation ID: `deleteVendor` Deletes one vendor. Its bank details and remittance address go with it, and so does its business partner unless a customer record shares that same business partner, which preserves it. While any vendor bill, vendor credit or payroll payment references the vendor, the call fails with VENDOR_IN_USE and reports how many of each still point at it. If journal entries have been posted against the vendor's business partner, the call fails with VENDOR_HAS_JOURNAL_ENTRIES. Both are checked before anything is written. To keep the history and remove the vendor from pickers, deactivate it instead with PATCH accounting/vendors/:id and status 'inactive'. The response is empty. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteVendor) ## 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. | | `id` | path | string | yes | The unique identifier for the vendor to delete. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `VENDOR_IN_USE` | 400 | | `VENDOR_HAS_JOURNAL_ENTRIES` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, VENDOR_IN_USE, VENDOR_HAS_JOURNAL_ENTRIES - `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 --- # Returns the vendors that match the given filters. `POST /accounting/vendors/search` Group: Accounting › Vendors · Operation ID: `searchVendor` All filters are optional and are combined with AND. Omit every filter to match all vendors. Lookups by vendorName, vendorNumber, mainContactEmail and externalVendorId live here rather than on the collection. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchVendor) ## 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`. | ## Request body Required, `application/json`: searchVendorInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A vendor must match every filter you send. Omit to return all vendors. | | `filters.id` | string | no | Limits results to the vendor with this identifier. | | `filters.vendorName` | string | no | Limits results to vendors with this exact name. The match is case-sensitive. | | `filters.vendorNumber` | string | no | Limits results to vendors with this vendor number. It is not guaranteed unique, so this can return several vendors. | | `filters.vendorType` | string | no | Limits results to vendors of this kind. Accepted values: 'supplier', 'contractor', 'tax', 'benefits', 'payrollProvider', 'healthInsurance'. Any other value matches no vendor. | | `filters.vendorSubType` | string | no | Limits results to vendors of this sub-type. Accepted values: 'direct', 'indirect'. Any other value matches no vendor. | | `filters.status` | string | no | Limits results to vendors in this lifecycle status. Accepted values: 'active', 'inactive'. Any other value matches no vendor. | | `filters.entityId` | string | no | Limits results to vendors registered under this legal entity. An id that matches no entity returns no vendor. See id on GET base/entities. | | `filters.mainContactEmail` | string | no | Limits results to vendors whose main contact has this email address. Useful for finding a duplicate before creating another vendor. | | `filters.externalVendorId` | string | no | Limits results to vendors with this external identifier. It is not unique, so this can return several vendors. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of vendors per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "status": "active" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchVendorOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The vendors on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the vendor. | | `items[].vendorName` | string | yes | The legal or trading name of the vendor. | | `items[].vendorNumber` | string | no | The vendor number, for example 'V-2041'. Absent until one has been assigned. | | `items[].vendorType` | string | yes | The kind of vendor. Possible values: 'supplier', 'contractor', 'tax', 'benefits', 'payrollProvider', 'healthInsurance'. | | `items[].vendorSubType` | string | no | A finer classification within the vendor type. Possible values: 'direct', 'indirect'. | | `items[].status` | string | yes | The lifecycle status of the vendor. Possible values: 'active', 'inactive'. An inactive vendor stays readable so historical documents still resolve, and is not offered for new ones. | | `items[].paymentTerms` | string | yes | The default payment-terms key applied to this vendor's bills, for example 'net30' or 'dueOnReceipt'. Stored as supplied. | | `items[].preferredCurrency` | string | no | The currency the vendor prefers to be paid in. Format ISO 4217, for example 'USD'. | | `items[].preferredLanguage` | string | no | The language the vendor prefers for messages. | | `items[].independentContractor` | string | no | Whether the vendor is an independent contractor. Possible values: 'yes', 'no', 'unsure'. | | `items[].externalVendorId` | string | no | The identifier this vendor has in an external system. It is not unique. | | `items[].origin` | string | no | The source system that created the vendor, for example 'app'. | | `items[].mainContactName` | string | no | The name of the vendor's main contact. | | `items[].mainContactEmail` | string | no | The email address of the vendor's main contact. | | `items[].mainContactPhone` | string | no | The phone number of the vendor's main contact. | | `items[].billingEmail` | string | no | The email address invoices and remittance advice are sent to. | | `items[].entityId` | string | no | The legal entity the vendor is registered under. See id on GET base/entities. | | `items[].defaultAccountId` | string | no | The GL account this vendor's spend defaults to. See id on GET accounting/accounts/:id. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the vendor last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Bank Accounts `GET /banking/bank-accounts` Group: Banking › Bank Accounts · Operation ID: `bankAccounts` Returns a list of bank accounts. To match on an external identifier, a name, an account number, or a linked entity or institution, use POST banking/bank-accounts/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bankAccounts) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `type` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `paymentAllowed` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `receivableAccount` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `payableAccount` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isReady` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `reconciliationStartDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, type, currency, balance, paymentAllowed, isReady, reconciliationStartDate, lastModifiedDate - Selectable: id, externalAccountId, externalAccountName, externalDescription, type, currency, balance, mask, reference, referenceType, referenceNumber, bankAccountCode, bankAccountHolderName, paymentAllowed, receivableAccount, payableAccount, isReady, reconciliationStartDate, transactionsImportDate, accountName, accountNumber, bankName, entityName, lastModifiedDate ## Response `200` Success Under `data`, an array of bankAccountsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the bank account. | | `externalAccountId` | string | no | The identifier this account has in the bank or aggregator it was connected from. Unique within the tenant. | | `externalAccountName` | string or null | no | The account name as reported by the bank or aggregator. | | `externalDescription` | string or null | no | The account description as reported by the bank or aggregator. | | `type` | string or null | no | The kind of account, as free-form text. For example 'checking', 'saving' or 'creditCard'. | | `currency` | string or null | no | The currency the account is held in. Format ISO 4217, for example 'USD'. balance is in this currency. | | `balance` | string (decimal) or null | no | The current account balance, in currency. | | `mask` | string or null | no | A masked form of the account number, for example '****1234'. Display it instead of reference where the full number is not needed. | | `reference` | string or null | no | The full account number, the IBAN or the ACH account number. | | `referenceType` | string or null | no | How referenceNumber is structured. Possible values: 'QRR', 'SCOR', 'NON'. 'QRR' is a Swiss QR reference, 'SCOR' an ISO 11649 creditor reference, and 'NON' none. | | `referenceNumber` | string or null | no | A structured payment reference for payments to this account, interpreted per referenceType. | | `bankAccountCode` | string or null | no | The bank identifier, the SWIFT/BIC code or the US routing number. | | `bankAccountHolderName` | string or null | no | The name the account is held in. This is personal data. | | `paymentAllowed` | boolean | no | Whether payments may be made from this account. | | `receivableAccount` | boolean or null | no | Whether this account may be used to receive money. | | `payableAccount` | boolean or null | no | Whether this account may be used to pay money out. | | `isReady` | boolean or null | no | Whether the account is fully set up and usable. | | `reconciliationStartDate` | string (date) | no | The date from which this account is reconciled. Format YYYY-MM-DD. Transactions before it are not reconciled. | | `transactionsImportDate` | string (date) or null | no | The date from which transactions are imported for this account. Format YYYY-MM-DD. | | `accountName` | string or null | no | The name of the linked chart-of-accounts account. | | `accountNumber` | string or null | no | The number of the linked chart-of-accounts account. This is a general-ledger account number, not the bank account number, which is reference. | | `bankName` | string or null | no | The name of the bank this account is held at. | | `entityName` | string or null | no | The name of the legal entity that owns this account. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the bank account last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a bank account. `POST /banking/bank-accounts` Group: Banking › Bank Accounts · Operation ID: `createBankAccount` The response contains only the new account's id. Read the record with GET banking/bank-accounts/:id. externalAccountId, accountId, reconciliationStartDate and paymentAllowed are required. A duplicate externalAccountId fails with DUPLICATE_EXTERNAL_ACCOUNT_ID, which names the existing account. accountId and financialInstitutionId each fail with REFERENCE_NOT_FOUND when they name no record. When referenceType is 'QRR', reference must be a Swiss QR-IBAN, whose fifth to ninth digits fall between 30000 and 31999. Anything else fails with INVALID_QR_IBAN. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createBankAccount) ## 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`: createBankAccountInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `externalAccountId` | string | yes | The account's identifier in the system that owns it, for example its IBAN or ACH account number. Must be unique within the tenant. | | `accountId` | string | yes | The general-ledger account this bank account posts to. See id on GET accounting/accounts/:id. | | `reconciliationStartDate` | string (date) | yes | The date from which transactions on this account are reconciled. Use format YYYY-MM-DD. | | `paymentAllowed` | boolean | yes | Whether outbound payments may be made from this account. | | `financialInstitutionId` | string | no | The financial institution holding the account. Omit when the account is not linked to one. There is no endpoint for it. | | `currency` | string | no | The account currency. Format ISO 4217, for example 'USD'. | | `type` | string | no | The kind of account, as free-form text. For example 'checking' or 'saving'. | | `reference` | string | no | The IBAN or ACH account number. When referenceType is 'QRR' this must be a Swiss QR-IBAN. | | `referenceType` | string | no | How payment references on this account are structured. Accepted values: 'QRR', 'SCOR', 'NON'. | | `referenceNumber` | string | no | A structured creditor reference number for this account. | | `receivableAccount` | boolean | no | Whether the account may receive incoming payments. Defaults to false. | Example: ```json { "externalAccountId": "CH9300762011623852957", "accountId": "d5397c8b-6d2b-47e9-83a3-712d9180711e", "reconciliationStartDate": "2026-01-01", "paymentAllowed": true, "financialInstitutionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "currency": "CHF", "type": "checking", "reference": "CH4431999123000889012", "referenceType": "QRR", "referenceNumber": "<referenceNumber>", "receivableAccount": false } ``` ## Response `200` Success Under `data` (createBankAccountOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created bank account. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `INVALID_FIELD_VALUE` | 400 | | `INVALID_QR_IBAN` | 400 | | `DUPLICATE_EXTERNAL_ACCOUNT_ID` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, INVALID_FIELD_VALUE, INVALID_QR_IBAN, DUPLICATE_EXTERNAL_ACCOUNT_ID - `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 --- # Returns the bank account with the given identifier. `GET /banking/bank-accounts/{id}` Group: Banking › Bank Accounts · Operation ID: `getBankAccount` Returns the account's payment credentials. reference is the IBAN or ACH number, bankAccountCode the SWIFT/BIC or US routing number, and bankAccountHolderName is personal data. mask is the safe display form. accountNumber is the chart-of-accounts number, not a bank one. A soft-deleted account is not returned. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getBankAccount) ## 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`. | | `id` | path | string | yes | The unique identifier for the bank account to return. | ## Response `200` Success Under `data` (getBankAccountOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the bank account. | | `externalAccountId` | string | yes | The identifier this account has in the bank or aggregator it was connected from. Unique within the tenant. | | `externalAccountName` | string | no | The account name as reported by the bank or aggregator. | | `externalDescription` | string | no | The account description as reported by the bank or aggregator. | | `type` | string | no | The kind of account, as free-form text. For example 'checking', 'saving' or 'creditCard'. | | `currency` | string | no | The currency the account is held in. Format ISO 4217, for example 'USD'. balance is in this currency. | | `balance` | string (decimal) | no | The current account balance, in currency. | | `mask` | string | no | A masked form of the account number, for example '****1234'. Display it instead of reference where the full number is not needed. | | `reference` | string | no | The full account number, the IBAN or the ACH account number. | | `referenceType` | string | no | How referenceNumber is structured. Possible values: 'QRR', 'SCOR', 'NON'. 'QRR' is a Swiss QR reference, 'SCOR' an ISO 11649 creditor reference, and 'NON' none. | | `referenceNumber` | string | no | A structured payment reference for payments to this account, interpreted per referenceType. | | `bankAccountCode` | string | no | The bank identifier, the SWIFT/BIC code or the US routing number. | | `bankAccountHolderName` | string | no | The name the account is held in. This is personal data. | | `paymentAllowed` | boolean | yes | Whether payments may be made from this account. | | `receivableAccount` | boolean | no | Whether this account may be used to receive money. | | `payableAccount` | boolean | no | Whether this account may be used to pay money out. | | `isReady` | boolean | no | Whether the account is fully set up and usable. | | `reconciliationStartDate` | string (date) | yes | The date from which this account is reconciled. Format YYYY-MM-DD. Transactions before it are not reconciled. | | `transactionsImportDate` | string (date) | no | The date from which transactions are imported for this account. Format YYYY-MM-DD. | | `accountId` | string | no | The chart-of-accounts account this bank account is linked to. See id on GET accounting/accounts/:id. | | `accountName` | string | no | The name of the linked chart-of-accounts account. | | `accountNumber` | string | no | The number of the linked chart-of-accounts account. This is a general-ledger account number, not the bank account number, which is reference. | | `bankName` | string | no | The name of the bank this account is held at. | | `financialInstitutionId` | string | no | The financial institution this account is held at. There is no endpoint for it. | | `entityId` | string | no | The legal entity that owns this account. See id on GET base/entities. | | `entityName` | string | no | The name of the legal entity that owns this account. | | `lastModifiedDate` | string (date-time) | yes | The date and time the bank account last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Bank Account `PATCH /banking/bank-accounts/{id}` Group: Banking › Bank Accounts · Operation ID: `updateBankAccount` Updates the bank account with the given identifier. Only the fields in the request body change. Every field is optional and omitting it leaves the stored value unchanged. The response is empty. Read the result with GET banking/bank-accounts/:id. reconciliationStartDate and paymentAllowed are set when the account is created and cannot be changed here. When referenceType is 'QRR', the QR-IBAN check runs against the incoming reference when one is sent and against the stored reference otherwise, and a failure is INVALID_QR_IBAN. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateBankAccount) ## 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. | | `id` | path | string | yes | The unique identifier for the bank account to update. | ## Request body Required, `application/json`: updateBankAccountInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `externalAccountId` | string | no | The account's identifier in the system that owns it, for example its IBAN or ACH account number. Must be unique within the tenant. Omit to leave unchanged. The account's own current value is not a collision. | | `accountId` | string | no | The general-ledger account this bank account posts to. Omit to leave unchanged. See id on GET accounting/accounts/:id. | | `financialInstitutionId` | string | no | The financial institution holding the account. Omit to leave unchanged. There is no endpoint for it. | | `currency` | string | no | The account currency. Format ISO 4217, for example 'USD'. Omit to leave unchanged. | | `type` | string | no | The kind of account, as free-form text. For example 'checking' or 'saving'. Omit to leave unchanged. | | `reference` | string | no | The IBAN or ACH account number. When referenceType is 'QRR' this must be a Swiss QR-IBAN. Omit to leave unchanged. | | `referenceType` | string | no | How payment references on this account are structured. Accepted values: 'QRR', 'SCOR', 'NON'. Omit to leave unchanged. | | `referenceNumber` | string | no | A structured creditor reference number for this account. Omit to leave unchanged. | | `receivableAccount` | boolean | no | Whether the account may receive incoming payments. Omit to leave unchanged. | Example: ```json { "externalAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "financialInstitutionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "currency": "<currency>", "type": "savings", "reference": "<reference>", "referenceType": "NON", "referenceNumber": "<referenceNumber>", "receivableAccount": true } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `INVALID_FIELD_VALUE` | 400 | | `INVALID_QR_IBAN` | 400 | | `DUPLICATE_EXTERNAL_ACCOUNT_ID` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND, INVALID_FIELD_VALUE, INVALID_QR_IBAN, DUPLICATE_EXTERNAL_ACCOUNT_ID - `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 --- # Deletes a bank account, keeping it recoverable. `DELETE /banking/bank-accounts/{id}` Group: Banking › Bank Accounts · Operation ID: `deleteBankAccount` The response is empty. This is a recoverable soft delete. The account is flagged deleted, unlinked from its chart of accounts, and, for an integration account that has a fingerprint, added to the recovery list behind "Recover Accounts". It disappears from the get, list and search endpoints at once, because those return only accounts that are not deleted. Two accounts get no recovery row and cannot be restored this way: a manual account, and any account without a fingerprint. This is not the counterpart of DELETE banking/bank-transactions/:id, which is an irreversible hard delete. Unmatched and suggested transactions on the account are hard-deleted as part of the removal, and for a SaltEdge account the bank consent is revoked. An account that has a matched or auto-matched transaction is refused with BANK_ACCOUNT_IN_USE. The underlying action can override that case, and it is deliberately not published, so this endpoint cannot force it through. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteBankAccount) ## 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. | | `id` | path | string | yes | The unique identifier for the bank account to delete. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `BANK_ACCOUNT_IN_USE` | 400 | | `REMOVAL_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, BANK_ACCOUNT_IN_USE, REMOVAL_FAILED - `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 --- # Returns the bank accounts that match the given filters. `POST /banking/bank-accounts/search` Group: Banking › Bank Accounts · Operation ID: `searchBankAccount` This is the only endpoint that matches a bank account by a name or an account number. Those fields are returned on the collection but can be filtered only here. Every filter is optional and they are combined with AND. Omit them all to return every bank account that is not soft-deleted. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchBankAccount) ## 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`. | ## Request body Required, `application/json`: searchBankAccountInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A bank account must match every filter you send. Omit to return all bank accounts. | | `filters.id` | string | no | Limits results to the bank account with this identifier. | | `filters.externalAccountId` | string | no | Limits results to the bank account with this external identifier. It is unique within the tenant, so at most one account matches. | | `filters.externalAccountName` | string | no | Limits results to bank accounts with this external account name. The match is exact. | | `filters.accountId` | string | no | Limits results to the bank account linked to this chart-of-accounts account. See id on GET accounting/accounts/:id. | | `filters.accountName` | string | no | Limits results to bank accounts whose linked chart-of-accounts account has this name. The match is exact. | | `filters.accountNumber` | string | no | Limits results to bank accounts whose linked chart-of-accounts account has this number. This is a general-ledger number, not a bank account number. The match is exact. | | `filters.entityId` | string | no | Limits results to bank accounts owned by this legal entity. See id on GET base/entities. | | `filters.financialInstitutionId` | string | no | Limits results to bank accounts held at this financial institution. There is no endpoint for it. | | `filters.type` | string | no | Limits results to bank accounts of this kind, for example 'checking'. The kind is free-form text, so an unknown value matches nothing. | | `filters.currency` | string | no | Limits results to bank accounts held in this currency. Format ISO 4217, for example 'USD'. | | `filters.paymentAllowed` | boolean | no | Limits results to accounts payments may be made from when true, and those they may not when false. | | `filters.receivableAccount` | boolean | no | Limits results to accounts that may receive money when true, and those that may not when false. | | `filters.isReady` | boolean | no | Limits results to accounts that are fully set up when true, and those that are not when false. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first request. | | `page.size` | number | no | The number of bank accounts per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "type": "checking", "paymentAllowed": true }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchBankAccountOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The bank accounts on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the bank account. | | `items[].externalAccountId` | string | yes | The identifier this account has in the bank or aggregator it was connected from. Unique within the tenant. | | `items[].externalAccountName` | string | no | The account name as reported by the bank or aggregator. | | `items[].externalDescription` | string | no | The account description as reported by the bank or aggregator. | | `items[].type` | string | no | The kind of account, as free-form text. For example 'checking', 'saving' or 'creditCard'. | | `items[].currency` | string | no | The currency the account is held in. Format ISO 4217, for example 'USD'. balance is in this currency. | | `items[].balance` | string (decimal) | no | The current account balance, in currency. | | `items[].mask` | string | no | A masked form of the account number, for example '****1234'. Display it instead of reference where the full number is not needed. | | `items[].reference` | string | no | The full account number, the IBAN or the ACH account number. | | `items[].referenceType` | string | no | How referenceNumber is structured. Possible values: 'QRR', 'SCOR', 'NON'. 'QRR' is a Swiss QR reference, 'SCOR' an ISO 11649 creditor reference, and 'NON' none. | | `items[].referenceNumber` | string | no | A structured payment reference for payments to this account, interpreted per referenceType. | | `items[].bankAccountCode` | string | no | The bank identifier, the SWIFT/BIC code or the US routing number. | | `items[].bankAccountHolderName` | string | no | The name the account is held in. This is personal data. | | `items[].paymentAllowed` | boolean | yes | Whether payments may be made from this account. | | `items[].receivableAccount` | boolean | no | Whether this account may be used to receive money. | | `items[].payableAccount` | boolean | no | Whether this account may be used to pay money out. | | `items[].isReady` | boolean | no | Whether the account is fully set up and usable. | | `items[].reconciliationStartDate` | string (date) | yes | The date from which this account is reconciled. Format YYYY-MM-DD. Transactions before it are not reconciled. | | `items[].transactionsImportDate` | string (date) | no | The date from which transactions are imported for this account. Format YYYY-MM-DD. | | `items[].accountId` | string | no | The chart-of-accounts account this bank account is linked to. See id on GET accounting/accounts/:id. | | `items[].accountName` | string | no | The name of the linked chart-of-accounts account. | | `items[].accountNumber` | string | no | The number of the linked chart-of-accounts account. This is a general-ledger account number, not the bank account number, which is reference. | | `items[].bankName` | string | no | The name of the bank this account is held at. | | `items[].financialInstitutionId` | string | no | The financial institution this account is held at. There is no endpoint for it. | | `items[].entityId` | string | no | The legal entity that owns this account. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the legal entity that owns this account. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the bank account last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Bank Entries `GET /banking/bank-entries` Group: Banking › Bank Entries · Operation ID: `bankEntries` Returns a list of bank entries. Each row is the entry header without its posting lines. To read the lines use GET banking/bank-entries/:id, and to match on the bank account use POST banking/bank-entries/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bankEntries) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `bankEntryNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `transactionDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `serviceDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `journalEntryNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `origin` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, bankEntryNumber, transactionDate, serviceDate, currency, status, journalEntryNumber, origin, lastModifiedDate - Selectable: id, bankEntryNumber, name, description, transactionDate, serviceDate, currency, status, journalEntryNumber, origin, bankAccountName, lastModifiedDate ## Response `200` Success Under `data`, an array of bankEntriesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the bank entry. | | `bankEntryNumber` | string or null | no | The bank entry number. Absent until one has been assigned. | | `name` | string or null | no | The display name of the bank entry. | | `description` | string or null | no | A free-form description of the entry. | | `transactionDate` | string (date) or null | no | The date of the transaction this entry records. Format YYYY-MM-DD. | | `serviceDate` | string (date) or null | no | The date the goods or services were delivered. Format YYYY-MM-DD. | | `currency` | string or null | no | The currency the entry is denominated in. Format ISO 4217, for example 'USD'. It governs every line amount. | | `status` | string or null | no | The reconciliation status of the entry. Possible values: 'matched', 'unmatched', 'voided', 'integrated'. | | `journalEntryNumber` | string or null | no | The number of the journal entry this entry posted to, when it has been posted. | | `origin` | string or null | no | A label for the source system the entry was created from. | | `bankAccountName` | string or null | no | The name of the bank general-ledger account. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the bank entry last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a bank entry with its lines. `POST /banking/bank-entries` Group: Banking › Bank Entries · Operation ID: `createBankEntry` The response contains only the new entry's id. Read the record with GET banking/bank-entries/:id. entityId, bankAccountId, transactionDate and at least one line are required. Every line amount is a decimal in the entry currency. No line may be zero, and the lines may not sum to zero. When currency is omitted the account's own currency is used. When supplied it must match the account's, or the call fails with CURRENCY_MISMATCH. The posting period is resolved across every book the entity posts to. A transaction date whose period is closed, or open in some books and closed in others, fails with POSTING_PERIOD_NOT_OPEN. attachments names already-uploaded files. On a tenant that requires an attachment, omitting them fails with ATTACHMENT_REQUIRED. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createBankEntry) ## 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`: createBankEntryInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `entityId` | string | yes | The legal entity the entry is posted under. See id on GET base/entities. | | `bankAccountId` | string | yes | The general-ledger account behind the bank account this entry posts against. See id on GET accounting/accounts/:id. | | `transactionDate` | string (date) | yes | The date the transaction took place. Use format YYYY-MM-DD. It decides the posting period. | | `currency` | string | no | The currency of the entry. Format ISO 4217, for example 'USD'. It governs every amount on the entry. Omit to use the account's own currency. When supplied it must match the account's currency. | | `status` | string | no | The reconciliation status. Accepted values: 'matched', 'unmatched', 'voided', 'integrated'. Defaults to 'unmatched'. | | `name` | string | no | A short name for the entry. | | `description` | string | no | A free-form description of the entry. | | `serviceDate` | string (date) | no | The date the goods or services were delivered. Use format YYYY-MM-DD. | | `origin` | string | no | The system or process the entry originated from. | | `externalUrl` | string | no | A link to the entry in the originating system. | | `lines` | array of object | yes | The lines of the entry, in the order they appear. At least one is required. No line may be zero, and the lines may not sum to zero. | | `lines[].accountId` | string | yes | The general-ledger account this line posts to. It must not be a grouping account. See id on GET accounting/accounts/:id. | | `lines[].amount` | string (decimal) | yes | The line amount, as a decimal in the entry currency. It must be non-zero. | | `lines[].description` | string | no | A free-form description of the line. | | `lines[].departmentId` | string | no | The department this line is attributed to. See id on GET departments/:id. | | `lines[].businessUnitId` | string | no | The business unit this line is attributed to. There is no endpoint for it. | | `lines[].businessPartnerId` | string | no | The business partner this line is attributed to. Required when vatPercentage is supplied. There is no endpoint for it. | | `lines[].lineType` | string | no | The kind of line. Accepted values: 'oneTime', 'prepaid'. Derived from the account's sub-type when omitted. | | `lines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. For example, 19 means 19%. Supplying it requires businessPartnerId. | | `lines[].vatAccountId` | string | no | The general-ledger account the VAT posts to. See id on GET accounting/accounts/:id. | | `lines[].reverseVatAccountId` | string | no | The general-ledger account the reverse-charge VAT posts to. See id on GET accounting/accounts/:id. | | `lines[].vatAmount` | string (decimal) | no | The VAT amount, as a decimal in the entry currency. Omit to derive it from vatPercentage. | | `lines[].netAmount` | string (decimal) | no | The net amount, as a decimal in the entry currency. Omit to derive it from amount less VAT. | | `attachments` | array of object | no | Files to attach to the entry, each naming an already-uploaded file by its id. Required on a tenant configured to require an attachment. | | `attachments[].fileName` | string | yes | The display name of the attached file. | | `attachments[].fileId` | string | yes | The identifier of the already-uploaded file. | Example: ```json { "entityId": "e8dc29b9-7e79-4709-bb32-95938bcb1bd1", "bankAccountId": "f50164c3-85e3-4dab-986e-3fdb39f8ecc1", "transactionDate": "2026-02-14", "currency": "USD", "status": "unmatched", "name": "February bank fees", "description": "<description>", "serviceDate": "2026-01-15", "origin": "<origin>", "externalUrl": "<externalUrl>", "lines": [ { "accountId": "9994c3ed-f812-44c1-9454-7f36b2bfb0fb", "amount": "-125.00", "description": "Account maintenance fee", "lineType": "oneTime" } ], "attachments": [ { "fileName": "<fileName>", "fileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ] } ``` ## Response `200` Success Under `data` (createBankEntryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created bank entry. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `INVALID_FIELD_VALUE` | 400 | | `POSTING_PERIOD_NOT_OPEN` | 400 | | `CURRENCY_MISMATCH` | 400 | | `ACCOUNT_ENTITY_MISMATCH` | 400 | | `GROUPING_ACCOUNT_NOT_ALLOWED` | 400 | | `BUSINESS_PARTNER_REQUIRED_FOR_VAT` | 400 | | `NO_LINES` | 400 | | `ZERO_LINE_AMOUNT` | 400 | | `ZERO_TOTAL_AMOUNT` | 400 | | `ATTACHMENT_REQUIRED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, INVALID_FIELD_VALUE, POSTING_PERIOD_NOT_OPEN, CURRENCY_MISMATCH, ACCOUNT_ENTITY_MISMATCH, GROUPING_ACCOUNT_NOT_ALLOWED, BUSINESS_PARTNER_REQUIRED_FOR_VAT, NO_LINES, ZERO_LINE_AMOUNT, ZERO_TOTAL_AMOUNT, ATTACHMENT_REQUIRED - `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 --- # Get Bank Entry `GET /banking/bank-entries/{id}` Group: Banking › Bank Entries · Operation ID: `getBankEntry` Returns the bank entry with the given identifier. Use the bank entry's id, not its bankEntryNumber. A bank entry records money moving in or out of a bank account. Its lines record the general-ledger postings, as decimals in the entry currency. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getBankEntry) ## 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`. | | `id` | path | string | yes | The unique identifier for the bank entry to return. | ## Response `200` Success Under `data` (getBankEntryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the bank entry. | | `bankEntryNumber` | string | no | The bank entry number. Absent until one has been assigned. | | `name` | string | no | The display name of the bank entry. | | `description` | string | no | A free-form description of the entry. | | `transactionDate` | string (date) | yes | The date of the transaction this entry records. Format YYYY-MM-DD. | | `serviceDate` | string (date) | no | The date the goods or services were delivered. Format YYYY-MM-DD. Defaults to the transaction date where the entity's country requires a service date. | | `currency` | string | no | The currency the entry is denominated in. Format ISO 4217, for example 'USD'. It governs every line amount. | | `status` | string | yes | The reconciliation status of the entry. Possible values: 'matched', 'unmatched', 'voided', 'integrated'. | | `journalEntryNumber` | string | no | The number of the journal entry this entry posted to, when it has been posted. | | `origin` | string | no | A label for the source system the entry was created from. | | `externalUrl` | string | no | A link to the source document in an external system. | | `bankAccountId` | string | no | The bank general-ledger account this entry is recorded against. See id on GET accounting/accounts/:id. | | `bankAccountName` | string | no | The name of the bank general-ledger account. | | `entityId` | string | no | The legal entity this entry is posted under. See id on GET base/entities. | | `lastModifiedDate` | string (date-time) | yes | The date and time the bank entry last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | no | The posting lines of the entry, one per general-ledger posting. An empty array when the entry has none. | | `lines[].id` | string | yes | The unique identifier for the line. | | `lines[].lineNumber` | string | no | The line number. | | `lines[].description` | string | no | A free-form description of the line. | | `lines[].lineType` | string | no | The kind of line. Possible values: 'oneTime', 'prepaid'. | | `lines[].amount` | string (decimal) | no | The gross line amount, including VAT, in the entry currency. | | `lines[].currency` | string | no | The currency of the line. Format ISO 4217. It always matches the entry currency. | | `lines[].netAmount` | string (decimal) | no | The line amount excluding VAT, in the entry currency. | | `lines[].vatAmount` | string (decimal) | no | The VAT amount on the line, in the entry currency. | | `lines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. For example, 19 means 19%. | | `lines[].origin` | string | no | A label for the source system, copied onto the line from the entry. | | `lines[].accountId` | string | no | The general-ledger account this line posts to. See id on GET accounting/accounts/:id. | | `lines[].departmentId` | string | no | The department this line is charged to. See id on GET departments/:id. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Bank Entry `PATCH /banking/bank-entries/{id}` Group: Banking › Bank Entries · Operation ID: `updateBankEntry` Updates the bank entry with the given identifier. Only the fields in the request body change. Use the bank entry's id, not its bankEntryNumber. Only the fields in the request body change, and the response is empty. Read the result with GET banking/bank-entries/:id. Lines are edited through three arrays: createLines, updateLines and deleteLines. They are applied in that order, deletes first, then updates, then creates. Omit an array for no operations of that kind, and omit all three to leave the lines unchanged. Lines you do not name are kept. Every line id in updateLines or deleteLines must belong to this entry. One that does not fails with LINE_NOT_FOUND and nothing is written. The resulting lines are still subject to the whole-entry rules: at least one line, no line of zero, and a non-zero total. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateBankEntry) ## 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. | | `id` | path | string | yes | The unique identifier for the bank entry to update. | ## Request body Required, `application/json`: updateBankEntryInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `entityId` | string | no | The legal entity the entry is posted under. See id on GET base/entities. | | `bankAccountId` | string | no | The general-ledger account behind the bank account this entry posts against. See id on GET accounting/accounts/:id. | | `transactionDate` | string (date) | no | The date the transaction took place. Use format YYYY-MM-DD. Changing it re-resolves the posting period. | | `currency` | string | no | The currency of the entry. Format ISO 4217, for example 'USD'. When supplied it must match the account's currency. | | `status` | string | no | The reconciliation status. Accepted values: 'matched', 'unmatched', 'voided', 'integrated'. | | `name` | string | no | A short name for the entry. | | `description` | string | no | A free-form description of the entry. | | `serviceDate` | string (date) | no | The date the goods or services were delivered. Use format YYYY-MM-DD. | | `origin` | string | no | The system or process the entry originated from. | | `externalUrl` | string | no | A link to the entry in the originating system. | | `createLines` | array of object | no | Lines to add. Each has the same shape as a line on create, with no id because the line does not exist yet. | | `createLines[].accountId` | string | yes | The general-ledger account this line posts to. It must not be a grouping account. See id on GET accounting/accounts/:id. | | `createLines[].amount` | string (decimal) | yes | The line amount, as a decimal in the entry currency. It must be non-zero. | | `createLines[].description` | string | no | A free-form description of the line. | | `createLines[].departmentId` | string | no | The department this line is attributed to. See id on GET departments/:id. | | `createLines[].businessUnitId` | string | no | The business unit this line is attributed to. There is no endpoint for it. | | `createLines[].businessPartnerId` | string | no | The business partner this line is attributed to. Required when vatPercentage is supplied. There is no endpoint for it. | | `createLines[].lineType` | string | no | The kind of line. Accepted values: 'oneTime', 'prepaid'. Derived from the account's sub-type when omitted. | | `createLines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. For example, 19 means 19%. Supplying it requires businessPartnerId. | | `createLines[].vatAccountId` | string | no | The general-ledger account the VAT posts to. See id on GET accounting/accounts/:id. | | `createLines[].reverseVatAccountId` | string | no | The general-ledger account the reverse-charge VAT posts to. See id on GET accounting/accounts/:id. | | `createLines[].vatAmount` | string (decimal) | no | The VAT amount, as a decimal in the entry currency. Omit to derive it from vatPercentage. | | `createLines[].netAmount` | string (decimal) | no | The net amount, as a decimal in the entry currency. Omit to derive it from amount less VAT. | | `updateLines` | array of object | no | Lines to change, each addressed by its id. Every field but the id is optional, and omitting one leaves that field unchanged. | | `updateLines[].id` | string | yes | The unique identifier for the line to change. It must belong to this bank entry. | | `updateLines[].accountId` | string | no | The general-ledger account this line posts to. It must not be a grouping account. See id on GET accounting/accounts/:id. | | `updateLines[].amount` | string (decimal) | no | The line amount, as a decimal in the entry currency. It must be non-zero. | | `updateLines[].description` | string | no | A free-form description of the line. | | `updateLines[].departmentId` | string | no | The department this line is attributed to. See id on GET departments/:id. | | `updateLines[].businessUnitId` | string | no | The business unit this line is attributed to. There is no endpoint for it. | | `updateLines[].businessPartnerId` | string | no | The business partner this line is attributed to. Required when vatPercentage is supplied. There is no endpoint for it. | | `updateLines[].lineType` | string | no | The kind of line. Accepted values: 'oneTime', 'prepaid'. Derived from the account's sub-type when omitted. | | `updateLines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. For example, 19 means 19%. Supplying it requires businessPartnerId. | | `updateLines[].vatAccountId` | string | no | The general-ledger account the VAT posts to. See id on GET accounting/accounts/:id. | | `updateLines[].reverseVatAccountId` | string | no | The general-ledger account the reverse-charge VAT posts to. See id on GET accounting/accounts/:id. | | `updateLines[].vatAmount` | string (decimal) | no | The VAT amount, as a decimal in the entry currency. Omit to derive it from vatPercentage. | | `updateLines[].netAmount` | string (decimal) | no | The net amount, as a decimal in the entry currency. Omit to derive it from amount less VAT. | | `deleteLines` | array of string | no | The identifiers of lines to remove. Each must belong to this bank entry. | Example: ```json { "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "bankAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "transactionDate": "2026-01-15", "currency": "<currency>", "status": "<status>", "name": "February bank fees — revised", "description": "<description>", "serviceDate": "2026-01-15", "origin": "<origin>", "externalUrl": "<externalUrl>", "createLines": [ { "accountId": "9994c3ed-f812-44c1-9454-7f36b2bfb0fb", "amount": "-25.00", "description": "Wire fee" } ], "updateLines": [ { "id": "7a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9", "amount": "-150.00" } ], "deleteLines": [ "1f2e3d4c-5b6a-7089-9a8b-7c6d5e4f3a2b" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `LINE_NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `INVALID_FIELD_VALUE` | 400 | | `POSTING_PERIOD_NOT_OPEN` | 400 | | `CURRENCY_MISMATCH` | 400 | | `ACCOUNT_ENTITY_MISMATCH` | 400 | | `GROUPING_ACCOUNT_NOT_ALLOWED` | 400 | | `BUSINESS_PARTNER_REQUIRED_FOR_VAT` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, LINE_NOT_FOUND, REFERENCE_NOT_FOUND, INVALID_FIELD_VALUE, POSTING_PERIOD_NOT_OPEN, CURRENCY_MISMATCH, ACCOUNT_ENTITY_MISMATCH, GROUPING_ACCOUNT_NOT_ALLOWED, BUSINESS_PARTNER_REQUIRED_FOR_VAT - `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 --- # Void Bank Entry `POST /banking/bank-entries/{id}/void` Group: Banking › Bank Entries · Operation ID: `voidBankEntry` Voids the bank entry with the given identifier. Voiding cannot be undone. Voids the bank entry and the journal entry behind it. Voiding is not deleting: the entry, its number and the audit trail all stay, and the ledger effect is withdrawn. There is no delete endpoint for a bank entry. Voiding cannot be undone. A second void fails with BANK_ENTRY_ALREADY_VOIDED. voidReason is required and is stored on the entry for the audit trail. An empty or whitespace-only string is refused. The journal entry is marked voided and no reversing entry is posted. Amortization-schedule journal entries derived from it are voided too, any pending approval becomes rejected, and bank transactions matched to the entry are unmatched. Attachments and lines are left unchanged. The posting period must be open. An entry with no entity or no posting period cannot have its journal entry voided and fails with MISSING_POSTING_CONTEXT. The response is empty. Read the entry back with GET banking/bank-entries/:id to see its voided state. voidReason is not published by any read endpoint, so it appears in the audit trail rather than over the API. [Open in the interactive reference](https://docs.api.lensing.ai/?op=voidBankEntry) ## 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. | | `id` | path | string | yes | The unique identifier for the bank entry to void. | ## Request body Required, `application/json`: voidBankEntryInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `voidReason` | string | yes | Why the entry is being voided. It is stored on the entry for the audit trail. An empty or whitespace-only string is refused. | Example: ```json { "voidReason": "Duplicate of BE-4471" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `VOID_REASON_REQUIRED` | 400 | | `BANK_ENTRY_ALREADY_VOIDED` | 400 | | `POSTING_PERIOD_NOT_OPEN` | 400 | | `MISSING_POSTING_CONTEXT` | 400 | | `VOID_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, VOID_REASON_REQUIRED, BANK_ENTRY_ALREADY_VOIDED, POSTING_PERIOD_NOT_OPEN, MISSING_POSTING_CONTEXT, VOID_FAILED - `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 --- # Returns the bank entries that match the given filters. `POST /banking/bank-entries/search` Group: Banking › Bank Entries · Operation ID: `searchBankEntry` Every filter is optional and they are combined with AND. Omit them all to return every bank entry. Each match includes the entry header and its posting lines. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchBankEntry) ## 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`. | ## Request body Required, `application/json`: searchBankEntryInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A bank entry must match every filter you send. Omit to return all bank entries. | | `filters.id` | string | no | Limits results to the bank entry with this identifier. | | `filters.bankEntryNumber` | string | no | Limits results to the bank entry with this number. The match is exact. | | `filters.journalEntryNumber` | string | no | Limits results to entries that posted to the journal entry with this number. The match is exact. | | `filters.status` | string | no | Limits results to entries in this status. Accepted values: 'matched', 'unmatched', 'voided', 'integrated'. Any other value matches nothing. | | `filters.currency` | string | no | Limits results to entries denominated in this currency. Format ISO 4217, for example 'USD'. | | `filters.bankAccountId` | string | no | Limits results to entries recorded against this bank general-ledger account. See id on GET accounting/accounts/:id. | | `filters.entityId` | string | no | Limits results to entries posted under this legal entity. See id on GET base/entities. | | `filters.origin` | string | no | Limits results to entries created by this source system. The match is exact. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first request. | | `page.size` | number | no | The number of bank entries per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "status": "unmatched" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchBankEntryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The bank entries on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the bank entry. | | `items[].bankEntryNumber` | string | no | The bank entry number. Absent until one has been assigned. | | `items[].name` | string | no | The display name of the bank entry. | | `items[].description` | string | no | A free-form description of the entry. | | `items[].transactionDate` | string (date) | yes | The date of the transaction this entry records. Format YYYY-MM-DD. | | `items[].serviceDate` | string (date) | no | The date the goods or services were delivered. Format YYYY-MM-DD. | | `items[].currency` | string | no | The currency the entry is denominated in. Format ISO 4217, for example 'USD'. It governs every line amount. | | `items[].status` | string | yes | The reconciliation status of the entry. Possible values: 'matched', 'unmatched', 'voided', 'integrated'. | | `items[].journalEntryNumber` | string | no | The number of the journal entry this entry posted to, when it has been posted. | | `items[].origin` | string | no | A label for the source system the entry was created from. | | `items[].externalUrl` | string | no | A link to the source document in an external system. | | `items[].bankAccountId` | string | no | The bank general-ledger account this entry is recorded against. See id on GET accounting/accounts/:id. | | `items[].bankAccountName` | string | no | The name of the bank general-ledger account. | | `items[].entityId` | string | no | The legal entity this entry is posted under. See id on GET base/entities. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the bank entry last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | no | The posting lines of the entry, one per general-ledger posting. An empty array when the entry has none. | | `items[].lines[].id` | string | yes | The unique identifier for the line. | | `items[].lines[].lineNumber` | string | no | The line number. | | `items[].lines[].description` | string | no | A free-form description of the line. | | `items[].lines[].lineType` | string | no | The kind of line. Possible values: 'oneTime', 'prepaid'. | | `items[].lines[].amount` | string (decimal) | no | The gross line amount, including VAT, in the entry currency. | | `items[].lines[].currency` | string | no | The currency of the line. Format ISO 4217. It always matches the entry currency. | | `items[].lines[].netAmount` | string (decimal) | no | The line amount excluding VAT, in the entry currency. | | `items[].lines[].vatAmount` | string (decimal) | no | The VAT amount on the line, in the entry currency. | | `items[].lines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. For example, 19 means 19%. | | `items[].lines[].origin` | string | no | A label for the source system, copied onto the line from the entry. | | `items[].lines[].accountId` | string | no | The general-ledger account this line posts to. See id on GET accounting/accounts/:id. | | `items[].lines[].departmentId` | string | no | The department this line is charged to. See id on GET departments/:id. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor to pass as page.cursor for the next page. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Bank Rules `GET /banking/bank-rules` Group: Banking › Bank Rules · Operation ID: `bankRules` List bank rules with structured filters. GET data-set exposing filter[field][op]= (eq/ne/gt/gte/lt/lte/in/notIn/between/like), sort=-lastModifiedDate, sparse fields=, and page[size]/page[cursor] cursor pagination. Returns the header only; the criteria, watched bank accounts and action object come from GET banking/bank-rules/:id or POST banking/bank-rules/search. Every request must carry the Api-Version header. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bankRules) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `bankRuleNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `rule` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `action` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `priority` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, bankRuleNumber, rule, action, status, priority, lastModifiedDate - Selectable: id, bankRuleNumber, rule, action, status, priority, description, lastModifiedDate ## Response `200` Success Under `data`, an array of bankRulesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | Stable, platform-assigned id (a UUID) of the bank rule — the public identifier used by GET banking/bank-rules/:id. Filter e.g. filter[id][in]=<id1>,<id2>. | | `bankRuleNumber` | string or null | no | Human-readable document number assigned from the BankRule number range, e.g. BR-000014. Filter target: filter[bankRuleNumber][eq]=BR-000014. | | `rule` | string | no | Name of the rule, unique across the tenant. Filter target: filter[rule][like]=Stripe. | | `action` | string | no | What the rule does when a transaction matches (createBankEntry, createCreditCardEntry). Filter target: filter[action][eq]=createBankEntry. | | `status` | string | no | Whether the rule is in force (active, inactive). Only an active rule can be run. Filter target: filter[status][eq]=active. | | `priority` | number | no | Evaluation order relative to other rules; lower runs first. Filter target: filter[priority][lte]=10. | | `description` | string or null | no | Free-text note on what the rule is for. | | `lastModifiedDate` | string (date-time) | no | Instant the rule last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Poll on this to sync incrementally rather than re-reading the whole collection. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Bank Rule `POST /banking/bank-rules` Group: Banking › Bank Rules · Operation ID: `createBankRule` Create a bank rule, with its criteria, watched bank accounts and action object, and return its id. Returns only the id; read the record back with GET banking/bank-rules/:id for its stored state. Every request must carry the Api-Version header. Creates one bank rule and returns its `id` — and **only** its id. Read the record back with `GET banking/bank-rules/{id}` if you need its stored state; `bankRuleNumber` comes from a gapless number range and is visible only on that follow-up GET. A rule is created whole: the header, at least one criterion, at least one watched bank account, and the action object describing the entry to create. All four are required — there is no partial rule. **The watched accounts constrain the action.** Every account must belong to one legal entity (`ACCOUNTS_SPAN_ENTITIES`), and they must be either all credit card accounts or all not (`MIXED_ACCOUNT_TYPES`). `createCreditCardEntry` then works only with credit card accounts and `createBankEntry` only with the rest; a mismatch is `ACTION_ACCOUNT_TYPE_MISMATCH`. **Only two of the four rule actions are implemented.** `createFundTransfer` and `autoMatch` are rejected with `RULE_ACTION_NOT_SUPPORTED`, which is why the input is a two-member union rather than the whole enum. **Operators are constrained by the criterion.** Numeric operators apply only to `amount`, text operators only to `description` and `payeePayor`, and an `amount` value must parse as a number. Anything else is `INVALID_CRITERIA` with the offending criterion named. All guards run before the first write, so a rejected request creates nothing. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createBankRule) ## 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`: createBankRuleInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `rule` | string | yes | Name of the rule. Must be unique across the tenant; a clash is DUPLICATE_RULE_NAME. | | `action` | string, one of `"createBankEntry"`, `"createCreditCardEntry"` | yes | What the rule does when a transaction matches. Only these two are implemented; createFundTransfer and autoMatch are rejected with RULE_ACTION_NOT_SUPPORTED. | | `status` | string, one of `"active"`, `"inactive"` | yes | Whether the rule is in force. Only an active rule can be run; deactivate a rule rather than deleting it to keep its history. | | `priority` | number | yes | Evaluation order relative to other rules; lower runs first. | | `description` | string | no | Free-text note on what the rule is for. | | `bankAccountIds` | array of string | yes | [fk: BankAccountApi.bankAccounts.id] The bank accounts this rule watches, given as their ids (UUIDs). At least one is required. All must belong to one legal entity, and must be either all credit card accounts or all not. | | `criteria` | array of object | yes | The conditions a bank transaction must meet. At least one is required; they are combined by each criterion's logicalOperator. | | `criteria[].criteria` | string, one of `"amount"`, `"description"`, `"payeePayor"` | yes | Which field of the transaction is tested. | | `criteria[].operator` | string | yes | How the field is compared. Numeric operators (equal, notEqual, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual) apply only to 'amount'; text operators (is, isNot, contains, doesNotContain and their caseInsensitive variants) apply only to 'description' and 'payeePayor'. A mismatch is INVALID_CRITERIA. | | `criteria[].logicalOperator` | string, one of `"and"`, `"or"` | no | How this criterion joins the next. Defaults to 'and' when omitted. | | `criteria[].value` | string | yes | The value compared against. Must parse as a number when criteria is 'amount'; free text otherwise. Required and non-empty. | | `actionObject` | object | yes | The entry the rule creates when a transaction matches. Required and must carry at least one field. | | `actionObject.accountId` | string | no | [fk: AccountApi.accounts.id] Account the created entry line books to, given as its id (a UUID). | | `actionObject.name` | string | no | Name stamped on the created entry. | | `actionObject.description` | string | no | Description stamped on the created entry and its line. | | `actionObject.billableExpense` | string, one of `"yes"`, `"no"` | no | Whether the created credit card entry line is a billable expense. Customer applies only when this is 'yes'. | | `actionObject.businessUnitId` | string | no | [fk: BusinessUnitApi.businessUnits.id] Business unit stamped on the created line, given as its id (a UUID). | | `actionObject.departmentId` | string | no | [fk: DepartmentApi.departments.id] Department stamped on the created line, given as its id (a UUID). | | `actionObject.customerId` | string | no | [fk: CustomerApi.customers.id] Customer stamped on the created line, given as its id (a UUID). | | `actionObject.vendorId` | string | no | [fk: VendorApi.vendors.id] Vendor stamped on the created line, given as its id (a UUID). | | `actionObject.intercompanyPartnerId` | string | no | [fk: BusinessPartnerApi.businessPartners.id] Intercompany partner, given as its id (a UUID). Applied only when the booking account is paired for elimination; ignored otherwise, which is what the rule engine itself does. | | `actionObject.valueAllocationTags` | array of string | no | Value-allocation tag codes stamped on the created line. Applied only for dimensions still enabled on the document the rule creates. | Example: ```json { "rule": "Stripe payouts", "action": "createBankEntry", "status": "active", "priority": 10, "description": "Book Stripe payouts to the clearing account", "bankAccountIds": [ "8f2c1d40-5a6b-4c7d-9e80-1f2a3b4c5d6e" ], "criteria": [ { "criteria": "description", "operator": "containsCaseInsensitive", "logicalOperator": "and", "value": "STRIPE" } ], "actionObject": { "accountId": "6d1e4b27-8a90-4c31-b5f2-7e8a9c0d1b23", "name": "Stripe payout", "description": "Auto-created by rule" } } ``` ## Response `200` Success Under `data` (createBankRuleOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Platform id (a UUID) assigned to the newly created bank rule. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `ACCOUNTS_REQUIRED` | 400 | | `CRITERIA_REQUIRED` | 400 | | `ACTION_OBJECT_REQUIRED` | 400 | | `ACCOUNTS_SPAN_ENTITIES` | 400 | | `MIXED_ACCOUNT_TYPES` | 400 | | `ACTION_ACCOUNT_TYPE_MISMATCH` | 400 | | `RULE_ACTION_NOT_SUPPORTED` | 400 | | `DUPLICATE_RULE_NAME` | 400 | | `INVALID_CRITERIA` | 400 | | `ACCOUNT_NOT_POSTABLE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, ACCOUNTS_REQUIRED, CRITERIA_REQUIRED, ACTION_OBJECT_REQUIRED, ACCOUNTS_SPAN_ENTITIES, MIXED_ACCOUNT_TYPES, ACTION_ACCOUNT_TYPE_MISMATCH, RULE_ACTION_NOT_SUPPORTED, DUPLICATE_RULE_NAME, INVALID_CRITERIA, ACCOUNT_NOT_POSTABLE - `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 --- # Get Bank Rule `GET /banking/bank-rules/{id}` Group: Banking › Bank Rules · Operation ID: `getBankRule` Fetch a single bank rule by its id (a UUID), bound from the :id path segment. Returns the full record: the header plus its criteria, the bank accounts it watches and its action object — the same item schema as POST banking/bank-rules/search. Read-only. Raises NOT_FOUND when no rule has that id (should map to 404). Every request must carry the Api-Version header. Fetch one bank rule by its `id` (a UUID), bound from the `:id` path segment. Returns the whole rule: the header, every criterion a transaction must meet, the bank accounts the rule watches, and the action object describing the entry it creates. Same item schema as `POST banking/bank-rules/search`; both read through `core/bankRule/searchBankRule.ts`, so they cannot drift. `GET banking/bank-rules` is a data-set and returns the header only — a data-set cannot express a nested array. `status` is `active` or `inactive`. Only an active rule can be run; deactivate a rule with `PATCH banking/bank-rules/{id}` rather than deleting it. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getBankRule) ## 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`. | | `id` | path | string | yes | Id of the bank rule to fetch (a UUID); bound from the :id path segment. | ## Response `200` Success Under `data` (getBankRuleOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Id of the bank rule (a UUID) — the public identifier used by this route. | | `bankRuleNumber` | string | no | Human-readable document number assigned from the BankRule number range, e.g. BR-000014. | | `rule` | string | yes | Name of the rule. Unique across the tenant. | | `action` | string | yes | What the rule does when a transaction matches. One of 'createBankEntry', 'createCreditCardEntry'. | | `status` | string | yes | Whether the rule is in force. One of 'active', 'inactive'. Only an active rule can be run. | | `priority` | number | yes | Evaluation order relative to other rules; lower runs first. | | `description` | string | no | Free-text note on what the rule is for. | | `externalId` | string | no | External system identifier, when this rule was migrated from another system. | | `lastModifiedDate` | string (date-time) | yes | Instant the rule last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). Poll on this to sync incrementally rather than re-reading the whole collection. | | `criteria` | array of object | yes | The conditions a bank transaction must meet, combined by each criterion's logicalOperator. Every rule has at least one. | | `criteria[].id` | string | yes | Id of the criterion row (a UUID). | | `criteria[].criteria` | string | yes | Which field of the transaction is tested. One of 'amount', 'description', 'payeePayor'. | | `criteria[].operator` | string | yes | How the field is compared. Numeric operators ('equal', 'notEqual', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual') apply only to 'amount'; text operators ('is', 'isNot', 'contains', 'doesNotContain' and their caseInsensitive variants) apply only to 'description' and 'payeePayor'. | | `criteria[].logicalOperator` | string | no | How this criterion joins the next. One of 'and', 'or'. | | `criteria[].value` | string | yes | The value compared against. A decimal string for 'amount', free text otherwise. | | `accounts` | array of object | yes | The bank accounts this rule watches. Every rule has at least one, and all of them belong to one legal entity and are either all credit card accounts or all not. | | `accounts[].id` | string | yes | Id of the mapping row (a UUID). | | `accounts[].bankAccountId` | string | yes | [fk: BankAccountApi.bankAccounts.id] The watched bank account, given as its id (a UUID). | | `accounts[].accountName` | string | no | Name of the bank account, denormalised for convenience. | | `actionObject` | object | no | The entry the rule creates when a transaction matches. Present on every rule created through this api. | | `actionObject.id` | string | yes | Id of the action object (a UUID). | | `actionObject.accountId` | string | no | [fk: AccountApi.accounts.id] Account the created entry line books to, given as its id (a UUID). | | `actionObject.name` | string | no | Name stamped on the created entry. | | `actionObject.description` | string | no | Description stamped on the created entry and its line. | | `actionObject.billableExpense` | string | no | Whether the created credit card entry line is a billable expense. One of 'yes', 'no'. Customer applies only when this is 'yes'. | | `actionObject.businessUnitId` | string | no | [fk: BusinessUnitApi.businessUnits.id] Business unit stamped on the created line, given as its id (a UUID). | | `actionObject.departmentId` | string | no | [fk: DepartmentApi.departments.id] Department stamped on the created line, given as its id (a UUID). | | `actionObject.customerId` | string | no | [fk: CustomerApi.customers.id] Customer stamped on the created line, given as its id (a UUID). | | `actionObject.vendorId` | string | no | [fk: VendorApi.vendors.id] Vendor stamped on the created line, given as its id (a UUID). | | `actionObject.intercompanyPartnerId` | string | no | [fk: BusinessPartnerApi.businessPartners.id] Intercompany partner, given as its id (a UUID). Applied only when the booking account is paired for elimination; ignored otherwise. | | `actionObject.valueAllocationTags` | array of string | no | Value-allocation tag codes stamped on the created line. Applied only for dimensions still enabled on the document the rule creates. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Bank Rule `PATCH /banking/bank-rules/{id}` Group: Banking › Bank Rules · Operation ID: `updateBankRule` Update a bank rule, addressed by the :id path segment. Header fields patch; criteria, bankAccountIds and actionObject replace the whole stored collection when present. Returns an empty response. Every request must carry the Api-Version header. Updates one bank rule, addressed by the `id` in the path. The response is empty; read the record back with `GET banking/bank-rules/{id}` to see its stored state. **Header fields patch; collections replace.** An omitted `rule`, `action`, `status`, `priority` or `description` is left unchanged. But `criteria`, `bankAccountIds` and `actionObject` are **whole-collection replacements** when present — the underlying handler deletes and recreates them, so a criterion left out of the array is deleted. Omit the array entirely to keep what is stored. Deactivate a rule with `{"status": "inactive"}` rather than deleting it; that keeps its history and stops it being run. The same guards as create apply to whatever the patch carries: account/entity consistency, action/account agreement, criterion well-formedness and name uniqueness. All run before the first write, so a rejected request changes nothing. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateBankRule) ## 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. | | `id` | path | string | yes | Id of the bank rule to update (a UUID); bound from the :id path segment. | ## Request body Required, `application/json`: updateBankRuleInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `rule` | string | no | New name for the rule. Must stay unique across the tenant. Omit to leave unchanged. | | `action` | string, one of `"createBankEntry"`, `"createCreditCardEntry"` | no | New action. Only these two are implemented. Omit to leave unchanged. | | `status` | string, one of `"active"`, `"inactive"` | no | New status. Set 'inactive' to retire a rule rather than deleting it. Omit to leave unchanged. | | `priority` | number | no | New evaluation order; lower runs first. Omit to leave unchanged. | | `description` | string | no | New free-text note. Omit to leave unchanged. | | `bankAccountIds` | array of string | no | [fk: BankAccountApi.bankAccounts.id] REPLACES the whole set of watched bank accounts when present. At least one id, all belonging to one legal entity and consistently credit card or not. Omit to keep the stored set. | | `criteria` | array of object | no | REPLACES the whole set of criteria when present — a criterion left out of this array is deleted. At least one is required when the array is given. Omit to keep the stored set. | | `criteria[].criteria` | string, one of `"amount"`, `"description"`, `"payeePayor"` | yes | Which field of the transaction is tested. | | `criteria[].operator` | string | yes | How the field is compared. Numeric operators apply only to 'amount'; text operators only to 'description' and 'payeePayor'. | | `criteria[].logicalOperator` | string, one of `"and"`, `"or"` | no | How this criterion joins the next. | | `criteria[].value` | string | yes | The value compared against. Must parse as a number when criteria is 'amount'. | | `actionObject` | object | no | REPLACES the stored action object when present. Omit to keep it unchanged. | | `actionObject.accountId` | string | no | [fk: AccountApi.accounts.id] Account the created entry line books to, given as its id (a UUID). | | `actionObject.name` | string | no | Name stamped on the created entry. | | `actionObject.description` | string | no | Description stamped on the created entry and its line. | | `actionObject.billableExpense` | string, one of `"yes"`, `"no"` | no | Whether the created credit card entry line is a billable expense. | | `actionObject.businessUnitId` | string | no | [fk: BusinessUnitApi.businessUnits.id] Business unit stamped on the created line, given as its id (a UUID). | | `actionObject.departmentId` | string | no | [fk: DepartmentApi.departments.id] Department stamped on the created line, given as its id (a UUID). | | `actionObject.customerId` | string | no | [fk: CustomerApi.customers.id] Customer stamped on the created line, given as its id (a UUID). | | `actionObject.vendorId` | string | no | [fk: VendorApi.vendors.id] Vendor stamped on the created line, given as its id (a UUID). | | `actionObject.intercompanyPartnerId` | string | no | [fk: BusinessPartnerApi.businessPartners.id] Intercompany partner, given as its id (a UUID). | | `actionObject.valueAllocationTags` | array of string | no | Value-allocation tag codes stamped on the created line. | Example: ```json { "rule": "<rule>", "action": "createBankEntry", "status": "inactive", "priority": 20, "description": "<description>", "bankAccountIds": [ "<bankAccountIds>" ], "criteria": [ { "criteria": "amount", "operator": "<operator>", "logicalOperator": "and", "value": "<value>" } ], "actionObject": { "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "<name>", "description": "<description>", "billableExpense": "yes", "businessUnitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "departmentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "vendorId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "intercompanyPartnerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "valueAllocationTags": [ "<valueAllocationTags>" ] } } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `ACCOUNTS_REQUIRED` | 400 | | `CRITERIA_REQUIRED` | 400 | | `ACCOUNTS_SPAN_ENTITIES` | 400 | | `MIXED_ACCOUNT_TYPES` | 400 | | `ACTION_ACCOUNT_TYPE_MISMATCH` | 400 | | `RULE_ACTION_NOT_SUPPORTED` | 400 | | `DUPLICATE_RULE_NAME` | 400 | | `INVALID_CRITERIA` | 400 | | `ACCOUNT_NOT_POSTABLE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND, ACCOUNTS_REQUIRED, CRITERIA_REQUIRED, ACCOUNTS_SPAN_ENTITIES, MIXED_ACCOUNT_TYPES, ACTION_ACCOUNT_TYPE_MISMATCH, RULE_ACTION_NOT_SUPPORTED, DUPLICATE_RULE_NAME, INVALID_CRITERIA, ACCOUNT_NOT_POSTABLE - `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 --- # Delete Bank Rule `DELETE /banking/bank-rules/{id}` Group: Banking › Bank Rules · Operation ID: `deleteBankRule` Delete a bank rule and its criteria, bank account mappings and action object, addressed by the :id path segment. A hard delete; entries the rule already created are untouched. Prefer PATCH with status 'inactive' to retire a rule. Returns an empty response. Every request must carry the Api-Version header. Deletes one bank rule, addressed by the `id` in the path. The response is empty. **This is a hard delete and it cascades.** The rule's criteria, its bank account mappings and its action object go with it. Entries the rule already created, and the matches it already made, are untouched — they belong to the documents, not to the rule. **Prefer deactivating.** `PATCH banking/bank-rules/{id}` with `{"status": "inactive"}` stops the rule running while keeping it readable and re-activatable. Delete when the rule was a mistake, not when it has served its purpose. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteBankRule) ## 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. | | `id` | path | string | yes | Id of the bank rule to delete (a UUID); bound from the :id path segment. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Run Bank Rules `POST /banking/bank-rules/run` Group: Banking › Bank Rules · Operation ID: `runBankRules` Run every active bank rule in priority order and report the bank transactions they matched between them, then refresh match suggestions. Takes no input. A rule that cannot run is skipped and listed in skippedBankRules with the reason why, rather than failing the sweep. matchedCount 0 means every rule ran and nothing met its criteria, which is a success. Only one bank rule run executes at a time per tenant, across this route, the "Run bank rules" button, the periodic schedule and the matching dashboard; a request made while another run is in flight returns RUN_ALREADY_IN_PROGRESS and posts nothing — it does not queue, wait or partially run. Every request must carry the Api-Version header. Runs every **active** bank rule, in priority order, and reports what they matched between them. No rule id, no body — this is the sweep the "Run bank rules" button and the periodic action trigger. Each rule creates the entry its action object describes for every unmatched transaction on its watched accounts that meets its criteria, posts it, and auto-matches the transaction. Rules run lowest `priority` first, so an earlier rule claims a transaction a later one would also have matched. Afterwards match suggestions are refreshed, so transactions no rule claimed still get proposals. **A rule that cannot run is skipped, not fatal.** It appears in `skippedBankRules` with a `reason` and `message` and the sweep continues — a rule missing criteria or an action object, or whose every mapped bank account has been deleted or is not ready, would otherwise abort the whole run and roll back the entries earlier rules had already posted. This is a deliberate difference from the UI, which stops at the first such rule. `reason` is `NO_ACCOUNTS_MAPPED`, `RULE_INCOMPLETE` or `RUN_FAILED`; read the rule with `GET banking/bank-rules/{id}` to see the rest of its state. **`matchedCount: 0` is a success, not a failure** — every rule ran and nothing met its criteria. `matchedCount` is the true total; `matchedBankTransactionIds` is capped at 500, so a shorter array does not mean less work happened. There is no per-rule run route — the UI has no single-rule run either, and the sweep is the only supported trigger. To run one rule alone, deactivate the others or narrow its criteria. **One run at a time, tenant-wide.** Rules run serially and each re-queries the unmatched transactions, so two overlapping sweeps could both claim the same transaction and post two entries against it. Every trigger — this route, the "Run bank rules" button, the periodic schedule and the matching dashboard's account-scoped run — takes the same lock. A request that arrives while another run is in flight returns `RUN_ALREADY_IN_PROGRESS` and does nothing: no rule ran, nothing was posted, and retrying once the other run finishes is safe. It does not queue or wait, so poll rather than holding the request open. Note the status is **400, not 409** — read `diagnostics[].code` to tell this apart from a malformed request. [Open in the interactive reference](https://docs.api.lensing.ai/?op=runBankRules) ## 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. | ## Response `200` Success Under `data` (runBankRulesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `matchedCount` | number | yes | How many bank transactions this run matched across every active rule. Zero is a normal outcome. | | `matchedBankTransactionIds` | array of string | yes | Ids (UUIDs) of the bank transactions matched, capped at 500. matchedCount is the true total, so a shorter array does not mean less work happened. | | `skippedBankRules` | array of object | yes | Active rules that could not run and were skipped so one malformed rule does not abort the sweep. There is no per-rule run route, so each entry carries the reason: NO_ACCOUNTS_MAPPED, RULE_INCOMPLETE (missing criteria or not exactly one action object), or RUN_FAILED when the rule itself threw — its mapped bank accounts are all deleted or not ready. Read the rule with GET banking/bank-rules/:id to see what it needs. | | `skippedBankRules[].bankRuleId` | string | yes | Id (UUID) of the skipped bank rule. | | `skippedBankRules[].reason` | string | yes | Why it was skipped: NO_ACCOUNTS_MAPPED, RULE_INCOMPLETE or RUN_FAILED. | | `skippedBankRules[].message` | string | yes | Human-readable explanation naming the rule and what it is missing. | ## 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 | | --- | --- | | `RUN_ALREADY_IN_PROGRESS` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: RUN_ALREADY_IN_PROGRESS - `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 --- # Search Bank Rule `POST /banking/bank-rules/search` Group: Banking › Bank Rules · Operation ID: `searchBankRule` Search bank rules by id, name, action or status, and page through the matches. Read-only. Returns the full record for each match — the header plus its criteria, watched bank accounts and action object; matching nothing is an empty items array with HTTP 200, not a 404. Every request must carry the Api-Version header. Search bank rules by id, name, action or status, and page through the matches. Returns the full record for each match — the header plus its criteria, watched bank accounts and action object — the same item schema as `GET banking/bank-rules/{id}`. Matching nothing is an empty `items` array with HTTP 200, not a 404. All filters are ANDed; omitting every one of them matches every rule. Pagination is cursor-based: pass the previous response's `pagination.nextCursor` as `page.cursor`. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchBankRule) ## 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`. | ## Request body Required, `application/json`: searchBankRuleInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | Search filters, nested as one object so they map onto the POST body. All are optional and are ANDed; omitting every one of them matches all bank rules. Omit the whole object to do exactly that. | | `filters.id` | string | no | Exact match on a rule's id (a UUID). This is how a single known rule is fetched. | | `filters.rule` | string | no | Exact match on the rule's name. Names are unique, so this returns at most one match. | | `filters.action` | string | no | Restrict to rules with this action. One of 'createBankEntry', 'createCreditCardEntry'; any other value yields an empty page. | | `filters.status` | string | no | Restrict to rules of this status. One of 'active', 'inactive'; any other value yields an empty page. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | Opaque forward cursor from a previous response's pagination.nextCursor; omit for the first page. Do not construct manually. | | `page.size` | number | no | Maximum number of bank rules to return. Defaults to 50, capped at 200; the server may return fewer, but never more. | Example: ```json { "filters": { "status": "active" }, "page": { "size": 25 } } ``` ## Response `200` Success Under `data` (searchBankRuleOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | Matching bank rules for this page, newest first. Empty when nothing matches. | | `items[].id` | string | yes | Id of the bank rule (a UUID) — the public identifier of this record. | | `items[].bankRuleNumber` | string | no | Human-readable document number assigned from the BankRule number range, e.g. BR-000014. | | `items[].rule` | string | yes | Name of the rule. Unique across the tenant. | | `items[].action` | string | yes | What the rule does when a transaction matches. One of 'createBankEntry', 'createCreditCardEntry'. | | `items[].status` | string | yes | Whether the rule is in force. One of 'active', 'inactive'. Only an active rule can be run. | | `items[].priority` | number | yes | Evaluation order relative to other rules; lower runs first. | | `items[].description` | string | no | Free-text note on what the rule is for. | | `items[].externalId` | string | no | External system identifier, when this rule was migrated from another system. | | `items[].lastModifiedDate` | string (date-time) | yes | Instant the rule last changed, as ISO 8601 UTC (YYYY-MM-DDTHH:mm:ss.sssZ). | | `items[].criteria` | array of object | yes | The conditions a bank transaction must meet, combined by each criterion's logicalOperator. Every rule has at least one. | | `items[].criteria[].id` | string | yes | Id of the criterion row (a UUID). | | `items[].criteria[].criteria` | string | yes | Which field of the transaction is tested. One of 'amount', 'description', 'payeePayor'. | | `items[].criteria[].operator` | string | yes | How the field is compared. Numeric operators apply only to 'amount'; text operators only to 'description' and 'payeePayor'. | | `items[].criteria[].logicalOperator` | string | no | How this criterion joins the next. One of 'and', 'or'. | | `items[].criteria[].value` | string | yes | The value compared against. A decimal string for 'amount', free text otherwise. | | `items[].accounts` | array of object | yes | The bank accounts this rule watches. Every rule has at least one, all belonging to one legal entity. | | `items[].accounts[].id` | string | yes | Id of the mapping row (a UUID). | | `items[].accounts[].bankAccountId` | string | yes | [fk: BankAccountApi.bankAccounts.id] The watched bank account, given as its id (a UUID). | | `items[].accounts[].accountName` | string | no | Name of the bank account, denormalised for convenience. | | `items[].actionObject` | object | no | The entry the rule creates when a transaction matches. | | `items[].actionObject.id` | string | yes | Id of the action object (a UUID). | | `items[].actionObject.accountId` | string | no | [fk: AccountApi.accounts.id] Account the created entry line books to, given as its id (a UUID). | | `items[].actionObject.name` | string | no | Name stamped on the created entry. | | `items[].actionObject.description` | string | no | Description stamped on the created entry and its line. | | `items[].actionObject.billableExpense` | string | no | Whether the created credit card entry line is a billable expense. One of 'yes', 'no'. | | `items[].actionObject.businessUnitId` | string | no | [fk: BusinessUnitApi.businessUnits.id] Business unit stamped on the created line, given as its id (a UUID). | | `items[].actionObject.departmentId` | string | no | [fk: DepartmentApi.departments.id] Department stamped on the created line, given as its id (a UUID). | | `items[].actionObject.customerId` | string | no | [fk: CustomerApi.customers.id] Customer stamped on the created line, given as its id (a UUID). | | `items[].actionObject.vendorId` | string | no | [fk: VendorApi.vendors.id] Vendor stamped on the created line, given as its id (a UUID). | | `items[].actionObject.intercompanyPartnerId` | string | no | [fk: BusinessPartnerApi.businessPartners.id] Intercompany partner, given as its id (a UUID). Applied only when the booking account is paired for elimination. | | `items[].actionObject.valueAllocationTags` | array of string | no | Value-allocation tag codes stamped on the created line. | | `pagination` | object | yes | Cursor pagination for this result set. | | `pagination.nextCursor` | string | no | Opaque cursor to pass as page.cursor for the next page; absent on the last page (mirrors the list/v1 data-set pagination.nextCursor). | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page (mirrors the list/v1 data-set pagination.hasMore). | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Bank Transactions `GET /banking/bank-transactions` Group: Banking › Bank Transactions · Operation ID: `bankTransactions` Returns a list of bank transactions. To filter by bank account or legal entity, use POST banking/bank-transactions/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bankTransactions) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `bankTransactionNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `externalTransactionId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `externalPaymentId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `externalCreationDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `matchStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, bankTransactionNumber, externalTransactionId, externalCreationDate, amount, currency, matchStatus, lastModifiedDate - Selectable: id, bankTransactionNumber, externalTransactionId, externalPaymentId, endToEndId, externalCreationDate, description, externalDescription, additionalInformation, payeeOrPayor, amount, currency, matchStatus, notes, transactionPaymentMethod, accountName, sourceBankName, entityName, lastModifiedDate ## Response `200` Success Under `data`, an array of bankTransactionsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the bank transaction. | | `bankTransactionNumber` | string or null | no | The bank transaction number. Absent until one has been assigned. | | `externalTransactionId` | string | no | The identifier this transaction has in the bank or aggregator it was imported from. Unique across transactions. | | `externalPaymentId` | string or null | no | The end-to-end payment identifier supplied by the integration, where the bank provides one. | | `endToEndId` | string or null | no | A payment reference recovered from the external transaction data by the integration. | | `externalCreationDate` | string (date) | no | The date the transaction was created in the source bank, not the date it was imported. Format YYYY-MM-DD. | | `description` | string or null | no | The description recorded on the transaction. | | `externalDescription` | string or null | no | The description as the bank supplied it, unmodified. | | `additionalInformation` | string or null | no | Any additional detail the bank supplied with the transaction. | | `payeeOrPayor` | string or null | no | The name of the counterparty the money went to or came from. | | `amount` | string (decimal) or null | no | The transaction amount, as a decimal in the transaction currency. A negative amount is money leaving the account and a positive amount is money arriving. | | `currency` | string or null | no | The currency of the transaction. Format ISO 4217, for example 'USD'. It governs the amount. | | `matchStatus` | string or null | no | The reconciliation state of the transaction, computed from its matches. The states are 'unmatched', 'suggested', 'pending', 'matched', 'autoMatched' and 'needsAttention'. | | `notes` | string or null | no | Free-form notes recorded against the transaction. | | `transactionPaymentMethod` | string or null | no | How the payment was made, as reported by the integration. The values are 'ACH', 'WIRE' and 'Unknown'. | | `accountName` | string or null | no | The name of the linked chart-of-accounts account. Suffixed with '(Inactive)' when the account link is broken. | | `sourceBankName` | string or null | no | The name of the bank the account is held at. | | `entityName` | string or null | no | The name of the legal entity the owning bank account belongs to. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the bank transaction last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a bank transaction. `POST /banking/bank-transactions` Group: Banking › Bank Transactions · Operation ID: `createBankTransaction` The response contains only the new transaction's id. Read the record with GET banking/bank-transactions/:id. bankAccountId, amount, currency, externalCreationDate and externalTransactionId are required. externalTransactionId must be unique across the tenant and is how the source system's own identifier is stored. A repeat fails with DUPLICATE_EXTERNAL_TRANSACTION_ID, which names the existing transaction. amount is a decimal in the transaction currency. A negative amount is money leaving the account. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createBankTransaction) ## 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`: createBankTransactionInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `bankAccountId` | string | yes | The bank account the transaction belongs to. See id on GET banking/bank-accounts/:id. | | `amount` | string (decimal) | yes | The transaction amount, as a decimal in the transaction currency. A negative amount is money leaving the account. | | `currency` | string | yes | The currency of the transaction. Format ISO 4217, for example 'USD'. It governs the amount. | | `externalCreationDate` | string (date) | yes | The date the transaction was created in the source system. Use format YYYY-MM-DD. | | `externalTransactionId` | string | yes | The transaction's identifier in the system it was imported from. Must be unique across the tenant. | | `description` | string | no | A human-readable description of the transaction. | | `externalDescription` | string | no | The description exactly as the source system supplied it, before any normalization. | | `payeeOrPayor` | string | no | The name of the counterparty the money went to or came from. | | `externalId` | string | no | Another identifier the source system has for this record, distinct from externalTransactionId and not unique. | | `transactionPaymentMethod` | string | no | How the payment was made. The values are 'ACH', 'WIRE' and 'Unknown'. | | `notes` | string | no | Free-form notes for the transaction. | | `externalPaymentId` | string | no | The identifier of the payment in the source system this transaction settles. | Example: ```json { "bankAccountId": "9effa2ff-bc94-44bc-b706-a5f95f792407", "amount": "-1250.00", "currency": "USD", "externalCreationDate": "2026-02-14", "externalTransactionId": "ACME-TXN-88213", "description": "Consulting, February", "externalDescription": "<externalDescription>", "payeeOrPayor": "Acme Consulting LLC", "externalId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "transactionPaymentMethod": "ACH", "notes": "<notes>", "externalPaymentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` ## Response `200` Success Under `data` (createBankTransactionOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created bank transaction. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `INVALID_FIELD_VALUE` | 400 | | `DUPLICATE_EXTERNAL_TRANSACTION_ID` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, INVALID_FIELD_VALUE, DUPLICATE_EXTERNAL_TRANSACTION_ID - `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 --- # Get Bank Transaction `GET /banking/bank-transactions/{id}` Group: Banking › Bank Transactions · Operation ID: `getBankTransaction` Returns the bank transaction with the given identifier. Use the transaction's id, not its bankTransactionNumber. A bank transaction is a single movement of funds on a bank account. There is no direction field: a negative amount is money out and a positive amount is money in. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getBankTransaction) ## 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`. | | `id` | path | string | yes | The unique identifier for the bank transaction to return. | ## Response `200` Success Under `data` (getBankTransactionOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the bank transaction. | | `bankTransactionNumber` | string | no | The bank transaction number. Absent until one has been assigned. | | `externalTransactionId` | string | yes | The identifier this transaction has in the bank or aggregator it was imported from. Unique across transactions. | | `externalPaymentId` | string | no | The end-to-end payment identifier supplied by the integration, where the bank provides one. | | `endToEndId` | string | no | A payment reference recovered from the external transaction data by the integration. | | `externalCreationDate` | string (date) | yes | The date the transaction was created in the source bank, not the date it was imported. Format YYYY-MM-DD. | | `description` | string | no | The description recorded on the transaction. | | `externalDescription` | string | no | The description as the bank supplied it, unmodified. | | `additionalInformation` | string | no | Any additional detail the bank supplied with the transaction. | | `payeeOrPayor` | string | no | The name of the counterparty the money went to or came from. | | `amount` | string (decimal) | no | The transaction amount, as a decimal in the transaction currency. A negative amount is money leaving the account and a positive amount is money arriving. | | `currency` | string | no | The currency of the transaction. Format ISO 4217, for example 'USD'. It governs the amount. | | `matchStatus` | string | no | The reconciliation state of the transaction, computed from its matches. The states are 'unmatched', 'suggested', 'pending', 'matched', 'autoMatched' and 'needsAttention'. | | `matchId` | string | no | The unique identifier for the match this transaction belongs to. Absent when the transaction is unmatched. There is no endpoint for it. | | `notes` | string | no | Free-form notes recorded against the transaction. | | `transactionPaymentMethod` | string | no | How the payment was made, as reported by the integration. The values are 'ACH', 'WIRE' and 'Unknown'. | | `bankAccountId` | string | no | The bank account this transaction belongs to. See id on GET banking/bank-accounts/:id. | | `accountName` | string | no | The name of the linked chart-of-accounts account. Suffixed with '(Inactive)' when the account link is broken. | | `sourceBankName` | string | no | The name of the bank the account is held at. | | `entityName` | string | no | The name of the legal entity the owning bank account belongs to. | | `lastModifiedDate` | string (date-time) | yes | The date and time the bank transaction last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Bank Transaction `PATCH /banking/bank-transactions/{id}` Group: Banking › Bank Transactions · Operation ID: `updateBankTransaction` Updates a bank transaction's external creation date, its only editable field. A matched transaction is refused, so unmatch it first. externalCreationDate is the only editable field, and only while the transaction is unmatched. A bank transaction is the bank's record of what happened, so amount, currency, externalTransactionId, bankAccountId and the other fields cannot be changed, and sending any of them is rejected. To correct anything else, delete the transaction and re-import it. Omitting externalCreationDate leaves the stored date unchanged. The response is empty. Read the result with GET banking/bank-transactions/:id. A matched, auto-matched, pending or needs-attention transaction is refused with TRANSACTION_MATCHED. Unmatch it first. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateBankTransaction) ## 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. | | `id` | path | string | yes | The unique identifier for the bank transaction to update. | ## Request body Required, `application/json`: updateBankTransactionInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `externalCreationDate` | string (date) | no | The new date the transaction was created in the source system. Use format YYYY-MM-DD. Omit to leave it unchanged. | Example: ```json { "externalCreationDate": "2026-02-17" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `TRANSACTION_MATCHED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, TRANSACTION_MATCHED - `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 --- # Delete Bank Transaction `DELETE /banking/bank-transactions/{id}` Group: Banking › Bank Transactions · Operation ID: `deleteBankTransaction` Permanently deletes a bank transaction. A matched transaction is refused, so unmatch it first. This delete is permanent and cannot be undone through this API. Unlike DELETE banking/bank-accounts/:id, which is a recoverable soft delete, the transaction is removed outright. The response is empty. A transaction that is matched, auto-matched, pending or needs-attention is refused with TRANSACTION_MATCHED rather than deleted. Unmatch it first. For a transaction whose bank account came from an integration, the delete also records that the transaction was removed, so the bank feed does not re-create it on the next sync. A manually created account has no such guard, and the row is deleted with nothing to prevent re-import. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteBankTransaction) ## 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. | | `id` | path | string | yes | The unique identifier for the bank transaction to delete. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `TRANSACTION_MATCHED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, TRANSACTION_MATCHED - `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 --- # Unmatch Bank Transaction `POST /banking/bank-transactions/{id}/unmatch` Group: Banking › Bank Transactions · Operation ID: `unmatchBankTransaction` Detach the match on a bank transaction, addressed by the :id path segment. Dissolves the whole match, returning every transaction in it to unmatched; the documents are returned to the unmatched pool rather than deleted. Returns an empty response. Every request must carry the Api-Version header. Detaches the match on one bank transaction, identified by the `id` in the path. The response is empty. **This unmatches the whole match, not just this transaction.** A match can span several transactions and several documents; unmatching any one of them dissolves the match and returns every transaction in it to unmatched. **A fund transfer is the exception**, because its legs are matched per bank account and so hold one match each. Unmatching one leg leaves the other matched; call this once per leg. **A bank or credit card entry created for the match must be voided first.** Such an entry exists only because of the match — typically one a bank rule created — so detaching it would leave an orphan. That is `ENTRY_MUST_BE_VOIDED`. An entry that came from an integration carries an `origin` and is exempt. The documents themselves are untouched: unmatching returns them to the unmatched pool, it does not delete or reverse them. [Open in the interactive reference](https://docs.api.lensing.ai/?op=unmatchBankTransaction) ## 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. | | `id` | path | string | yes | Id of the bank transaction to unmatch (a UUID); bound from the :id path segment. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `NOT_MATCHED` | 400 | | `ENTRY_MUST_BE_VOIDED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, NOT_MATCHED, ENTRY_MUST_BE_VOIDED - `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 --- # Match Bank Transactions `POST /banking/bank-transactions/match` Group: Banking › Bank Transactions · Operation ID: `matchBankTransactions` Match one or more bank transactions against one or more documents named by their sfdType and sfdId pair, reconciling them. The document amounts must total the bank transaction amounts exactly. A fund transfer is the one case whose transactions may sit on different bank accounts, and it yields one match per leg. Runs in one transaction: a refusal reconciles nothing. Every request must carry the Api-Version header. Matches bank transactions against documents and reconciles them. Both sides are arrays, so this covers 1:1, 1:N, M:1 and M:N in one route — there is no separate bulk form and no transaction in the path. **A document is named by the `sfdType` + `sfdId` pair, and both are required.** `sfdId` is only unique WITHIN a type — a bank entry and an outbound payment can carry the same id — so the pair is the key. `sfdId` is the document's own id (a UUID). **The amounts must balance.** The documents must total exactly what the bank transactions total, otherwise `AMOUNT_MISMATCH` names both figures. Send several documents when one does not cover the transactions, and several `bankTransactionIds` to reconcile a group against one document set. A repeated id is counted once. **Every transaction must share one bank account and legal entity**, except for a fund transfer (below) — otherwise `TRANSACTIONS_SPAN_ACCOUNTS`. **A fund transfer is the one cross-account case.** Send the sending and receiving transactions together with the single `fundTransfer` document and nothing else. Because its legs sit on different accounts, this produces **one match per leg**, so `matches` carries two entries rather than one; the legs may even belong to different legal entities. A fund transfer cannot be mixed with other documents (`FUND_TRANSFER_MUST_BE_ALONE`) and covers at most two transactions (`TOO_MANY_FUND_TRANSFER_TRANSACTIONS`). `journalEntryLineId` narrows to a single line of a document that reconciles line by line, such as an outbound payment or a manual journal entry. Bank and credit card entries always match as a whole document, so a line given for those is ignored. Matching is reversible — `POST banking/bank-transactions/{id}/unmatch` on any member detaches the whole match. Everything runs in one transaction: a refusal reconciles nothing. [Open in the interactive reference](https://docs.api.lensing.ai/?op=matchBankTransactions) ## 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`: matchBankTransactionsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `bankTransactionIds` | array of string | yes | Ids (UUIDs) of the bank transactions to reconcile. At least one is required. Every transaction must belong to the same bank account and legal entity, except when the document set is a single fund transfer, whose two legs sit on different accounts. A repeated id is counted once. | | `documents` | array of object | yes | The documents to reconcile the transactions against. At least one is required, and their amounts must total the transaction amounts exactly. A 'fundTransfer' must be the only entry. | | `documents[].sfdType` | string, one of `"outboundPayment"`, `"customerPayment"`, `"manualJournalEntry"`, `"bankEntryNew"`, `"creditCardEntry"`, `"fundTransfer"`, `"payroll"` | yes | Which kind of document this is. Both sfdType and sfdId are required: sfdId is only unique within a type, so the pair is the key. 'netZeroImpact' cannot be named by a caller — it is an internal construct the suggestion engine mints. | | `documents[].sfdId` | string | yes | Id of the document itself (a UUID) — the outbound payment, journal entry, bank entry, credit card entry, fund transfer or payroll detail. | | `documents[].journalEntryLineId` | string | no | [fk: JournalEntryApi.journalEntryLines.id] Narrows to a single line of a document that reconciles line by line, such as an outbound payment or a manual journal entry, given as the line's id (a UUID). Bank and credit card entries always match as a whole document and ignore this. | Example: ```json { "bankTransactionIds": [ "9f2c6a10-7d34-4c8e-9b21-5a0f3e6d81b7" ], "documents": [ { "sfdType": "bankEntryNew", "sfdId": "c41d0b58-2e97-4f6a-8d13-7b5e9c204af6" } ] } ``` ## Response `200` Success Under `data` (matchBankTransactionsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `matches` | array of object | yes | The match records created. Ordinarily one; a fund transfer yields one per leg, because its legs sit on different bank accounts and cannot share a match. | | `matches[].matchId` | string | yes | Id of the match record (a UUID). | | `matches[].status` | string | yes | Status the match settled at. One of 'matched', 'autoMatched', 'pending' — 'pending' when a document has no posted journal entry line yet. | | `matches[].bankTransactionIds` | array of string | yes | Ids (UUIDs) of the bank transactions this particular match covers. | | `matchedBankTransactionIds` | array of string | yes | Ids (UUIDs) of every bank transaction reconciled, in the order supplied, with repeats collapsed. | | `matchedDocuments` | array of object | yes | The documents that were matched, echoed with the amount each contributed. | | `matchedDocuments[].sfdType` | string | yes | Kind of the document, as supplied. | | `matchedDocuments[].sfdId` | string | yes | Id of the document (a UUID), as supplied. | | `matchedDocuments[].amount` | string | yes | Amount this document contributed to the match, as a bare decimal. | | `matchedDocuments[].currency` | string | yes | Currency of the amount, as an ISO 4217 code. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `ALREADY_MATCHED` | 400 | | `NO_DOCUMENTS` | 400 | | `NO_TRANSACTIONS` | 400 | | `INVALID_DOCUMENT_REFERENCE` | 400 | | `DOCUMENT_NOT_MATCHABLE` | 400 | | `MULTIPLE_FUND_TRANSFERS` | 400 | | `FUND_TRANSFER_MUST_BE_ALONE` | 400 | | `TOO_MANY_FUND_TRANSFER_TRANSACTIONS` | 400 | | `ACCOUNT_NOT_IN_RECONCILIATION_BOOK` | 400 | | `AMOUNT_MISMATCH` | 400 | | `TRANSACTIONS_SPAN_ACCOUNTS` | 400 | | `CURRENCY_MISMATCH` | 400 | | `BEFORE_RECONCILIATION_START_DATE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, ALREADY_MATCHED, NO_DOCUMENTS, NO_TRANSACTIONS, INVALID_DOCUMENT_REFERENCE, DOCUMENT_NOT_MATCHABLE, MULTIPLE_FUND_TRANSFERS, FUND_TRANSFER_MUST_BE_ALONE, TOO_MANY_FUND_TRANSFER_TRANSACTIONS, ACCOUNT_NOT_IN_RECONCILIATION_BOOK, AMOUNT_MISMATCH, TRANSACTIONS_SPAN_ACCOUNTS, CURRENCY_MISMATCH, BEFORE_RECONCILIATION_START_DATE - `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 --- # Returns the bank transactions that match the given filters. `POST /banking/bank-transactions/search` Group: Banking › Bank Transactions · Operation ID: `searchBankTransaction` Every filter is optional and they are combined with AND. Omit them all to return every bank transaction. Each match is the full transaction record. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchBankTransaction) ## 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`. | ## Request body Required, `application/json`: searchBankTransactionInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A bank transaction must match every filter you send. Omit to return all bank transactions. | | `filters.id` | string | no | Limits results to the bank transaction with this identifier. | | `filters.bankTransactionNumber` | string | no | Limits results to the bank transaction with this number. The match is exact. | | `filters.externalTransactionId` | string | no | Limits results to the transaction with this external identifier. The match is exact. | | `filters.externalPaymentId` | string | no | Limits results to transactions with this end-to-end payment identifier. The match is exact. | | `filters.bankAccountId` | string | no | Limits results to transactions on this bank account. See id on GET banking/bank-accounts/:id. | | `filters.entityId` | string | no | Limits results to transactions whose bank account belongs to this legal entity. See id on GET base/entities. | | `filters.currency` | string | no | Limits results to transactions in this currency. Format ISO 4217, for example 'USD'. | | `filters.matchStatus` | string | no | Limits results to transactions in this reconciliation state. The states are 'unmatched', 'suggested', 'pending', 'matched', 'autoMatched' and 'needsAttention'. Any other value matches nothing. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of bank transactions per page, between 1 and 200. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "matchStatus": "unmatched" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchBankTransactionOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The bank transactions on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the bank transaction. | | `items[].bankTransactionNumber` | string | no | The bank transaction number. Absent until one has been assigned. | | `items[].externalTransactionId` | string | yes | The identifier this transaction has in the bank or aggregator it was imported from. Unique across transactions. | | `items[].externalPaymentId` | string | no | The end-to-end payment identifier supplied by the integration, where the bank provides one. | | `items[].endToEndId` | string | no | A payment reference recovered from the external transaction data by the integration. | | `items[].externalCreationDate` | string (date) | yes | The date the transaction was created in the source bank, not the date it was imported. Format YYYY-MM-DD. | | `items[].description` | string | no | The description recorded on the transaction. | | `items[].externalDescription` | string | no | The description as the bank supplied it, unmodified. | | `items[].additionalInformation` | string | no | Any additional detail the bank supplied with the transaction. | | `items[].payeeOrPayor` | string | no | The name of the counterparty the money went to or came from. | | `items[].amount` | string (decimal) | no | The transaction amount, as a decimal in the transaction currency. A negative amount is money leaving the account and a positive amount is money arriving. | | `items[].currency` | string | no | The currency of the transaction. Format ISO 4217, for example 'USD'. It governs the amount. | | `items[].matchStatus` | string | no | The reconciliation state of the transaction, computed from its matches. The states are 'unmatched', 'suggested', 'pending', 'matched', 'autoMatched' and 'needsAttention'. | | `items[].matchId` | string | no | The unique identifier for the match this transaction belongs to. Absent when the transaction is unmatched. There is no endpoint for it. | | `items[].notes` | string | no | Free-form notes recorded against the transaction. | | `items[].transactionPaymentMethod` | string | no | How the payment was made, as reported by the integration. The values are 'ACH', 'WIRE' and 'Unknown'. | | `items[].bankAccountId` | string | no | The bank account this transaction belongs to. See id on GET banking/bank-accounts/:id. | | `items[].accountName` | string | no | The name of the linked chart-of-accounts account. Suffixed with '(Inactive)' when the account link is broken. | | `items[].sourceBankName` | string | no | The name of the bank the account is held at. | | `items[].entityName` | string | no | The name of the legal entity the owning bank account belongs to. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the bank transaction last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Credit Card Entries `GET /banking/credit-card-entries` Group: Banking › Credit Card Entries · Operation ID: `creditCardEntries` Returns a list of credit card entries. Each row is the entry header without its posting lines. To read the lines use GET banking/credit-card-entries/:id, and to match on the credit card account use POST banking/credit-card-entries/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=creditCardEntries) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `creditCardEntryNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `transactionDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `serviceDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `journalEntryNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `origin` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, creditCardEntryNumber, transactionDate, serviceDate, currency, status, journalEntryNumber, origin, lastModifiedDate - Selectable: id, creditCardEntryNumber, name, description, transactionDate, serviceDate, currency, status, journalEntryNumber, origin, creditCardAccountName, lastModifiedDate ## Response `200` Success Under `data`, an array of creditCardEntriesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the credit card entry. | | `creditCardEntryNumber` | string or null | no | The credit card entry number. Absent until one has been assigned. | | `name` | string or null | no | The display name of the credit card entry. | | `description` | string or null | no | A free-form description of the entry. | | `transactionDate` | string (date) or null | no | The date of the transaction this entry records. Format YYYY-MM-DD. | | `serviceDate` | string (date) or null | no | The date the goods or services were delivered. Format YYYY-MM-DD. | | `currency` | string or null | no | The currency the entry is denominated in. Format ISO 4217, for example 'USD'. It governs every line amount. | | `status` | string or null | no | The reconciliation status of the entry. Possible values: 'Matched', 'Unmatched', 'Posted', 'Voided', 'Integrated'. | | `journalEntryNumber` | string or null | no | The number of the journal entry this entry posted to, when it has been posted. | | `origin` | string or null | no | A label for the source system the entry was created from. | | `creditCardAccountName` | string or null | no | The name of the credit card general-ledger account. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the credit card entry last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a credit card entry with its lines. `POST /banking/credit-card-entries` Group: Banking › Credit Card Entries · Operation ID: `createCreditCardEntry` The response contains only the new entry's id. Read the record with GET banking/credit-card-entries/:id. entityId, creditCardAccountId, transactionDate and at least one line are required. Every line amount is a decimal in the entry currency. No line may be zero, and the lines may not sum to zero. When currency is omitted the account's own currency is used. When supplied it must match the account's, or the call fails with CURRENCY_MISMATCH. The posting period is resolved across every book the entity posts to. A transaction date whose period is closed, or open in some books and closed in others, fails with POSTING_PERIOD_NOT_OPEN. attachments names already-uploaded files. On a tenant that requires an attachment, omitting them fails with ATTACHMENT_REQUIRED. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createCreditCardEntry) ## 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`: createCreditCardEntryInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `entityId` | string | yes | The legal entity the entry is posted under. See id on GET base/entities. | | `creditCardAccountId` | string | yes | The general-ledger account behind the credit card account this entry posts against. See id on GET accounting/accounts/:id. | | `transactionDate` | string (date) | yes | The date the transaction took place. Use format YYYY-MM-DD. It decides the posting period. | | `currency` | string | no | The currency of the entry. Format ISO 4217, for example 'USD'. It governs every amount on the entry. Omit to use the account's own currency. When supplied it must match the account's currency. | | `status` | string | no | The reconciliation status. Accepted values: 'Matched', 'Unmatched', 'Posted', 'Voided', 'Integrated'. Defaults to 'Unmatched'. | | `name` | string | no | A short name for the entry. | | `description` | string | no | A free-form description of the entry. | | `serviceDate` | string (date) | no | The date the goods or services were delivered. Use format YYYY-MM-DD. | | `origin` | string | no | The system or process the entry originated from. | | `externalUrl` | string | no | A link to the entry in the originating system. | | `lines` | array of object | yes | The lines of the entry, in the order they appear. At least one is required. No line may be zero, and the lines may not sum to zero. | | `lines[].accountId` | string | yes | The general-ledger account this line posts to. It must not be a grouping account. See id on GET accounting/accounts/:id. | | `lines[].amount` | string (decimal) | yes | The line amount, as a decimal in the entry currency. It must be non-zero. | | `lines[].description` | string | no | A free-form description of the line. | | `lines[].departmentId` | string | no | The department this line is attributed to. See id on GET departments/:id. | | `lines[].businessUnitId` | string | no | The business unit this line is attributed to. There is no endpoint for it. | | `lines[].businessPartnerId` | string | no | The business partner this line is attributed to. Required when vatPercentage is supplied. There is no endpoint for it. | | `lines[].lineType` | string | no | The kind of line. Accepted values: 'oneTime', 'prepaid'. Derived from the account's sub-type when omitted. | | `lines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. For example, 19 means 19%. Supplying it requires businessPartnerId. | | `lines[].vatAccountId` | string | no | The general-ledger account the VAT posts to. See id on GET accounting/accounts/:id. | | `lines[].reverseVatAccountId` | string | no | The general-ledger account the reverse-charge VAT posts to. See id on GET accounting/accounts/:id. | | `lines[].vatAmount` | string (decimal) | no | The VAT amount, as a decimal in the entry currency. Omit to derive it from vatPercentage. | | `lines[].netAmount` | string (decimal) | no | The net amount, as a decimal in the entry currency. Omit to derive it from amount less VAT. | | `attachments` | array of object | no | Files to attach to the entry, each naming an already-uploaded file by its id. Required on a tenant configured to require an attachment. | | `attachments[].fileName` | string | yes | The display name of the attached file. | | `attachments[].fileId` | string | yes | The identifier of the already-uploaded file. | Example: ```json { "entityId": "e8dc29b9-7e79-4709-bb32-95938bcb1bd1", "creditCardAccountId": "f50164c3-85e3-4dab-986e-3fdb39f8ecc1", "transactionDate": "2026-02-14", "currency": "USD", "status": "unmatched", "name": "February bank fees", "description": "<description>", "serviceDate": "2026-01-15", "origin": "<origin>", "externalUrl": "<externalUrl>", "lines": [ { "accountId": "9994c3ed-f812-44c1-9454-7f36b2bfb0fb", "amount": "-125.00", "description": "Account maintenance fee", "lineType": "oneTime" } ], "attachments": [ { "fileName": "<fileName>", "fileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ] } ``` ## Response `200` Success Under `data` (createCreditCardEntryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created credit card entry. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `INVALID_FIELD_VALUE` | 400 | | `POSTING_PERIOD_NOT_OPEN` | 400 | | `CURRENCY_MISMATCH` | 400 | | `ACCOUNT_ENTITY_MISMATCH` | 400 | | `GROUPING_ACCOUNT_NOT_ALLOWED` | 400 | | `BUSINESS_PARTNER_REQUIRED_FOR_VAT` | 400 | | `NO_LINES` | 400 | | `ZERO_LINE_AMOUNT` | 400 | | `ZERO_TOTAL_AMOUNT` | 400 | | `ATTACHMENT_REQUIRED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, INVALID_FIELD_VALUE, POSTING_PERIOD_NOT_OPEN, CURRENCY_MISMATCH, ACCOUNT_ENTITY_MISMATCH, GROUPING_ACCOUNT_NOT_ALLOWED, BUSINESS_PARTNER_REQUIRED_FOR_VAT, NO_LINES, ZERO_LINE_AMOUNT, ZERO_TOTAL_AMOUNT, ATTACHMENT_REQUIRED - `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 --- # Get Credit Card Entry `GET /banking/credit-card-entries/{id}` Group: Banking › Credit Card Entries · Operation ID: `getCreditCardEntry` Returns the credit card entry with the given identifier. Use the credit card entry's id, not its creditCardEntryNumber. A credit card entry records a charge or credit posted against a credit card account. Its lines record the general-ledger postings, as decimals in the entry currency. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getCreditCardEntry) ## 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`. | | `id` | path | string | yes | The unique identifier for the credit card entry to return. | ## Response `200` Success Under `data` (getCreditCardEntryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the credit card entry. | | `creditCardEntryNumber` | string | no | The credit card entry number. Absent until one has been assigned. | | `name` | string | no | The display name of the credit card entry. | | `description` | string | no | A free-form description of the entry. | | `transactionDate` | string (date) | yes | The date of the transaction this entry records. Format YYYY-MM-DD. | | `serviceDate` | string (date) | no | The date the goods or services were delivered. Format YYYY-MM-DD. Defaults to the transaction date where the entity's country requires a service date. | | `currency` | string | no | The currency the entry is denominated in. Format ISO 4217, for example 'USD'. It governs every line amount. | | `status` | string | yes | The reconciliation status of the entry. Possible values: 'Matched', 'Unmatched', 'Posted', 'Voided', 'Integrated'. | | `journalEntryNumber` | string | no | The number of the journal entry this entry posted to, when it has been posted. | | `origin` | string | no | A label for the source system the entry was created from. | | `externalUrl` | string | no | A link to the source document in an external system. | | `creditCardAccountId` | string | no | The credit card general-ledger account this entry is recorded against. See id on GET accounting/accounts/:id. | | `creditCardAccountName` | string | no | The name of the credit card general-ledger account. | | `entityId` | string | no | The legal entity this entry is posted under. See id on GET base/entities. | | `lastModifiedDate` | string (date-time) | yes | The date and time the credit card entry last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | no | The posting lines of the entry, one per general-ledger posting. An empty array when the entry has none. | | `lines[].id` | string | yes | The unique identifier for the line. | | `lines[].lineNumber` | string | no | The line number. | | `lines[].description` | string | no | A free-form description of the line. | | `lines[].lineType` | string | no | The kind of line. Possible values: 'oneTime', 'prepaid'. | | `lines[].amount` | string (decimal) | no | The gross line amount, including VAT, in the entry currency. | | `lines[].currency` | string | no | The currency of the line. Format ISO 4217. It always matches the entry currency. | | `lines[].netAmount` | string (decimal) | no | The line amount excluding VAT, in the entry currency. | | `lines[].vatAmount` | string (decimal) | no | The VAT amount on the line, in the entry currency. | | `lines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. For example, 19 means 19%. | | `lines[].origin` | string | no | A label for the source system, copied onto the line from the entry. | | `lines[].accountId` | string | no | The general-ledger account this line posts to. See id on GET accounting/accounts/:id. | | `lines[].departmentId` | string | no | The department this line is charged to. See id on GET departments/:id. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Credit Card Entry `PATCH /banking/credit-card-entries/{id}` Group: Banking › Credit Card Entries · Operation ID: `updateCreditCardEntry` Updates the credit card entry with the given identifier. Only the fields in the request body change. Use the credit card entry's id, not its creditCardEntryNumber. Only the fields in the request body change, and the response is empty. Read the result with GET banking/credit-card-entries/:id. Lines are edited through three arrays: createLines, updateLines and deleteLines. They are applied in that order, deletes first, then updates, then creates. Omit an array for no operations of that kind, and omit all three to leave the lines unchanged. Lines you do not name are kept. Every line id in updateLines or deleteLines must belong to this entry. One that does not fails with LINE_NOT_FOUND and nothing is written. The resulting lines are still subject to the whole-entry rules: at least one line, no line of zero, and a non-zero total. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateCreditCardEntry) ## 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. | | `id` | path | string | yes | The unique identifier for the credit card entry to update. | ## Request body Required, `application/json`: updateCreditCardEntryInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `entityId` | string | no | The legal entity the entry is posted under. See id on GET base/entities. | | `creditCardAccountId` | string | no | The general-ledger account behind the credit card account this entry posts against. See id on GET accounting/accounts/:id. | | `transactionDate` | string (date) | no | The date the transaction took place. Use format YYYY-MM-DD. Changing it re-resolves the posting period. | | `currency` | string | no | The currency of the entry. Format ISO 4217, for example 'USD'. When supplied it must match the account's currency. | | `status` | string | no | The reconciliation status. Accepted values: 'Matched', 'Unmatched', 'Posted', 'Voided', 'Integrated'. | | `name` | string | no | A short name for the entry. | | `description` | string | no | A free-form description of the entry. | | `serviceDate` | string (date) | no | The date the goods or services were delivered. Use format YYYY-MM-DD. | | `origin` | string | no | The system or process the entry originated from. | | `externalUrl` | string | no | A link to the entry in the originating system. | | `createLines` | array of object | no | Lines to add. Each has the same shape as a line on create, with no id because the line does not exist yet. | | `createLines[].accountId` | string | yes | The general-ledger account this line posts to. It must not be a grouping account. See id on GET accounting/accounts/:id. | | `createLines[].amount` | string (decimal) | yes | The line amount, as a decimal in the entry currency. It must be non-zero. | | `createLines[].description` | string | no | A free-form description of the line. | | `createLines[].departmentId` | string | no | The department this line is attributed to. See id on GET departments/:id. | | `createLines[].businessUnitId` | string | no | The business unit this line is attributed to. There is no endpoint for it. | | `createLines[].businessPartnerId` | string | no | The business partner this line is attributed to. Required when vatPercentage is supplied. There is no endpoint for it. | | `createLines[].lineType` | string | no | The kind of line. Accepted values: 'oneTime', 'prepaid'. Derived from the account's sub-type when omitted. | | `createLines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. For example, 19 means 19%. Supplying it requires businessPartnerId. | | `createLines[].vatAccountId` | string | no | The general-ledger account the VAT posts to. See id on GET accounting/accounts/:id. | | `createLines[].reverseVatAccountId` | string | no | The general-ledger account the reverse-charge VAT posts to. See id on GET accounting/accounts/:id. | | `createLines[].vatAmount` | string (decimal) | no | The VAT amount, as a decimal in the entry currency. Omit to derive it from vatPercentage. | | `createLines[].netAmount` | string (decimal) | no | The net amount, as a decimal in the entry currency. Omit to derive it from amount less VAT. | | `updateLines` | array of object | no | Lines to change, each addressed by its id. Every field but the id is optional, and omitting one leaves that field unchanged. | | `updateLines[].id` | string | yes | The unique identifier for the line to change. It must belong to this credit card entry. | | `updateLines[].accountId` | string | no | The general-ledger account this line posts to. It must not be a grouping account. See id on GET accounting/accounts/:id. | | `updateLines[].amount` | string (decimal) | no | The line amount, as a decimal in the entry currency. It must be non-zero. | | `updateLines[].description` | string | no | A free-form description of the line. | | `updateLines[].departmentId` | string | no | The department this line is attributed to. See id on GET departments/:id. | | `updateLines[].businessUnitId` | string | no | The business unit this line is attributed to. There is no endpoint for it. | | `updateLines[].businessPartnerId` | string | no | The business partner this line is attributed to. Required when vatPercentage is supplied. There is no endpoint for it. | | `updateLines[].lineType` | string | no | The kind of line. Accepted values: 'oneTime', 'prepaid'. Derived from the account's sub-type when omitted. | | `updateLines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. For example, 19 means 19%. Supplying it requires businessPartnerId. | | `updateLines[].vatAccountId` | string | no | The general-ledger account the VAT posts to. See id on GET accounting/accounts/:id. | | `updateLines[].reverseVatAccountId` | string | no | The general-ledger account the reverse-charge VAT posts to. See id on GET accounting/accounts/:id. | | `updateLines[].vatAmount` | string (decimal) | no | The VAT amount, as a decimal in the entry currency. Omit to derive it from vatPercentage. | | `updateLines[].netAmount` | string (decimal) | no | The net amount, as a decimal in the entry currency. Omit to derive it from amount less VAT. | | `deleteLines` | array of string | no | The identifiers of lines to remove. Each must belong to this credit card entry. | Example: ```json { "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "creditCardAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "transactionDate": "2026-01-15", "currency": "<currency>", "status": "<status>", "name": "February bank fees — revised", "description": "<description>", "serviceDate": "2026-01-15", "origin": "<origin>", "externalUrl": "<externalUrl>", "createLines": [ { "accountId": "9994c3ed-f812-44c1-9454-7f36b2bfb0fb", "amount": "-25.00", "description": "Wire fee" } ], "updateLines": [ { "id": "7a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9", "amount": "-150.00" } ], "deleteLines": [ "1f2e3d4c-5b6a-7089-9a8b-7c6d5e4f3a2b" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `LINE_NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `INVALID_FIELD_VALUE` | 400 | | `POSTING_PERIOD_NOT_OPEN` | 400 | | `CURRENCY_MISMATCH` | 400 | | `ACCOUNT_ENTITY_MISMATCH` | 400 | | `GROUPING_ACCOUNT_NOT_ALLOWED` | 400 | | `BUSINESS_PARTNER_REQUIRED_FOR_VAT` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, LINE_NOT_FOUND, REFERENCE_NOT_FOUND, INVALID_FIELD_VALUE, POSTING_PERIOD_NOT_OPEN, CURRENCY_MISMATCH, ACCOUNT_ENTITY_MISMATCH, GROUPING_ACCOUNT_NOT_ALLOWED, BUSINESS_PARTNER_REQUIRED_FOR_VAT - `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 --- # Void Credit Card Entry `POST /banking/credit-card-entries/{id}/void` Group: Banking › Credit Card Entries · Operation ID: `voidCreditCardEntry` Voids the credit card entry with the given identifier. Voiding cannot be undone. Voids the credit card entry and the journal entry behind it. Voiding is not deleting: the entry, its number and the audit trail all stay, and the ledger effect is withdrawn. There is no delete endpoint for a credit card entry. Voiding cannot be undone. A second void fails with CREDIT_CARD_ENTRY_ALREADY_VOIDED. voidReason is required and is stored on the entry for the audit trail. An empty or whitespace-only string is refused. The journal entry is marked voided and no reversing entry is posted. Amortization-schedule journal entries derived from it are voided too, any pending approval becomes rejected, and bank transactions matched to the entry are unmatched. Attachments and lines are left unchanged. The posting period must be open. An entry with no entity or no posting period cannot have its journal entry voided and fails with MISSING_POSTING_CONTEXT. The response is empty. Read the entry back with GET banking/credit-card-entries/:id to see its voided state. voidReason is not published by any read endpoint, so it appears in the audit trail rather than over the API. [Open in the interactive reference](https://docs.api.lensing.ai/?op=voidCreditCardEntry) ## 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. | | `id` | path | string | yes | The unique identifier for the credit card entry to void. | ## Request body Required, `application/json`: voidCreditCardEntryInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `voidReason` | string | yes | Why the entry is being voided. It is stored on the entry for the audit trail. An empty or whitespace-only string is refused. | Example: ```json { "voidReason": "Duplicate of CCE-2201" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `VOID_REASON_REQUIRED` | 400 | | `CREDIT_CARD_ENTRY_ALREADY_VOIDED` | 400 | | `POSTING_PERIOD_NOT_OPEN` | 400 | | `MISSING_POSTING_CONTEXT` | 400 | | `VOID_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, VOID_REASON_REQUIRED, CREDIT_CARD_ENTRY_ALREADY_VOIDED, POSTING_PERIOD_NOT_OPEN, MISSING_POSTING_CONTEXT, VOID_FAILED - `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 --- # Returns the credit card entries that match the given filters. `POST /banking/credit-card-entries/search` Group: Banking › Credit Card Entries · Operation ID: `searchCreditCardEntry` Every filter is optional and they are combined with AND. Omit them all to return every credit card entry. Each match includes the entry header and its posting lines. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchCreditCardEntry) ## 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`. | ## Request body Required, `application/json`: searchCreditCardEntryInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A credit card entry must match every filter you send. Omit to return all credit card entries. | | `filters.id` | string | no | Limits results to the credit card entry with this identifier. | | `filters.creditCardEntryNumber` | string | no | Limits results to the credit card entry with this number. The match is exact. | | `filters.journalEntryNumber` | string | no | Limits results to entries that posted to the journal entry with this number. The match is exact. | | `filters.status` | string | no | Limits results to entries in this status. Accepted values: 'Matched', 'Unmatched', 'Posted', 'Voided', 'Integrated'. Any other value matches nothing. | | `filters.currency` | string | no | Limits results to entries denominated in this currency. Format ISO 4217, for example 'USD'. | | `filters.creditCardAccountId` | string | no | Limits results to entries recorded against this credit card general-ledger account. See id on GET accounting/accounts/:id. | | `filters.entityId` | string | no | Limits results to entries posted under this legal entity. See id on GET base/entities. | | `filters.origin` | string | no | Limits results to entries created by this source system. The match is exact. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first request. | | `page.size` | number | no | The number of credit card entries per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "status": "Unmatched" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchCreditCardEntryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The credit card entries on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the credit card entry. | | `items[].creditCardEntryNumber` | string | no | The credit card entry number. Absent until one has been assigned. | | `items[].name` | string | no | The display name of the credit card entry. | | `items[].description` | string | no | A free-form description of the entry. | | `items[].transactionDate` | string (date) | yes | The date of the transaction this entry records. Format YYYY-MM-DD. | | `items[].serviceDate` | string (date) | no | The date the goods or services were delivered. Format YYYY-MM-DD. | | `items[].currency` | string | no | The currency the entry is denominated in. Format ISO 4217, for example 'USD'. It governs every line amount. | | `items[].status` | string | yes | The reconciliation status of the entry. Possible values: 'Matched', 'Unmatched', 'Posted', 'Voided', 'Integrated'. | | `items[].journalEntryNumber` | string | no | The number of the journal entry this entry posted to, when it has been posted. | | `items[].origin` | string | no | A label for the source system the entry was created from. | | `items[].externalUrl` | string | no | A link to the source document in an external system. | | `items[].creditCardAccountId` | string | no | The credit card general-ledger account this entry is recorded against. See id on GET accounting/accounts/:id. | | `items[].creditCardAccountName` | string | no | The name of the credit card general-ledger account. | | `items[].entityId` | string | no | The legal entity this entry is posted under. See id on GET base/entities. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the credit card entry last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | no | The posting lines of the entry, one per general-ledger posting. An empty array when the entry has none. | | `items[].lines[].id` | string | yes | The unique identifier for the line. | | `items[].lines[].lineNumber` | string | no | The line number. | | `items[].lines[].description` | string | no | A free-form description of the line. | | `items[].lines[].lineType` | string | no | The kind of line. Possible values: 'oneTime', 'prepaid'. | | `items[].lines[].amount` | string (decimal) | no | The gross line amount, including VAT, in the entry currency. | | `items[].lines[].currency` | string | no | The currency of the line. Format ISO 4217. It always matches the entry currency. | | `items[].lines[].netAmount` | string (decimal) | no | The line amount excluding VAT, in the entry currency. | | `items[].lines[].vatAmount` | string (decimal) | no | The VAT amount on the line, in the entry currency. | | `items[].lines[].vatPercentage` | string (decimal) | no | The VAT rate applied, as a percentage. For example, 19 means 19%. | | `items[].lines[].origin` | string | no | A label for the source system, copied onto the line from the entry. | | `items[].lines[].accountId` | string | no | The general-ledger account this line posts to. See id on GET accounting/accounts/:id. | | `items[].lines[].departmentId` | string | no | The department this line is charged to. See id on GET departments/:id. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor to pass as page.cursor for the next page. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Fund Transfers `GET /banking/fund-transfers` Group: Banking › Fund Transfers · Operation ID: `fundTransfers` Returns a list of fund transfers. To match on a linked account, a legal entity, or a journal entry number, use POST banking/fund-transfers/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=fundTransfers) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `fundTransferNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `transferDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isVoided` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isIntercompany` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `origin` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `externalPaymentId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, fundTransferNumber, transferDate, transferAmount, currency, isVoided, isIntercompany, lastModifiedDate - Selectable: id, fundTransferNumber, name, description, transferDate, transferAmount, currency, isVoided, isIntercompany, origin, externalPaymentId, fromAccountName, toAccountName, entityName, toEntityName, lastModifiedDate ## Response `200` Success Under `data`, an array of fundTransfersRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the fund transfer. | | `fundTransferNumber` | string or null | no | The fund transfer number, for example 'FT-71122'. Absent while none has been assigned. | | `name` | string or null | no | The display name of the fund transfer. | | `description` | string or null | no | A free-form description of the transfer. | | `transferDate` | string (date) or null | no | The date the money moved. Format YYYY-MM-DD. | | `transferAmount` | string (decimal) or null | no | The amount moved, in the transfer currency. | | `currency` | string or null | no | The transfer currency. Format ISO 4217, for example 'USD'. transferAmount is in this currency. | | `isVoided` | boolean or null | no | Whether the transfer has been voided. A voided transfer is kept for audit rather than deleted. | | `isIntercompany` | boolean or null | no | Whether the transfer moves money between two legal entities rather than between two accounts of one entity. When true, toEntityId names the receiving entity. | | `origin` | string or null | no | The system or process the transfer came from. | | `externalPaymentId` | string or null | no | The identifier of this payment in the external system that initiated the transfer. It is not unique. | | `fromAccountName` | string or null | no | The name of the account the money left. | | `toAccountName` | string or null | no | The name of the account the money arrived in. | | `entityName` | string or null | no | The name of the legal entity the transfer is posted under. | | `toEntityName` | string or null | no | The name of the receiving legal entity on an intercompany transfer. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the fund transfer last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a fund transfer. `POST /banking/fund-transfers` Group: Banking › Fund Transfers · Operation ID: `createFundTransfer` The response contains only the new transfer's id. Read the record with GET banking/fund-transfers/:id. entityId, fromAccountId, toAccountId, transferDate and transferAmount are required. A fund transfer always has one 'from' line and one 'to' line. They are derived from the header rather than sent in the request, and GET banking/fund-transfers/:id returns them. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createFundTransfer) ## 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`: createFundTransferInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `entityId` | string | yes | The legal entity to post the transfer under. See id on GET base/entities. | | `fromAccountId` | string | yes | The general-ledger account the money leaves. It must differ from toAccountId. See id on GET accounting/accounts/:id. | | `toAccountId` | string | yes | The general-ledger account the money arrives in. It must differ from fromAccountId. See id on GET accounting/accounts/:id. | | `transferDate` | string (date) | yes | The date the transfer takes place. Use format YYYY-MM-DD. It sets the posting period. | | `transferAmount` | string (decimal) | yes | The amount leaving the from-account, in the transfer currency. It must be greater than zero. | | `currency` | string | no | The currency of the transfer amount. Format ISO 4217, for example 'USD'. Omit to use the from-account's own currency. A value that does not match it fails with CURRENCY_MISMATCH. | | `toAmount` | string (decimal) | no | The amount actually received in the to-account, in that account's currency. Supply it for a cross-currency transfer. Omit to mirror the amount sent. | | `exchangeRate` | string (decimal) | no | The exchange rate applied between the two account currencies. It must be greater than zero. | | `status` | string | no | The payment status. Accepted values: 'recorded', 'inProgress', 'completed', 'voided', 'failed', 'noStatus', 'unknown'. Defaults to 'recorded'. | | `name` | string | no | A short name for the transfer, shown in listings. | | `description` | string | no | A free-form description of the transfer. It is copied onto both lines. | | `toEntityId` | string | no | The receiving legal entity, for a transfer between entities. Defaults to entityId. See id on GET base/entities. | | `origin` | string | no | The system or process the transfer came from. | | `externalUrl` | string | no | An absolute http or https link to the transfer in the originating system. | | `externalPaymentId` | string | no | The identifier of the payment in the source system this transfer settles. | Example: ```json { "entityId": "e8dc29b9-7e79-4709-bb32-95938bcb1bd1", "fromAccountId": "f50164c3-85e3-4dab-986e-3fdb39f8ecc1", "toAccountId": "9994c3ed-f812-44c1-9454-7f36b2bfb0fb", "transferDate": "2026-02-14", "transferAmount": "2500.00", "currency": "<currency>", "toAmount": "19.99", "exchangeRate": "19.99", "status": "<status>", "name": "February sweep", "description": "<description>", "toEntityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "origin": "<origin>", "externalUrl": "<externalUrl>", "externalPaymentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` ## Response `200` Success Under `data` (createFundTransferOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created fund transfer. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `INVALID_FIELD_VALUE` | 400 | | `SAME_ACCOUNT` | 400 | | `TRANSFER_AMOUNT_NOT_POSITIVE` | 400 | | `INVALID_EXCHANGE_RATE` | 400 | | `GROUPING_ACCOUNT_NOT_ALLOWED` | 400 | | `ACCOUNT_CURRENCY_MISSING` | 400 | | `CURRENCY_MISMATCH` | 400 | | `ACCOUNT_ENTITY_MISMATCH` | 400 | | `POSTING_PERIOD_NOT_OPEN` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, INVALID_FIELD_VALUE, SAME_ACCOUNT, TRANSFER_AMOUNT_NOT_POSITIVE, INVALID_EXCHANGE_RATE, GROUPING_ACCOUNT_NOT_ALLOWED, ACCOUNT_CURRENCY_MISSING, CURRENCY_MISMATCH, ACCOUNT_ENTITY_MISMATCH, POSTING_PERIOD_NOT_OPEN - `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 --- # Get Fund Transfer `GET /banking/fund-transfers/{id}` Group: Banking › Fund Transfers · Operation ID: `getFundTransfer` Returns the fund transfer with the given identifier. Use the fund transfer's id, not its fundTransferNumber. A fund transfer moves money between two accounts. lines contains one line per side, each tagged with its accountType. On an intercompany transfer the two sides belong to different legal entities, and toEntityId names the receiving one. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getFundTransfer) ## 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`. | | `id` | path | string | yes | The unique identifier for the fund transfer to return. | ## Response `200` Success Under `data` (getFundTransferOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the fund transfer. | | `fundTransferNumber` | string | no | The fund transfer number, for example 'FT-71122'. Absent while none has been assigned. | | `name` | string | no | The display name of the fund transfer. | | `description` | string | no | A free-form description of the transfer. | | `transferDate` | string (date) | yes | The date the money moved. Format YYYY-MM-DD. | | `transferAmount` | string (decimal) | no | The amount moved, in the transfer currency. | | `currency` | string | no | The transfer currency. Format ISO 4217, for example 'USD'. transferAmount is in this currency. | | `exchangeRate` | string (decimal) | no | The exchange rate applied when the two accounts are held in different currencies. | | `isVoided` | boolean | no | Whether the transfer has been voided. A voided transfer is kept for audit rather than deleted. | | `isIntercompany` | boolean | no | Whether the transfer moves money between two legal entities rather than between two accounts of one entity. When true, toEntityId names the receiving entity. | | `journalEntryNumber` | string | no | The number of the journal entry this transfer posted to, on GET accounting/journal-entries. Absent until the transfer has posted. | | `origin` | string | no | The system or process the transfer came from. | | `externalUrl` | string | no | An absolute http or https link to the source document in an external system. | | `externalPaymentId` | string | no | The identifier of this payment in the external system that initiated the transfer. It is not unique. | | `fromAccountId` | string | no | The general-ledger account the money left. See id on GET accounting/accounts/:id. | | `fromAccountName` | string | no | The name of the account the money left. | | `toAccountId` | string | no | The general-ledger account the money arrived in. See id on GET accounting/accounts/:id. | | `toAccountName` | string | no | The name of the account the money arrived in. | | `entityId` | string | no | The legal entity the transfer is posted under. On an intercompany transfer this is the sending entity. See id on GET base/entities. | | `entityName` | string | no | The name of the legal entity the transfer is posted under. | | `toEntityId` | string | no | The receiving legal entity on an intercompany transfer. Absent on a same-entity transfer. See id on GET base/entities. | | `toEntityName` | string | no | The name of the receiving legal entity on an intercompany transfer. | | `lastModifiedDate` | string (date-time) | yes | The date and time the fund transfer last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | no | The two sides of the transfer, one line per side. Read accountType to tell them apart rather than relying on order. | | `lines[].id` | string | yes | The unique identifier for the line. | | `lines[].lineNumber` | string | no | The line number. | | `lines[].accountType` | string | yes | Which side of the transfer this line is. Possible values: 'from', 'to'. | | `lines[].description` | string | no | A free-form description of the line. | | `lines[].amount` | string (decimal) | no | The line amount, in the line currency. | | `lines[].currency` | string | no | The currency of the line. Format ISO 4217, for example 'USD'. It differs from the transfer currency only on a cross-currency transfer. | | `lines[].bankFee` | string (decimal) | no | The bank fee charged on this side of the transfer, in the line currency. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Fund Transfer `PATCH /banking/fund-transfers/{id}` Group: Banking › Fund Transfers · Operation ID: `updateFundTransfer` Updates the fund transfer with the given identifier. Only the fields in the request body change. Use the fund transfer's id, not its fundTransferNumber. Everything omitted from the request body is left as it was. The two lines are re-derived from the header on every update, the same as on create, so changing fromAccountId, toAccountId, transferAmount or toAmount changes the lines. There is no line collection to edit. The response is empty. Read the updated record with GET banking/fund-transfers/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateFundTransfer) ## 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. | | `id` | path | string | yes | The unique identifier for the fund transfer to update. | ## Request body Required, `application/json`: updateFundTransferInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `entityId` | string | no | The legal entity to post the transfer under. See id on GET base/entities. | | `fromAccountId` | string | no | The general-ledger account the money leaves. It must differ from toAccountId. See id on GET accounting/accounts/:id. | | `toAccountId` | string | no | The general-ledger account the money arrives in. It must differ from fromAccountId. See id on GET accounting/accounts/:id. | | `transferDate` | string (date) | no | The date the transfer takes place. Use format YYYY-MM-DD. It sets the posting period. | | `transferAmount` | string (decimal) | no | The amount leaving the from-account, in the transfer currency. It must be greater than zero. | | `currency` | string | no | The currency of the transfer amount. Format ISO 4217, for example 'USD'. Omit to use the from-account's own currency. A value that does not match it fails with CURRENCY_MISMATCH. | | `toAmount` | string (decimal) | no | The amount actually received in the to-account, in that account's currency. Supply it for a cross-currency transfer. Omit to mirror the amount sent. | | `exchangeRate` | string (decimal) | no | The exchange rate applied between the two account currencies. It must be greater than zero. | | `status` | string | no | The payment status. Accepted values: 'recorded', 'inProgress', 'completed', 'voided', 'failed', 'noStatus', 'unknown'. Defaults to 'recorded'. | | `name` | string | no | A short name for the transfer, shown in listings. | | `description` | string | no | A free-form description of the transfer. It is copied onto both lines. | | `toEntityId` | string | no | The receiving legal entity, for a transfer between entities. Defaults to entityId. See id on GET base/entities. | | `origin` | string | no | The system or process the transfer came from. | | `externalUrl` | string | no | An absolute http or https link to the transfer in the originating system. | | `externalPaymentId` | string | no | The identifier of the payment in the source system this transfer settles. | Example: ```json { "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "fromAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "toAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "transferDate": "2026-01-15", "transferAmount": "2750.00", "currency": "<currency>", "toAmount": "19.99", "exchangeRate": "19.99", "status": "<status>", "name": "February sweep — revised", "description": "<description>", "toEntityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "origin": "<origin>", "externalUrl": "<externalUrl>", "externalPaymentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `INVALID_FIELD_VALUE` | 400 | | `SAME_ACCOUNT` | 400 | | `TRANSFER_AMOUNT_NOT_POSITIVE` | 400 | | `INVALID_EXCHANGE_RATE` | 400 | | `GROUPING_ACCOUNT_NOT_ALLOWED` | 400 | | `ACCOUNT_CURRENCY_MISSING` | 400 | | `CURRENCY_MISMATCH` | 400 | | `ACCOUNT_ENTITY_MISMATCH` | 400 | | `POSTING_PERIOD_NOT_OPEN` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND, INVALID_FIELD_VALUE, SAME_ACCOUNT, TRANSFER_AMOUNT_NOT_POSITIVE, INVALID_EXCHANGE_RATE, GROUPING_ACCOUNT_NOT_ALLOWED, ACCOUNT_CURRENCY_MISSING, CURRENCY_MISMATCH, ACCOUNT_ENTITY_MISMATCH, POSTING_PERIOD_NOT_OPEN - `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 --- # Voids a fund transfer. `POST /banking/fund-transfers/{id}/void` Group: Banking › Fund Transfers · Operation ID: `voidFundTransfer` Voiding is not deleting. The transfer, its number and its audit trail all stay, and the ledger effect is withdrawn. There is no endpoint to delete a fund transfer. Voiding is terminal. Nothing un-voids a transfer, and a second void fails with FUND_TRANSFER_ALREADY_VOIDED. The journal entry behind the transfer is stamped voided rather than reversed, so no reversing entry is posted. Amortization-schedule journal entries derived from it are voided too, any pending approval becomes rejected, and bank transactions matched to the transfer are unmatched. Attachments and lines are left unchanged. The posting period must be open for every entity the transfer touches. An intercompany transfer touches two, and either one being closed refuses the void. The response is empty. Read the transfer back with GET banking/fund-transfers/:id to see its voided state. voidReason is stored on the transfer but no read endpoint returns it. [Open in the interactive reference](https://docs.api.lensing.ai/?op=voidFundTransfer) ## 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. | | `id` | path | string | yes | The unique identifier for the fund transfer to void. | ## Request body Required, `application/json`: voidFundTransferInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `voidReason` | string | yes | The reason the transfer is being voided. It is stored on the transfer and shown in the audit trail. An empty or whitespace-only value is refused. | Example: ```json { "voidReason": "Duplicate of FT-881" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `VOID_REASON_REQUIRED` | 400 | | `FUND_TRANSFER_ALREADY_VOIDED` | 400 | | `POSTING_PERIOD_NOT_OPEN` | 400 | | `VOID_FAILED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, VOID_REASON_REQUIRED, FUND_TRANSFER_ALREADY_VOIDED, POSTING_PERIOD_NOT_OPEN, VOID_FAILED - `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 --- # Returns the fund transfers that match the given filters. `POST /banking/fund-transfers/search` Group: Banking › Fund Transfers · Operation ID: `searchFundTransfer` Every filter is optional and they are combined with AND. Omit them all to return every fund transfer. fromAccountId and toAccountId are separate filters, so you can find what left an account, what arrived in it, or both. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchFundTransfer) ## 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`. | ## Request body Required, `application/json`: searchFundTransferInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A fund transfer must match every filter you send. Omit to return all fund transfers. | | `filters.id` | string | no | Limits results to the fund transfer with this identifier. | | `filters.fundTransferNumber` | string | no | Limits results to the fund transfer with this number. The match is exact. | | `filters.journalEntryNumber` | string | no | Limits results to fund transfers posted to the journal entry with this number. The match is exact. | | `filters.externalPaymentId` | string | no | Limits results to fund transfers with this external payment identifier. It is not unique, so several transfers can match. The match is exact. | | `filters.fromAccountId` | string | no | Limits results to transfers that left this general-ledger account. See id on GET accounting/accounts/:id. | | `filters.toAccountId` | string | no | Limits results to transfers that arrived in this general-ledger account. See id on GET accounting/accounts/:id. | | `filters.entityId` | string | no | Limits results to transfers posted under this legal entity. See id on GET base/entities. | | `filters.entityName` | string | no | Limits results to transfers posted under the legal entity with this name. The match is exact and case-sensitive. | | `filters.isVoided` | boolean | no | Limits results to voided transfers when true, and to live transfers when false. Omit to return both. | | `filters.isIntercompany` | boolean | no | Limits results to intercompany transfers when true, and to same-entity transfers when false. Omit to return both. | | `filters.origin` | string | no | Limits results to transfers with this origin. The match is exact. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first request. | | `page.size` | number | no | The number of fund transfers per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "isVoided": false }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchFundTransferOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The fund transfers on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the fund transfer. | | `items[].fundTransferNumber` | string | no | The fund transfer number, for example 'FT-71122'. Absent while none has been assigned. | | `items[].name` | string | no | The display name of the fund transfer. | | `items[].description` | string | no | A free-form description of the transfer. | | `items[].transferDate` | string (date) | yes | The date the money moved. Format YYYY-MM-DD. | | `items[].transferAmount` | string (decimal) | no | The amount moved, in the transfer currency. | | `items[].currency` | string | no | The transfer currency. Format ISO 4217, for example 'USD'. transferAmount is in this currency. | | `items[].exchangeRate` | string (decimal) | no | The exchange rate applied when the two accounts are held in different currencies. | | `items[].isVoided` | boolean | no | Whether the transfer has been voided. A voided transfer is kept for audit rather than deleted. | | `items[].isIntercompany` | boolean | no | Whether the transfer moves money between two legal entities rather than between two accounts of one entity. When true, toEntityId names the receiving entity. | | `items[].journalEntryNumber` | string | no | The number of the journal entry this transfer posted to, on GET accounting/journal-entries. Absent until the transfer has posted. | | `items[].origin` | string | no | The system or process the transfer came from. | | `items[].externalUrl` | string | no | An absolute http or https link to the source document in an external system. | | `items[].externalPaymentId` | string | no | The identifier of this payment in the external system that initiated the transfer. It is not unique. | | `items[].fromAccountId` | string | no | The general-ledger account the money left. See id on GET accounting/accounts/:id. | | `items[].fromAccountName` | string | no | The name of the account the money left. | | `items[].toAccountId` | string | no | The general-ledger account the money arrived in. See id on GET accounting/accounts/:id. | | `items[].toAccountName` | string | no | The name of the account the money arrived in. | | `items[].entityId` | string | no | The legal entity the transfer is posted under. On an intercompany transfer this is the sending entity. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the legal entity the transfer is posted under. | | `items[].toEntityId` | string | no | The receiving legal entity on an intercompany transfer. Absent on a same-entity transfer. See id on GET base/entities. | | `items[].toEntityName` | string | no | The name of the receiving legal entity on an intercompany transfer. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the fund transfer last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | no | The two sides of the transfer, one line per side. Read accountType to tell them apart rather than relying on order. | | `items[].lines[].id` | string | yes | The unique identifier for the line. | | `items[].lines[].lineNumber` | string | no | The line number. | | `items[].lines[].accountType` | string | yes | Which side of the transfer this line is. Possible values: 'from', 'to'. | | `items[].lines[].description` | string | no | A free-form description of the line. | | `items[].lines[].amount` | string (decimal) | no | The line amount, in the line currency. | | `items[].lines[].currency` | string | no | The currency of the line. Format ISO 4217, for example 'USD'. It differs from the transfer currency only on a cross-currency transfer. | | `items[].lines[].bankFee` | string (decimal) | no | The bank fee charged on this side of the transfer, in the line currency. | | `pagination` | object | yes | The pagination details for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Approval Policies `GET /base/approval-policies` Group: Base › Approval Policies · Operation ID: `approvalPolicies` Returns a list of approval policies, each as its header only. Use GET base/approval-policies/:id or POST base/approval-policies/search for the rule lines, approvers and notification settings. [Open in the interactive reference](https://docs.api.lensing.ai/?op=approvalPolicies) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `policyHeaderNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `businessObject` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isAllEntities` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `startDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `endDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, policyHeaderNumber, businessObject, status, startDate, endDate, lastModifiedDate - Selectable: id, policyHeaderNumber, businessObject, status, isAllEntities, entityName, startDate, endDate, submitterCannotApprove, approverCommentEnabled, lastModifiedDate ## Response `200` Success Under `data`, an array of approvalPoliciesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the approval policy. | | `policyHeaderNumber` | string or null | no | The policy number, assigned when the policy is created and stable across edits. | | `businessObject` | string or null | no | The code of the business object the policy governs, for example 'VendorBill', 'ExpenseReport' or 'PurchaseOrder'. | | `status` | string or null | no | The lifecycle status of the policy. Possible values: 'active', 'inActive'. Only active policies are matched against new documents. Documents already in flight keep approving against an inActive policy. | | `isAllEntities` | boolean or null | no | Whether the policy applies to every legal entity rather than the one named by entityName. | | `entityName` | string or null | no | The name of the legal entity the policy applies to. Absent when isAllEntities is true. | | `startDate` | string (date) or null | no | The first date the policy is eligible for matching. Format YYYY-MM-DD. Absent when there is no lower bound. | | `endDate` | string (date) or null | no | The last date the policy is eligible for matching. Format YYYY-MM-DD. Absent when there is no upper bound. | | `submitterCannotApprove` | boolean or null | no | Whether the document submitter is stopped from approving their own document. When true, their approval does not count toward fulfillment and they are not notified as an approver. | | `approverCommentEnabled` | boolean or null | no | Whether approvers are prompted for an optional comment at approve time. The comment is stored on the approval trail. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the approval policy last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Approval Policy `POST /base/approval-policies` Group: Base › Approval Policies · Operation ID: `createApprovalPolicy` Creates an approval policy with its rule lines, approvers, exclusion lines and notification settings. The response contains only the new policy's id. The response contains only the new policy's id. Read the record back with GET base/approval-policies/:id for its stored state, including the assigned policyHeaderNumber. Scope the policy to exactly one thing: name an entityId or set isAllEntities. Naming neither fails with MISSING_ENTITY_SCOPE. Rule lines are evaluated independently. A document picks up the approvers of every line it matches, so two plain lines mean either condition brings in its own approvers, not that both must hold. To require several conditions together, send one line with criteria __COMPOSITE__ whose single value is a JSON AND or OR expression. Every rule line needs at least one approver, or the create fails with LINE_WITHOUT_APPROVERS. An active policy whose business object and entity coverage overlap an existing active policy fails with CONFLICTING_ACTIVE_POLICY. Create it with status 'inActive', or deactivate the other first. Send every value as a string, for example ["10000"], not [10000]. Only a line with valueType 'noValue' may omit the value. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createApprovalPolicy) ## 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`: createApprovalPolicyInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `entityId` | string | no | The legal entity the policy applies to. Required unless isAllEntities is true. Leave it out when using isAllEntities. See id on GET base/entities. | | `isAllEntities` | boolean | no | Whether the policy applies to every legal entity instead of a single one. When true, leave entityId out. | | `businessObject` | string | yes | The code of the business object the policy governs. It must be a registered approval business object, for example 'VendorBill', 'ExpenseReport', 'PurchaseOrder', 'PurchaseRequest' or 'JournalEntry'. An unregistered code fails with UNKNOWN_BUSINESS_OBJECT, which lists the registered codes. It cannot be changed after creation. | | `status` | string, one of `"active"`, `"inActive"` | no | The lifecycle status of the policy. Defaults to 'active', which makes it eligible for matching immediately. Create it 'inActive' to stage a policy that would otherwise collide with a live one. | | `startDate` | string | no | The first date the policy is eligible for matching. Use format YYYY-MM-DD. Omit for no lower bound. | | `endDate` | string | no | The last date the policy is eligible for matching. Use format YYYY-MM-DD. Omit for no upper bound. | | `submitterCannotApprove` | boolean | no | Whether the document submitter is stopped from approving their own document. When true, their approval does not count toward fulfillment and they are not notified as an approver. Defaults to false. | | `approverCommentEnabled` | boolean | no | Whether approvers are prompted for an optional comment at approve time. The comment is stored on the approval trail. Defaults to false. | | `notification` | object | no | The notification channels used when this policy's approvers and submitters are told about a document. Omit to leave the tenant's defaults in place. | | `notification.emailNotification` | boolean | no | Whether to email approvers. | | `notification.nativeNotification` | boolean | no | Whether to raise an in-product notification for approvers. | | `notification.slackNotification` | boolean | no | Whether to notify approvers on Slack. | | `lines` | array of object | yes | The policy's rule lines. It must not be empty. Rule lines are evaluated independently: a document picks up the approvers of every line it matches. To require several conditions together, use one __COMPOSITE__ line rather than several plain lines. | | `lines[].criteria` | string | yes | The document field the rule tests. Use the criteria names the document type publishes, for example 'Vendor', 'Total Amount', 'Department', 'Line Amounts' or 'Upon Creation'. Pass the literal __COMPOSITE__ to combine several conditions in one line. | | `lines[].operator` | string | yes | The comparison to apply to the criteria, for example 'Equal', 'Not Equal', 'Greater Than', 'Less Than', 'In', 'Intersect' or the Any and All variants for array fields. Use '-' for a criteria that takes no value. | | `lines[].value` | array of string | no | The values to compare against, each as a string, for example ["10000"], not [10000]. Omit only when valueType is 'noValue'. A 'number' valueType takes exactly one numeric element. A 'composite' valueType takes exactly one element holding the JSON expression, for example {"operator":"AND","children":[{"criteriaName":"Vendor","operator":"Equal","value":["Acme"],"valueType":"multivalue"},{"criteriaName":"Total Amount","operator":"Less Than","value":["10000"],"valueType":"number"}]}. | | `lines[].valueType` | string, one of `"number"`, `"multivalue"`, `"value"`, `"noValue"`, `"arrayField"`, `"composite"` | yes | How to interpret value. 'number' is a single numeric comparison, 'multivalue' one or more discrete values, 'value' a single discrete value, 'noValue' a criteria that takes none such as 'Upon Creation', 'arrayField' the Any, All and Intersect operators over a line-level array, and 'composite' a __COMPOSITE__ expression. | | `lines[].step` | number | yes | The approval order, starting at 1. Lines that share a step are approved together, and a higher step is reached only once the lower steps are satisfied. | | `lines[].requireAction` | string, one of `"All"`, `"Any"` | no | Whether all of the line's approvers must approve, or any one of them is enough. Defaults to 'All'. | | `lines[].approvers` | array of object | yes | The approvers for this line. It must not be empty, because a line with no approver can never be satisfied. | | `lines[].approvers[].approverId` | number | yes | The approver. For approverType 'employee' or 'supremeApprover' it is the employee, and for 'manager' it is the manager-hierarchy record. It is an integer, not a UUID. There is no endpoint for it. | | `lines[].approvers[].approverType` | string, one of `"employee"`, `"manager"`, `"supremeApprover"` | yes | The kind of approver. 'employee' names a specific person, 'manager' resolves the submitter's manager through a hierarchy at approval time, and 'supremeApprover' is a person who may approve at any step. | | `exclusionLines` | array of object | no | The exclusion lines. A document matching any of them is exempted from the policy entirely, whatever the rule lines say. Omit for no exclusions. | | `exclusionLines[].criteria` | string | yes | | | `exclusionLines[].operator` | string | yes | | | `exclusionLines[].value` | array of string | no | The values to compare against, each as a string. Omit only when valueType is 'noValue'. | | `exclusionLines[].valueType` | string, one of `"number"`, `"multivalue"`, `"value"`, `"noValue"`, `"arrayField"`, `"composite"` | yes | | Example: ```json { "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "isAllEntities": true, "businessObject": "VendorBill", "status": "active", "startDate": "<startDate>", "endDate": "<endDate>", "submitterCannotApprove": true, "approverCommentEnabled": true, "notification": { "emailNotification": true }, "lines": [ { "criteria": "Total Amount", "operator": "Greater Than", "value": [ "10000" ], "valueType": "number", "step": 1, "requireAction": "All", "approvers": [ { "approverId": 42, "approverType": "employee" } ] } ], "exclusionLines": [ { "criteria": "<criteria>", "operator": "<operator>", "value": [ "<value>" ], "valueType": "number" } ] } ``` ## Response `200` Success Under `data` (createApprovalPolicyOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created approval policy. Every other approval-policy endpoint is keyed by it. | ## 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 | | --- | --- | | `MISSING_ENTITY_SCOPE` | 400 | | `ENTITY_NOT_FOUND` | 400 | | `UNKNOWN_BUSINESS_OBJECT` | 400 | | `NO_LINES` | 400 | | `LINE_WITHOUT_APPROVERS` | 400 | | `MISSING_LINE_VALUE` | 400 | | `INVALID_NUMBER_VALUE` | 400 | | `INVALID_COMPOSITE_EXPRESSION` | 400 | | `CONFLICTING_ACTIVE_POLICY` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: MISSING_ENTITY_SCOPE, ENTITY_NOT_FOUND, UNKNOWN_BUSINESS_OBJECT, NO_LINES, LINE_WITHOUT_APPROVERS, MISSING_LINE_VALUE, INVALID_NUMBER_VALUE, INVALID_COMPOSITE_EXPRESSION, CONFLICTING_ACTIVE_POLICY - `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 --- # Get Approval Policy `GET /base/approval-policies/{id}` Group: Base › Approval Policies · Operation ID: `getApprovalPolicy` Returns the approval policy with the given identifier, with its rule lines, approvers, exclusion lines and notification settings. Returns the policy in full: the header, every rule line with its criteria and approvers, the exclusion lines and the notification settings. The output mirrors what create and update take, so you can read a policy, change one line and send it back. A document picks up the approvers of every rule line it matches. Approver names are resolved for readability and are absent when the referenced employee or manager no longer exists. approverId stays authoritative. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getApprovalPolicy) ## 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`. | | `id` | path | string | yes | The unique identifier for the approval policy to return. | ## Response `200` Success Under `data` (getApprovalPolicyOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the approval policy. | | `policyHeaderNumber` | string | no | The policy number, assigned when the policy is created and stable across edits. | | `entityId` | string | no | The legal entity the policy applies to. Absent when isAllEntities is true. See id on GET base/entities. | | `entityName` | string | no | The name of the legal entity the policy applies to. Absent when isAllEntities is true. | | `isAllEntities` | boolean | no | Whether the policy applies to every legal entity rather than the one named by entityId. | | `businessObject` | string | yes | The code of the business object the policy governs, for example 'VendorBill', 'ExpenseReport' or 'PurchaseOrder'. | | `status` | string | yes | The lifecycle status of the policy. Possible values: 'active', 'inActive'. Only active policies are matched against new documents. | | `startDate` | string | no | The first date the policy is eligible for matching. Format YYYY-MM-DD. Absent when there is no lower bound. | | `endDate` | string | no | The last date the policy is eligible for matching. Format YYYY-MM-DD. Absent when there is no upper bound. | | `submitterCannotApprove` | boolean | no | Whether the document submitter is stopped from approving their own document. | | `approverCommentEnabled` | boolean | no | Whether approvers are prompted for an optional comment at approve time. | | `lastModifiedDate` | string (date-time) | yes | The date and time the approval policy last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `notification` | object | no | The notification channels used when approvers and submitters are told about this policy's documents. Absent when they have never been configured for this policy. | | `notification.emailNotification` | boolean | no | Whether approvers are emailed. | | `notification.nativeNotification` | boolean | no | Whether approvers get an in-product notification. | | `notification.slackNotification` | boolean | no | Whether approvers are notified on Slack. | | `lines` | array of object | yes | The policy's rule lines, ordered by step. A document picks up the approvers of every line it matches. | | `lines[].id` | string | yes | The unique identifier for the rule line. Pass it in updateLines or deleteLines on PATCH base/approval-policies/:id. | | `lines[].policyLineNumber` | string | no | The rule line number, assigned when the line is created. | | `lines[].criteria` | string | yes | The document field the rule tests, for example 'Vendor' or 'Total Amount'. The literal __COMPOSITE__ marks a line whose value is a JSON AND or OR expression over several criteria. | | `lines[].operator` | string | yes | The comparison applied to the criteria, for example 'Equal' or 'Less Than'. | | `lines[].value` | array of string | yes | The values the criteria is compared against, each as a string. For a __COMPOSITE__ line this is a single-element array holding the JSON expression. | | `lines[].valueType` | string | no | How the value is interpreted. Possible values: 'number', 'multivalue', 'value', 'noValue', 'arrayField', 'composite'. | | `lines[].step` | number | yes | The approval order, starting at 1. Lines that share a step are evaluated and approved together. | | `lines[].requireAction` | string | no | Whether all of the line's approvers must approve, or any one of them is enough. Possible values: 'All', 'Any'. | | `lines[].approvers` | array of object | yes | The approvers assigned to this line. A line always has at least one. | | `lines[].approvers[].id` | string | yes | The unique identifier for the approver assignment. Pass it in updateApprovers or deleteApprovers on PATCH base/approval-policies/:id. | | `lines[].approvers[].approverId` | number | yes | The approver. For approverType 'employee' or 'supremeApprover' it is the employee, and for 'manager' it is the manager-hierarchy record. It is an integer, not a UUID. There is no endpoint for it. | | `lines[].approvers[].approverType` | string | yes | The kind of approver. Possible values: 'employee', 'manager', 'supremeApprover', 'position'. 'employee' names a specific person, 'manager' resolves the submitter's manager through a hierarchy at approval time, and 'supremeApprover' is a person who may approve at any step. | | `lines[].approvers[].approverName` | string | no | The resolved display name of the approver. Absent when the referenced employee or manager no longer exists. | | `exclusionLines` | array of object | yes | The exclusion lines. A document matching any of them is exempted from the policy entirely. | | `exclusionLines[].id` | string | yes | The unique identifier for the exclusion line. Pass it in updateExclusionLines or deleteExclusionLines on PATCH base/approval-policies/:id. | | `exclusionLines[].criteria` | string | yes | The document field the exclusion tests. | | `exclusionLines[].operator` | string | yes | The comparison applied to the criteria. | | `exclusionLines[].value` | array of string | yes | The values the criteria is compared against, each as a string. | | `exclusionLines[].valueType` | string | no | How the value is interpreted. Possible values: 'number', 'multivalue', 'value', 'noValue', 'arrayField', 'composite'. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Approval Policy `PATCH /base/approval-policies/{id}` Group: Base › Approval Policies · Operation ID: `updateApprovalPolicy` Updates the approval policy with the given identifier. Only the fields in the request body change. The response is empty. Read the stored state back with GET base/approval-policies/:id. Omitting a field leaves it unchanged, at every level: the header's own fields, a line's fields and an approver's fields. Child collections are edited with three arrays, never by re-sending the collection. createLines adds lines, updateLines changes lines matched by id, and deleteLines removes lines by id, taking their approvers with them. The same split applies to exclusionLines and to each line's approvers. A line you do not mention survives. businessObject cannot be changed. A policy for a different document type is a different policy, so create one and delete this. Setting status to 'active', or moving the policy onto entity coverage that another active policy already covers, fails with CONFLICTING_ACTIVE_POLICY. Editing a policy that already overlaps another is allowed, so a tenant that acquired overlapping policies can still correct them. After your operations the policy must still have at least one rule line, and every surviving line at least one approver, otherwise NO_LINES or LINE_WITHOUT_APPROVERS. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateApprovalPolicy) ## 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. | | `id` | path | string | yes | The unique identifier for the approval policy to update. | ## Request body Required, `application/json`: updateApprovalPolicyInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `entityId` | string | no | The legal entity the policy applies to. Omit to leave unchanged. Ignored when isAllEntities is true. See id on GET base/entities. | | `isAllEntities` | boolean | no | Whether the policy applies to every legal entity. Set true to widen it to all, false to narrow it back to the entity in entityId. Omit to leave unchanged. | | `status` | string, one of `"active"`, `"inActive"` | no | The lifecycle status of the policy. Omit to leave unchanged. Setting 'inActive' stops the policy matching new documents, while documents already in the workflow keep approving against it, which makes it the safe alternative to deleting. | | `startDate` | string | no | The first date the policy is eligible for matching. Use format YYYY-MM-DD. Omit to leave unchanged. | | `endDate` | string | no | The last date the policy is eligible for matching. Use format YYYY-MM-DD. Omit to leave unchanged. | | `submitterCannotApprove` | boolean | no | Whether the document submitter is stopped from approving their own document. Omit to leave unchanged. | | `approverCommentEnabled` | boolean | no | Whether approvers are prompted for an optional comment at approve time. Omit to leave unchanged. | | `notification` | object | no | The notification channels for this policy. This object replaces the stored settings, so a channel left out is turned off. Omit the object to leave the settings unchanged. | | `notification.emailNotification` | boolean | no | | | `notification.nativeNotification` | boolean | no | | | `notification.slackNotification` | boolean | no | | | `createLines` | array of object | no | The rule lines to add. Each is a whole new line with no id, carrying its approvers as a plain array. | | `createLines[].criteria` | string | yes | The document field the rule tests, for example 'Vendor', 'Total Amount' or 'Upon Creation'. Pass the literal __COMPOSITE__ to combine several conditions in one line. | | `createLines[].operator` | string | yes | The comparison to apply to the criteria, for example 'Equal', 'Not Equal', 'Greater Than', 'Less Than', 'In', 'Intersect' or the Any and All variants for array fields. Use '-' for a criteria that takes no value. | | `createLines[].value` | array of string | no | The values to compare against, each as a string. Omit only when valueType is 'noValue'. | | `createLines[].valueType` | string, one of `"number"`, `"multivalue"`, `"value"`, `"noValue"`, `"arrayField"`, `"composite"` | yes | How to interpret value. 'number' is a single numeric comparison, 'multivalue' one or more discrete values, 'value' a single discrete value, 'noValue' a criteria that takes none such as 'Upon Creation', 'arrayField' the Any, All and Intersect operators over a line-level array, and 'composite' a __COMPOSITE__ expression. | | `createLines[].step` | number | yes | The approval order, starting at 1. Lines that share a step are approved together, and a higher step is reached only once the lower steps are satisfied. | | `createLines[].requireAction` | string, one of `"All"`, `"Any"` | no | Whether all of the line's approvers must approve, or any one of them is enough. Defaults to 'All'. | | `createLines[].approvers` | array of object | yes | The approvers for this line. It must not be empty. | | `createLines[].approvers[].approverId` | number | yes | The approver. For approverType 'employee' or 'supremeApprover' it is the employee, and for 'manager' it is the manager-hierarchy record. It is an integer, not a UUID. There is no endpoint for it. | | `createLines[].approvers[].approverType` | string, one of `"employee"`, `"manager"`, `"supremeApprover"` | yes | | | `updateLines` | array of object | no | The rule lines to change, matched by id. Every field except id is optional, and omitting one leaves it unchanged. Each line edits its own approvers through the nested createApprovers, updateApprovers and deleteApprovers arrays. | | `updateLines[].id` | string | yes | The id of the existing rule line, from the line's id on GET base/approval-policies/:id. An id that does not belong to this policy fails with UNKNOWN_LINE. | | `updateLines[].criteria` | string | no | The document field the rule tests. Omit to leave it unchanged. | | `updateLines[].operator` | string | no | The comparison to apply to the criteria. Omit to leave it unchanged. | | `updateLines[].value` | array of string | no | The values to compare against, each as a string. This replaces the whole list rather than merging into it. Omit to leave it unchanged. | | `updateLines[].valueType` | string, one of `"number"`, `"multivalue"`, `"value"`, `"noValue"`, `"arrayField"`, `"composite"` | no | How to interpret value. Omit to leave it unchanged. | | `updateLines[].step` | number | no | The approval order, starting at 1. Omit to leave it unchanged. | | `updateLines[].requireAction` | string, one of `"All"`, `"Any"` | no | Whether all of the line's approvers must approve, or any one of them is enough. Omit to leave it unchanged. | | `updateLines[].createApprovers` | array of object | no | The approvers to add to this line. | | `updateLines[].createApprovers[].approverId` | number | yes | | | `updateLines[].createApprovers[].approverType` | string, one of `"employee"`, `"manager"`, `"supremeApprover"` | yes | | | `updateLines[].updateApprovers` | array of object | no | The approvers to change, matched by id. An id that does not belong to this line fails with UNKNOWN_APPROVER. | | `updateLines[].updateApprovers[].id` | string | yes | The id of the existing approver assignment, from the approver's id on GET base/approval-policies/:id. | | `updateLines[].updateApprovers[].approverId` | number | no | The approver. Omit to leave it unchanged. | | `updateLines[].updateApprovers[].approverType` | string, one of `"employee"`, `"manager"`, `"supremeApprover"` | no | The kind of approver. Omit to leave it unchanged. | | `updateLines[].deleteApprovers` | array of string | no | The ids of approver assignments to remove from this line. Removing the last approver of a surviving line fails with LINE_WITHOUT_APPROVERS. | | `deleteLines` | array of string | no | The ids of rule lines to remove. Deleting a line also removes its approvers, so do not list them separately. | | `createExclusionLines` | array of object | no | The exclusion lines to add. | | `createExclusionLines[].criteria` | string | yes | | | `createExclusionLines[].operator` | string | yes | | | `createExclusionLines[].value` | array of string | no | The values to compare against, each as a string. Omit only when valueType is 'noValue'. | | `createExclusionLines[].valueType` | string, one of `"number"`, `"multivalue"`, `"value"`, `"noValue"`, `"arrayField"`, `"composite"` | yes | | | `updateExclusionLines` | array of object | no | The exclusion lines to change, matched by id. Omitting a field leaves it unchanged. | | `updateExclusionLines[].id` | string | yes | The id of the existing exclusion line. An id that does not belong to this policy fails with UNKNOWN_EXCLUSION_LINE. | | `updateExclusionLines[].criteria` | string | no | | | `updateExclusionLines[].operator` | string | no | | | `updateExclusionLines[].value` | array of string | no | | | `updateExclusionLines[].valueType` | string, one of `"number"`, `"multivalue"`, `"value"`, `"noValue"`, `"arrayField"`, `"composite"` | no | | | `deleteExclusionLines` | array of string | no | The ids of exclusion lines to remove. | Example: ```json { "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "isAllEntities": true, "status": "inActive", "startDate": "<startDate>", "endDate": "<endDate>", "submitterCannotApprove": true, "approverCommentEnabled": true, "notification": { "emailNotification": true, "nativeNotification": true, "slackNotification": true }, "createLines": [ { "criteria": "<criteria>", "operator": "<operator>", "value": [ "<value>" ], "valueType": "number", "step": 0, "requireAction": "All", "approvers": [ { "approverId": 0, "approverType": "employee" } ] } ], "updateLines": [ { "id": "4e2b7a90-5c31-4d68-9f02-8a1c6b45e7d3", "value": [ "25000" ], "createApprovers": [ { "approverId": 77, "approverType": "employee" } ], "deleteApprovers": [ "b8c5d2e1-3f47-4a90-8e16-5d2b9c703f8a" ] } ], "deleteLines": [ "<deleteLines>" ], "createExclusionLines": [ { "criteria": "<criteria>", "operator": "<operator>", "value": [ "<value>" ], "valueType": "number" } ], "updateExclusionLines": [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "criteria": "<criteria>", "operator": "<operator>", "value": [ "<value>" ], "valueType": "number" } ], "deleteExclusionLines": [ "1a3c5e70-9b24-4f18-a06d-7c8e2b41d95f" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `MISSING_ENTITY_SCOPE` | 400 | | `ENTITY_NOT_FOUND` | 400 | | `UNKNOWN_LINE` | 400 | | `UNKNOWN_APPROVER` | 400 | | `UNKNOWN_EXCLUSION_LINE` | 400 | | `NO_LINES` | 400 | | `LINE_WITHOUT_APPROVERS` | 400 | | `MISSING_LINE_VALUE` | 400 | | `INVALID_NUMBER_VALUE` | 400 | | `INVALID_COMPOSITE_EXPRESSION` | 400 | | `CONFLICTING_ACTIVE_POLICY` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, MISSING_ENTITY_SCOPE, ENTITY_NOT_FOUND, UNKNOWN_LINE, UNKNOWN_APPROVER, UNKNOWN_EXCLUSION_LINE, NO_LINES, LINE_WITHOUT_APPROVERS, MISSING_LINE_VALUE, INVALID_NUMBER_VALUE, INVALID_COMPOSITE_EXPRESSION, CONFLICTING_ACTIVE_POLICY - `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 --- # Delete Approval Policy `DELETE /base/approval-policies/{id}` Group: Base › Approval Policies · Operation ID: `deleteApprovalPolicy` Deletes an approval policy and everything on it. Refuses a policy that documents already in approval still reference. The response is empty. Capture the policyHeaderNumber from a GET first if you need it for a record. Deactivating is almost always the better choice: set the policy to status 'inActive' with PATCH base/approval-policies/:id, and documents already in the workflow keep approving against it while the audit trail stays intact. Delete is for a policy created in error. A policy that documents still reference cannot be deleted. If any approve or reject decision was recorded under it, or any document froze a snapshot of it when it entered approval, the call fails with POLICY_IN_USE. Deleting an id that does not exist fails with NOT_FOUND. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteApprovalPolicy) ## 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. | | `id` | path | string | yes | The unique identifier for the approval policy to delete. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `POLICY_IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, POLICY_IN_USE - `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 --- # Search Approval Policies `POST /base/approval-policies/search` Group: Base › Approval Policies · Operation ID: `searchApprovalPolicies` Returns the approval policies that match the given filters, each in full with its rule lines, approvers, exclusion lines and notification settings. Returns each match in full, the same record GET base/approval-policies/:id returns. Use this endpoint rather than the collection when you match on a legal entity: entityId and entityName are accepted only in the request body, and the collection returns entityName but will not filter or sort on it. Every filter is optional and they are combined with AND. Omitting all of them matches every policy. An entityId that matches no entity returns an empty page, not an error. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchApprovalPolicies) ## 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`. | ## Request body Required, `application/json`: searchApprovalPoliciesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. A policy must match every filter you send. Omit to return all approval policies. | | `filters.businessObject` | string | no | Limits results to policies governing this business object, for example 'VendorBill'. A code that matches no registered business object returns an empty page. | | `filters.status` | string | no | Limits results to policies in this lifecycle status. Accepted values: 'active', 'inActive'. Any other value returns an empty page. | | `filters.entityId` | string | no | Limits results to policies scoped to this legal entity, given as the entity id. Accepted only in the request body. An id that matches no entity returns an empty page, not an error. This does not include the all-entities policies. Combine with isAllEntities to widen. See id on GET base/entities. | | `filters.entityName` | string | no | Limits results to policies whose legal entity name contains this text. The match is case-sensitive. Accepted only in the request body. Use entityId when you already have the id. | | `filters.isAllEntities` | boolean | no | Limits results to policies that cover every legal entity when true, or only single-entity policies when false. | | `filters.policyHeaderNumber` | string | no | Limits results to the policy with this policy number. The match is exact. | | `filters.startDate` | object | no | Limits results by the policy's startDate. Both bounds are inclusive. Use format YYYY-MM-DD, and omit either side for an open-ended range. | | `filters.startDate.start` | string | no | | | `filters.startDate.end` | string | no | | | `filters.endDate` | object | no | Limits results by the policy's endDate. Both bounds are inclusive. Use format YYYY-MM-DD, and omit either side for an open-ended range. | | `filters.endDate.start` | string | no | | | `filters.endDate.end` | string | no | | | `page` | object | no | Cursor pagination. Omit the whole object for the first page at the default size. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of policies per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "businessObject": "VendorBill", "status": "active" }, "page": { "size": 25 } } ``` ## Response `200` Success Under `data` (searchApprovalPoliciesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The approval policies that match, oldest first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the approval policy. | | `items[].policyHeaderNumber` | string | no | The policy number, assigned when the policy is created and stable across edits. | | `items[].entityId` | string | no | The legal entity the policy applies to. Absent when isAllEntities is true. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the legal entity the policy applies to. Absent when isAllEntities is true. | | `items[].isAllEntities` | boolean | no | Whether the policy applies to every legal entity rather than the one named by entityId. | | `items[].businessObject` | string | yes | The code of the business object the policy governs, for example 'VendorBill', 'ExpenseReport' or 'PurchaseOrder'. | | `items[].status` | string | yes | The lifecycle status of the policy. Possible values: 'active', 'inActive'. Only active policies are matched against new documents. | | `items[].startDate` | string | no | The first date the policy is eligible for matching. Format YYYY-MM-DD. Absent when there is no lower bound. | | `items[].endDate` | string | no | The last date the policy is eligible for matching. Format YYYY-MM-DD. Absent when there is no upper bound. | | `items[].submitterCannotApprove` | boolean | no | Whether the document submitter is stopped from approving their own document. | | `items[].approverCommentEnabled` | boolean | no | Whether approvers are prompted for an optional comment at approve time. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the approval policy last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].notification` | object | no | The notification channels used when approvers and submitters are told about this policy's documents. Absent when they have never been configured for this policy. | | `items[].notification.emailNotification` | boolean | no | | | `items[].notification.nativeNotification` | boolean | no | | | `items[].notification.slackNotification` | boolean | no | | | `items[].lines` | array of object | yes | The policy's rule lines, ordered by step. A document picks up the approvers of every line it matches. | | `items[].lines[].id` | string | yes | | | `items[].lines[].policyLineNumber` | string | no | | | `items[].lines[].criteria` | string | yes | The document field the rule tests, for example 'Vendor' or 'Total Amount'. The literal __COMPOSITE__ marks a line whose value is a JSON AND or OR expression over several criteria. | | `items[].lines[].operator` | string | yes | | | `items[].lines[].value` | array of string | yes | The values the criteria is compared against, each as a string. For a __COMPOSITE__ line this is a single-element array holding the JSON expression. | | `items[].lines[].valueType` | string | no | | | `items[].lines[].step` | number | yes | The approval order, starting at 1. Lines that share a step are evaluated and approved together. | | `items[].lines[].requireAction` | string | no | | | `items[].lines[].approvers` | array of object | yes | The approvers assigned to this line. A line always has at least one. | | `items[].lines[].approvers[].id` | string | yes | | | `items[].lines[].approvers[].approverId` | number | yes | The approver. For approverType 'employee' or 'supremeApprover' it is the employee, and for 'manager' it is the manager-hierarchy record. It is an integer, not a UUID. There is no endpoint for it. | | `items[].lines[].approvers[].approverType` | string | yes | | | `items[].lines[].approvers[].approverName` | string | no | The resolved display name of the approver. Absent when the referenced employee or manager no longer exists. | | `items[].exclusionLines` | array of object | yes | The exclusion lines. A document matching any of them is exempted from the policy entirely. | | `items[].exclusionLines[].id` | string | yes | | | `items[].exclusionLines[].criteria` | string | yes | | | `items[].exclusionLines[].operator` | string | yes | | | `items[].exclusionLines[].value` | array of string | yes | | | `items[].exclusionLines[].valueType` | string | no | | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Entities `GET /base/entities` Group: Base › Entities · Operation ID: `entities` Returns a list of legal entities. Their id values are the entityId that most other resources reference. [Open in the interactive reference](https://docs.api.lensing.ai/?op=entities) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `timeZone` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `country` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `localeCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `startDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `endDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, currency, timeZone, status, entityType, country, entityCode, localeCode, startDate, endDate, lastModifiedDate - Selectable: id, entityName, entityNumber, currency, timeZone, status, entityType, country, entityCode, localeCode, startDate, endDate, lastModifiedDate ## Response `200` Success Under `data`, an array of entitiesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the legal entity. Other resources reference it as entityId. | | `entityName` | string | no | The name of the legal entity, for example 'Acme Inc.'. Unique within the tenant. | | `entityNumber` | string or null | no | The entity's business number, separate from its name. Absent until one is assigned. | | `currency` | string or null | no | The entity's functional currency, its default for reporting and posting. Format ISO 4217, for example 'USD'. Absent when none has been set. | | `timeZone` | string or null | no | The time zone the entity operates in. Format IANA time-zone name, for example 'Europe/Berlin'. New entities default to 'Etc/UTC'. Absent when none has been set. | | `status` | string or null | no | The lifecycle status of the entity. Possible values: 'active', 'inactive', 'created'. 'created' is the default for a new entity. | | `entityType` | string or null | no | Whether the entity is a legal entity or a non-legal reporting entity. Possible values: 'LegalEntity', 'NonLegalReportingEntity'. Defaults to 'LegalEntity'. | | `country` | string or null | no | The country the entity is registered in. Format ISO 3166-1 alpha-2, for example 'US'. Absent when none has been set. | | `entityCode` | string or null | no | A short code for the entity, separate from its entityNumber. Absent until one is assigned. | | `localeCode` | string or null | no | The locale for documents sent from this entity, setting the language and regional format. Format BCP 47, for example 'en-US'. Absent when none has been set. | | `startDate` | string (date) or null | no | The date the entity became effective. Format YYYY-MM-DD. Absent when none has been set. | | `endDate` | string (date) or null | no | The date the entity stops being effective. Format YYYY-MM-DD. Absent when the entity has no scheduled end. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the entity last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # List Custom Dimensions `GET /base/custom-dimensions` Group: Base › Reporting Tags · Operation ID: `listCustomDimensions` Returns the custom value dimensions this tenant has defined, the tenant-specific axes that extend reporting tags beyond the nine built-in dimensions. Each entry gives a dimension's identifier and display name. To list the values inside one dimension, use GET base/custom-dimensions/{customDimensionUuid}/values. [Open in the interactive reference](https://docs.api.lensing.ai/?op=listCustomDimensions) ## 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`. | | `pageSize` | query | number | no | The number of dimensions per page, between 1 and 200. Defaults to 50. A value above 200 is treated as 200, not rejected. | | `cursor` | query | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | ## Response `200` Success Under `data` (listCustomDimensionsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The custom dimensions on this page, ordered by identifier. An empty array when the tenant has defined none. | | `items[].customDimensionUuid` | string | yes | The unique identifier for the custom value dimension. It is the customDimensionUuid on a 'custom' row from POST base/reporting-tags/resolve. | | `items[].name` | string | yes | The display name of the custom dimension. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as the cursor query parameter. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more dimensions remain beyond this page. | ## Errors | Code | Status | | --- | --- | | `INVALID_CURSOR` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_CURSOR - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # List Custom Dimension Values `GET /base/custom-dimensions/{customDimensionUuid}/values` Group: Base › Reporting Tags · Operation ID: `listCustomDimensionValues` Returns the tag values defined within one custom value dimension, identified by its identifier in the path. Each row carries the modelUuid that POST base/reporting-tags/resolve accepts. Use GET base/custom-dimensions first to learn which dimension identifiers exist. Inactive values are excluded unless includeInactive is true. [Open in the interactive reference](https://docs.api.lensing.ai/?op=listCustomDimensionValues) ## 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`. | | `customDimensionUuid` | path | string | yes | The unique identifier for the custom value dimension whose values to return. Take it from the customDimensionUuid of a GET base/custom-dimensions row. | | `includeInactive` | query | boolean | no | Whether to include values that are no longer active. Defaults to false. | | `pageSize` | query | number | no | The number of values per page, between 1 and 200. Defaults to 50. A value above 200 is treated as 200, not rejected. | | `cursor` | query | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | ## Response `200` Success Under `data` (listCustomDimensionValuesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The values on this page. An empty array when the dimension holds none. A dimension identifier that matches nothing fails with NOT_FOUND instead. | | `items[].dimensionCode` | string | yes | The dimension this value belongs to. Always 'custom' on this endpoint. | | `items[].modelId` | number | yes | The numeric id of the value record, stored internally on a journal entry's allocation line. | | `items[].modelUuid` | string | yes | The unique identifier for the value record. Pass it as modelUuid to POST base/reporting-tags/resolve, paired with dimensionCode 'custom'. | | `items[].name` | string | yes | The display name of the value. | | `items[].isActive` | boolean | yes | Whether the value is currently active and can be assigned to new transactions. | | `items[].customDimensionUuid` | string | no | The unique identifier for the custom dimension this value belongs to. It is the same identifier given in the path. | | `items[].dimensionName` | string | no | The display name of the parent custom dimension. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as the cursor query parameter. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more values remain beyond this page. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | | `INVALID_CURSOR` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, INVALID_CURSOR - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Resolve Reporting Tags `POST /base/reporting-tags/resolve` Group: Base › Reporting Tags · Operation ID: `resolveReportingTags` Decodes a batch of reporting tag references into their display rows, for a consumer that holds many at once such as a page of journal entries to render. Each reference is a dimensionCode and modelUuid pair: a modelUuid is unique within its dimension but not across dimensions, so both halves are needed to identify a tag. This decodes references you already hold and does not enumerate tag values. For custom tags, discover them with GET base/custom-dimensions/{customDimensionUuid}/values. References that do not resolve are absent from the response, so match results on the pair, not by position. At most 100 references per call. [Open in the interactive reference](https://docs.api.lensing.ai/?op=resolveReportingTags) ## 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`. | ## Request body Required, `application/json`: resolveReportingTagsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `reportingTags` | array of object | yes | The references to decode, as dimensionCode and modelUuid pairs. At most 100 per call. More than 100 fails with TOO_MANY_REFERENCES. An empty array returns an empty items array rather than an error. Duplicate references are collapsed, so each distinct reference appears at most once in the response. | | `reportingTags[].dimensionCode` | string | yes | The dimension the reference points into. Accepted values: 'department', 'businessUnit', 'team', 'location', 'region', 'employee', 'vendor', 'customer', 'valueCenter', 'custom'. An unknown code fails the whole call with UNSUPPORTED_DIMENSION_CODE. | | `reportingTags[].modelUuid` | string | yes | The unique identifier of the dimension record the reference points at. It is unique within its dimension but not across dimensions, so dimensionCode is required with it. A value that resolves to nothing is dropped from the response, unlike an unknown dimensionCode. Obtain it from the document that carries the tag, or for custom values from GET base/custom-dimensions/{customDimensionUuid}/values. | Example: ```json { "reportingTags": [ { "dimensionCode": "<dimensionCode>", "modelUuid": "<modelUuid>" } ] } ``` ## Response `200` Success Under `data` (resolveReportingTagsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The resolved rows, in no guaranteed order and possibly shorter than the input, since references that did not resolve are absent. Match on the dimensionCode and modelUuid pair, never on position. | | `items[].dimensionCode` | string | yes | The dimension this value belongs to. Possible values: 'department', 'businessUnit', 'team', 'location', 'region', 'employee', 'vendor', 'customer', 'valueCenter', 'custom'. | | `items[].modelId` | number | yes | The numeric id of the dimension record, stored internally on a journal entry's allocation line. Output only. References are made by modelUuid. | | `items[].modelUuid` | string | yes | The unique identifier of the dimension record this tag references. Present on every row. | | `items[].name` | string | yes | The display name of the tag value. | | `items[].isActive` | boolean | yes | Whether the tag value is currently active and can be assigned to new transactions. A deactivated value still resolves, so existing references keep working. | | `items[].customDimensionUuid` | string | no | The unique identifier for the custom dimension this value belongs to. Present only when dimensionCode is 'custom'. Absent, not null, on the nine system dimensions. | | `items[].dimensionName` | string | no | The display name of the parent custom dimension. Present only when dimensionCode is 'custom'. Absent, not null, on the nine system dimensions. | | `unresolvedCount` | number | yes | How many of the requested references did not resolve. Zero when every reference resolved. Duplicates in the request are collapsed before this is counted. | ## Errors | Code | Status | | --- | --- | | `UNSUPPORTED_DIMENSION_CODE` | 400 | | `TOO_MANY_REFERENCES` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: UNSUPPORTED_DIMENSION_CODE, TOO_MANY_REFERENCES - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # List Opportunities `GET /crm/opportunities` Group: CRM › Opportunities · Operation ID: `listOpportunities` Returns a list of opportunities, most recently created first. To filter by opportunityName, opportunityNumber or crmId, or by more than one value of a field, use POST crm/opportunities/search. Only ids, dates and an exact currency filter this endpoint. To filter by opportunityName, opportunityNumber or crmId, or to pass more than one value of a field, use POST crm/opportunities/search. The item schema is the same here, on GET crm/opportunities/:id and on POST crm/opportunities/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=listOpportunities) ## 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`. | | `entityId` | query | string | no | Limits results to opportunities owned by this legal entity. Omit for every entity. | | `customerId` | query | string | no | Limits results to opportunities for this customer or prospect, matched against either. Omit for every customer. | | `currency` | query | string | no | Limits results to opportunities in this currency. Format ISO 4217, for example 'USD'. Omit for every currency. | | `createdFrom` | query | string | no | Limits results to opportunities created at or after this time, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Inclusive. | | `createdTo` | query | string | no | Limits results to opportunities created at or before this time, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Inclusive. | | `modifiedFrom` | query | string | no | Limits results to opportunities changed at or after this time, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Inclusive. | | `modifiedTo` | query | string | no | Limits results to opportunities changed at or before this time, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Inclusive. | | `cursor` | query | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `pageSize` | query | number | no | The number of opportunities per page. Defaults to 50, and a value above 200 is treated as 200, not rejected. | ## Response `200` Success Under `data` (listOpportunitiesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The opportunities on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the opportunity. | | `items[].opportunityNumber` | string | no | The system-generated opportunity number, for example 'OP-1042'. Absent while none has been assigned. | | `items[].entityId` | string | yes | The legal entity that owns the opportunity. | | `items[].currency` | string | yes | The currency the opportunity is denominated in, and the currency of amount. Format ISO 4217, for example 'USD'. | | `items[].customerId` | string | no | The identifier of the customer or prospect this opportunity is for. customerPath says which node it points at. Absent when it points at neither. See id on GET accounting/customers/:id for a customer, or GET crm/prospects/:id for a prospect. | | `items[].customerPath` | string | no | The node customerId points at. 'urn:evst:everest:fin/accounting:field/node:Customer.id' when customerId is a customer, 'urn:evst:everest:crm:field/node:Prospect.id' when it is a prospect. Absent when neither resolves. | | `items[].endCustomerId` | string | no | The identifier of the end customer behind a partner customer. Absent unless one is on record. See id on GET accounting/customers/:id. | | `items[].priceBookId` | string | yes | The identifier of the price book quotes on this opportunity are priced from. See id on GET accounting/price-books/:id. | | `items[].stageId` | string | yes | The identifier of the stage the opportunity currently sits in. There is no endpoint for it. | | `items[].opportunityName` | string | no | The free-text display name of the opportunity. Absent when none is set. | | `items[].closeDate` | string (date) | no | The expected close date of the deal. Format YYYY-MM-DD. Absent when none is set. | | `items[].amount` | string (decimal) | no | The expected deal amount, as a decimal in the opportunity's currency. Absent when none is set. | | `items[].crmId` | string | no | The identifier of this opportunity in the external system it was imported from, for example a Salesforce record id. It is free text and not unique. Absent when none is set. | | `items[].dataSource` | string | no | The system the record originated from, for example 'Everest' or 'Salesforce'. Absent when none is set. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the opportunity record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].quotes` | array of object | yes | Every quote linked to this opportunity. An empty array when it has none. | | `items[].quotes[].quoteId` | string | yes | The identifier of the linked quote. See id on GET crm/quotes/:id. | | `items[].quotes[].isPrimary` | boolean | yes | Whether this is a primary quote for the opportunity. It is not guaranteed to be true for exactly one quote. | | `pagination` | object | yes | The pagination details for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it back as cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether another page follows. A full page may still be the last, so branch on this. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Get Opportunity `GET /crm/opportunities/{id}` Group: CRM › Opportunities · Operation ID: `getOpportunity` Returns the opportunity with the given identifier. Use the opportunity's id, not its opportunityNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getOpportunity) ## 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`. | | `id` | path | string | yes | The unique identifier for the opportunity to return. | ## Response `200` Success Under `data` (getOpportunityOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the opportunity. | | `opportunityNumber` | string | no | The system-generated opportunity number, for example 'OP-1042'. Absent while none has been assigned. | | `entityId` | string | yes | The legal entity that owns the opportunity. | | `currency` | string | yes | The currency the opportunity is denominated in, and the currency of amount. Format ISO 4217, for example 'USD'. | | `customerId` | string | no | The identifier of the customer or prospect this opportunity is for. customerPath says which node it points at. Absent when it points at neither. See id on GET accounting/customers/:id for a customer, or GET crm/prospects/:id for a prospect. | | `customerPath` | string | no | The node customerId points at. 'urn:evst:everest:fin/accounting:field/node:Customer.id' when customerId is a customer, 'urn:evst:everest:crm:field/node:Prospect.id' when it is a prospect. Absent when neither resolves. | | `endCustomerId` | string | no | The identifier of the end customer behind a partner customer. Absent unless one is on record. See id on GET accounting/customers/:id. | | `priceBookId` | string | yes | The identifier of the price book quotes on this opportunity are priced from. See id on GET accounting/price-books/:id. | | `stageId` | string | yes | The identifier of the stage the opportunity currently sits in. There is no endpoint for it. | | `opportunityName` | string | no | The free-text display name of the opportunity. Absent when none is set. | | `closeDate` | string (date) | no | The expected close date of the deal. Format YYYY-MM-DD. Absent when none is set. | | `amount` | string (decimal) | no | The expected deal amount, as a decimal in the opportunity's currency. Absent when none is set. | | `crmId` | string | no | The identifier of this opportunity in the external system it was imported from, for example a Salesforce record id. It is free text and not unique. Absent when none is set. | | `dataSource` | string | no | The system the record originated from, for example 'Everest' or 'Salesforce'. Absent when none is set. | | `lastModifiedDate` | string (date-time) | no | The date and time the opportunity record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `quotes` | array of object | yes | Every quote linked to this opportunity. An empty array when it has none. | | `quotes[].quoteId` | string | yes | The identifier of the linked quote. See id on GET crm/quotes/:id. | | `quotes[].isPrimary` | boolean | yes | Whether this is a primary quote for the opportunity. It is not guaranteed to be true for exactly one quote. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the opportunities that match the given criteria. `POST /crm/opportunities/search` Group: CRM › Opportunities · Operation ID: `searchOpportunities` Use this endpoint for criteria a query string cannot carry: opportunityName is free text, and every criterion is a list, so you can pass several values of one field at once. Criteria combine with AND, and a list within one criterion is an OR. For example, currencies ['USD', 'EUR'] matches either. The item schema is the same here, on GET crm/opportunities and on GET crm/opportunities/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchOpportunities) ## 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`. | ## Request body Required, `application/json`: searchOpportunitiesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | no | The filters to apply. An opportunity must match every filter you send. Omit all of them to return every opportunity you can see. | | `filters.id` | string | no | Limits results to the opportunity with this id. | | `filters.opportunityNumber` | string | no | Limits results to the opportunity with this number, for example 'OP-1042'. The match is exact. | | `filters.opportunityName` | string | no | Limits results to opportunities whose name contains this text. The match is case-insensitive. | | `filters.entityIds` | array of string | no | Limits results to opportunities owned by any of these legal entities. Omit or pass an empty array for every entity. | | `filters.customerIds` | array of string | no | Limits results to opportunities for any of these customers or prospects, matched against either. Omit or pass an empty array for every customer. | | `filters.currencies` | array of string | no | Limits results to opportunities in any of these currencies. Each is an ISO 4217 code, for example 'USD'. Omit or pass an empty array for every currency. | | `filters.crmId` | string | no | Limits results to opportunities with this external system identifier. The match is exact. | | `filters.createdDate` | object | no | Limits results by when the opportunity was created, in UTC. Both bounds are inclusive, and omitting one leaves that side open. | | `filters.createdDate.start` | string | no | The earliest creation time to include, inclusive. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `filters.createdDate.end` | string | no | The latest creation time to include, inclusive. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `filters.lastModifiedDate` | object | no | Limits results by when the opportunity last changed, in UTC. Both bounds are inclusive, and omitting one leaves that side open. | | `filters.lastModifiedDate.start` | string | no | The earliest change time to include, inclusive. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `filters.lastModifiedDate.end` | string | no | The latest change time to include, inclusive. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The page to return. Omit for the first page at the default size. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of opportunities per page. Defaults to 50, and a value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "opportunityNumber": "<opportunityNumber>", "opportunityName": "<opportunityName>", "entityIds": [ "<entityIds>" ], "customerIds": [ "<customerIds>" ], "currencies": [ "<currencies>" ], "crmId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "createdDate": { "start": "<start>", "end": "<end>" }, "lastModifiedDate": { "start": "<start>", "end": "<end>" } }, "page": { "cursor": "<cursor>", "size": 0 } } ``` ## Response `200` Success Under `data` (searchOpportunitiesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The opportunities on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the opportunity. | | `items[].opportunityNumber` | string | no | The system-generated opportunity number, for example 'OP-1042'. Absent while none has been assigned. | | `items[].entityId` | string | yes | The legal entity that owns the opportunity. | | `items[].currency` | string | yes | The currency the opportunity is denominated in, and the currency of amount. Format ISO 4217, for example 'USD'. | | `items[].customerId` | string | no | The identifier of the customer or prospect this opportunity is for. customerPath says which node it points at. Absent when it points at neither. See id on GET accounting/customers/:id for a customer, or GET crm/prospects/:id for a prospect. | | `items[].customerPath` | string | no | The node customerId points at. 'urn:evst:everest:fin/accounting:field/node:Customer.id' when customerId is a customer, 'urn:evst:everest:crm:field/node:Prospect.id' when it is a prospect. Absent when neither resolves. | | `items[].endCustomerId` | string | no | The identifier of the end customer behind a partner customer. Absent unless one is on record. See id on GET accounting/customers/:id. | | `items[].priceBookId` | string | yes | The identifier of the price book quotes on this opportunity are priced from. See id on GET accounting/price-books/:id. | | `items[].stageId` | string | yes | The identifier of the stage the opportunity currently sits in. There is no endpoint for it. | | `items[].opportunityName` | string | no | The free-text display name of the opportunity. Absent when none is set. | | `items[].closeDate` | string (date) | no | The expected close date of the deal. Format YYYY-MM-DD. Absent when none is set. | | `items[].amount` | string (decimal) | no | The expected deal amount, as a decimal in the opportunity's currency. Absent when none is set. | | `items[].crmId` | string | no | The identifier of this opportunity in the external system it was imported from, for example a Salesforce record id. It is free text and not unique. Absent when none is set. | | `items[].dataSource` | string | no | The system the record originated from, for example 'Everest' or 'Salesforce'. Absent when none is set. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the opportunity record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].quotes` | array of object | yes | Every quote linked to this opportunity. An empty array when it has none. | | `items[].quotes[].quoteId` | string | yes | The identifier of the linked quote. See id on GET crm/quotes/:id. | | `items[].quotes[].isPrimary` | boolean | yes | Whether this is a primary quote for the opportunity. It is not guaranteed to be true for exactly one quote. | | `pagination` | object | yes | The pagination details for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it back as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether another page follows. A full page may still be the last, so branch on this. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # List Prospects `GET /crm/prospects` Group: CRM › Prospects · Operation ID: `listProspects` Returns a list of prospects, most recently created first. To filter by prospectName, billingEmail, crmId or prospectNumber, use POST crm/prospects/search. Only ids, dates, customerType, dataSource, defaultCurrency and booleans filter this endpoint. To filter by prospectName, billingEmail, crmId or prospectNumber, use POST crm/prospects/search. converted true returns the prospects already turned into customers, false those not yet converted. An unrecognized customerType or dataSource returns an empty page, not an error. GET crm/prospects omits contacts[], which GET crm/prospects/:id and POST crm/prospects/search return. [Open in the interactive reference](https://docs.api.lensing.ai/?op=listProspects) ## 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`. | | `prospectId` | query | string | no | Limits results to the prospect with this id. | | `customerType` | query | string | no | Limits results to prospects of this commercial relationship. Possible values: 'default', 'partner', 'endCustomer'. Any other value matches no prospect. Omit for every type. | | `dataSource` | query | string | no | Limits results to prospects that originated in this system, for example 'Everest', 'Salesforce' or 'HubSpot'. A value naming no source matches no prospect. Omit for every source. | | `defaultCurrency` | query | string | no | Limits results to prospects whose default currency is this code. Format ISO 4217, for example 'USD'. Omit for every currency. | | `entityId` | query | string | no | Limits results to prospects owned by this legal entity. Omit for every entity. | | `customerId` | query | string | no | Limits results to prospects converted to this customer. Omit for every customer. | | `converted` | query | boolean | no | Limits results to prospects already converted to a customer when true, and to those not yet converted when false. Omit for both. | | `sameAsBillToAddress` | query | boolean | no | Limits results to prospects whose sold-to address repeats their bill-to address when true, and to the rest when false. Omit for both. | | `modifiedFrom` | query | string | no | Limits results to prospects changed at or after this time, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Inclusive. | | `modifiedTo` | query | string | no | Limits results to prospects changed at or before this time, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Inclusive. | | `cursor` | query | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `pageSize` | query | number | no | The number of prospects per page. Defaults to 50, and a value above 200 is treated as 200, not rejected. | ## Response `200` Success Under `data` (listProspectsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The prospects on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the prospect. | | `items[].prospectNumber` | string | no | The prospect number, for example 'PR-1042'. Absent while none has been assigned. | | `items[].prospectName` | string | yes | The name of the prospect organization. | | `items[].billingEmail` | string | no | The email address invoices for this prospect would be sent to. | | `items[].crmId` | string | no | The identifier of this prospect in the external CRM system it was integrated from. | | `items[].customerType` | string | no | The commercial relationship. Possible values: 'default', 'partner', 'endCustomer'. | | `items[].dataSource` | string | no | The system the prospect originated in, for example 'Everest', 'Salesforce' or 'HubSpot'. | | `items[].defaultCurrency` | string | no | The currency quotes for this prospect default to. Format ISO 4217, for example 'USD'. | | `items[].entityId` | string | yes | The legal entity that owns the prospect. | | `items[].entityName` | string | no | The name of the legal entity that owns the prospect. See entityName on GET base/entities. | | `items[].customerId` | string | no | The identifier of the customer created from this prospect. Absent until the prospect has been converted, so its presence marks a converted prospect. See id on GET accounting/customers/:id. | | `items[].sameAsBillToAddress` | boolean | no | Whether the sold-to address repeats the bill-to address. soldToAddress is populated either way, so you never have to resolve this flag to read an address. | | `items[].billToAddress` | object | no | The prospect's bill-to address, with the default contact for that role. Absent when none is recorded. | | `items[].billToAddress.id` | string | yes | The unique identifier for the address. | | `items[].billToAddress.line1` | string | no | The first address line: street number and name, or PO box. | | `items[].billToAddress.line2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `items[].billToAddress.city` | string | no | The city or locality. | | `items[].billToAddress.stateProvince` | string | no | The state, province or region. Absent where the country has no such division. | | `items[].billToAddress.country` | string | no | The country, as a name or ISO 3166-1 code. | | `items[].billToAddress.zipCode` | string | no | The postal or ZIP code. | | `items[].billToAddress.attention` | string | no | The person or department that mail to this address is for. | | `items[].billToAddress.contact` | object | no | The default contact for this address role. Absent when the prospect has no default contact for it. | | `items[].billToAddress.contact.id` | string | yes | The unique identifier for the contact. | | `items[].billToAddress.contact.displayName` | string | no | The contact's display name, assembled from the name parts. | | `items[].billToAddress.contact.salutation` | string | no | The contact's form of address, for example 'Ms' or 'Dr'. | | `items[].billToAddress.contact.firstName` | string | no | The contact's given name. | | `items[].billToAddress.contact.middleName` | string | no | The contact's middle name or initial. | | `items[].billToAddress.contact.lastName` | string | no | The contact's family name. | | `items[].billToAddress.contact.suffix` | string | no | The contact's name suffix, for example 'Jr' or 'PhD'. | | `items[].billToAddress.contact.emailAddress` | string | no | The email address the contact is reached at. | | `items[].billToAddress.contact.workPhone` | string | no | The contact's work telephone number. | | `items[].soldToAddress` | object | no | The prospect's sold-to address, with the default contact for that role. Absent when none is recorded. | | `items[].soldToAddress.id` | string | yes | The unique identifier for the address. | | `items[].soldToAddress.line1` | string | no | The first address line: street number and name, or PO box. | | `items[].soldToAddress.line2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `items[].soldToAddress.city` | string | no | The city or locality. | | `items[].soldToAddress.stateProvince` | string | no | The state, province or region. Absent where the country has no such division. | | `items[].soldToAddress.country` | string | no | The country, as a name or ISO 3166-1 code. | | `items[].soldToAddress.zipCode` | string | no | The postal or ZIP code. | | `items[].soldToAddress.attention` | string | no | The person or department that mail to this address is for. | | `items[].soldToAddress.contact` | object | no | The default contact for this address role. Absent when the prospect has no default contact for it. | | `items[].soldToAddress.contact.id` | string | yes | The unique identifier for the contact. | | `items[].soldToAddress.contact.displayName` | string | no | The contact's display name, assembled from the name parts. | | `items[].soldToAddress.contact.salutation` | string | no | The contact's form of address, for example 'Ms' or 'Dr'. | | `items[].soldToAddress.contact.firstName` | string | no | The contact's given name. | | `items[].soldToAddress.contact.middleName` | string | no | The contact's middle name or initial. | | `items[].soldToAddress.contact.lastName` | string | no | The contact's family name. | | `items[].soldToAddress.contact.suffix` | string | no | The contact's name suffix, for example 'Jr' or 'PhD'. | | `items[].soldToAddress.contact.emailAddress` | string | no | The email address the contact is reached at. | | `items[].soldToAddress.contact.workPhone` | string | no | The contact's work telephone number. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the prospect record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination details for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it back as cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether another page follows. A full page may still be the last, so branch on this. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Get Prospect `GET /crm/prospects/{id}` Group: CRM › Prospects · Operation ID: `getProspect` Returns the prospect with the given identifier. Use the prospect's id, not its prospectNumber. Returns one prospect by its id. To look a prospect up by its number, use POST crm/prospects/search with prospectNumber. This endpoint and POST crm/prospects/search return contacts[], every contact recorded against the prospect. GET crm/prospects omits it. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getProspect) ## 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`. | | `id` | path | string | yes | The unique identifier for the prospect to return. | ## Response `200` Success Under `data` (getProspectOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the prospect. | | `prospectNumber` | string | no | The prospect number, for example 'PR-1042'. Absent while none has been assigned. | | `prospectName` | string | yes | The name of the prospect organization. | | `billingEmail` | string | no | The email address invoices for this prospect would be sent to. | | `crmId` | string | no | The identifier of this prospect in the external CRM system it was integrated from. | | `customerType` | string | no | The commercial relationship. Possible values: 'default', 'partner', 'endCustomer'. | | `dataSource` | string | no | The system the prospect originated in, for example 'Everest', 'Salesforce' or 'HubSpot'. | | `defaultCurrency` | string | no | The currency quotes for this prospect default to. Format ISO 4217, for example 'USD'. | | `entityId` | string | yes | The legal entity that owns the prospect. | | `entityName` | string | no | The name of the legal entity that owns the prospect. See entityName on GET base/entities. | | `customerId` | string | no | The identifier of the customer created from this prospect. Absent until the prospect has been converted, so its presence marks a converted prospect. See id on GET accounting/customers/:id. | | `sameAsBillToAddress` | boolean | no | Whether the sold-to address repeats the bill-to address. soldToAddress is populated either way, so you never have to resolve this flag to read an address. | | `billToAddress` | object | no | The prospect's bill-to address, with the default contact for that role. Absent when none is recorded. | | `billToAddress.id` | string | yes | The unique identifier for the address. | | `billToAddress.line1` | string | no | The first address line: street number and name, or PO box. | | `billToAddress.line2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `billToAddress.city` | string | no | The city or locality. | | `billToAddress.stateProvince` | string | no | The state, province or region. Absent where the country has no such division. | | `billToAddress.country` | string | no | The country, as a name or ISO 3166-1 code. | | `billToAddress.zipCode` | string | no | The postal or ZIP code. | | `billToAddress.attention` | string | no | The person or department that mail to this address is for. | | `billToAddress.contact` | object | no | The default contact for this address role. Absent when the prospect has no default contact for it. | | `billToAddress.contact.id` | string | yes | The unique identifier for the contact. | | `billToAddress.contact.displayName` | string | no | The contact's display name, assembled from the name parts. | | `billToAddress.contact.salutation` | string | no | The contact's form of address, for example 'Ms' or 'Dr'. | | `billToAddress.contact.firstName` | string | no | The contact's given name. | | `billToAddress.contact.middleName` | string | no | The contact's middle name or initial. | | `billToAddress.contact.lastName` | string | no | The contact's family name. | | `billToAddress.contact.suffix` | string | no | The contact's name suffix, for example 'Jr' or 'PhD'. | | `billToAddress.contact.emailAddress` | string | no | The email address the contact is reached at. | | `billToAddress.contact.workPhone` | string | no | The contact's work telephone number. | | `soldToAddress` | object | no | The prospect's sold-to address, with the default contact for that role. Absent when none is recorded. | | `soldToAddress.id` | string | yes | The unique identifier for the address. | | `soldToAddress.line1` | string | no | The first address line: street number and name, or PO box. | | `soldToAddress.line2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `soldToAddress.city` | string | no | The city or locality. | | `soldToAddress.stateProvince` | string | no | The state, province or region. Absent where the country has no such division. | | `soldToAddress.country` | string | no | The country, as a name or ISO 3166-1 code. | | `soldToAddress.zipCode` | string | no | The postal or ZIP code. | | `soldToAddress.attention` | string | no | The person or department that mail to this address is for. | | `soldToAddress.contact` | object | no | The default contact for this address role. Absent when the prospect has no default contact for it. | | `soldToAddress.contact.id` | string | yes | The unique identifier for the contact. | | `soldToAddress.contact.displayName` | string | no | The contact's display name, assembled from the name parts. | | `soldToAddress.contact.salutation` | string | no | The contact's form of address, for example 'Ms' or 'Dr'. | | `soldToAddress.contact.firstName` | string | no | The contact's given name. | | `soldToAddress.contact.middleName` | string | no | The contact's middle name or initial. | | `soldToAddress.contact.lastName` | string | no | The contact's family name. | | `soldToAddress.contact.suffix` | string | no | The contact's name suffix, for example 'Jr' or 'PhD'. | | `soldToAddress.contact.emailAddress` | string | no | The email address the contact is reached at. | | `soldToAddress.contact.workPhone` | string | no | The contact's work telephone number. | | `lastModifiedDate` | string (date-time) | yes | The date and time the prospect record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `contacts` | array of object | yes | Every contact recorded against the prospect, with the two per-role defaults flagged. An empty array when the prospect has none. GET crm/prospects/:id and POST crm/prospects/search return this array. GET crm/prospects omits it. | | `contacts[].id` | string | yes | The unique identifier for the contact. | | `contacts[].displayName` | string | no | The contact's display name, assembled from the name parts. | | `contacts[].salutation` | string | no | The contact's form of address, for example 'Ms' or 'Dr'. | | `contacts[].firstName` | string | no | The contact's given name. | | `contacts[].middleName` | string | no | The contact's middle name or initial. | | `contacts[].lastName` | string | no | The contact's family name. | | `contacts[].suffix` | string | no | The contact's name suffix, for example 'Jr' or 'PhD'. | | `contacts[].emailAddress` | string | no | The email address the contact is reached at. | | `contacts[].workPhone` | string | no | The contact's work telephone number. | | `contacts[].isDefaultBillToContact` | boolean | yes | Whether this is the default contact for the prospect's bill-to address. At most one contact is. | | `contacts[].isDefaultSoldToContact` | boolean | yes | Whether this is the default contact for the prospect's sold-to address. At most one contact is. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the prospects that match the given criteria. `POST /crm/prospects/search` Group: CRM › Prospects · Operation ID: `searchProspects` Use this endpoint for criteria a query string cannot carry: text matches the prospect name and billingEmail an email, each of customerType, dataSource and defaultCurrency takes several values at once, and crmId and prospectNumber are kept out of the URL. Criteria combine with AND, and a list within one criterion is an OR. A match returns the prospect in full, including contacts[], which GET crm/prospects omits. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchProspects) ## 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`. | ## Request body Required, `application/json`: searchProspectsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `text` | string | no | Limits results to prospects whose name contains this text. The match is case-insensitive. | | `billingEmail` | string | no | Limits results to the prospect with this billing email address. The match is exact. | | `crmId` | string | no | Limits results to the prospect with this identifier in the external CRM system. The match is exact. | | `prospectNumber` | string | no | Limits results to the prospect with this number, for example 'PR-1042'. The match is exact. | | `customerType` | array of string, one of `"default"`, `"partner"`, `"endCustomer"` | no | Limits results to prospects of any of these commercial relationships. Possible values: 'default', 'partner', 'endCustomer'. Omit or pass an empty array for every type. | | `dataSource` | array of string | no | Limits results to prospects that originated in any of these systems, for example 'Everest', 'Salesforce' or 'HubSpot'. Omit or pass an empty array for every source. | | `defaultCurrency` | array of string | no | Limits results to prospects whose default currency is any of these codes. Each is an ISO 4217 code, for example 'USD'. Omit or pass an empty array for every currency. | | `entityId` | string | no | Limits results to prospects owned by this legal entity. Omit for every entity. | | `customerId` | string | no | Limits results to prospects converted to this customer. Omit for every customer. | | `converted` | boolean | no | Limits results to prospects already converted to a customer when true, and to those not yet converted when false. Omit for both. | | `sameAsBillToAddress` | boolean | no | Limits results to prospects whose sold-to address repeats their bill-to address when true, and to the rest when false. Omit for both. | | `lastModifiedDate` | object | no | Limits results by when the prospect last changed, in UTC. Both bounds are inclusive, and omitting one leaves that side open. | | `lastModifiedDate.start` | string | no | The earliest change time to include, inclusive. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate.end` | string | no | The latest change time to include, inclusive. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The page to return. Omit for the first page at the default size. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of prospects per page. Defaults to 50, and a value above 200 is treated as 200, not rejected. | Example: ```json { "text": "northwind", "billingEmail": "<billingEmail>", "crmId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "prospectNumber": "<prospectNumber>", "customerType": [ "partner", "endCustomer" ], "dataSource": [ "<dataSource>" ], "defaultCurrency": [ "USD" ], "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "converted": true, "sameAsBillToAddress": true, "lastModifiedDate": { "start": "<start>", "end": "<end>" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchProspectsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The prospects on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the prospect. | | `items[].prospectNumber` | string | no | The prospect number, for example 'PR-1042'. Absent while none has been assigned. | | `items[].prospectName` | string | yes | The name of the prospect organization. | | `items[].billingEmail` | string | no | The email address invoices for this prospect would be sent to. | | `items[].crmId` | string | no | The identifier of this prospect in the external CRM system it was integrated from. | | `items[].customerType` | string | no | The commercial relationship. Possible values: 'default', 'partner', 'endCustomer'. | | `items[].dataSource` | string | no | The system the prospect originated in, for example 'Everest', 'Salesforce' or 'HubSpot'. | | `items[].defaultCurrency` | string | no | The currency quotes for this prospect default to. Format ISO 4217, for example 'USD'. | | `items[].entityId` | string | yes | The legal entity that owns the prospect. | | `items[].entityName` | string | no | The name of the legal entity that owns the prospect. See entityName on GET base/entities. | | `items[].customerId` | string | no | The identifier of the customer created from this prospect. Absent until the prospect has been converted, so its presence marks a converted prospect. See id on GET accounting/customers/:id. | | `items[].sameAsBillToAddress` | boolean | no | Whether the sold-to address repeats the bill-to address. soldToAddress is populated either way, so you never have to resolve this flag to read an address. | | `items[].billToAddress` | object | no | The prospect's bill-to address, with the default contact for that role. Absent when none is recorded. | | `items[].billToAddress.id` | string | yes | The unique identifier for the address. | | `items[].billToAddress.line1` | string | no | The first address line: street number and name, or PO box. | | `items[].billToAddress.line2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `items[].billToAddress.city` | string | no | The city or locality. | | `items[].billToAddress.stateProvince` | string | no | The state, province or region. Absent where the country has no such division. | | `items[].billToAddress.country` | string | no | The country, as a name or ISO 3166-1 code. | | `items[].billToAddress.zipCode` | string | no | The postal or ZIP code. | | `items[].billToAddress.attention` | string | no | The person or department that mail to this address is for. | | `items[].billToAddress.contact` | object | no | The default contact for this address role. Absent when the prospect has no default contact for it. | | `items[].billToAddress.contact.id` | string | yes | The unique identifier for the contact. | | `items[].billToAddress.contact.displayName` | string | no | The contact's display name, assembled from the name parts. | | `items[].billToAddress.contact.salutation` | string | no | The contact's form of address, for example 'Ms' or 'Dr'. | | `items[].billToAddress.contact.firstName` | string | no | The contact's given name. | | `items[].billToAddress.contact.middleName` | string | no | The contact's middle name or initial. | | `items[].billToAddress.contact.lastName` | string | no | The contact's family name. | | `items[].billToAddress.contact.suffix` | string | no | The contact's name suffix, for example 'Jr' or 'PhD'. | | `items[].billToAddress.contact.emailAddress` | string | no | The email address the contact is reached at. | | `items[].billToAddress.contact.workPhone` | string | no | The contact's work telephone number. | | `items[].soldToAddress` | object | no | The prospect's sold-to address, with the default contact for that role. Absent when none is recorded. | | `items[].soldToAddress.id` | string | yes | The unique identifier for the address. | | `items[].soldToAddress.line1` | string | no | The first address line: street number and name, or PO box. | | `items[].soldToAddress.line2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `items[].soldToAddress.city` | string | no | The city or locality. | | `items[].soldToAddress.stateProvince` | string | no | The state, province or region. Absent where the country has no such division. | | `items[].soldToAddress.country` | string | no | The country, as a name or ISO 3166-1 code. | | `items[].soldToAddress.zipCode` | string | no | The postal or ZIP code. | | `items[].soldToAddress.attention` | string | no | The person or department that mail to this address is for. | | `items[].soldToAddress.contact` | object | no | The default contact for this address role. Absent when the prospect has no default contact for it. | | `items[].soldToAddress.contact.id` | string | yes | The unique identifier for the contact. | | `items[].soldToAddress.contact.displayName` | string | no | The contact's display name, assembled from the name parts. | | `items[].soldToAddress.contact.salutation` | string | no | The contact's form of address, for example 'Ms' or 'Dr'. | | `items[].soldToAddress.contact.firstName` | string | no | The contact's given name. | | `items[].soldToAddress.contact.middleName` | string | no | The contact's middle name or initial. | | `items[].soldToAddress.contact.lastName` | string | no | The contact's family name. | | `items[].soldToAddress.contact.suffix` | string | no | The contact's name suffix, for example 'Jr' or 'PhD'. | | `items[].soldToAddress.contact.emailAddress` | string | no | The email address the contact is reached at. | | `items[].soldToAddress.contact.workPhone` | string | no | The contact's work telephone number. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the prospect record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].contacts` | array of object | yes | Every contact recorded against the prospect, with the two per-role defaults flagged. An empty array when the prospect has none. GET crm/prospects/:id and POST crm/prospects/search return this array. GET crm/prospects omits it. | | `items[].contacts[].id` | string | yes | The unique identifier for the contact. | | `items[].contacts[].displayName` | string | no | The contact's display name, assembled from the name parts. | | `items[].contacts[].salutation` | string | no | The contact's form of address, for example 'Ms' or 'Dr'. | | `items[].contacts[].firstName` | string | no | The contact's given name. | | `items[].contacts[].middleName` | string | no | The contact's middle name or initial. | | `items[].contacts[].lastName` | string | no | The contact's family name. | | `items[].contacts[].suffix` | string | no | The contact's name suffix, for example 'Jr' or 'PhD'. | | `items[].contacts[].emailAddress` | string | no | The email address the contact is reached at. | | `items[].contacts[].workPhone` | string | no | The contact's work telephone number. | | `items[].contacts[].isDefaultBillToContact` | boolean | yes | Whether this is the default contact for the prospect's bill-to address. At most one contact is. | | `items[].contacts[].isDefaultSoldToContact` | boolean | yes | Whether this is the default contact for the prospect's sold-to address. At most one contact is. | | `pagination` | object | yes | The pagination details for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it back as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether another page follows. A full page may still be the last, so branch on this. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # List Quotes `GET /crm/quotes` Group: CRM › Quotes · Operation ID: `listQuotes` Returns a list of quotes, most recently created first. To filter by memo text, quote number or sales arrangement, or by more than one value of a field, use POST crm/quotes/search. Only ids, enums, booleans and dates filter this endpoint. To filter by the memo text, quoteNumber or salesArrangementId, or to pass more than one value of a field, use POST crm/quotes/search. An unrecognized filter value matches nothing rather than failing. The item schema here is the quote header alone. GET crm/quotes/:id and POST crm/quotes/search also return the quote's priced lines. [Open in the interactive reference](https://docs.api.lensing.ai/?op=listQuotes) ## 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`. | | `quoteId` | query | string | no | Limits results to the quote with this id. | | `quoteType` | query | string | no | Limits results to quotes of this kind. Possible values: 'new', 'upsell', 'renewal', 'changeOrder'. Omit for every kind. | | `status` | query | string | no | Limits results to quotes in this business state. Possible values: 'created', 'underApproval', 'approved', 'underSigning', 'signed', 'completed'. Omit for every state. | | `approvalStatus` | query | string | no | Limits results to quotes in this approval state. Possible values: 'pendingSubmission', 'pendingApproval', 'approved', 'rejected'. Omit for every state. | | `isPrimary` | query | boolean | no | Limits results to primary quotes when true, or non-primary quotes when false. Pair with opportunityId to find an opportunity's primary quote. Omit for both. | | `opportunityId` | query | string | no | Limits results to quotes of this opportunity. Omit for every opportunity. | | `quoteDateFrom` | query | string | no | Limits results to quotes drawn up on or after this date. Format YYYY-MM-DD. Inclusive. | | `quoteDateTo` | query | string | no | Limits results to quotes drawn up on or before this date. Format YYYY-MM-DD. Inclusive. | | `modifiedFrom` | query | string | no | Limits results to quotes changed at or after this time, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Inclusive. | | `modifiedTo` | query | string | no | Limits results to quotes changed at or before this time, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Inclusive. | | `cursor` | query | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `pageSize` | query | number | no | The number of quotes per page. Defaults to 50, and a value above 200 is treated as 200, not rejected. | ## Response `200` Success Under `data` (listQuotesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The quotes on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the quote. | | `items[].quoteNumber` | string | no | The system-generated quote number, for example 'QT-1042'. Absent while none has been assigned. | | `items[].quoteType` | string | yes | The kind of quote. Possible values: 'new', 'upsell', 'renewal', 'changeOrder'. | | `items[].status` | string | no | Where the quote stands in its business process. Possible values: 'created', 'underApproval', 'approved', 'underSigning', 'signed', 'completed'. Absent on a quote that has not entered the process. | | `items[].approvalStatus` | string | no | Where the quote stands in its approval process. Possible values: 'pendingSubmission', 'pendingApproval', 'approved', 'rejected'. Absent until the approval process has run. | | `items[].isPrimary` | boolean | yes | Whether this is the primary quote of its opportunity. | | `items[].quoteDate` | string (date) | yes | The date the quote was drawn up. Format YYYY-MM-DD. Product prices are validated against the opportunity's price book as of this date. | | `items[].serviceStartDate` | string (date) | yes | The first date products or services on this quote are delivered. Format YYYY-MM-DD. No subscription on the quote starts before it. | | `items[].signDate` | string (date) | no | The date the quote was signed by the customer and the legal entity. Format YYYY-MM-DD. Absent when not recorded. | | `items[].paymentTerm` | string | yes | The code of the payment term applied to invoices created from this quote. See code on GET accounting/payment-terms. | | `items[].memo` | string | no | A free-text memo recorded on the quote. Absent when none is set. | | `items[].needsPSACalculation` | boolean | no | Whether the quote takes its product data from a PSA project rather than from the product catalog. | | `items[].currency` | string | no | The currency of every amount on the quote. A quote takes the currency of its opportunity and has none of its own. Format ISO 4217, for example 'USD'. | | `items[].totalPrice` | string (decimal) | no | The sum of the quote's lines before tax, less any one-time discount, in the quote's currency. | | `items[].oneTimeDiscountAmount` | string (decimal) | no | A one-time discount applied to the whole quote, in the quote's currency. | | `items[].oneTimeDiscountAllocation` | string | no | How oneTimeDiscountAmount is spread across the quote's invoices. Possible values: 'firstInvoice', 'alignToInvoiceFrequency', 'custom'. | | `items[].billableTimeEstimate` | string (decimal) | no | The estimated total of the quote's billable-time lines, in the quote's currency. | | `items[].billableTimeCappedPrice` | string (decimal) | no | The ceiling on the total of the quote's billable-time lines, in the quote's currency. | | `items[].opportunityId` | string | yes | The opportunity this quote belongs to. See id on GET crm/opportunities/:id. | | `items[].opportunityName` | string | no | The name of the opportunity this quote belongs to. Absent when the opportunity has no name. | | `items[].opportunityNumber` | string | no | The opportunity number of the opportunity this quote belongs to, for example 'OP-1042'. Absent while none has been assigned. | | `items[].billToAddressId` | string | no | The bill-to address on the quote, a copy the quote keeps of its own rather than the source it was copied from. There is no endpoint for it. | | `items[].soldToAddressId` | string | no | The sold-to address on the quote, a copy the quote keeps of its own rather than the source it was copied from. There is no endpoint for it. | | `items[].salesArrangementId` | string | no | The sales arrangement this quote is assigned to. Set on change-order quotes. There is no endpoint for it. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the quote record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination details for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it back as cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether another page follows. A full page may still be the last, so branch on this. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Get Quote `GET /crm/quotes/{id}` Group: CRM › Quotes · Operation ID: `getQuote` Returns the quote with the given identifier. Use the quote's id, not its quoteNumber. Both this endpoint and POST crm/quotes/search return the whole quote. The header comes with subscriptions[], the subscriptions on the quote, and nonRecurringProducts[], the products that belong to no subscription. The products under a subscription are in subscriptions[].quoteProducts[], and both product sets share the same schema. GET crm/quotes returns the header alone. Every amount in the response is in the quote's currency. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getQuote) ## 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`. | | `id` | path | string | yes | The unique identifier for the quote to return. | ## Response `200` Success Under `data` (getQuoteOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the quote. | | `quoteNumber` | string | no | The system-generated quote number, for example 'QT-1042'. Absent while none has been assigned. | | `quoteType` | string | yes | The kind of quote. Possible values: 'new', 'upsell', 'renewal', 'changeOrder'. | | `status` | string | no | Where the quote stands in its business process. Possible values: 'created', 'underApproval', 'approved', 'underSigning', 'signed', 'completed'. Absent on a quote that has not entered the process. | | `approvalStatus` | string | no | Where the quote stands in its approval process. Possible values: 'pendingSubmission', 'pendingApproval', 'approved', 'rejected'. Absent until the approval process has run. | | `isPrimary` | boolean | yes | Whether this is the primary quote of its opportunity. | | `quoteDate` | string (date) | yes | The date the quote was drawn up. Format YYYY-MM-DD. Product prices are validated against the opportunity's price book as of this date. | | `serviceStartDate` | string (date) | yes | The first date products or services on this quote are delivered. Format YYYY-MM-DD. No subscription on the quote starts before it. | | `signDate` | string (date) | no | The date the quote was signed by the customer and the legal entity. Format YYYY-MM-DD. Absent when not recorded. | | `paymentTerm` | string | yes | The code of the payment term applied to invoices created from this quote. See code on GET accounting/payment-terms. | | `memo` | string | no | A free-text memo recorded on the quote. Absent when none is set. | | `needsPSACalculation` | boolean | no | Whether the quote takes its product data from a PSA project rather than from the product catalog. | | `currency` | string | no | The currency of every amount on the quote. A quote takes the currency of its opportunity and has none of its own. Format ISO 4217, for example 'USD'. | | `totalPrice` | string (decimal) | no | The sum of the quote's lines before tax, less any one-time discount, in the quote's currency. | | `oneTimeDiscountAmount` | string (decimal) | no | A one-time discount applied to the whole quote, in the quote's currency. | | `oneTimeDiscountAllocation` | string | no | How oneTimeDiscountAmount is spread across the quote's invoices. Possible values: 'firstInvoice', 'alignToInvoiceFrequency', 'custom'. | | `billableTimeEstimate` | string (decimal) | no | The estimated total of the quote's billable-time lines, in the quote's currency. | | `billableTimeCappedPrice` | string (decimal) | no | The ceiling on the total of the quote's billable-time lines, in the quote's currency. | | `opportunityId` | string | yes | The opportunity this quote belongs to. See id on GET crm/opportunities/:id. | | `opportunityName` | string | no | The name of the opportunity this quote belongs to. Absent when the opportunity has no name. | | `opportunityNumber` | string | no | The opportunity number of the opportunity this quote belongs to, for example 'OP-1042'. Absent while none has been assigned. | | `billToAddressId` | string | no | The bill-to address on the quote, a copy the quote keeps of its own rather than the source it was copied from. There is no endpoint for it. | | `soldToAddressId` | string | no | The sold-to address on the quote, a copy the quote keeps of its own rather than the source it was copied from. There is no endpoint for it. | | `salesArrangementId` | string | no | The sales arrangement this quote is assigned to. Set on change-order quotes. There is no endpoint for it. | | `lastModifiedDate` | string (date-time) | yes | The date and time the quote record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `subscriptions` | array of object | yes | The subscriptions on the quote, one per subscription, each with the products under it. An empty array when the quote has no recurring products. GET crm/quotes omits it and returns the quote header alone. | | `subscriptions[].id` | string | yes | The unique identifier for the quote subscription. | | `subscriptions[].termType` | string | yes | Whether the subscription runs to a fixed end date or renews indefinitely. Possible values: 'termed', 'evergreen'. | | `subscriptions[].serviceStartDate` | string (date) | yes | The first date the subscription delivers service. Format YYYY-MM-DD. Never earlier than the quote's serviceStartDate. | | `subscriptions[].serviceEndDate` | string (date) | no | The last date the subscription delivers service. Format YYYY-MM-DD. Set on a termed subscription and absent on an evergreen one. | | `subscriptions[].autoRenew` | boolean | no | Whether the subscription renews automatically at the end of its term. | | `subscriptions[].renewalTerm` | number | no | The renewal term in months. Meaningful only when autoRenew is true. | | `subscriptions[].quoteProducts` | array of object | yes | The products in this subscription, each with its priced lines. | | `subscriptions[].quoteProducts[].id` | string | yes | The unique identifier for the quote product. | | `subscriptions[].quoteProducts[].quoteProductNumber` | string | no | The system-assigned number of the quote product. Absent while none has been assigned. | | `subscriptions[].quoteProducts[].productId` | string | no | The catalog product for this quote product. See id on GET accounting/products/:id. | | `subscriptions[].quoteProducts[].productCode` | string | no | The code of that product in the catalog. | | `subscriptions[].quoteProducts[].productName` | string | no | The name of that product in the catalog. | | `subscriptions[].quoteProducts[].isRampDeal` | boolean | no | Whether this product is priced as a ramp deal, with quantities or prices stepping over time. | | `subscriptions[].quoteProducts[].rampInterval` | string | no | How the ramp steps. Possible values: 'annually', 'specificDates'. Absent unless isRampDeal is true. | | `subscriptions[].quoteProducts[].rampInvoiceTiming` | string | no | How a ramp deal is invoiced. Possible values: 'allAtOnce', 'perInterval'. Absent unless isRampDeal is true. | | `subscriptions[].quoteProducts[].orderIndex` | number | no | The position of this product within the quote, for display ordering. | | `subscriptions[].quoteProducts[].quoteProductLines` | array of object | yes | The priced lines of this quote product. A product has at least one. | | `subscriptions[].quoteProducts[].quoteProductLines[].id` | string | yes | The unique identifier for the quote product line. | | `subscriptions[].quoteProducts[].quoteProductLines[].quoteProductLineNumber` | string | no | The system-assigned number of the quote product line. Absent while none has been assigned. | | `subscriptions[].quoteProducts[].quoteProductLines[].productLineId` | string | no | The catalog product line this line was priced from. See id on GET accounting/product-lines/:id. | | `subscriptions[].quoteProducts[].quoteProductLines[].productLineName` | string | no | The name of that catalog product line. | | `subscriptions[].quoteProducts[].quoteProductLines[].parentQuoteProductLineId` | string | no | The parent line this line hangs off, as an allowance or overage line does. Absent on a top-level line. | | `subscriptions[].quoteProducts[].quoteProductLines[].lineType` | string | yes | What this line charges for. Possible values: 'recurring', 'oneTime', 'commitWithOverage', 'payAsYouGo', 'usage', 'prepaid', 'billableTime', 'billableTravelExpense', 'billablePassThrough'. | | `subscriptions[].quoteProducts[].quoteProductLines[].quantity` | string (decimal) | no | The quantity on this line, as a count, not an amount. For a ramp deal the per-period quantity is on each ramp instead. | | `subscriptions[].quoteProducts[].quoteProductLines[].listPrice` | string (decimal) | no | The per-unit list price before discount, in the quote's currency. | | `subscriptions[].quoteProducts[].quoteProductLines[].unitPrice` | string (decimal) | no | The per-unit price after discount, in the quote's currency. | | `subscriptions[].quoteProducts[].quoteProductLines[].discountAmount` | string (decimal) | no | The per-unit discount, in the quote's currency. | | `subscriptions[].quoteProducts[].quoteProductLines[].discountPercentage` | string (decimal) | no | The per-unit discount as a percentage. For example, 10 is 10%. It is not an amount. | | `subscriptions[].quoteProducts[].quoteProductLines[].extendedListPrice` | string (decimal) | no | The list price extended across the quantity, in the quote's currency. | | `subscriptions[].quoteProducts[].quoteProductLines[].cappedPrice` | string (decimal) | no | The ceiling on what this line may total, in the quote's currency. Set on billable-time lines. | | `subscriptions[].quoteProducts[].quoteProductLines[].totalPrice` | string (decimal) | no | The total for this line, in the quote's currency. | | `subscriptions[].quoteProducts[].quoteProductLines[].annualRecurringRevenue` | string (decimal) | no | The annualized recurring revenue attributed to this line, in the quote's currency. | | `subscriptions[].quoteProducts[].quoteProductLines[].invoiceFrequency` | string | no | How often this line is invoiced. Possible values: 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `subscriptions[].quoteProducts[].quoteProductLines[].priceModel` | string | no | How the price is derived from quantity. Possible values: 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. For a tiered or volume model the bands are in lineTiers. For a rateCard model they are in rateCardEntries. | | `subscriptions[].quoteProducts[].quoteProductLines[].pricingTerm` | string | no | The period the unit price is quoted for. Possible values: 'monthly', 'biMonthly', 'once', 'quarterly', 'semiAnnually', 'annually'. | | `subscriptions[].quoteProducts[].quoteProductLines[].unitOfMeasure` | string | no | The unit the quantity is counted in. Possible values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `subscriptions[].quoteProducts[].quoteProductLines[].overConsumptionBehavior` | string | no | What happens when consumption exceeds a prepaid balance. Possible values: 'allowNegative', 'overage'. Set on prepaid lines. | | `subscriptions[].quoteProducts[].quoteProductLines[].rolloverAtRenewal` | boolean | no | Whether an unused prepaid balance carries forward at renewal. Set on prepaid lines. | | `subscriptions[].quoteProducts[].quoteProductLines[].serviceStartDate` | string (date) | no | The first date this line delivers service. Format YYYY-MM-DD. Defaults to the quote's serviceStartDate. | | `subscriptions[].quoteProducts[].quoteProductLines[].serviceEndDate` | string (date) | no | The last date this line delivers service. Format YYYY-MM-DD. Absent while open-ended. | | `subscriptions[].quoteProducts[].quoteProductLines[].orderIndex` | number | no | The position of this line within its quote product, for display ordering. | | `subscriptions[].quoteProducts[].quoteProductLines[].lineTiers` | array of object | yes | The tiered or volume pricing bands for this line, ordered by fromQuantity. An empty array unless priceModel is a banded model. | | `subscriptions[].quoteProducts[].quoteProductLines[].lineTiers[].id` | string | yes | The unique identifier for the tier band. | | `subscriptions[].quoteProducts[].quoteProductLines[].lineTiers[].fromQuantity` | string (decimal) | yes | The lower bound of the band, inclusive. A quantity, not an amount. | | `subscriptions[].quoteProducts[].quoteProductLines[].lineTiers[].toQuantity` | string (decimal) | no | The upper bound of the band, inclusive. Absent on the open-ended top band. | | `subscriptions[].quoteProducts[].quoteProductLines[].lineTiers[].priceModel` | string | yes | How this band prices. Possible values: 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. | | `subscriptions[].quoteProducts[].quoteProductLines[].lineTiers[].unitPrice` | string (decimal) | no | The per-unit price for this band after discount, in the quote's currency. | | `subscriptions[].quoteProducts[].quoteProductLines[].lineTiers[].listPrice` | string (decimal) | no | The per-unit list price for this band before discount, in the quote's currency. | | `subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries` | array of object | yes | The rate-card pricing for this line, one entry per billable component. An empty array unless priceModel is 'rateCard'. | | `subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].id` | string | yes | The unique identifier for the rate-card entry. | | `subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].identifier` | string | yes | The identifier of the entry, unique within its line. | | `subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].description` | string | no | A label that distinguishes entries that otherwise have the same data. | | `subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].billableComponentId` | string | no | The billable component this entry prices. Absent when it names none. There is no endpoint for it. | | `subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].unitPrice` | string (decimal) | no | The per-unit price after discount, in the quote's currency. | | `subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].listPrice` | string (decimal) | no | The per-unit list price before discount, in the quote's currency. | | `subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].unitOfMeasure` | string | yes | The unit the price is quoted per. Possible values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].quantity` | string (decimal) | no | The quantity of billable units on this entry, as a count, not an amount. | | `subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].usageType` | string | no | What the usage represents. Possible values: 'consumption', 'overage', 'thresholdCounter', 'payg'. | | `subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].conversion` | string (decimal) | no | The factor that converts usage units to billable units. For example, 0.1 when 1 gigabyte is 0.1 credits. It is a ratio, not an amount. | | `subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].billableUnitOfMeasure` | string | no | The unit the entry is billed in, which may differ from unitOfMeasure. Possible values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `subscriptions[].quoteProducts[].quoteProductLines[].invoiceMilestones` | array of object | yes | The invoice milestones for this line, when it is invoiced against milestones rather than on a frequency. An empty array otherwise. | | `subscriptions[].quoteProducts[].quoteProductLines[].invoiceMilestones[].id` | string | yes | The unique identifier for the invoice milestone. | | `subscriptions[].quoteProducts[].quoteProductLines[].invoiceMilestones[].description` | string | no | What the milestone covers, as recorded on the quote. | | `subscriptions[].quoteProducts[].quoteProductLines[].invoiceMilestones[].totalPrice` | string (decimal) | no | The amount invoiced at this milestone, in the quote's currency. | | `subscriptions[].quoteProducts[].quoteProductLines[].invoiceMilestones[].percentage` | string (decimal) | no | The share of the line total this milestone invoices as a percentage. For example, 25 is 25%. It is not an amount. | | `subscriptions[].quoteProducts[].quoteProductLines[].invoiceMilestones[].projectedDate` | string (date) | no | The date the milestone is expected to be reached. Format YYYY-MM-DD. | | `nonRecurringProducts` | array of object | yes | The products on the quote that belong to no subscription, such as one-time charges, billable time and pass-through expenses. An empty array when every product is under a subscription. GET crm/quotes omits it and returns the quote header alone. | | `nonRecurringProducts[].id` | string | yes | The unique identifier for the quote product. | | `nonRecurringProducts[].quoteProductNumber` | string | no | The system-assigned number of the quote product. Absent while none has been assigned. | | `nonRecurringProducts[].productId` | string | no | The catalog product for this quote product. See id on GET accounting/products/:id. | | `nonRecurringProducts[].productCode` | string | no | The code of that product in the catalog. | | `nonRecurringProducts[].productName` | string | no | The name of that product in the catalog. | | `nonRecurringProducts[].isRampDeal` | boolean | no | Whether this product is priced as a ramp deal, with quantities or prices stepping over time. | | `nonRecurringProducts[].rampInterval` | string | no | How the ramp steps. Possible values: 'annually', 'specificDates'. Absent unless isRampDeal is true. | | `nonRecurringProducts[].rampInvoiceTiming` | string | no | How a ramp deal is invoiced. Possible values: 'allAtOnce', 'perInterval'. Absent unless isRampDeal is true. | | `nonRecurringProducts[].orderIndex` | number | no | The position of this product within the quote, for display ordering. | | `nonRecurringProducts[].quoteProductLines` | array of object | yes | The priced lines of this quote product. A product has at least one. | | `nonRecurringProducts[].quoteProductLines[].id` | string | yes | The unique identifier for the quote product line. | | `nonRecurringProducts[].quoteProductLines[].quoteProductLineNumber` | string | no | The system-assigned number of the quote product line. Absent while none has been assigned. | | `nonRecurringProducts[].quoteProductLines[].productLineId` | string | no | The catalog product line this line was priced from. See id on GET accounting/product-lines/:id. | | `nonRecurringProducts[].quoteProductLines[].productLineName` | string | no | The name of that catalog product line. | | `nonRecurringProducts[].quoteProductLines[].parentQuoteProductLineId` | string | no | The parent line this line hangs off, as an allowance or overage line does. Absent on a top-level line. | | `nonRecurringProducts[].quoteProductLines[].lineType` | string | yes | What this line charges for. Possible values: 'recurring', 'oneTime', 'commitWithOverage', 'payAsYouGo', 'usage', 'prepaid', 'billableTime', 'billableTravelExpense', 'billablePassThrough'. | | `nonRecurringProducts[].quoteProductLines[].quantity` | string (decimal) | no | The quantity on this line, as a count, not an amount. For a ramp deal the per-period quantity is on each ramp instead. | | `nonRecurringProducts[].quoteProductLines[].listPrice` | string (decimal) | no | The per-unit list price before discount, in the quote's currency. | | `nonRecurringProducts[].quoteProductLines[].unitPrice` | string (decimal) | no | The per-unit price after discount, in the quote's currency. | | `nonRecurringProducts[].quoteProductLines[].discountAmount` | string (decimal) | no | The per-unit discount, in the quote's currency. | | `nonRecurringProducts[].quoteProductLines[].discountPercentage` | string (decimal) | no | The per-unit discount as a percentage. For example, 10 is 10%. It is not an amount. | | `nonRecurringProducts[].quoteProductLines[].extendedListPrice` | string (decimal) | no | The list price extended across the quantity, in the quote's currency. | | `nonRecurringProducts[].quoteProductLines[].cappedPrice` | string (decimal) | no | The ceiling on what this line may total, in the quote's currency. Set on billable-time lines. | | `nonRecurringProducts[].quoteProductLines[].totalPrice` | string (decimal) | no | The total for this line, in the quote's currency. | | `nonRecurringProducts[].quoteProductLines[].annualRecurringRevenue` | string (decimal) | no | The annualized recurring revenue attributed to this line, in the quote's currency. | | `nonRecurringProducts[].quoteProductLines[].invoiceFrequency` | string | no | How often this line is invoiced. Possible values: 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `nonRecurringProducts[].quoteProductLines[].priceModel` | string | no | How the price is derived from quantity. Possible values: 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. For a tiered or volume model the bands are in lineTiers. For a rateCard model they are in rateCardEntries. | | `nonRecurringProducts[].quoteProductLines[].pricingTerm` | string | no | The period the unit price is quoted for. Possible values: 'monthly', 'biMonthly', 'once', 'quarterly', 'semiAnnually', 'annually'. | | `nonRecurringProducts[].quoteProductLines[].unitOfMeasure` | string | no | The unit the quantity is counted in. Possible values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `nonRecurringProducts[].quoteProductLines[].overConsumptionBehavior` | string | no | What happens when consumption exceeds a prepaid balance. Possible values: 'allowNegative', 'overage'. Set on prepaid lines. | | `nonRecurringProducts[].quoteProductLines[].rolloverAtRenewal` | boolean | no | Whether an unused prepaid balance carries forward at renewal. Set on prepaid lines. | | `nonRecurringProducts[].quoteProductLines[].serviceStartDate` | string (date) | no | The first date this line delivers service. Format YYYY-MM-DD. Defaults to the quote's serviceStartDate. | | `nonRecurringProducts[].quoteProductLines[].serviceEndDate` | string (date) | no | The last date this line delivers service. Format YYYY-MM-DD. Absent while open-ended. | | `nonRecurringProducts[].quoteProductLines[].orderIndex` | number | no | The position of this line within its quote product, for display ordering. | | `nonRecurringProducts[].quoteProductLines[].lineTiers` | array of object | yes | The tiered or volume pricing bands for this line, ordered by fromQuantity. An empty array unless priceModel is a banded model. | | `nonRecurringProducts[].quoteProductLines[].lineTiers[].id` | string | yes | The unique identifier for the tier band. | | `nonRecurringProducts[].quoteProductLines[].lineTiers[].fromQuantity` | string (decimal) | yes | The lower bound of the band, inclusive. A quantity, not an amount. | | `nonRecurringProducts[].quoteProductLines[].lineTiers[].toQuantity` | string (decimal) | no | The upper bound of the band, inclusive. Absent on the open-ended top band. | | `nonRecurringProducts[].quoteProductLines[].lineTiers[].priceModel` | string | yes | How this band prices. Possible values: 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. | | `nonRecurringProducts[].quoteProductLines[].lineTiers[].unitPrice` | string (decimal) | no | The per-unit price for this band after discount, in the quote's currency. | | `nonRecurringProducts[].quoteProductLines[].lineTiers[].listPrice` | string (decimal) | no | The per-unit list price for this band before discount, in the quote's currency. | | `nonRecurringProducts[].quoteProductLines[].rateCardEntries` | array of object | yes | The rate-card pricing for this line, one entry per billable component. An empty array unless priceModel is 'rateCard'. | | `nonRecurringProducts[].quoteProductLines[].rateCardEntries[].id` | string | yes | The unique identifier for the rate-card entry. | | `nonRecurringProducts[].quoteProductLines[].rateCardEntries[].identifier` | string | yes | The identifier of the entry, unique within its line. | | `nonRecurringProducts[].quoteProductLines[].rateCardEntries[].description` | string | no | A label that distinguishes entries that otherwise have the same data. | | `nonRecurringProducts[].quoteProductLines[].rateCardEntries[].billableComponentId` | string | no | The billable component this entry prices. Absent when it names none. There is no endpoint for it. | | `nonRecurringProducts[].quoteProductLines[].rateCardEntries[].unitPrice` | string (decimal) | no | The per-unit price after discount, in the quote's currency. | | `nonRecurringProducts[].quoteProductLines[].rateCardEntries[].listPrice` | string (decimal) | no | The per-unit list price before discount, in the quote's currency. | | `nonRecurringProducts[].quoteProductLines[].rateCardEntries[].unitOfMeasure` | string | yes | The unit the price is quoted per. Possible values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `nonRecurringProducts[].quoteProductLines[].rateCardEntries[].quantity` | string (decimal) | no | The quantity of billable units on this entry, as a count, not an amount. | | `nonRecurringProducts[].quoteProductLines[].rateCardEntries[].usageType` | string | no | What the usage represents. Possible values: 'consumption', 'overage', 'thresholdCounter', 'payg'. | | `nonRecurringProducts[].quoteProductLines[].rateCardEntries[].conversion` | string (decimal) | no | The factor that converts usage units to billable units. For example, 0.1 when 1 gigabyte is 0.1 credits. It is a ratio, not an amount. | | `nonRecurringProducts[].quoteProductLines[].rateCardEntries[].billableUnitOfMeasure` | string | no | The unit the entry is billed in, which may differ from unitOfMeasure. Possible values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `nonRecurringProducts[].quoteProductLines[].invoiceMilestones` | array of object | yes | The invoice milestones for this line, when it is invoiced against milestones rather than on a frequency. An empty array otherwise. | | `nonRecurringProducts[].quoteProductLines[].invoiceMilestones[].id` | string | yes | The unique identifier for the invoice milestone. | | `nonRecurringProducts[].quoteProductLines[].invoiceMilestones[].description` | string | no | What the milestone covers, as recorded on the quote. | | `nonRecurringProducts[].quoteProductLines[].invoiceMilestones[].totalPrice` | string (decimal) | no | The amount invoiced at this milestone, in the quote's currency. | | `nonRecurringProducts[].quoteProductLines[].invoiceMilestones[].percentage` | string (decimal) | no | The share of the line total this milestone invoices as a percentage. For example, 25 is 25%. It is not an amount. | | `nonRecurringProducts[].quoteProductLines[].invoiceMilestones[].projectedDate` | string (date) | no | The date the milestone is expected to be reached. Format YYYY-MM-DD. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the quotes that match the given criteria. `POST /crm/quotes/search` Group: CRM › Quotes · Operation ID: `searchQuotes` Use this endpoint for criteria a query string cannot carry: text matches the quote memo, each multi-valued criterion lets you pass several values of one field, and quoteNumber and salesArrangementId are kept out of the URL. Criteria combine with AND, and a list within one criterion is an OR. For example, approvalStatus ['pendingApproval', 'approved'] matches either. A match comes back in full, with subscriptions[] and nonRecurringProducts[], the same as GET crm/quotes/:id. GET crm/quotes returns the header alone. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchQuotes) ## 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`. | ## Request body Required, `application/json`: searchQuotesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `text` | string | no | Limits results to quotes whose memo contains this text. The match is case-insensitive. Omit to match every quote. | | `quoteNumber` | string | no | Limits results to the quote with this number, for example 'QT-1042'. The match is exact. | | `quoteType` | array of string, one of `"new"`, `"upsell"`, `"renewal"`, `"changeOrder"` | no | Limits results to quotes of any of these kinds. Possible values: 'new', 'upsell', 'renewal', 'changeOrder'. Omit or pass an empty array for every kind. | | `status` | array of string, one of `"created"`, `"underApproval"`, `"approved"`, `"underSigning"`, `"signed"`, `"completed"` | no | Limits results to quotes in any of these business states. Possible values: 'created', 'underApproval', 'approved', 'underSigning', 'signed', 'completed'. Omit or pass an empty array for every state. | | `approvalStatus` | array of string, one of `"pendingSubmission"`, `"pendingApproval"`, `"approved"`, `"rejected"` | no | Limits results to quotes in any of these approval states. Possible values: 'pendingSubmission', 'pendingApproval', 'approved', 'rejected'. Omit or pass an empty array for every state. | | `isPrimary` | boolean | no | Limits results to primary quotes when true, or non-primary quotes when false. Omit for both. | | `opportunityId` | string | no | Limits results to quotes of this opportunity. Omit for every opportunity. | | `salesArrangementId` | string | no | Limits results to quotes of this sales arrangement. Omit for every arrangement. | | `quoteDate` | object | no | Limits results by the date the quote was drawn up. Both bounds are inclusive, and omitting one leaves that side open. | | `quoteDate.start` | string | no | The earliest quote date to include, inclusive. Format YYYY-MM-DD. | | `quoteDate.end` | string | no | The latest quote date to include, inclusive. Format YYYY-MM-DD. | | `lastModifiedDate` | object | no | Limits results by when the quote last changed, in UTC. Both bounds are inclusive, and omitting one leaves that side open. | | `lastModifiedDate.start` | string | no | The earliest change time to include, inclusive. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate.end` | string | no | The latest change time to include, inclusive. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The page to return. Omit for the first page at the default size. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of quotes per page. Defaults to 50, and a value above 200 is treated as 200, not rejected. | Example: ```json { "text": "renegotiated", "quoteNumber": "<quoteNumber>", "quoteType": [ "changeOrder" ], "status": [ "created" ], "approvalStatus": [ "pendingApproval", "approved" ], "isPrimary": true, "opportunityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "salesArrangementId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quoteDate": { "start": "<start>", "end": "<end>" }, "lastModifiedDate": { "start": "<start>", "end": "<end>" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchQuotesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The quotes on this page, most recently created first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the quote. | | `items[].quoteNumber` | string | no | The system-generated quote number, for example 'QT-1042'. Absent while none has been assigned. | | `items[].quoteType` | string | yes | The kind of quote. Possible values: 'new', 'upsell', 'renewal', 'changeOrder'. | | `items[].status` | string | no | Where the quote stands in its business process. Possible values: 'created', 'underApproval', 'approved', 'underSigning', 'signed', 'completed'. Absent on a quote that has not entered the process. | | `items[].approvalStatus` | string | no | Where the quote stands in its approval process. Possible values: 'pendingSubmission', 'pendingApproval', 'approved', 'rejected'. Absent until the approval process has run. | | `items[].isPrimary` | boolean | yes | Whether this is the primary quote of its opportunity. | | `items[].quoteDate` | string (date) | yes | The date the quote was drawn up. Format YYYY-MM-DD. Product prices are validated against the opportunity's price book as of this date. | | `items[].serviceStartDate` | string (date) | yes | The first date products or services on this quote are delivered. Format YYYY-MM-DD. No subscription on the quote starts before it. | | `items[].signDate` | string (date) | no | The date the quote was signed by the customer and the legal entity. Format YYYY-MM-DD. Absent when not recorded. | | `items[].paymentTerm` | string | yes | The code of the payment term applied to invoices created from this quote. See code on GET accounting/payment-terms. | | `items[].memo` | string | no | A free-text memo recorded on the quote. Absent when none is set. | | `items[].needsPSACalculation` | boolean | no | Whether the quote takes its product data from a PSA project rather than from the product catalog. | | `items[].currency` | string | no | The currency of every amount on the quote. A quote takes the currency of its opportunity and has none of its own. Format ISO 4217, for example 'USD'. | | `items[].totalPrice` | string (decimal) | no | The sum of the quote's lines before tax, less any one-time discount, in the quote's currency. | | `items[].oneTimeDiscountAmount` | string (decimal) | no | A one-time discount applied to the whole quote, in the quote's currency. | | `items[].oneTimeDiscountAllocation` | string | no | How oneTimeDiscountAmount is spread across the quote's invoices. Possible values: 'firstInvoice', 'alignToInvoiceFrequency', 'custom'. | | `items[].billableTimeEstimate` | string (decimal) | no | The estimated total of the quote's billable-time lines, in the quote's currency. | | `items[].billableTimeCappedPrice` | string (decimal) | no | The ceiling on the total of the quote's billable-time lines, in the quote's currency. | | `items[].opportunityId` | string | yes | The opportunity this quote belongs to. See id on GET crm/opportunities/:id. | | `items[].opportunityName` | string | no | The name of the opportunity this quote belongs to. Absent when the opportunity has no name. | | `items[].opportunityNumber` | string | no | The opportunity number of the opportunity this quote belongs to, for example 'OP-1042'. Absent while none has been assigned. | | `items[].billToAddressId` | string | no | The bill-to address on the quote, a copy the quote keeps of its own rather than the source it was copied from. There is no endpoint for it. | | `items[].soldToAddressId` | string | no | The sold-to address on the quote, a copy the quote keeps of its own rather than the source it was copied from. There is no endpoint for it. | | `items[].salesArrangementId` | string | no | The sales arrangement this quote is assigned to. Set on change-order quotes. There is no endpoint for it. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the quote record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].subscriptions` | array of object | yes | The subscriptions on the quote, one per subscription, each with the products under it. An empty array when the quote has no recurring products. GET crm/quotes omits it and returns the quote header alone. | | `items[].subscriptions[].id` | string | yes | The unique identifier for the quote subscription. | | `items[].subscriptions[].termType` | string | yes | Whether the subscription runs to a fixed end date or renews indefinitely. Possible values: 'termed', 'evergreen'. | | `items[].subscriptions[].serviceStartDate` | string (date) | yes | The first date the subscription delivers service. Format YYYY-MM-DD. Never earlier than the quote's serviceStartDate. | | `items[].subscriptions[].serviceEndDate` | string (date) | no | The last date the subscription delivers service. Format YYYY-MM-DD. Set on a termed subscription and absent on an evergreen one. | | `items[].subscriptions[].autoRenew` | boolean | no | Whether the subscription renews automatically at the end of its term. | | `items[].subscriptions[].renewalTerm` | number | no | The renewal term in months. Meaningful only when autoRenew is true. | | `items[].subscriptions[].quoteProducts` | array of object | yes | The products in this subscription, each with its priced lines. | | `items[].subscriptions[].quoteProducts[].id` | string | yes | The unique identifier for the quote product. | | `items[].subscriptions[].quoteProducts[].quoteProductNumber` | string | no | The system-assigned number of the quote product. Absent while none has been assigned. | | `items[].subscriptions[].quoteProducts[].productId` | string | no | The catalog product for this quote product. See id on GET accounting/products/:id. | | `items[].subscriptions[].quoteProducts[].productCode` | string | no | The code of that product in the catalog. | | `items[].subscriptions[].quoteProducts[].productName` | string | no | The name of that product in the catalog. | | `items[].subscriptions[].quoteProducts[].isRampDeal` | boolean | no | Whether this product is priced as a ramp deal, with quantities or prices stepping over time. | | `items[].subscriptions[].quoteProducts[].rampInterval` | string | no | How the ramp steps. Possible values: 'annually', 'specificDates'. Absent unless isRampDeal is true. | | `items[].subscriptions[].quoteProducts[].rampInvoiceTiming` | string | no | How a ramp deal is invoiced. Possible values: 'allAtOnce', 'perInterval'. Absent unless isRampDeal is true. | | `items[].subscriptions[].quoteProducts[].orderIndex` | number | no | The position of this product within the quote, for display ordering. | | `items[].subscriptions[].quoteProducts[].quoteProductLines` | array of object | yes | The priced lines of this quote product. A product has at least one. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].id` | string | yes | The unique identifier for the quote product line. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].quoteProductLineNumber` | string | no | The system-assigned number of the quote product line. Absent while none has been assigned. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].productLineId` | string | no | The catalog product line this line was priced from. See id on GET accounting/product-lines/:id. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].productLineName` | string | no | The name of that catalog product line. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].parentQuoteProductLineId` | string | no | The parent line this line hangs off, as an allowance or overage line does. Absent on a top-level line. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].lineType` | string | yes | What this line charges for. Possible values: 'recurring', 'oneTime', 'commitWithOverage', 'payAsYouGo', 'usage', 'prepaid', 'billableTime', 'billableTravelExpense', 'billablePassThrough'. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].quantity` | string (decimal) | no | The quantity on this line, as a count, not an amount. For a ramp deal the per-period quantity is on each ramp instead. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].listPrice` | string (decimal) | no | The per-unit list price before discount, in the quote's currency. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].unitPrice` | string (decimal) | no | The per-unit price after discount, in the quote's currency. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].discountAmount` | string (decimal) | no | The per-unit discount, in the quote's currency. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].discountPercentage` | string (decimal) | no | The per-unit discount as a percentage. For example, 10 is 10%. It is not an amount. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].extendedListPrice` | string (decimal) | no | The list price extended across the quantity, in the quote's currency. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].cappedPrice` | string (decimal) | no | The ceiling on what this line may total, in the quote's currency. Set on billable-time lines. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].totalPrice` | string (decimal) | no | The total for this line, in the quote's currency. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].annualRecurringRevenue` | string (decimal) | no | The annualized recurring revenue attributed to this line, in the quote's currency. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].invoiceFrequency` | string | no | How often this line is invoiced. Possible values: 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].priceModel` | string | no | How the price is derived from quantity. Possible values: 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. For a tiered or volume model the bands are in lineTiers. For a rateCard model they are in rateCardEntries. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].pricingTerm` | string | no | The period the unit price is quoted for. Possible values: 'monthly', 'biMonthly', 'once', 'quarterly', 'semiAnnually', 'annually'. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].unitOfMeasure` | string | no | The unit the quantity is counted in. Possible values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].overConsumptionBehavior` | string | no | What happens when consumption exceeds a prepaid balance. Possible values: 'allowNegative', 'overage'. Set on prepaid lines. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].rolloverAtRenewal` | boolean | no | Whether an unused prepaid balance carries forward at renewal. Set on prepaid lines. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].serviceStartDate` | string (date) | no | The first date this line delivers service. Format YYYY-MM-DD. Defaults to the quote's serviceStartDate. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].serviceEndDate` | string (date) | no | The last date this line delivers service. Format YYYY-MM-DD. Absent while open-ended. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].orderIndex` | number | no | The position of this line within its quote product, for display ordering. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].lineTiers` | array of object | yes | The tiered or volume pricing bands for this line, ordered by fromQuantity. An empty array unless priceModel is a banded model. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].lineTiers[].id` | string | yes | The unique identifier for the tier band. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].lineTiers[].fromQuantity` | string (decimal) | yes | The lower bound of the band, inclusive. A quantity, not an amount. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].lineTiers[].toQuantity` | string (decimal) | no | The upper bound of the band, inclusive. Absent on the open-ended top band. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].lineTiers[].priceModel` | string | yes | How this band prices. Possible values: 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].lineTiers[].unitPrice` | string (decimal) | no | The per-unit price for this band after discount, in the quote's currency. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].lineTiers[].listPrice` | string (decimal) | no | The per-unit list price for this band before discount, in the quote's currency. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries` | array of object | yes | The rate-card pricing for this line, one entry per billable component. An empty array unless priceModel is 'rateCard'. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].id` | string | yes | The unique identifier for the rate-card entry. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].identifier` | string | yes | The identifier of the entry, unique within its line. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].description` | string | no | A label that distinguishes entries that otherwise have the same data. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].billableComponentId` | string | no | The billable component this entry prices. Absent when it names none. There is no endpoint for it. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].unitPrice` | string (decimal) | no | The per-unit price after discount, in the quote's currency. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].listPrice` | string (decimal) | no | The per-unit list price before discount, in the quote's currency. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].unitOfMeasure` | string | yes | The unit the price is quoted per. Possible values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].quantity` | string (decimal) | no | The quantity of billable units on this entry, as a count, not an amount. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].usageType` | string | no | What the usage represents. Possible values: 'consumption', 'overage', 'thresholdCounter', 'payg'. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].conversion` | string (decimal) | no | The factor that converts usage units to billable units. For example, 0.1 when 1 gigabyte is 0.1 credits. It is a ratio, not an amount. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].rateCardEntries[].billableUnitOfMeasure` | string | no | The unit the entry is billed in, which may differ from unitOfMeasure. Possible values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].invoiceMilestones` | array of object | yes | The invoice milestones for this line, when it is invoiced against milestones rather than on a frequency. An empty array otherwise. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].invoiceMilestones[].id` | string | yes | The unique identifier for the invoice milestone. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].invoiceMilestones[].description` | string | no | What the milestone covers, as recorded on the quote. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].invoiceMilestones[].totalPrice` | string (decimal) | no | The amount invoiced at this milestone, in the quote's currency. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].invoiceMilestones[].percentage` | string (decimal) | no | The share of the line total this milestone invoices as a percentage. For example, 25 is 25%. It is not an amount. | | `items[].subscriptions[].quoteProducts[].quoteProductLines[].invoiceMilestones[].projectedDate` | string (date) | no | The date the milestone is expected to be reached. Format YYYY-MM-DD. | | `items[].nonRecurringProducts` | array of object | yes | The products on the quote that belong to no subscription, such as one-time charges, billable time and pass-through expenses. An empty array when every product is under a subscription. GET crm/quotes omits it and returns the quote header alone. | | `items[].nonRecurringProducts[].id` | string | yes | The unique identifier for the quote product. | | `items[].nonRecurringProducts[].quoteProductNumber` | string | no | The system-assigned number of the quote product. Absent while none has been assigned. | | `items[].nonRecurringProducts[].productId` | string | no | The catalog product for this quote product. See id on GET accounting/products/:id. | | `items[].nonRecurringProducts[].productCode` | string | no | The code of that product in the catalog. | | `items[].nonRecurringProducts[].productName` | string | no | The name of that product in the catalog. | | `items[].nonRecurringProducts[].isRampDeal` | boolean | no | Whether this product is priced as a ramp deal, with quantities or prices stepping over time. | | `items[].nonRecurringProducts[].rampInterval` | string | no | How the ramp steps. Possible values: 'annually', 'specificDates'. Absent unless isRampDeal is true. | | `items[].nonRecurringProducts[].rampInvoiceTiming` | string | no | How a ramp deal is invoiced. Possible values: 'allAtOnce', 'perInterval'. Absent unless isRampDeal is true. | | `items[].nonRecurringProducts[].orderIndex` | number | no | The position of this product within the quote, for display ordering. | | `items[].nonRecurringProducts[].quoteProductLines` | array of object | yes | The priced lines of this quote product. A product has at least one. | | `items[].nonRecurringProducts[].quoteProductLines[].id` | string | yes | The unique identifier for the quote product line. | | `items[].nonRecurringProducts[].quoteProductLines[].quoteProductLineNumber` | string | no | The system-assigned number of the quote product line. Absent while none has been assigned. | | `items[].nonRecurringProducts[].quoteProductLines[].productLineId` | string | no | The catalog product line this line was priced from. See id on GET accounting/product-lines/:id. | | `items[].nonRecurringProducts[].quoteProductLines[].productLineName` | string | no | The name of that catalog product line. | | `items[].nonRecurringProducts[].quoteProductLines[].parentQuoteProductLineId` | string | no | The parent line this line hangs off, as an allowance or overage line does. Absent on a top-level line. | | `items[].nonRecurringProducts[].quoteProductLines[].lineType` | string | yes | What this line charges for. Possible values: 'recurring', 'oneTime', 'commitWithOverage', 'payAsYouGo', 'usage', 'prepaid', 'billableTime', 'billableTravelExpense', 'billablePassThrough'. | | `items[].nonRecurringProducts[].quoteProductLines[].quantity` | string (decimal) | no | The quantity on this line, as a count, not an amount. For a ramp deal the per-period quantity is on each ramp instead. | | `items[].nonRecurringProducts[].quoteProductLines[].listPrice` | string (decimal) | no | The per-unit list price before discount, in the quote's currency. | | `items[].nonRecurringProducts[].quoteProductLines[].unitPrice` | string (decimal) | no | The per-unit price after discount, in the quote's currency. | | `items[].nonRecurringProducts[].quoteProductLines[].discountAmount` | string (decimal) | no | The per-unit discount, in the quote's currency. | | `items[].nonRecurringProducts[].quoteProductLines[].discountPercentage` | string (decimal) | no | The per-unit discount as a percentage. For example, 10 is 10%. It is not an amount. | | `items[].nonRecurringProducts[].quoteProductLines[].extendedListPrice` | string (decimal) | no | The list price extended across the quantity, in the quote's currency. | | `items[].nonRecurringProducts[].quoteProductLines[].cappedPrice` | string (decimal) | no | The ceiling on what this line may total, in the quote's currency. Set on billable-time lines. | | `items[].nonRecurringProducts[].quoteProductLines[].totalPrice` | string (decimal) | no | The total for this line, in the quote's currency. | | `items[].nonRecurringProducts[].quoteProductLines[].annualRecurringRevenue` | string (decimal) | no | The annualized recurring revenue attributed to this line, in the quote's currency. | | `items[].nonRecurringProducts[].quoteProductLines[].invoiceFrequency` | string | no | How often this line is invoiced. Possible values: 'monthly', 'biMonthly', 'quarterly', 'semiAnnually', 'annually', 'once', 'custom', 'template', 'milestone', 'allAtOnce', 'termEnd'. | | `items[].nonRecurringProducts[].quoteProductLines[].priceModel` | string | no | How the price is derived from quantity. Possible values: 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. For a tiered or volume model the bands are in lineTiers. For a rateCard model they are in rateCardEntries. | | `items[].nonRecurringProducts[].quoteProductLines[].pricingTerm` | string | no | The period the unit price is quoted for. Possible values: 'monthly', 'biMonthly', 'once', 'quarterly', 'semiAnnually', 'annually'. | | `items[].nonRecurringProducts[].quoteProductLines[].unitOfMeasure` | string | no | The unit the quantity is counted in. Possible values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `items[].nonRecurringProducts[].quoteProductLines[].overConsumptionBehavior` | string | no | What happens when consumption exceeds a prepaid balance. Possible values: 'allowNegative', 'overage'. Set on prepaid lines. | | `items[].nonRecurringProducts[].quoteProductLines[].rolloverAtRenewal` | boolean | no | Whether an unused prepaid balance carries forward at renewal. Set on prepaid lines. | | `items[].nonRecurringProducts[].quoteProductLines[].serviceStartDate` | string (date) | no | The first date this line delivers service. Format YYYY-MM-DD. Defaults to the quote's serviceStartDate. | | `items[].nonRecurringProducts[].quoteProductLines[].serviceEndDate` | string (date) | no | The last date this line delivers service. Format YYYY-MM-DD. Absent while open-ended. | | `items[].nonRecurringProducts[].quoteProductLines[].orderIndex` | number | no | The position of this line within its quote product, for display ordering. | | `items[].nonRecurringProducts[].quoteProductLines[].lineTiers` | array of object | yes | The tiered or volume pricing bands for this line, ordered by fromQuantity. An empty array unless priceModel is a banded model. | | `items[].nonRecurringProducts[].quoteProductLines[].lineTiers[].id` | string | yes | The unique identifier for the tier band. | | `items[].nonRecurringProducts[].quoteProductLines[].lineTiers[].fromQuantity` | string (decimal) | yes | The lower bound of the band, inclusive. A quantity, not an amount. | | `items[].nonRecurringProducts[].quoteProductLines[].lineTiers[].toQuantity` | string (decimal) | no | The upper bound of the band, inclusive. Absent on the open-ended top band. | | `items[].nonRecurringProducts[].quoteProductLines[].lineTiers[].priceModel` | string | yes | How this band prices. Possible values: 'flat', 'perUnit', 'tiered', 'volume', 'stairstep', 'rateCard', 'thresholdBased'. | | `items[].nonRecurringProducts[].quoteProductLines[].lineTiers[].unitPrice` | string (decimal) | no | The per-unit price for this band after discount, in the quote's currency. | | `items[].nonRecurringProducts[].quoteProductLines[].lineTiers[].listPrice` | string (decimal) | no | The per-unit list price for this band before discount, in the quote's currency. | | `items[].nonRecurringProducts[].quoteProductLines[].rateCardEntries` | array of object | yes | The rate-card pricing for this line, one entry per billable component. An empty array unless priceModel is 'rateCard'. | | `items[].nonRecurringProducts[].quoteProductLines[].rateCardEntries[].id` | string | yes | The unique identifier for the rate-card entry. | | `items[].nonRecurringProducts[].quoteProductLines[].rateCardEntries[].identifier` | string | yes | The identifier of the entry, unique within its line. | | `items[].nonRecurringProducts[].quoteProductLines[].rateCardEntries[].description` | string | no | A label that distinguishes entries that otherwise have the same data. | | `items[].nonRecurringProducts[].quoteProductLines[].rateCardEntries[].billableComponentId` | string | no | The billable component this entry prices. Absent when it names none. There is no endpoint for it. | | `items[].nonRecurringProducts[].quoteProductLines[].rateCardEntries[].unitPrice` | string (decimal) | no | The per-unit price after discount, in the quote's currency. | | `items[].nonRecurringProducts[].quoteProductLines[].rateCardEntries[].listPrice` | string (decimal) | no | The per-unit list price before discount, in the quote's currency. | | `items[].nonRecurringProducts[].quoteProductLines[].rateCardEntries[].unitOfMeasure` | string | yes | The unit the price is quoted per. Possible values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `items[].nonRecurringProducts[].quoteProductLines[].rateCardEntries[].quantity` | string (decimal) | no | The quantity of billable units on this entry, as a count, not an amount. | | `items[].nonRecurringProducts[].quoteProductLines[].rateCardEntries[].usageType` | string | no | What the usage represents. Possible values: 'consumption', 'overage', 'thresholdCounter', 'payg'. | | `items[].nonRecurringProducts[].quoteProductLines[].rateCardEntries[].conversion` | string (decimal) | no | The factor that converts usage units to billable units. For example, 0.1 when 1 gigabyte is 0.1 credits. It is a ratio, not an amount. | | `items[].nonRecurringProducts[].quoteProductLines[].rateCardEntries[].billableUnitOfMeasure` | string | no | The unit the entry is billed in, which may differ from unitOfMeasure. Possible values: 'instance', 'gigabyte', 'terabyte', 'user', 'seat', 'minute', 'hour', 'day', 'month', 'each', 'license', 'activeUser', 'megabyte', 'message', 'text', 'platform', 'employee', 'personnel', 'credit', 'currency', 'package', 'case', 'word', 'record'. | | `items[].nonRecurringProducts[].quoteProductLines[].invoiceMilestones` | array of object | yes | The invoice milestones for this line, when it is invoiced against milestones rather than on a frequency. An empty array otherwise. | | `items[].nonRecurringProducts[].quoteProductLines[].invoiceMilestones[].id` | string | yes | The unique identifier for the invoice milestone. | | `items[].nonRecurringProducts[].quoteProductLines[].invoiceMilestones[].description` | string | no | What the milestone covers, as recorded on the quote. | | `items[].nonRecurringProducts[].quoteProductLines[].invoiceMilestones[].totalPrice` | string (decimal) | no | The amount invoiced at this milestone, in the quote's currency. | | `items[].nonRecurringProducts[].quoteProductLines[].invoiceMilestones[].percentage` | string (decimal) | no | The share of the line total this milestone invoices as a percentage. For example, 25 is 25%. It is not an amount. | | `items[].nonRecurringProducts[].quoteProductLines[].invoiceMilestones[].projectedDate` | string (date) | no | The date the milestone is expected to be reached. Format YYYY-MM-DD. | | `pagination` | object | yes | The pagination details for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it back as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether another page follows. A full page may still be the last, so branch on this. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Account Determination Rules `GET /inventory/account-determination-rules` Group: Inventory › Account Determination Rules · Operation ID: `accountDeterminationRules` Returns a list of account determination rules, headers only. To filter by a dimension or read a whole rule, use POST inventory/account-determination-rules/search. This collection returns the header only: id, ruleName, priority, isActive and lastModifiedDate. It returns none of the dimensions, accounts or overrides, and cannot filter by any of them. Read a whole rule, or filter by entity, book, item type, item category, location or transaction type, with GET inventory/account-determination-rules/:id or POST inventory/account-determination-rules/search. ruleName is orderable but not filterable, because matching it would mean a substring query, which no endpoint here offers. [Open in the interactive reference](https://docs.api.lensing.ai/?op=accountDeterminationRules) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `priority` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isActive` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, ruleName, priority, isActive, lastModifiedDate - Selectable: id, ruleName, priority, isActive, lastModifiedDate ## Response `200` Success Under `data`, an array of accountDeterminationRulesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the rule. | | `ruleName` | string | no | A free-text label for the rule. It is not unique and is never read when matching. | | `priority` | number or null | no | The tie-breaker between rules of equal specificity. The lower number wins. An absent priority counts as 0, which outranks any positive value. | | `isActive` | boolean or null | no | Whether the rule takes part in account determination. When false, the rule is skipped. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the rule header last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Adding or changing a reason-code override does not change it. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Account Determination Rule `POST /inventory/account-determination-rules` Group: Inventory › Account Determination Rules · Operation ID: `createAccountDeterminationRule` Creates an account determination rule. The combination of transaction type, item type, item category and location must be unique within the entity. matching.entityId is the only required dimension. Every dimension you omit makes the rule a wildcard on it, so a rule with only an entity is that entity's catch-all. Only one rule per entity may hold a given combination of transaction type, item type, item category and location, with absent values compared equal. A collision fails with DUPLICATE_RULE_TUPLE, which names the existing rule. The accounting book is not part of that combination, so two rules that differ only in book still collide. Every account is optional. Leave one unassigned and a less specific rule decides that posting. The response is only the created rule's id. Read the rule back with GET inventory/account-determination-rules/:id to see its stored state, including the ids assigned to its overrides. Use POST inventory/account-determination-rules/bulk to create several at once. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createAccountDeterminationRule) ## 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`: createAccountDeterminationRuleInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ruleName` | string | yes | A free-text label for the rule. Required. It is not unique and is never read when matching. It names the rule in a duplicate error and in the application. | | `priority` | number | no | The tie-breaker between rules of equal specificity. The lower number wins. An omitted priority counts as 0, which outranks any positive value, so set it on every rule when precedence matters. | | `isActive` | boolean | no | Whether the rule takes part in account determination. Defaults to true. | | `matching` | object | yes | The dimensions this rule is pinned to. Omitting a dimension makes the rule match every value of it. Only one rule per entity may hold a given combination of transaction type, item type, item category and location. | | `matching.entityId` | string | yes | The legal entity this rule belongs to. Required. See id on GET base/entities. Matching and the duplicate check are both scoped to it, so a posting only ever sees rules of its own entity. | | `matching.bookId` | string | no | The accounting book this rule is pinned to. See id on GET accounting/books/:id. Omit to leave the rule matching every value of this dimension. | | `matching.itemTypeId` | string | no | The item type this rule is pinned to. See id on GET inventory/item-types/:id. Omit to leave the rule matching every value of this dimension. | | `matching.itemCategoryId` | string | no | The item category this rule is pinned to. See id on GET inventory/item-categories/:id. Omit to leave the rule matching every value of this dimension. | | `matching.locationId` | string | no | The warehouse location this rule is pinned to. See id on GET inventory/warehouses/:id. Omit to leave the rule matching every value of this dimension. | | `matching.inventoryLocationType` | string | no | An advisory classification of the location. It is stored on the rule and never read when matching, so it does not affect which rule the posting selects. Omit for none. | | `matching.transactionTypeContext` | string, one of `"RECEIPT"`, `"SHIPMENT"`, `"SALES_RETURN"`, `"SALES_INVOICE"`, `"SALES_CREDIT_MEMO"`, `"PURCHASE_RETURN"`, `"PURCHASE_INVOICE"`, `"PURCHASE_CREDIT_MEMO"`, `"INVENTORY_INCREASE"`, `"INVENTORY_DECREASE"`, `"INVENTORY_REVALUATION"`, `"WAREHOUSE_TRANSFER"`, `"INTERCOMPANY_TRANSFER"`, `"DEFAULT"` | no | The document kind this rule is pinned to. Passing DEFAULT and omitting the field are equivalent: both make the rule match every transaction type. WAREHOUSE_TRANSFER and INTERCOMPANY_TRANSFER are the kinds used for transfer postings. | | `accounts` | object | no | The GL accounts this rule assigns. Every one is optional. Leave one unassigned and the rule does not decide that posting, so a less specific rule does. | | `accounts.inventoryAssetAccountId` | string | no | The account for inventory value on the balance sheet. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accounts.cogsAccountId` | string | no | The cost of goods sold account. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accounts.cogsClearingAccountId` | string | no | The cost of goods sold clearing account. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accounts.deferredCogsAccountId` | string | no | The deferred cost of goods sold account. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accounts.salesRevenueAccountId` | string | no | The sales revenue account. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accounts.salesReturnAccountId` | string | no | The sales return account. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unassigned. | | `accounts.salesStockInTransitAccountId` | string | no | The sales stock-in-transit account. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accounts.grniClearingAccountId` | string | no | The goods-received-not-invoiced clearing account, for inventory items. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accounts.grncClearingAccountId` | string | no | The goods-returned-not-credited clearing account. It is the offset leg booked on purchase returns and credit memos. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unassigned. | | `accounts.servicesReceivedNotInvoicedAccountId` | string | no | The clearing account for services received but not yet invoiced, for non-inventory items. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accounts.expenseAccountId` | string | no | The expense account for non-inventory items and description lines. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accounts.purchasePriceVarianceAccountId` | string | no | The purchase price variance account, under standard costing. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accounts.inventoryAdjustmentGainAccountId` | string | no | The account for positive inventory count and adjustment differences. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unassigned. | | `accounts.inventoryAdjustmentLossAccountId` | string | no | The account for negative inventory count and adjustment differences. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unassigned. | | `accounts.revaluationGainAccountId` | string | no | The account for gains from inventory revaluation. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accounts.revaluationLossAccountId` | string | no | The account for losses from inventory revaluation. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accounts.soldQuantitiesRevaluationAccountId` | string | no | The account for the revaluation of quantities already sold. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accounts.purchaseReturnVarianceGainAccountId` | string | no | The account for gains on purchase returns. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unassigned. | | `accounts.purchaseReturnVarianceLossAccountId` | string | no | The account for losses on purchase returns. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unassigned. | | `accounts.fxRateDiffAccountId` | string | no | The account that absorbs FX rate differences between receipt and invoice posting. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `reasonCodeOverrides` | array of object | no | The per-reason-code account overrides. When a document line uses one of these reason codes, its account replaces the rule account named by parentAccountType. | | `reasonCodeOverrides[].reasonCodeId` | string | yes | The reason code that triggers this override. See id on GET inventory/reason-codes/:id. | | `reasonCodeOverrides[].accountId` | string | yes | The account to post instead of the rule account. See id on GET accounting/accounts/:id. | | `reasonCodeOverrides[].parentAccountType` | string, one of `"inventoryAdjustmentGainAccount"`, `"inventoryAdjustmentLossAccount"`, `"salesReturnAccount"`, `"purchaseReturnVarianceGainAccount"`, `"purchaseReturnVarianceLossAccount"`, `"grncClearingAccount"` | yes | Which rule account this override replaces. Only six of the twenty accounts can be overridden. | Example: ```json { "ruleName": "Finished goods — receipts", "priority": 10, "isActive": true, "matching": { "entityId": "81e8beb1-1415-4012-9012-8138580f43dc", "itemCategoryId": "a2f47b91-0c6d-4e38-95b1-7d3e6a428c50", "transactionTypeContext": "RECEIPT" }, "accounts": { "inventoryAssetAccountId": "1c9e4b07-52da-4f38-9016-3b7e05c2a94f", "grniClearingAccountId": "4f0b7d29-6e13-4a85-b2c7-90d1f83e6a52" }, "reasonCodeOverrides": [ { "reasonCodeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "parentAccountType": "inventoryAdjustmentGainAccount" } ] } ``` ## Response `200` Success Under `data` (createAccountDeterminationRuleOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created rule. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `DUPLICATE_RULE_TUPLE` | 400 | | `UNKNOWN_PARENT_ACCOUNT_TYPE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, DUPLICATE_RULE_TUPLE, UNKNOWN_PARENT_ACCOUNT_TYPE - `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 --- # Get Account Determination Rule `GET /inventory/account-determination-rules/{id}` Group: Inventory › Account Determination Rules · Operation ID: `getAccountDeterminationRule` Returns the account determination rule with the given identifier, with its dimensions, accounts and overrides. The response has the same fields as POST inventory/account-determination-rules/search. The collection GET inventory/account-determination-rules returns the header only and none of the dimensions, accounts or overrides. Read matching as a set of wildcards: an absent dimension matches every value of it, and only entityId is always present. accounts contains only the accounts the rule assigns. An account it leaves unset is absent, and a less specific rule decides that posting. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getAccountDeterminationRule) ## 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`. | | `id` | path | string | yes | The unique identifier for the rule to return. | ## Response `200` Success Under `data` (getAccountDeterminationRuleOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the rule. | | `ruleName` | string | yes | A free-text label for the rule. It is not unique and is never read when matching. | | `priority` | number | no | The tie-breaker between rules of equal specificity. The lower number wins. An absent priority counts as 0, which outranks any positive value. | | `isActive` | boolean | no | Whether the rule takes part in account determination. When false, the rule is skipped. | | `matching` | object | yes | The dimensions this rule is pinned to. An absent dimension matches every value of it. | | `matching.entityId` | string | yes | The legal entity this rule belongs to. See id on GET base/entities. Matching is always scoped to one entity, so this is never absent. | | `matching.bookId` | string | no | The accounting book this rule is pinned to. See id on GET accounting/books/:id. Absent means the rule matches every value of this dimension. | | `matching.itemTypeId` | string | no | The item type this rule is pinned to. See id on GET inventory/item-types/:id. Absent means the rule matches every value of this dimension. | | `matching.itemCategoryId` | string | no | The item category this rule is pinned to. See id on GET inventory/item-categories/:id. Absent means the rule matches every value of this dimension. | | `matching.locationId` | string | no | The warehouse location this rule is pinned to. See id on GET inventory/warehouses/:id. Absent means the rule matches every value of this dimension. | | `matching.inventoryLocationType` | string | no | An advisory classification of the location. It is stored on the rule and never read when matching, so it does not affect which rule the posting selects. Absent when none was set. | | `matching.transactionTypeContext` | string, one of `"RECEIPT"`, `"SHIPMENT"`, `"SALES_RETURN"`, `"SALES_INVOICE"`, `"SALES_CREDIT_MEMO"`, `"PURCHASE_RETURN"`, `"PURCHASE_INVOICE"`, `"PURCHASE_CREDIT_MEMO"`, `"INVENTORY_INCREASE"`, `"INVENTORY_DECREASE"`, `"INVENTORY_REVALUATION"`, `"WAREHOUSE_TRANSFER"`, `"INTERCOMPANY_TRANSFER"`, `"DEFAULT"` | no | The document kind this rule is pinned to. The wildcard that matches every transaction type reads back in two forms: the field is absent, or it is DEFAULT. Do not treat absence alone as the wildcard, because the transaction type can be DEFAULT explicitly. WAREHOUSE_TRANSFER and INTERCOMPANY_TRANSFER are the kinds used for transfer postings. | | `accounts` | object | yes | The GL accounts this rule assigns. An account the rule does not assign is absent, and a less specific rule decides that posting. | | `accounts.inventoryAssetAccountId` | string | no | The account for inventory value on the balance sheet. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `accounts.cogsAccountId` | string | no | The cost of goods sold account. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `accounts.cogsClearingAccountId` | string | no | The cost of goods sold clearing account. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `accounts.deferredCogsAccountId` | string | no | The deferred cost of goods sold account. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `accounts.salesRevenueAccountId` | string | no | The sales revenue account. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `accounts.salesReturnAccountId` | string | no | The sales return account. See id on GET accounting/accounts/:id. It can be overridden per reason code. Absent when the rule assigns none. | | `accounts.salesStockInTransitAccountId` | string | no | The sales stock-in-transit account. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `accounts.grniClearingAccountId` | string | no | The goods-received-not-invoiced clearing account, for inventory items. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `accounts.grncClearingAccountId` | string | no | The goods-returned-not-credited clearing account. It is the offset leg booked on purchase returns and credit memos. See id on GET accounting/accounts/:id. It can be overridden per reason code. Absent when the rule assigns none. | | `accounts.servicesReceivedNotInvoicedAccountId` | string | no | The clearing account for services received but not yet invoiced, for non-inventory items. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `accounts.expenseAccountId` | string | no | The expense account for non-inventory items and description lines. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `accounts.purchasePriceVarianceAccountId` | string | no | The purchase price variance account, under standard costing. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `accounts.inventoryAdjustmentGainAccountId` | string | no | The account for positive inventory count and adjustment differences. See id on GET accounting/accounts/:id. It can be overridden per reason code. Absent when the rule assigns none. | | `accounts.inventoryAdjustmentLossAccountId` | string | no | The account for negative inventory count and adjustment differences. See id on GET accounting/accounts/:id. It can be overridden per reason code. Absent when the rule assigns none. | | `accounts.revaluationGainAccountId` | string | no | The account for gains from inventory revaluation. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `accounts.revaluationLossAccountId` | string | no | The account for losses from inventory revaluation. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `accounts.soldQuantitiesRevaluationAccountId` | string | no | The account for the revaluation of quantities already sold. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `accounts.purchaseReturnVarianceGainAccountId` | string | no | The account for gains on purchase returns. See id on GET accounting/accounts/:id. It can be overridden per reason code. Absent when the rule assigns none. | | `accounts.purchaseReturnVarianceLossAccountId` | string | no | The account for losses on purchase returns. See id on GET accounting/accounts/:id. It can be overridden per reason code. Absent when the rule assigns none. | | `accounts.fxRateDiffAccountId` | string | no | The account that absorbs FX rate differences between receipt and invoice posting. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `reasonCodeOverrides` | array of object | yes | The per-reason-code account overrides, ordered by id. An empty array when the rule has none. When a document line uses one of these reason codes, its account replaces the rule account named by parentAccountType. It is not returned by the collection GET inventory/account-determination-rules. | | `reasonCodeOverrides[].id` | string | yes | The unique identifier for the override. updateReasonCodeOverrides and deleteReasonCodeOverrides address it on PATCH. | | `reasonCodeOverrides[].reasonCodeId` | string | yes | The reason code that triggers this override. See id on GET inventory/reason-codes/:id. | | `reasonCodeOverrides[].accountId` | string | yes | The account posted instead of the rule account. See id on GET accounting/accounts/:id. | | `reasonCodeOverrides[].parentAccountType` | string, one of `"inventoryAdjustmentGainAccount"`, `"inventoryAdjustmentLossAccount"`, `"salesReturnAccount"`, `"purchaseReturnVarianceGainAccount"`, `"purchaseReturnVarianceLossAccount"`, `"grncClearingAccount"` | yes | Which rule account this override replaces. Only six of the twenty accounts can be overridden. | | `lastModifiedDate` | string (date-time) | no | The date and time the rule header last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Adding or changing a reason-code override does not change it. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Account Determination Rule `PATCH /inventory/account-determination-rules/{id}` Group: Inventory › Account Determination Rules · Operation ID: `updateAccountDeterminationRule` Updates the account determination rule with the given identifier. Only the fields in the request body change. Everything omitted from the request body is left as it was, at every level. Omit accounts and all of them stay as they are. Include accounts and omit one account inside it, and only that one stays as it is. null clears, and it means different things by field. On a matching dimension, null widens the rule to every value of that dimension. On an account, null unassigns it, so a less specific rule decides that posting. The entity cannot be changed. To move a rule, create it under the other entity and delete this one. Changing a dimension re-runs the duplicate check against the whole resulting combination, so widening one dimension can collide with a rule the request never mentioned, and DUPLICATE_RULE_TUPLE names it. The overrides take three arrays: createReasonCodeOverrides, updateReasonCodeOverrides keyed by each override's id, and deleteReasonCodeOverrides taking ids. Omit all three to leave them unchanged. They apply in the order delete, update, then create, so one call can retire an override and add a replacement for the same reason code. The response is only the rule's id. Read the updated rule with GET inventory/account-determination-rules/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateAccountDeterminationRule) ## 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. | | `id` | path | string | yes | The unique identifier for the rule to update. | ## Request body Required, `application/json`: updateAccountDeterminationRuleInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ruleName` | string | no | A free-text label for the rule. Omit to leave it unchanged. | | `priority` | number | no | The tie-breaker between rules of equal specificity. The lower number wins. Omit to leave it unchanged. | | `isActive` | boolean | no | Whether the rule takes part in account determination. Omit to leave it unchanged. Set it to false to retire a rule without deleting it. | | `matching` | object | no | Changes to the dimensions this rule is pinned to. There is no entityId here, because a rule cannot change entity. Passing null for a dimension widens the rule to every value of it. Any change re-runs the duplicate check against the whole resulting combination. | | `matching.bookId` | string or null | no | The accounting book this rule is pinned to. See id on GET accounting/books/:id. Omit to leave it unchanged. Pass null to widen the rule to every value of this dimension. | | `matching.itemTypeId` | string or null | no | The item type this rule is pinned to. See id on GET inventory/item-types/:id. Omit to leave it unchanged. Pass null to widen the rule to every value of this dimension. | | `matching.itemCategoryId` | string or null | no | The item category this rule is pinned to. See id on GET inventory/item-categories/:id. Omit to leave it unchanged. Pass null to widen the rule to every value of this dimension. | | `matching.locationId` | string or null | no | The warehouse location this rule is pinned to. See id on GET inventory/warehouses/:id. Omit to leave it unchanged. Pass null to widen the rule to every value of this dimension. | | `matching.inventoryLocationType` | string or null | no | An advisory classification of the location. It is stored on the rule and never read when matching, so it does not affect which rule the posting selects. Omit to leave it unchanged. Pass null to clear it. | | `matching.transactionTypeContext` | string, one of `"RECEIPT"`, `"SHIPMENT"`, `"SALES_RETURN"`, `"SALES_INVOICE"`, `"SALES_CREDIT_MEMO"`, `"PURCHASE_RETURN"`, `"PURCHASE_INVOICE"`, `"PURCHASE_CREDIT_MEMO"`, `"INVENTORY_INCREASE"`, `"INVENTORY_DECREASE"`, `"INVENTORY_REVALUATION"`, `"WAREHOUSE_TRANSFER"`, `"INTERCOMPANY_TRANSFER"`, `"DEFAULT"` or null | no | The document kind this rule is pinned to. Omit to leave it unchanged. DEFAULT and null are both the wildcard that matches every transaction type. WAREHOUSE_TRANSFER and INTERCOMPANY_TRANSFER are the kinds used for transfer postings. | | `accounts` | object | no | Changes to the GL accounts this rule assigns. Omit the whole object to leave all of them unchanged. Omit one account to leave it unchanged. Pass null for one to unassign it, so a less specific rule decides that posting. | | `accounts.inventoryAssetAccountId` | string or null | no | The account for inventory value on the balance sheet. See id on GET accounting/accounts/:id. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.cogsAccountId` | string or null | no | The cost of goods sold account. See id on GET accounting/accounts/:id. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.cogsClearingAccountId` | string or null | no | The cost of goods sold clearing account. See id on GET accounting/accounts/:id. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.deferredCogsAccountId` | string or null | no | The deferred cost of goods sold account. See id on GET accounting/accounts/:id. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.salesRevenueAccountId` | string or null | no | The sales revenue account. See id on GET accounting/accounts/:id. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.salesReturnAccountId` | string or null | no | The sales return account. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.salesStockInTransitAccountId` | string or null | no | The sales stock-in-transit account. See id on GET accounting/accounts/:id. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.grniClearingAccountId` | string or null | no | The goods-received-not-invoiced clearing account, for inventory items. See id on GET accounting/accounts/:id. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.grncClearingAccountId` | string or null | no | The goods-returned-not-credited clearing account. It is the offset leg booked on purchase returns and credit memos. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.servicesReceivedNotInvoicedAccountId` | string or null | no | The clearing account for services received but not yet invoiced, for non-inventory items. See id on GET accounting/accounts/:id. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.expenseAccountId` | string or null | no | The expense account for non-inventory items and description lines. See id on GET accounting/accounts/:id. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.purchasePriceVarianceAccountId` | string or null | no | The purchase price variance account, under standard costing. See id on GET accounting/accounts/:id. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.inventoryAdjustmentGainAccountId` | string or null | no | The account for positive inventory count and adjustment differences. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.inventoryAdjustmentLossAccountId` | string or null | no | The account for negative inventory count and adjustment differences. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.revaluationGainAccountId` | string or null | no | The account for gains from inventory revaluation. See id on GET accounting/accounts/:id. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.revaluationLossAccountId` | string or null | no | The account for losses from inventory revaluation. See id on GET accounting/accounts/:id. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.soldQuantitiesRevaluationAccountId` | string or null | no | The account for the revaluation of quantities already sold. See id on GET accounting/accounts/:id. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.purchaseReturnVarianceGainAccountId` | string or null | no | The account for gains on purchase returns. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.purchaseReturnVarianceLossAccountId` | string or null | no | The account for losses on purchase returns. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `accounts.fxRateDiffAccountId` | string or null | no | The account that absorbs FX rate differences between receipt and invoice posting. See id on GET accounting/accounts/:id. Omit to leave it unchanged. Pass null to unassign it, so a less specific rule decides that posting. | | `createReasonCodeOverrides` | array of object | no | Reason-code overrides to add. Each is a full override with no id, because the row does not exist yet. | | `createReasonCodeOverrides[].reasonCodeId` | string | yes | The reason code that triggers this override. See id on GET inventory/reason-codes/:id. | | `createReasonCodeOverrides[].accountId` | string | yes | The account to post instead of the rule account. See id on GET accounting/accounts/:id. | | `createReasonCodeOverrides[].parentAccountType` | string, one of `"inventoryAdjustmentGainAccount"`, `"inventoryAdjustmentLossAccount"`, `"salesReturnAccount"`, `"purchaseReturnVarianceGainAccount"`, `"purchaseReturnVarianceLossAccount"`, `"grncClearingAccount"` | yes | Which rule account this override replaces. Only six of the twenty accounts can be overridden. | | `updateReasonCodeOverrides` | array of object | no | Reason-code overrides to change, each keyed by the override's own id. An override id belonging to another rule fails with OVERRIDE_NOT_FOUND. | | `updateReasonCodeOverrides[].id` | string | yes | The unique identifier for the override to change. Read it from the override's id on GET inventory/account-determination-rules/:id. | | `updateReasonCodeOverrides[].accountId` | string | no | The account to post instead of the rule account. See id on GET accounting/accounts/:id. Omit to leave it unchanged. | | `updateReasonCodeOverrides[].parentAccountType` | string, one of `"inventoryAdjustmentGainAccount"`, `"inventoryAdjustmentLossAccount"`, `"salesReturnAccount"`, `"purchaseReturnVarianceGainAccount"`, `"purchaseReturnVarianceLossAccount"`, `"grncClearingAccount"` | no | Which rule account this override replaces. Only six of the twenty accounts can be overridden. Omit to leave it unchanged. | | `deleteReasonCodeOverrides` | array of string | no | The unique identifiers for the reason-code overrides to remove. They are applied before createReasonCodeOverrides, so one call can retire an override and add a replacement for the same reason code. | Example: ```json { "ruleName": "<ruleName>", "priority": 0, "isActive": true, "matching": { "bookId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "itemTypeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "itemCategoryId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "locationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "inventoryLocationType": "<inventoryLocationType>", "transactionTypeContext": "RECEIPT" }, "accounts": { "cogsAccountId": "9a4d18e2-70bc-4f51-8d36-2e0b74f9a3c1" }, "createReasonCodeOverrides": [ { "reasonCodeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "parentAccountType": "inventoryAdjustmentGainAccount" } ], "updateReasonCodeOverrides": [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "parentAccountType": "inventoryAdjustmentGainAccount" } ], "deleteReasonCodeOverrides": [ "<deleteReasonCodeOverrides>" ] } ``` ## Response `200` Success Under `data` (updateAccountDeterminationRuleOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the rule. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `DUPLICATE_RULE_TUPLE` | 400 | | `OVERRIDE_NOT_FOUND` | 400 | | `UNKNOWN_PARENT_ACCOUNT_TYPE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND, DUPLICATE_RULE_TUPLE, OVERRIDE_NOT_FOUND, UNKNOWN_PARENT_ACCOUNT_TYPE - `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 --- # Deletes the account determination rule with the given identifier. `DELETE /inventory/account-determination-rules/{id}` Group: Inventory › Account Determination Rules · Operation ID: `deleteAccountDeterminationRule` There is no in-use guard, and that is deliberate. A rule is configuration read when a document posts, not a record other rows point at, so deleting one changes which accounts future postings hit and leaves every existing journal entry as it was. Deleting a rule can leave a posting with no matching rule, which surfaces at posting time. To take a rule out of play reversibly, PATCH inventory/account-determination-rules/:id with isActive false instead. The overrides go with the rule. The response is the deleted rule's id and ruleName. Use POST inventory/account-determination-rules/bulk-delete to delete several at once. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteAccountDeterminationRule) ## 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. | | `id` | path | string | yes | The unique identifier for the rule to delete. | ## Response `200` Success Under `data` (deleteAccountDeterminationRuleOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted rule. | | `ruleName` | string | yes | A free-text label for the rule. It is not unique and is never read when matching. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Creates several account determination rules in one call. `POST /inventory/account-determination-rules/bulk` Group: Inventory › Account Determination Rules · Operation ID: `bulkCreateAccountDeterminationRules` All or nothing. Every entry is checked before any is written, so one rejected entry leaves the store untouched and the whole call fails. The duplicate check compares the entries against each other as well as against stored rules, so two entries pinning the same combination within one entity collide. The response is the created rules in the same order as the input. Use POST inventory/account-determination-rules to create a single rule. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateAccountDeterminationRules) ## 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`: bulkCreateAccountDeterminationRulesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `accountDeterminationRules` | array of object | yes | The rules to create, each with its dimensions, accounts and overrides. If any entry is invalid, nothing is created and the whole call fails. | | `accountDeterminationRules[].ruleName` | string | yes | A free-text label for the rule. Required. It is not unique and is never read when matching. It names the rule in a duplicate error and in the application. | | `accountDeterminationRules[].priority` | number | no | The tie-breaker between rules of equal specificity. The lower number wins. An omitted priority counts as 0, which outranks any positive value, so set it on every rule when precedence matters. | | `accountDeterminationRules[].isActive` | boolean | no | Whether the rule takes part in account determination. Defaults to true. | | `accountDeterminationRules[].matching` | object | yes | The dimensions this rule is pinned to. Omitting a dimension makes the rule match every value of it. Only one rule per entity may hold a given combination of transaction type, item type, item category and location. | | `accountDeterminationRules[].matching.entityId` | string | yes | The legal entity this rule belongs to. Required. See id on GET base/entities. Matching and the duplicate check are both scoped to it, so a posting only ever sees rules of its own entity. | | `accountDeterminationRules[].matching.bookId` | string | no | The accounting book this rule is pinned to. See id on GET accounting/books/:id. Omit to leave the rule matching every value of this dimension. | | `accountDeterminationRules[].matching.itemTypeId` | string | no | The item type this rule is pinned to. See id on GET inventory/item-types/:id. Omit to leave the rule matching every value of this dimension. | | `accountDeterminationRules[].matching.itemCategoryId` | string | no | The item category this rule is pinned to. See id on GET inventory/item-categories/:id. Omit to leave the rule matching every value of this dimension. | | `accountDeterminationRules[].matching.locationId` | string | no | The warehouse location this rule is pinned to. See id on GET inventory/warehouses/:id. Omit to leave the rule matching every value of this dimension. | | `accountDeterminationRules[].matching.inventoryLocationType` | string | no | An advisory classification of the location. It is stored on the rule and never read when matching, so it does not affect which rule the posting selects. Omit for none. | | `accountDeterminationRules[].matching.transactionTypeContext` | string, one of `"RECEIPT"`, `"SHIPMENT"`, `"SALES_RETURN"`, `"SALES_INVOICE"`, `"SALES_CREDIT_MEMO"`, `"PURCHASE_RETURN"`, `"PURCHASE_INVOICE"`, `"PURCHASE_CREDIT_MEMO"`, `"INVENTORY_INCREASE"`, `"INVENTORY_DECREASE"`, `"INVENTORY_REVALUATION"`, `"WAREHOUSE_TRANSFER"`, `"INTERCOMPANY_TRANSFER"`, `"DEFAULT"` | no | The document kind this rule is pinned to. Passing DEFAULT and omitting the field are equivalent: both make the rule match every transaction type. WAREHOUSE_TRANSFER and INTERCOMPANY_TRANSFER are the kinds used for transfer postings. | | `accountDeterminationRules[].accounts` | object | no | The GL accounts this rule assigns. Every one is optional. Leave one unassigned and the rule does not decide that posting, so a less specific rule does. | | `accountDeterminationRules[].accounts.inventoryAssetAccountId` | string | no | The account for inventory value on the balance sheet. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.cogsAccountId` | string | no | The cost of goods sold account. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.cogsClearingAccountId` | string | no | The cost of goods sold clearing account. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.deferredCogsAccountId` | string | no | The deferred cost of goods sold account. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.salesRevenueAccountId` | string | no | The sales revenue account. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.salesReturnAccountId` | string | no | The sales return account. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.salesStockInTransitAccountId` | string | no | The sales stock-in-transit account. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.grniClearingAccountId` | string | no | The goods-received-not-invoiced clearing account, for inventory items. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.grncClearingAccountId` | string | no | The goods-returned-not-credited clearing account. It is the offset leg booked on purchase returns and credit memos. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.servicesReceivedNotInvoicedAccountId` | string | no | The clearing account for services received but not yet invoiced, for non-inventory items. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.expenseAccountId` | string | no | The expense account for non-inventory items and description lines. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.purchasePriceVarianceAccountId` | string | no | The purchase price variance account, under standard costing. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.inventoryAdjustmentGainAccountId` | string | no | The account for positive inventory count and adjustment differences. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.inventoryAdjustmentLossAccountId` | string | no | The account for negative inventory count and adjustment differences. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.revaluationGainAccountId` | string | no | The account for gains from inventory revaluation. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.revaluationLossAccountId` | string | no | The account for losses from inventory revaluation. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.soldQuantitiesRevaluationAccountId` | string | no | The account for the revaluation of quantities already sold. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.purchaseReturnVarianceGainAccountId` | string | no | The account for gains on purchase returns. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.purchaseReturnVarianceLossAccountId` | string | no | The account for losses on purchase returns. See id on GET accounting/accounts/:id. It can be overridden per reason code. Omit to leave it unassigned. | | `accountDeterminationRules[].accounts.fxRateDiffAccountId` | string | no | The account that absorbs FX rate differences between receipt and invoice posting. See id on GET accounting/accounts/:id. Omit to leave it unassigned. | | `accountDeterminationRules[].reasonCodeOverrides` | array of object | no | The per-reason-code account overrides. When a document line uses one of these reason codes, its account replaces the rule account named by parentAccountType. | | `accountDeterminationRules[].reasonCodeOverrides[].reasonCodeId` | string | yes | The reason code that triggers this override. See id on GET inventory/reason-codes/:id. | | `accountDeterminationRules[].reasonCodeOverrides[].accountId` | string | yes | The account to post instead of the rule account. See id on GET accounting/accounts/:id. | | `accountDeterminationRules[].reasonCodeOverrides[].parentAccountType` | string, one of `"inventoryAdjustmentGainAccount"`, `"inventoryAdjustmentLossAccount"`, `"salesReturnAccount"`, `"purchaseReturnVarianceGainAccount"`, `"purchaseReturnVarianceLossAccount"`, `"grncClearingAccount"` | yes | Which rule account this override replaces. Only six of the twenty accounts can be overridden. | Example: ```json { "accountDeterminationRules": [ { "ruleName": "Finished goods — receipts", "priority": 10, "matching": { "entityId": "81e8beb1-1415-4012-9012-8138580f43dc", "itemCategoryId": "a2f47b91-0c6d-4e38-95b1-7d3e6a428c50", "transactionTypeContext": "RECEIPT" }, "accounts": { "inventoryAssetAccountId": "1c9e4b07-52da-4f38-9016-3b7e05c2a94f", "grniClearingAccountId": "4f0b7d29-6e13-4a85-b2c7-90d1f83e6a52" } } ] } ``` ## Response `200` Success Under `data` (bulkCreateAccountDeterminationRulesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `created` | array of object | yes | The created rules, in the same order as the input. Match entries to inputs by index. | | `created[].id` | string | yes | The unique identifier for the created rule. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `DUPLICATE_RULE_TUPLE` | 400 | | `UNKNOWN_PARENT_ACCOUNT_TYPE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, DUPLICATE_RULE_TUPLE, UNKNOWN_PARENT_ACCOUNT_TYPE - `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 --- # Deletes several account determination rules in one call. `POST /inventory/account-determination-rules/bulk-delete` Group: Inventory › Account Determination Rules · Operation ID: `bulkDeleteAccountDeterminationRules` All or nothing. If any identifier matches no rule, nothing is deleted and the whole call fails, and index on the error says which entry. A rule is configuration, not a record other rows point at, so there is no in-use guard: an identifier that resolves is deleted, and its reason-code overrides go with it. The response is the deleted rules in the same order as the input. Use DELETE inventory/account-determination-rules/:id to delete a single rule. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteAccountDeterminationRules) ## 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`: bulkDeleteAccountDeterminationRulesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers for the rules to delete. If any identifier matches nothing, nothing is deleted and the whole call fails. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteAccountDeterminationRulesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `deleted` | array of object | yes | The deleted rules, in the same order as the input. Match entries to inputs by index. | | `deleted[].id` | string | yes | The unique identifier for the deleted rule. | | `deleted[].ruleName` | string | yes | The label the deleted rule held. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Returns the account determination rules that match the given filters. `POST /inventory/account-determination-rules/search` Group: Inventory › Account Determination Rules · Operation ID: `searchAccountDeterminationRules` Every filter matches a stored dimension, not matching behavior: a filter on itemTypeId finds the rules pinned to that item type, not the wildcard rules that would also apply to it. This endpoint returns the whole rule and is the only one that can filter by entity, book, item type, item category, location or transaction type, which the collection cannot. Reviewing one entity's configuration means filters.entityId. There is no free-text filter. An identifier that matches nothing returns an empty page. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchAccountDeterminationRules) ## 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`. | ## Request body Required, `application/json`: searchAccountDeterminationRulesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. A rule must match every filter you send. Each filter matches a stored dimension, not matching behavior: a filter on itemTypeId finds the rules pinned to that item type, not the wildcard rules that would also apply to it. Send an empty object to return all rules. | | `filters.entityId` | string | no | Limits results to the rules belonging to this legal entity. See id on GET base/entities. This is the usual way to review one entity's configuration. An identifier that matches no entity returns an empty page. | | `filters.bookId` | string | no | Limits results to the rules pinned to this accounting book. See id on GET accounting/books/:id. Rules with no book, which apply to every book, are not returned. | | `filters.itemTypeId` | string | no | Limits results to the rules pinned to this item type. See id on GET inventory/item-types/:id. Wildcard rules are not returned. | | `filters.itemCategoryId` | string | no | Limits results to the rules pinned to this item category. See id on GET inventory/item-categories/:id. Wildcard rules are not returned. | | `filters.locationId` | string | no | Limits results to the rules pinned to this warehouse location. See id on GET inventory/warehouses/:id. Wildcard rules are not returned. | | `filters.transactionTypeContext` | string, one of `"RECEIPT"`, `"SHIPMENT"`, `"SALES_RETURN"`, `"SALES_INVOICE"`, `"SALES_CREDIT_MEMO"`, `"PURCHASE_RETURN"`, `"PURCHASE_INVOICE"`, `"PURCHASE_CREDIT_MEMO"`, `"INVENTORY_INCREASE"`, `"INVENTORY_DECREASE"`, `"INVENTORY_REVALUATION"`, `"WAREHOUSE_TRANSFER"`, `"INTERCOMPANY_TRANSFER"`, `"DEFAULT"` | no | Limits results to the rules pinned to this document kind. Filtering on DEFAULT returns the rules whose transaction type is DEFAULT. A rule whose transaction type is absent is not returned. | | `filters.isActive` | boolean | no | Limits results to rules that take part in account determination when true, or to disabled ones when false. Omit to match both. | | `filters.modifiedSince` | string (date-time) | no | Limits results to rules whose header changed on or after this time. Format YYYY-MM-DDTHH:mm:ss.sssZ. Adding or changing a reason-code override does not change lastModifiedDate. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of rules per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "entityId": "81e8beb1-1415-4012-9012-8138580f43dc", "isActive": true }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchAccountDeterminationRulesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The rules on this page, ordered by id. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the rule. | | `items[].ruleName` | string | yes | A free-text label for the rule. It is not unique and is never read when matching. | | `items[].priority` | number | no | The tie-breaker between rules of equal specificity. The lower number wins. An absent priority counts as 0, which outranks any positive value. | | `items[].isActive` | boolean | no | Whether the rule takes part in account determination. When false, the rule is skipped. | | `items[].matching` | object | yes | The dimensions this rule is pinned to. An absent dimension matches every value of it. | | `items[].matching.entityId` | string | yes | The legal entity this rule belongs to. See id on GET base/entities. Matching is always scoped to one entity, so this is never absent. | | `items[].matching.bookId` | string | no | The accounting book this rule is pinned to. See id on GET accounting/books/:id. Absent means the rule matches every value of this dimension. | | `items[].matching.itemTypeId` | string | no | The item type this rule is pinned to. See id on GET inventory/item-types/:id. Absent means the rule matches every value of this dimension. | | `items[].matching.itemCategoryId` | string | no | The item category this rule is pinned to. See id on GET inventory/item-categories/:id. Absent means the rule matches every value of this dimension. | | `items[].matching.locationId` | string | no | The warehouse location this rule is pinned to. See id on GET inventory/warehouses/:id. Absent means the rule matches every value of this dimension. | | `items[].matching.inventoryLocationType` | string | no | An advisory classification of the location. It is stored on the rule and never read when matching, so it does not affect which rule the posting selects. Absent when none was set. | | `items[].matching.transactionTypeContext` | string, one of `"RECEIPT"`, `"SHIPMENT"`, `"SALES_RETURN"`, `"SALES_INVOICE"`, `"SALES_CREDIT_MEMO"`, `"PURCHASE_RETURN"`, `"PURCHASE_INVOICE"`, `"PURCHASE_CREDIT_MEMO"`, `"INVENTORY_INCREASE"`, `"INVENTORY_DECREASE"`, `"INVENTORY_REVALUATION"`, `"WAREHOUSE_TRANSFER"`, `"INTERCOMPANY_TRANSFER"`, `"DEFAULT"` | no | The document kind this rule is pinned to. The wildcard that matches every transaction type reads back in two forms: the field is absent, or it is DEFAULT. Do not treat absence alone as the wildcard, because the transaction type can be DEFAULT explicitly. WAREHOUSE_TRANSFER and INTERCOMPANY_TRANSFER are the kinds used for transfer postings. | | `items[].accounts` | object | yes | The GL accounts this rule assigns. An account the rule does not assign is absent, and a less specific rule decides that posting. | | `items[].accounts.inventoryAssetAccountId` | string | no | The account for inventory value on the balance sheet. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `items[].accounts.cogsAccountId` | string | no | The cost of goods sold account. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `items[].accounts.cogsClearingAccountId` | string | no | The cost of goods sold clearing account. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `items[].accounts.deferredCogsAccountId` | string | no | The deferred cost of goods sold account. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `items[].accounts.salesRevenueAccountId` | string | no | The sales revenue account. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `items[].accounts.salesReturnAccountId` | string | no | The sales return account. See id on GET accounting/accounts/:id. It can be overridden per reason code. Absent when the rule assigns none. | | `items[].accounts.salesStockInTransitAccountId` | string | no | The sales stock-in-transit account. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `items[].accounts.grniClearingAccountId` | string | no | The goods-received-not-invoiced clearing account, for inventory items. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `items[].accounts.grncClearingAccountId` | string | no | The goods-returned-not-credited clearing account. It is the offset leg booked on purchase returns and credit memos. See id on GET accounting/accounts/:id. It can be overridden per reason code. Absent when the rule assigns none. | | `items[].accounts.servicesReceivedNotInvoicedAccountId` | string | no | The clearing account for services received but not yet invoiced, for non-inventory items. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `items[].accounts.expenseAccountId` | string | no | The expense account for non-inventory items and description lines. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `items[].accounts.purchasePriceVarianceAccountId` | string | no | The purchase price variance account, under standard costing. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `items[].accounts.inventoryAdjustmentGainAccountId` | string | no | The account for positive inventory count and adjustment differences. See id on GET accounting/accounts/:id. It can be overridden per reason code. Absent when the rule assigns none. | | `items[].accounts.inventoryAdjustmentLossAccountId` | string | no | The account for negative inventory count and adjustment differences. See id on GET accounting/accounts/:id. It can be overridden per reason code. Absent when the rule assigns none. | | `items[].accounts.revaluationGainAccountId` | string | no | The account for gains from inventory revaluation. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `items[].accounts.revaluationLossAccountId` | string | no | The account for losses from inventory revaluation. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `items[].accounts.soldQuantitiesRevaluationAccountId` | string | no | The account for the revaluation of quantities already sold. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `items[].accounts.purchaseReturnVarianceGainAccountId` | string | no | The account for gains on purchase returns. See id on GET accounting/accounts/:id. It can be overridden per reason code. Absent when the rule assigns none. | | `items[].accounts.purchaseReturnVarianceLossAccountId` | string | no | The account for losses on purchase returns. See id on GET accounting/accounts/:id. It can be overridden per reason code. Absent when the rule assigns none. | | `items[].accounts.fxRateDiffAccountId` | string | no | The account that absorbs FX rate differences between receipt and invoice posting. See id on GET accounting/accounts/:id. Absent when the rule assigns none. | | `items[].reasonCodeOverrides` | array of object | yes | The per-reason-code account overrides, ordered by id. An empty array when the rule has none. When a document line uses one of these reason codes, its account replaces the rule account named by parentAccountType. It is not returned by the collection GET inventory/account-determination-rules. | | `items[].reasonCodeOverrides[].id` | string | yes | The unique identifier for the override. updateReasonCodeOverrides and deleteReasonCodeOverrides address it on PATCH. | | `items[].reasonCodeOverrides[].reasonCodeId` | string | yes | The reason code that triggers this override. See id on GET inventory/reason-codes/:id. | | `items[].reasonCodeOverrides[].accountId` | string | yes | The account posted instead of the rule account. See id on GET accounting/accounts/:id. | | `items[].reasonCodeOverrides[].parentAccountType` | string, one of `"inventoryAdjustmentGainAccount"`, `"inventoryAdjustmentLossAccount"`, `"salesReturnAccount"`, `"purchaseReturnVarianceGainAccount"`, `"purchaseReturnVarianceLossAccount"`, `"grncClearingAccount"` | yes | Which rule account this override replaces. Only six of the twenty accounts can be overridden. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the rule header last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Adding or changing a reason-code override does not change it. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more rules remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of batches. `GET /inventory/batch-numbers` Group: Inventory › Batch Numbers · Operation ID: `batchNumbers` This collection omits cost, originalCost and currency. Read those from GET inventory/batch-numbers/:id or POST inventory/batch-numbers/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=batchNumbers) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `batchNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `manufacturerBatchNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `itemId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `expirationDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `batchStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `temperatureBreach` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `qcStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `active` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, batchNumber, expirationDate, batchStatus, lastModifiedDate - Selectable: id, batchNumber, manufacturerBatchNumber, itemId, itemCode, itemName, locationId, locationCode, locationName, quantity, inStock, expirationDate, manufactureDate, bestBeforeDate, entryDate, batchStatus, temperatureBreach, qcStatus, active, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of batchNumbersRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the batch. | | `batchNumber` | string or null | no | The batch number assigned when the stock was received. It cannot be changed. | | `manufacturerBatchNumber` | string or null | no | The manufacturer's own batch reference for this batch, where it differs from the assigned batch number. | | `itemId` | string or null | no | The unique identifier for the item this batch is of. See id on GET inventory/items/:id. | | `itemCode` | string or null | no | The code of that item. | | `itemName` | string or null | no | The display name of that item. | | `locationId` | string or null | no | The unique identifier for the warehouse holding the batch. See id on GET inventory/warehouses/:id. | | `locationCode` | string or null | no | The code of that warehouse. | | `locationName` | string or null | no | The display name of that warehouse. | | `quantity` | string (decimal) or null | no | The quantity that arrived in the batch. | | `inStock` | string (decimal) or null | no | The quantity still on hand. A status of 'QUARANTINED' or 'EXPIRED' does not reduce it. | | `expirationDate` | string (date) or null | no | The date the batch expires. Format YYYY-MM-DD. | | `manufactureDate` | string (date) or null | no | The date the batch was made. Format YYYY-MM-DD. | | `bestBeforeDate` | string (date) or null | no | The best-before date printed on the batch, which may differ from the expiry. Format YYYY-MM-DD. | | `entryDate` | string (date) or null | no | The date the batch entered the warehouse. Format YYYY-MM-DD. | | `batchStatus` | string or null | no | The status of the batch. Possible values: 'ACTIVE', 'QUARANTINED', 'EXPIRED', 'DISPOSED'. Only 'ACTIVE' stock can be picked, and none of the other three values reduces the quantity on hand. | | `temperatureBreach` | boolean or null | no | Whether the batch has been outside its storage temperature range. | | `qcStatus` | string or null | no | The tenant's own quality-control state for the batch. This is free text. | | `active` | boolean or null | no | Whether the batch can be selected on new documents. | | `createdDate` | string (date-time) or null | no | The date and time the batch record was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the batch record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the batch with the given identifier. `GET /inventory/batch-numbers/{id}` Group: Inventory › Batch Numbers · Operation ID: `getBatchNumber` Returns the same fields as POST inventory/batch-numbers/search. The collection GET inventory/batch-numbers omits cost, originalCost and currency. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getBatchNumber) ## 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`. | | `id` | path | string | yes | The unique identifier for the batch to return. | ## Response `200` Success Under `data` (getBatchNumberOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the batch. | | `batchNumber` | string | no | The batch number assigned when the stock was received. It cannot be changed. | | `manufacturerBatchNumber` | string | no | The manufacturer's own batch reference for this batch, where it differs from the assigned batch number. | | `itemId` | string | no | The unique identifier for the item this batch is of. See id on GET inventory/items/:id. | | `itemCode` | string | no | The code of that item. | | `itemName` | string | no | The display name of that item. | | `locationId` | string | no | The unique identifier for the warehouse holding the batch. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The code of that warehouse. | | `locationName` | string | no | The display name of that warehouse. | | `quantity` | string (decimal) | no | The quantity that arrived in the batch. | | `inStock` | string (decimal) | no | The quantity still on hand. A status of 'QUARANTINED' or 'EXPIRED' does not reduce it. | | `cost` | string (decimal) | no | The value the batch is carried at per unit, in the currency named by currency. | | `originalCost` | string (decimal) | no | The value the batch was carried at per unit when it arrived, in the currency named by currency. | | `currency` | string | no | The currency the cost fields are stated in. Format ISO 4217, for example 'USD'. | | `expirationDate` | string (date) | no | The date the batch expires. Format YYYY-MM-DD. | | `manufactureDate` | string (date) | no | The date the batch was made. Format YYYY-MM-DD. | | `bestBeforeDate` | string (date) | no | The best-before date printed on the batch, which may differ from the expiry. Format YYYY-MM-DD. | | `entryDate` | string (date) | no | The date the batch entered the warehouse. Format YYYY-MM-DD. | | `batchStatus` | string | no | The status of the batch. Possible values: 'ACTIVE', 'QUARANTINED', 'EXPIRED', 'DISPOSED'. Only 'ACTIVE' stock can be picked, and none of the other three values reduces the quantity on hand. | | `temperatureBreach` | boolean | no | Whether the batch has been outside its storage temperature range. | | `qcStatus` | string | no | The tenant's own quality-control state for the batch. This is free text. | | `active` | boolean | no | Whether the batch can be selected on new documents. | | `createdDate` | string (date-time) | no | The date and time the batch record was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the batch record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Batch Number `PATCH /inventory/batch-numbers/{id}` Group: Inventory › Batch Numbers · Operation ID: `updateBatchNumber` Updates the batch with the given identifier. Only the manufacturer batch reference, the dates, the batch status, the temperature-breach flag, the quality-control state and the active flag can be changed. Only the fields present in the request are written. The cost fields, the quantity, the stock on hand and the location are written by the documents that move and value the stock and cannot be sent here. Changing batchStatus does not change the quantity: 'QUARANTINED', 'EXPIRED' and 'DISPOSED' each stop the batch being picked, and the stock stays on hand and valued until a document takes it away. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateBatchNumber) ## 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. | | `id` | path | string | yes | The unique identifier for the batch to update. | ## Request body Required, `application/json`: updateBatchNumberInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `manufacturerBatchNumber` | string | no | The manufacturer's own batch reference. | | `expirationDate` | string (date) | no | The date the batch expires. Use format YYYY-MM-DD. | | `manufactureDate` | string (date) | no | The date the batch was made. Use format YYYY-MM-DD. | | `bestBeforeDate` | string (date) | no | The best-before date printed on the batch. Use format YYYY-MM-DD. | | `entryDate` | string (date) | no | The date the batch entered the warehouse. Use format YYYY-MM-DD. | | `batchStatus` | string | no | The status of the batch. Accepted values: 'ACTIVE', 'QUARANTINED', 'EXPIRED', 'DISPOSED'. Only 'ACTIVE' stock can be picked, and none of the other three reduces the quantity on hand. | | `temperatureBreach` | boolean | no | Whether the batch has been outside its storage temperature range. | | `qcStatus` | string | no | The tenant's own quality-control state for the batch. This is free text. | | `active` | boolean | no | Whether the batch can be selected on new documents. | Example: ```json { "manufacturerBatchNumber": "<manufacturerBatchNumber>", "expirationDate": "2026-01-15", "manufactureDate": "2026-01-15", "bestBeforeDate": "2026-01-15", "entryDate": "2026-01-15", "batchStatus": "QUARANTINED", "temperatureBreach": true, "qcStatus": "Awaiting lab result", "active": true } ``` ## Response `200` Success Under `data` (updateBatchNumberOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the batch. | | `batchStatus` | string | no | The status of the batch after the update. Possible values: 'ACTIVE', 'QUARANTINED', 'EXPIRED', 'DISPOSED'. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `UNKNOWN_STATUS` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, UNKNOWN_STATUS - `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 --- # Returns the batches that match the given filters. `POST /inventory/batch-numbers/search` Group: Inventory › Batch Numbers · Operation ID: `searchBatchNumbers` Every criterion is exact and criteria combine with AND. Send an empty filters object to return every batch. Use expiringBefore for a shelf-life sweep of everything due to expire by a date, which you can narrow to one item or warehouse and combine with inStock for what is still on hand. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchBatchNumbers) ## 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`. | ## Request body Required, `application/json`: searchBatchNumbersInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. A batch must match every filter you send. Send an empty object to return all batches. | | `filters.batchNumber` | string | no | Limits results to the batch with this batch number. The match is exact. | | `filters.manufacturerBatchNumber` | string | no | Limits results to batches with this manufacturer batch reference. The match is exact. | | `filters.itemId` | string | no | Limits results to batches of this item. An identifier that is not an item's matches no batch. | | `filters.locationId` | string | no | Limits results to batches in this warehouse. | | `filters.batchStatus` | string | no | Limits results to batches with this status. Accepted values: 'ACTIVE', 'QUARANTINED', 'EXPIRED', 'DISPOSED'. | | `filters.temperatureBreach` | boolean | no | Limits results to batches with the given temperatureBreach value. | | `filters.qcStatus` | string | no | Limits results to batches with this quality-control state. The match is exact. | | `filters.active` | boolean | no | Limits results to batches with the given active value. | | `filters.inStock` | boolean | no | Limits results to batches with stock left when true, and to exhausted batches when false. | | `filters.expiringBefore` | string (date) | no | Limits results to batches expiring on or before this date. Format YYYY-MM-DD. | | `filters.expiringAfter` | string (date) | no | Limits results to batches expiring on or after this date. Format YYYY-MM-DD. | | `filters.modifiedSince` | string (date-time) | no | Limits results to batches changed on or after this time. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of batches per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "expiringBefore": "2026-12-31", "inStock": true }, "page": { "cursor": "<cursor>", "size": 0 } } ``` ## Response `200` Success Under `data` (searchBatchNumbersOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The batches on this page, oldest first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the batch. | | `items[].batchNumber` | string | no | The batch number assigned when the stock was received. It cannot be changed. | | `items[].manufacturerBatchNumber` | string | no | The manufacturer's own batch reference for this batch, where it differs from the assigned batch number. | | `items[].itemId` | string | no | The unique identifier for the item this batch is of. See id on GET inventory/items/:id. | | `items[].itemCode` | string | no | The code of that item. | | `items[].itemName` | string | no | The display name of that item. | | `items[].locationId` | string | no | The unique identifier for the warehouse holding the batch. See id on GET inventory/warehouses/:id. | | `items[].locationCode` | string | no | The code of that warehouse. | | `items[].locationName` | string | no | The display name of that warehouse. | | `items[].quantity` | string (decimal) | no | The quantity that arrived in the batch. | | `items[].inStock` | string (decimal) | no | The quantity still on hand. A status of 'QUARANTINED' or 'EXPIRED' does not reduce it. | | `items[].cost` | string (decimal) | no | The value the batch is carried at per unit, in the currency named by currency. | | `items[].originalCost` | string (decimal) | no | The value the batch was carried at per unit when it arrived, in the currency named by currency. | | `items[].currency` | string | no | The currency the cost fields are stated in. Format ISO 4217, for example 'USD'. | | `items[].expirationDate` | string (date) | no | The date the batch expires. Format YYYY-MM-DD. | | `items[].manufactureDate` | string (date) | no | The date the batch was made. Format YYYY-MM-DD. | | `items[].bestBeforeDate` | string (date) | no | The best-before date printed on the batch, which may differ from the expiry. Format YYYY-MM-DD. | | `items[].entryDate` | string (date) | no | The date the batch entered the warehouse. Format YYYY-MM-DD. | | `items[].batchStatus` | string | no | The status of the batch. Possible values: 'ACTIVE', 'QUARANTINED', 'EXPIRED', 'DISPOSED'. Only 'ACTIVE' stock can be picked, and none of the other three values reduces the quantity on hand. | | `items[].temperatureBreach` | boolean | no | Whether the batch has been outside its storage temperature range. | | `items[].qcStatus` | string | no | The tenant's own quality-control state for the batch. This is free text. | | `items[].active` | boolean | no | Whether the batch can be selected on new documents. | | `items[].createdDate` | string (date-time) | no | The date and time the batch record was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the batch record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | no | True when more batches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of batch transactions. `GET /inventory/batch-transactions` Group: Inventory › Batch Transactions · Operation ID: `batchTransactions` This collection returns the same fields as GET inventory/batch-transactions/:id and POST inventory/batch-transactions/search. Nothing is withheld. [Open in the interactive reference](https://docs.api.lensing.ai/?op=batchTransactions) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `batchNumberId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `batchNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `expirationDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `itemId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `itemCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `transactionDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `sourceDocumentLineType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, transactionDate, expirationDate, batchNumber, itemCode, locationCode, lastModifiedDate - Selectable: id, batchNumberId, batchNumber, expirationDate, transactionId, itemId, itemCode, itemName, locationId, locationCode, locationName, unitId, unitCode, transactionDate, inQuantity, outQuantity, unitInQuantity, unitOutQuantity, cost, valueDelta, currency, sourceDocumentLineType, lastModifiedDate ## Response `200` Success Under `data`, an array of batchTransactionsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the batch transaction line, not the ledger row. The ledger row's identifier is transactionId. | | `batchNumberId` | string | no | The unique identifier for the batch that moved. See id on GET inventory/batch-numbers/:id. | | `batchNumber` | string or null | no | The batch number as printed on the label, for example 'LOT-2026-08-14'. It is not unique across items. | | `expirationDate` | string (date) or null | no | The date the batch expires. Format YYYY-MM-DD. | | `transactionId` | string or null | no | The unique identifier for the ledger row this batch line belongs to. See id on GET inventory/inventory-transactions/:id. | | `itemId` | string or null | no | The unique identifier for the item. See id on GET inventory/items/:id. | | `itemCode` | string or null | no | The code of the item. | | `itemName` | string or null | no | The display name of the item. | | `locationId` | string or null | no | The unique identifier for the warehouse the movement was at. See id on GET inventory/warehouses/:id. | | `locationCode` | string or null | no | The code of that warehouse. | | `locationName` | string or null | no | The display name of that warehouse. | | `unitId` | string or null | no | The unique identifier for the unit the movement was recorded in. See id on GET inventory/unit-of-measures/:id. | | `unitCode` | string or null | no | The code of that unit, for example 'BOX'. | | `transactionDate` | string (date) or null | no | The date the movement was posted. Format YYYY-MM-DD. | | `inQuantity` | string (decimal) or null | no | The quantity of this batch that moved into stock, in the item's stocking unit. | | `outQuantity` | string (decimal) or null | no | The quantity of this batch that moved out of stock, in the item's stocking unit. | | `unitInQuantity` | string (decimal) or null | no | The same inbound movement in the transaction's own unit. | | `unitOutQuantity` | string (decimal) or null | no | The same outbound movement in the transaction's own unit. | | `cost` | string (decimal) or null | no | The unit cost of the batch at this movement, in the currency named by currency. | | `valueDelta` | string (decimal) or null | no | The change in the batch's carried value at this movement, in the currency named by currency. | | `currency` | string or null | no | The currency the cost fields are stated in. Format ISO 4217, for example 'USD'. | | `sourceDocumentLineType` | string or null | no | The kind of document line that produced the movement. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the batch line last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Get Batch Transaction `GET /inventory/batch-transactions/{id}` Group: Inventory › Batch Transactions · Operation ID: `getBatchTransaction` Returns the batch transaction with the given identifier. Use the batch line's identifier, not the ledger row's. Returns the same fields as GET inventory/batch-transactions and POST inventory/batch-transactions/search. The id is the batch line's, not the ledger row's, and passing a ledger row's identifier returns NOT_FOUND. See transactionId for the ledger row, whose full detail is on GET inventory/inventory-transactions/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getBatchTransaction) ## 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`. | | `id` | path | string | yes | The unique identifier for the batch transaction line to return. Use the batch line's identifier, not the ledger row's. | ## Response `200` Success Under `data` (getBatchTransactionOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the batch transaction line, not the ledger row. The ledger row's identifier is transactionId. | | `batchNumberId` | string | yes | The unique identifier for the batch that moved. See id on GET inventory/batch-numbers/:id. | | `batchNumber` | string | no | The batch number as printed on the label, for example 'LOT-2026-08-14'. It is not unique across items. | | `expirationDate` | string (date) | no | The date the batch expires. Format YYYY-MM-DD. | | `transactionId` | string | no | The unique identifier for the ledger row this batch line belongs to. See id on GET inventory/inventory-transactions/:id. | | `itemId` | string | no | The unique identifier for the item. See id on GET inventory/items/:id. | | `itemCode` | string | no | The code of the item. | | `itemName` | string | no | The display name of the item. | | `locationId` | string | no | The unique identifier for the warehouse the movement was at. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The code of that warehouse. | | `locationName` | string | no | The display name of that warehouse. | | `unitId` | string | no | The unique identifier for the unit the movement was recorded in. See id on GET inventory/unit-of-measures/:id. | | `unitCode` | string | no | The code of that unit, for example 'BOX'. | | `transactionDate` | string (date) | no | The date the movement was posted. Format YYYY-MM-DD. | | `inQuantity` | string (decimal) | no | The quantity of this batch that moved into stock, in the item's stocking unit. | | `outQuantity` | string (decimal) | no | The quantity of this batch that moved out of stock, in the item's stocking unit. | | `unitInQuantity` | string (decimal) | no | The same inbound movement in the transaction's own unit. | | `unitOutQuantity` | string (decimal) | no | The same outbound movement in the transaction's own unit. | | `cost` | string (decimal) | no | The unit cost of the batch at this movement, in the currency named by currency. | | `valueDelta` | string (decimal) | no | The change in the batch's carried value at this movement, in the currency named by currency. | | `currency` | string | no | The currency the cost fields are stated in. Format ISO 4217, for example 'USD'. | | `sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | The kind of document line that produced the movement. | | `lastModifiedDate` | string (date-time) | no | The date and time the batch line last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the batch transactions that match the given filters. `POST /inventory/batch-transactions/search` Group: Inventory › Batch Transactions · Operation ID: `searchBatchTransactions` Every criterion is exact and criteria combine with AND. Send an empty filters object to return every batch transaction. batchNumber matches the number printed on the label, and itemId, locationId and the date bounds describe the ledger row and are resolved through it. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchBatchTransactions) ## 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`. | ## Request body Required, `application/json`: searchBatchTransactionsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. A batch transaction must match every filter you send. Send an empty object to return all batch transactions. | | `filters.batchNumberId` | string | no | Limits results to movements of the batch with this identifier. | | `filters.batchNumber` | string | no | Limits results to movements of the batch with this number, for example 'LOT-2026-08-14'. The match is exact. Batch numbers are not unique across items, so this can match several batches. | | `filters.itemId` | string | no | Limits results to movements of this item. | | `filters.locationId` | string | no | Limits results to movements at this warehouse. | | `filters.fromDate` | string (date) | no | Limits results to movements on or after this date. Format YYYY-MM-DD. | | `filters.toDate` | string (date) | no | Limits results to movements on or before this date. Format YYYY-MM-DD. | | `filters.modifiedSince` | string (date-time) | no | Limits results to batch lines changed on or after this time. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of batch transactions per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "batchNumber": "LOT-2026-08-14" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchBatchTransactionsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The batch transactions on this page, oldest first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the batch transaction line, not the ledger row. The ledger row's identifier is transactionId. | | `items[].batchNumberId` | string | yes | The unique identifier for the batch that moved. See id on GET inventory/batch-numbers/:id. | | `items[].batchNumber` | string | no | The batch number as printed on the label, for example 'LOT-2026-08-14'. It is not unique across items. | | `items[].expirationDate` | string (date) | no | The date the batch expires. Format YYYY-MM-DD. | | `items[].transactionId` | string | no | The unique identifier for the ledger row this batch line belongs to. See id on GET inventory/inventory-transactions/:id. | | `items[].itemId` | string | no | The unique identifier for the item. See id on GET inventory/items/:id. | | `items[].itemCode` | string | no | The code of the item. | | `items[].itemName` | string | no | The display name of the item. | | `items[].locationId` | string | no | The unique identifier for the warehouse the movement was at. See id on GET inventory/warehouses/:id. | | `items[].locationCode` | string | no | The code of that warehouse. | | `items[].locationName` | string | no | The display name of that warehouse. | | `items[].unitId` | string | no | The unique identifier for the unit the movement was recorded in. See id on GET inventory/unit-of-measures/:id. | | `items[].unitCode` | string | no | The code of that unit, for example 'BOX'. | | `items[].transactionDate` | string (date) | no | The date the movement was posted. Format YYYY-MM-DD. | | `items[].inQuantity` | string (decimal) | no | The quantity of this batch that moved into stock, in the item's stocking unit. | | `items[].outQuantity` | string (decimal) | no | The quantity of this batch that moved out of stock, in the item's stocking unit. | | `items[].unitInQuantity` | string (decimal) | no | The same inbound movement in the transaction's own unit. | | `items[].unitOutQuantity` | string (decimal) | no | The same outbound movement in the transaction's own unit. | | `items[].cost` | string (decimal) | no | The unit cost of the batch at this movement, in the currency named by currency. | | `items[].valueDelta` | string (decimal) | no | The change in the batch's carried value at this movement, in the currency named by currency. | | `items[].currency` | string | no | The currency the cost fields are stated in. Format ISO 4217, for example 'USD'. | | `items[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | The kind of document line that produced the movement. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the batch line last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more batch transactions remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Bins `GET /inventory/bins` Group: Inventory › Bins · Operation ID: `bins` Returns a list of bins. To find bins that have no capacity limit, use POST inventory/bins/search. This collection returns the same scalar fields as GET inventory/bins/:id and POST inventory/bins/search. It does not return capacity, which is computed per bin and comes back only on GET inventory/bins/:id. Sorting on aisle, rack and shelfLevel walks the warehouse in order. To find bins that have no capacity limit at all, use POST inventory/bins/search with withoutLimits. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bins) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `binCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `barcode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `aisle` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `rack` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `zone` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isPickingBin` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isReceivingBin` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isProductionBin` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `capacityCheckMode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `tempZone` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `supportsPerishable` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `active` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, binCode, aisle, rack, shelfLevel, active, lastModifiedDate - Selectable: id, binCode, barcode, locationId, locationCode, locationName, aisle, rack, shelfLevel, zone, itemPosition, isPickingBin, isReceivingBin, isProductionBin, maxQuantity, maxWeight, maxWeightUnit, maxVolume, maxVolumeUnit, capacityCheckMode, tempZone, tempMin, tempMax, supportsPerishable, active, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of binsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the bin. | | `binCode` | string or null | no | The short code that identifies the bin, for example 'A-01-1'. It is unique within the tenant, not per warehouse, and cannot be changed after the bin is created. | | `barcode` | string or null | no | The barcode printed on the bin label. It is unique within the tenant and cannot be changed after the bin is created. | | `locationId` | string or null | no | The unique identifier for the warehouse the bin is in. It cannot be changed after the bin is created. See id on GET inventory/warehouses/:id. | | `locationCode` | string or null | no | The code of that warehouse. | | `locationName` | string or null | no | The name of that warehouse. | | `aisle` | string or null | no | The aisle in the warehouse layout. | | `rack` | string or null | no | The rack within the aisle. | | `shelfLevel` | string or null | no | The shelf level within the rack. | | `zone` | string or null | no | The logical zone of the bin, as your tenant names them. For example 'Picking', 'Storage' or 'Quarantine'. | | `itemPosition` | string or null | no | The position within the shelf, where a bin is subdivided further. | | `isPickingBin` | boolean or null | no | Whether the bin is a picking location, a fast-access place stock is taken from to fulfill shipments. | | `isReceivingBin` | boolean or null | no | Whether the bin stages newly received goods before they are put away. | | `isProductionBin` | boolean or null | no | Whether the bin feeds or receives production. | | `maxQuantity` | number or null | no | The largest number of units the bin will hold. It is meaningful only when everything in the bin is counted in the same unit. | | `maxWeight` | string (decimal) or null | no | The largest weight the bin will hold, in maxWeightUnit. | | `maxWeightUnit` | string or null | no | The unit maxWeight is given in. Possible values: 'G', 'Kg', 'Lbs'. | | `maxVolume` | string (decimal) or null | no | The largest volume the bin will hold, in maxVolumeUnit. | | `maxVolumeUnit` | string or null | no | The unit maxVolume is given in. Possible values: 'L'. | | `capacityCheckMode` | string or null | no | What happens when a movement would exceed a limit. Possible values: 'HardBlock', which refuses the movement, and 'SoftWarning', which allows it and flags it. | | `tempZone` | string or null | no | The temperature zone the bin belongs to, as your tenant names them. For example 'Ambient', 'Chilled' or 'Frozen'. | | `tempMin` | string (decimal) or null | no | The lowest temperature kept in the bin, in degrees Celsius. | | `tempMax` | string (decimal) or null | no | The highest temperature kept in the bin, in degrees Celsius. | | `supportsPerishable` | boolean or null | no | Whether perishable stock may be kept in the bin. | | `active` | boolean or null | no | Whether the bin may be chosen for new movements. When false, the stock already in the bin stays there but no new movement can be assigned to it. | | `createdDate` | string (date-time) or null | no | The date and time the bin was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the bin record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a bin. `POST /inventory/bins` Group: Inventory › Bins · Operation ID: `createBin` A warehouse must be given its bins before any stock moves in it. Once it has inventory transactions that recorded no bin, its first bin is refused with WAREHOUSE_NOT_BIN_TRACKABLE and the warehouse can never become bin-tracked. A warehouse that already has one bin can always have more. Only binCode and locationId are required. The response is the created bin's id and code. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createBin) ## 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`: createBinInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `binCode` | string | yes | The short code that identifies the bin, for example 'A-01-1'. Required. It is unique within the tenant, not per warehouse, and cannot be changed after the bin is created. | | `locationId` | string | yes | The unique identifier for the warehouse the bin is in. Required. It cannot be changed after the bin is created. See id on GET inventory/warehouses/:id. | | `barcode` | string | no | The barcode printed on the bin label. It is unique within the tenant and cannot be changed after the bin is created. | | `aisle` | string | no | The aisle in the warehouse layout. | | `rack` | string | no | The rack within the aisle. | | `shelfLevel` | string | no | The shelf level within the rack. | | `zone` | string | no | The logical zone of the bin, as your tenant names them. For example 'Picking', 'Storage' or 'Quarantine'. | | `itemPosition` | string | no | The position within the shelf, where a bin is subdivided further. | | `isPickingBin` | boolean | no | Whether the bin is a picking location, a fast-access place stock is taken from to fulfill shipments. | | `isReceivingBin` | boolean | no | Whether the bin stages newly received goods before they are put away. | | `isProductionBin` | boolean | no | Whether the bin feeds or receives production. | | `maxQuantity` | number | no | The largest number of units the bin will hold. It is meaningful only when everything in the bin is counted in the same unit. | | `maxWeight` | string (decimal) | no | The largest weight the bin will hold, in maxWeightUnit. Send it together with maxWeightUnit. A limit given without its unit is read as no limit, so the bin would accept any weight. | | `maxWeightUnit` | string | no | The unit maxWeight is given in. Accepted values: 'G', 'Kg', 'Lbs'. Send it together with maxWeight, or leave both out. | | `maxVolume` | string (decimal) | no | The largest volume the bin will hold, in maxVolumeUnit. Send it together with maxVolumeUnit. A limit given without its unit is read as no limit. | | `maxVolumeUnit` | string | no | The unit maxVolume is given in. Accepted values: 'L'. Send it together with maxVolume, or leave both out. | | `capacityCheckMode` | string | no | What happens when a movement would exceed a limit. Accepted values: 'HardBlock', which refuses the movement, and 'SoftWarning', which allows it and flags it. | | `tempZone` | string | no | The temperature zone the bin belongs to, as your tenant names them. For example 'Ambient', 'Chilled' or 'Frozen'. | | `tempMin` | string (decimal) | no | The lowest temperature kept in the bin, in degrees Celsius. It must not be above tempMax. | | `tempMax` | string (decimal) | no | The highest temperature kept in the bin, in degrees Celsius. | | `supportsPerishable` | boolean | no | Whether perishable stock may be kept in the bin. | | `active` | boolean | no | Whether the bin may be chosen for new movements. Defaults to true. | Example: ```json { "binCode": "A-01-1", "locationId": "f5272ae0-0f0b-47d2-b78a-2158fec2cc32", "barcode": "<barcode>", "aisle": "A", "rack": "01", "shelfLevel": "1", "zone": "Picking", "itemPosition": "<itemPosition>", "isPickingBin": true, "isReceivingBin": true, "isProductionBin": true, "maxQuantity": 0, "maxWeight": "250", "maxWeightUnit": "Kg", "maxVolume": "19.99", "maxVolumeUnit": "<maxVolumeUnit>", "capacityCheckMode": "HardBlock", "tempZone": "<tempZone>", "tempMin": "19.99", "tempMax": "19.99", "supportsPerishable": true, "active": true } ``` ## Response `200` Success Under `data` (createBinOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created bin. | | `binCode` | string | no | The bin's code. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `DUPLICATE_CODE` | 400 | | `WAREHOUSE_NOT_BIN_TRACKABLE` | 400 | | `UNKNOWN_VALUE` | 400 | | `INCOMPLETE_LIMIT` | 400 | | `INVALID_RANGE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, DUPLICATE_CODE, WAREHOUSE_NOT_BIN_TRACKABLE, UNKNOWN_VALUE, INCOMPLETE_LIMIT, INVALID_RANGE - `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 --- # Returns the bin with the given identifier, with its live utilization. `GET /inventory/bins/{id}` Group: Inventory › Bins · Operation ID: `getBin` This endpoint returns capacity, the bin's live utilization, which no other endpoint returns. capacity is absent when the bin has no limit configured, because there is nothing to measure against, and such a bin accepts anything. Read capacity.mixedUnits before trusting capacity.currentQuantity: with more than one stocking unit in the bin, the quantity is a sum of unlike things and the weight and volume dimensions are the ones to use. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getBin) ## 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`. | | `id` | path | string | yes | The unique identifier for the bin to return. | ## Response `200` Success Under `data` (getBinOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the bin. | | `binCode` | string | no | The short code that identifies the bin, for example 'A-01-1'. It is unique within the tenant, not per warehouse, and cannot be changed after the bin is created. | | `barcode` | string | no | The barcode printed on the bin label. It is unique within the tenant and cannot be changed after the bin is created. | | `locationId` | string | no | The unique identifier for the warehouse the bin is in. It cannot be changed after the bin is created. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The code of that warehouse. | | `locationName` | string | no | The name of that warehouse. | | `aisle` | string | no | The aisle in the warehouse layout. | | `rack` | string | no | The rack within the aisle. | | `shelfLevel` | string | no | The shelf level within the rack. | | `zone` | string | no | The logical zone of the bin, as your tenant names them. For example 'Picking', 'Storage' or 'Quarantine'. | | `itemPosition` | string | no | The position within the shelf, where a bin is subdivided further. | | `isPickingBin` | boolean | no | Whether the bin is a picking location, a fast-access place stock is taken from to fulfill shipments. | | `isReceivingBin` | boolean | no | Whether the bin stages newly received goods before they are put away. | | `isProductionBin` | boolean | no | Whether the bin feeds or receives production. | | `maxQuantity` | number | no | The largest number of units the bin will hold. It is meaningful only when everything in the bin is counted in the same unit. | | `maxWeight` | string (decimal) | no | The largest weight the bin will hold, in maxWeightUnit. | | `maxWeightUnit` | string | no | The unit maxWeight is given in. Possible values: 'G', 'Kg', 'Lbs'. | | `maxVolume` | string (decimal) | no | The largest volume the bin will hold, in maxVolumeUnit. | | `maxVolumeUnit` | string | no | The unit maxVolume is given in. Possible values: 'L'. | | `capacityCheckMode` | string | no | What happens when a movement would exceed a limit. Possible values: 'HardBlock', which refuses the movement, and 'SoftWarning', which allows it and flags it. | | `tempZone` | string | no | The temperature zone the bin belongs to, as your tenant names them. For example 'Ambient', 'Chilled' or 'Frozen'. | | `tempMin` | string (decimal) | no | The lowest temperature kept in the bin, in degrees Celsius. | | `tempMax` | string (decimal) | no | The highest temperature kept in the bin, in degrees Celsius. | | `supportsPerishable` | boolean | no | Whether perishable stock may be kept in the bin. | | `active` | boolean | no | Whether the bin may be chosen for new movements. When false, the stock already in the bin stays there but no new movement can be assigned to it. | | `createdDate` | string (date-time) | no | The date and time the bin was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the bin record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `capacity` | object | no | What is in the bin now, measured against what it will hold. Present on this endpoint only. Absent when the bin has no limit configured, because there is nothing to measure a utilization against. | | `capacity.currentQuantity` | string (decimal) | no | The number of units in the bin. | | `capacity.currentWeight` | string (decimal) | no | The total weight of what is in the bin, normalized to kilograms whatever unit the limit is given in. | | `capacity.currentVolume` | string (decimal) | no | The total volume of what is in the bin, normalized to cubic meters. | | `capacity.quantityUtilizationPct` | string (decimal) | no | How full the bin is against maxQuantity, as a percentage. | | `capacity.weightUtilizationPct` | string (decimal) | no | How full the bin is against maxWeight, as a percentage. | | `capacity.volumeUtilizationPct` | string (decimal) | no | How full the bin is against maxVolume, as a percentage. | | `capacity.status` | string | no | How full the bin is overall. Possible values: 'ok', 'near_full', 'exceeded'. 'exceeded' means the bin is over a limit it was given, which is possible because a 'SoftWarning' bin allows the movement that crosses the limit. | | `capacity.mixedUnits` | boolean | no | Whether the bin holds items counted in more than one stocking unit. When true, currentQuantity is a sum of unlike things and the quantity utilization means nothing, so use the weight and volume instead. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Bin `PATCH /inventory/bins/{id}` Group: Inventory › Bins · Operation ID: `updateBin` Updates the bin with the given identifier. Only the fields in the request body change. Everything omitted from the request body is left as it was. binCode, barcode and locationId cannot be changed and are not accepted here. The limit and range checks read the stored bin as well as the request: adding maxWeight to a bin that has no maxWeightUnit is refused, because the result would be a limit with no unit. The response is the bin's id. Set active to false to stop the bin being chosen for new movements without touching what is in it. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateBin) ## 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. | | `id` | path | string | yes | The unique identifier for the bin to update. | ## Request body Required, `application/json`: updateBinInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `aisle` | string | no | The aisle in the warehouse layout. | | `rack` | string | no | The rack within the aisle. | | `shelfLevel` | string | no | The shelf level within the rack. | | `zone` | string | no | The logical zone of the bin, as your tenant names them. For example 'Picking', 'Storage' or 'Quarantine'. | | `itemPosition` | string | no | The position within the shelf, where a bin is subdivided further. | | `isPickingBin` | boolean | no | Whether the bin is a picking location, a fast-access place stock is taken from to fulfill shipments. | | `isReceivingBin` | boolean | no | Whether the bin stages newly received goods before they are put away. | | `isProductionBin` | boolean | no | Whether the bin feeds or receives production. | | `maxQuantity` | number | no | The largest number of units the bin will hold. It is meaningful only when everything in the bin is counted in the same unit. | | `maxWeight` | string (decimal) | no | The largest weight the bin will hold, in maxWeightUnit. Send it together with maxWeightUnit. A limit given without its unit is read as no limit, so the bin would accept any weight. | | `maxWeightUnit` | string | no | The unit maxWeight is given in. Accepted values: 'G', 'Kg', 'Lbs'. Send it together with maxWeight, or leave both out. | | `maxVolume` | string (decimal) | no | The largest volume the bin will hold, in maxVolumeUnit. Send it together with maxVolumeUnit. A limit given without its unit is read as no limit. | | `maxVolumeUnit` | string | no | The unit maxVolume is given in. Accepted values: 'L'. Send it together with maxVolume, or leave both out. | | `capacityCheckMode` | string | no | What happens when a movement would exceed a limit. Accepted values: 'HardBlock', which refuses the movement, and 'SoftWarning', which allows it and flags it. | | `tempZone` | string | no | The temperature zone the bin belongs to, as your tenant names them. For example 'Ambient', 'Chilled' or 'Frozen'. | | `tempMin` | string (decimal) | no | The lowest temperature kept in the bin, in degrees Celsius. It must not be above tempMax. | | `tempMax` | string (decimal) | no | The highest temperature kept in the bin, in degrees Celsius. | | `supportsPerishable` | boolean | no | Whether perishable stock may be kept in the bin. | | `active` | boolean | no | Whether the bin may be chosen for new movements. Set it to false to retire the bin without deleting it: the stock already in it stays where it is. | Example: ```json { "aisle": "<aisle>", "rack": "<rack>", "shelfLevel": "<shelfLevel>", "zone": "<zone>", "itemPosition": "<itemPosition>", "isPickingBin": true, "isReceivingBin": true, "isProductionBin": true, "maxQuantity": 0, "maxWeight": "300", "maxWeightUnit": "<maxWeightUnit>", "maxVolume": "19.99", "maxVolumeUnit": "<maxVolumeUnit>", "capacityCheckMode": "HardBlock", "tempZone": "<tempZone>", "tempMin": "19.99", "tempMax": "19.99", "supportsPerishable": true, "active": true } ``` ## Response `200` Success Under `data` (updateBinOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the bin. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `DUPLICATE_CODE` | 400 | | `WAREHOUSE_NOT_BIN_TRACKABLE` | 400 | | `UNKNOWN_VALUE` | 400 | | `INCOMPLETE_LIMIT` | 400 | | `INVALID_RANGE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND, DUPLICATE_CODE, WAREHOUSE_NOT_BIN_TRACKABLE, UNKNOWN_VALUE, INCOMPLETE_LIMIT, INVALID_RANGE - `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 --- # Removes the bin with the given identifier. `DELETE /inventory/bins/{id}` Group: Inventory › Bins · Operation ID: `deleteBin` A bin that still holds stock cannot be deleted, because deleting it would leave that stock with no place recorded. Move the stock out first, or set active to false with PATCH inventory/bins/:id to close the bin while its contents stay where they are. Use POST inventory/bins/bulk-delete to remove several at once. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteBin) ## 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. | | `id` | path | string | yes | The unique identifier for the bin to delete. | ## Response `200` Success Under `data` (deleteBinOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the removed bin. | | `binCode` | string | no | The bin's code. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, IN_USE - `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 --- # Creates several bins in one call. `POST /inventory/bins/bulk` Group: Inventory › Bins · Operation ID: `bulkCreateBins` All or nothing. If any entry is invalid, nothing is created and the whole call fails. Two entries claiming the same binCode or barcode collide just as an entry colliding with a stored bin does. Every rule of the single create applies to each entry, including that a capacity limit must be sent with its unit. Use POST inventory/bins to create a single bin. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateBins) ## 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`: bulkCreateBinsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `bins` | array of object | yes | The bins to create. If any entry is invalid, nothing is created and the whole call fails. | | `bins[].binCode` | string | yes | The short code that identifies the bin, for example 'A-01-1'. Required. It is unique within the tenant, not per warehouse, and cannot be changed after the bin is created. | | `bins[].locationId` | string | yes | The unique identifier for the warehouse the bin is in. Required. It cannot be changed after the bin is created. See id on GET inventory/warehouses/:id. | | `bins[].barcode` | string | no | The barcode printed on the bin label. It is unique within the tenant and cannot be changed after the bin is created. | | `bins[].aisle` | string | no | The aisle in the warehouse layout. | | `bins[].rack` | string | no | The rack within the aisle. | | `bins[].shelfLevel` | string | no | The shelf level within the rack. | | `bins[].zone` | string | no | The logical zone of the bin, as your tenant names them. For example 'Picking', 'Storage' or 'Quarantine'. | | `bins[].itemPosition` | string | no | The position within the shelf, where a bin is subdivided further. | | `bins[].isPickingBin` | boolean | no | Whether the bin is a picking location, a fast-access place stock is taken from to fulfill shipments. | | `bins[].isReceivingBin` | boolean | no | Whether the bin stages newly received goods before they are put away. | | `bins[].isProductionBin` | boolean | no | Whether the bin feeds or receives production. | | `bins[].maxQuantity` | number | no | The largest number of units the bin will hold. It is meaningful only when everything in the bin is counted in the same unit. | | `bins[].maxWeight` | string (decimal) | no | The largest weight the bin will hold, in maxWeightUnit. Send it together with maxWeightUnit. A limit given without its unit is read as no limit, so the bin would accept any weight. | | `bins[].maxWeightUnit` | string | no | The unit maxWeight is given in. Accepted values: 'G', 'Kg', 'Lbs'. Send it together with maxWeight, or leave both out. | | `bins[].maxVolume` | string (decimal) | no | The largest volume the bin will hold, in maxVolumeUnit. Send it together with maxVolumeUnit. A limit given without its unit is read as no limit. | | `bins[].maxVolumeUnit` | string | no | The unit maxVolume is given in. Accepted values: 'L'. Send it together with maxVolume, or leave both out. | | `bins[].capacityCheckMode` | string | no | What happens when a movement would exceed a limit. Accepted values: 'HardBlock', which refuses the movement, and 'SoftWarning', which allows it and flags it. | | `bins[].tempZone` | string | no | The temperature zone the bin belongs to, as your tenant names them. For example 'Ambient', 'Chilled' or 'Frozen'. | | `bins[].tempMin` | string (decimal) | no | The lowest temperature kept in the bin, in degrees Celsius. It must not be above tempMax. | | `bins[].tempMax` | string (decimal) | no | The highest temperature kept in the bin, in degrees Celsius. | | `bins[].supportsPerishable` | boolean | no | Whether perishable stock may be kept in the bin. | | `bins[].active` | boolean | no | Whether the bin may be chosen for new movements. Defaults to true. | Example: ```json { "bins": [ { "binCode": "A-01-1", "locationId": "f5272ae0-0f0b-47d2-b78a-2158fec2cc32", "aisle": "A", "rack": "01", "shelfLevel": "1", "isPickingBin": true }, { "binCode": "A-01-2", "locationId": "f5272ae0-0f0b-47d2-b78a-2158fec2cc32", "aisle": "A", "rack": "01", "shelfLevel": "2", "isPickingBin": true } ] } ``` ## Response `200` Success Under `data` (bulkCreateBinsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `created` | array of object | yes | The created bins, in the same order as the input. Match entries to inputs by index. | | `created[].id` | string | yes | The unique identifier for the created bin. | | `created[].binCode` | string | no | The bin's code. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `DUPLICATE_CODE` | 400 | | `WAREHOUSE_NOT_BIN_TRACKABLE` | 400 | | `UNKNOWN_VALUE` | 400 | | `INCOMPLETE_LIMIT` | 400 | | `INVALID_RANGE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, DUPLICATE_CODE, WAREHOUSE_NOT_BIN_TRACKABLE, UNKNOWN_VALUE, INCOMPLETE_LIMIT, INVALID_RANGE - `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 --- # Removes several bins in one call. `POST /inventory/bins/bulk-delete` Group: Inventory › Bins · Operation ID: `bulkDeleteBins` All or nothing. If any bin is unknown or still holds stock, nothing is removed and the whole call fails. Use DELETE inventory/bins/:id to remove a single bin. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteBins) ## 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`: bulkDeleteBinsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers for the bins to remove. If any identifier matches nothing or its bin still holds stock, nothing is removed and the whole call fails. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteBinsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `deleted` | array of object | yes | The removed bins, in the same order as the input. Match entries to inputs by index. | | `deleted[].id` | string | yes | The unique identifier for the removed bin. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, IN_USE - `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 --- # Returns the bins that match the given filters. `POST /inventory/bins/search` Group: Inventory › Bins · Operation ID: `searchBins` Every criterion is exact and criteria combine with AND. Send an empty filters object to return every bin. withoutLimits returns only the bins that were given no quantity, weight or volume limit, the ones no capacity check can protect. capacity is not returned here: it is computed per bin and comes back only on GET inventory/bins/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchBins) ## 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`. | ## Request body Required, `application/json`: searchBinsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. A bin must match every filter you send. Send an empty object to return all bins. | | `filters.binCode` | string | no | Limits results to the bin with this code. The match is exact. Codes are unique, so this matches at most one bin. | | `filters.barcode` | string | no | Limits results to the bin with this barcode. The match is exact. Barcodes are unique, so this matches at most one bin. | | `filters.locationId` | string | no | Limits results to bins in the warehouse with this identifier. An identifier that is not a warehouse matches nothing. See id on GET inventory/warehouses/:id. | | `filters.aisle` | string | no | Limits results to bins in this aisle. The match is exact. | | `filters.rack` | string | no | Limits results to bins on this rack. The match is exact. | | `filters.zone` | string | no | Limits results to bins in this zone. The match is exact. | | `filters.isPickingBin` | boolean | no | Limits results to picking locations when true, or to bins that are not when false. Omit to match both. | | `filters.isReceivingBin` | boolean | no | Limits results to bins that stage newly received goods when true, or to bins that do not when false. Omit to match both. | | `filters.isProductionBin` | boolean | no | Limits results to bins that feed or receive production when true, or to bins that do not when false. Omit to match both. | | `filters.supportsPerishable` | boolean | no | Limits results to bins that may keep perishable stock when true, or to bins that may not when false. Omit to match both. | | `filters.capacityCheckMode` | string | no | Limits results to bins with this capacity check mode. Accepted values: 'HardBlock', 'SoftWarning'. | | `filters.tempZone` | string | no | Limits results to bins in this temperature zone. The match is exact. | | `filters.active` | boolean | no | Limits results to bins that may be chosen for new movements when true, or to retired ones when false. Omit to match both. | | `filters.withoutLimits` | boolean | no | True to return only bins that have no quantity, weight or volume limit, the ones no capacity check can protect. | | `filters.modifiedSince` | string (date-time) | no | Limits results to bins changed on or after this time. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of bins per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "withoutLimits": true, "isPickingBin": true }, "page": { "cursor": "<cursor>", "size": 0 } } ``` ## Response `200` Success Under `data` (searchBinsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The bins on this page, oldest first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the bin. | | `items[].binCode` | string | no | The short code that identifies the bin, for example 'A-01-1'. It is unique within the tenant, not per warehouse, and cannot be changed after the bin is created. | | `items[].barcode` | string | no | The barcode printed on the bin label. It is unique within the tenant and cannot be changed after the bin is created. | | `items[].locationId` | string | no | The unique identifier for the warehouse the bin is in. It cannot be changed after the bin is created. See id on GET inventory/warehouses/:id. | | `items[].locationCode` | string | no | The code of that warehouse. | | `items[].locationName` | string | no | The name of that warehouse. | | `items[].aisle` | string | no | The aisle in the warehouse layout. | | `items[].rack` | string | no | The rack within the aisle. | | `items[].shelfLevel` | string | no | The shelf level within the rack. | | `items[].zone` | string | no | The logical zone of the bin, as your tenant names them. For example 'Picking', 'Storage' or 'Quarantine'. | | `items[].itemPosition` | string | no | The position within the shelf, where a bin is subdivided further. | | `items[].isPickingBin` | boolean | no | Whether the bin is a picking location, a fast-access place stock is taken from to fulfill shipments. | | `items[].isReceivingBin` | boolean | no | Whether the bin stages newly received goods before they are put away. | | `items[].isProductionBin` | boolean | no | Whether the bin feeds or receives production. | | `items[].maxQuantity` | number | no | The largest number of units the bin will hold. It is meaningful only when everything in the bin is counted in the same unit. | | `items[].maxWeight` | string (decimal) | no | The largest weight the bin will hold, in maxWeightUnit. | | `items[].maxWeightUnit` | string | no | The unit maxWeight is given in. Possible values: 'G', 'Kg', 'Lbs'. | | `items[].maxVolume` | string (decimal) | no | The largest volume the bin will hold, in maxVolumeUnit. | | `items[].maxVolumeUnit` | string | no | The unit maxVolume is given in. Possible values: 'L'. | | `items[].capacityCheckMode` | string | no | What happens when a movement would exceed a limit. Possible values: 'HardBlock', which refuses the movement, and 'SoftWarning', which allows it and flags it. | | `items[].tempZone` | string | no | The temperature zone the bin belongs to, as your tenant names them. For example 'Ambient', 'Chilled' or 'Frozen'. | | `items[].tempMin` | string (decimal) | no | The lowest temperature kept in the bin, in degrees Celsius. | | `items[].tempMax` | string (decimal) | no | The highest temperature kept in the bin, in degrees Celsius. | | `items[].supportsPerishable` | boolean | no | Whether perishable stock may be kept in the bin. | | `items[].active` | boolean | no | Whether the bin may be chosen for new movements. When false, the stock already in the bin stays there but no new movement can be assigned to it. | | `items[].createdDate` | string (date-time) | no | The date and time the bin was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the bin record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | no | True when more bins remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Inventory Decreases `GET /inventory/inventory-decreases` Group: Inventory › Inventory Decreases · Operation ID: `inventoryDecreases` Returns a list of inventory decreases. Each row is a header, with lineCount in place of the lines. To filter by item or return the lines, use POST inventory/inventory-decreases/search. This returns inventory decreases as header rows, without their lines: a collection field cannot contain a nested array, so the row publishes lineCount in place of the lines. GET inventory/inventory-decreases/:id and POST inventory/inventory-decreases/search return the full document. To filter by item, or to list documents with their lines, use the search endpoint, which takes itemId and the same filters. [Open in the interactive reference](https://docs.api.lensing.ai/?op=inventoryDecreases) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `reasonCodeId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `reasonCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, documentNumber, documentDate, documentStatus, createdDate, lastModifiedDate - Selectable: id, documentNumber, documentStatus, documentDate, entityId, entityName, locationId, locationCode, locationName, reasonCodeId, reasonCode, reasonCodeDescription, currency, lineCount, notes, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of inventoryDecreasesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the inventory decrease. | | `documentNumber` | string or null | no | The inventory decrease's document number, for example 'DEC-42'. Absent while none has been assigned. | | `documentStatus` | string | no | The status of the inventory decrease. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `documentDate` | string (date) | no | The date the adjustment is dated. It drives the posting period, so it is not the same as when the record was created. Format YYYY-MM-DD. | | `entityId` | string | no | The unique identifier for the legal entity the document belongs to. See id on GET base/entities. | | `entityName` | string or null | no | The display name of that legal entity. | | `locationId` | string or null | no | The unique identifier for the warehouse location. One location for the whole document, and every line uses it. Absent only on a document with no lines. See id on GET inventory/warehouses/:id. | | `locationCode` | string or null | no | The short code of that warehouse location. | | `locationName` | string or null | no | The display name of that warehouse location. | | `reasonCodeId` | string or null | no | The unique identifier for the reason code, when one was given. See id on GET inventory/reason-codes/:id. | | `reasonCode` | string or null | no | The short code of that reason code. | | `reasonCodeDescription` | string or null | no | The description of that reason code. | | `currency` | string or null | no | The currency the inventory decrease is valued in, the legal entity's functional currency. Format ISO 4217, for example 'EUR'. | | `lineCount` | number or null | no | The number of lines the inventory decrease has. The collection returns it because the lines themselves are not on the collection. | | `notes` | string or null | no | Free text on the inventory decrease. | | `createdDate` | string (date-time) or null | no | The date and time the inventory decrease was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the inventory decrease record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Inventory Decrease `POST /inventory/inventory-decreases` Group: Inventory › Inventory Decreases · Operation ID: `createInventoryDecrease` Creates an inventory decrease as a draft. It moves no stock until you commit it. This creates a draft and posts nothing. No stock moves and no journal entry is written until you commit it with POST inventory/inventory-decreases/:id/commit, so you can build the document and check it before it posts. The response is the created document, in the same shape GET inventory/inventory-decreases/:id returns. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createInventoryDecrease) ## 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`: createInventoryDecreaseInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `locationId` | string | yes | The unique identifier for the warehouse location. One location for the whole document, and every line comes out of it. Required. See id on GET inventory/warehouses/:id. | | `entityId` | string | no | The unique identifier for the legal entity. Omit it to use the location's owning entity. See id on GET base/entities. | | `documentDate` | string (date) | no | The date the adjustment is dated. It drives the posting period. Defaults to today. Use format YYYY-MM-DD. | | `reasonCodeId` | string | no | The unique identifier for a reason code explaining the adjustment. See id on GET inventory/reason-codes/:id. | | `notes` | string | no | Free text on the inventory decrease. | | `lines` | array of object | yes | The lines of the inventory decrease. At least one is required. | | `lines[].itemId` | string | yes | The unique identifier for the item whose stock this line removes. See id on GET inventory/items/:id. | | `lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `lines[].quantity` | string (decimal) | yes | The quantity to remove, in that unit. A positive number greater than zero. | | `lines[].unitPrice` | string (decimal) | no | The value of one unit, in the inventory decrease's currency. Optional: omit it to value the stock at the cost it already has, which is what a write-off ordinarily wants. A supplied value must be greater than zero. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches to take the quantity from. Required for a batch-tracked item, and the quantities must add up to the line's quantity. Every batch named must already exist for the item, since a decrease removes lots and does not create them. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch being removed. Give this or batchNumber. | | `lines[].batches[].batchNumber` | string | no | The batch number as printed, for example 'LOT-2026-08-14'. It is matched against the batches of this line's item, since the same number on another item is a different lot. Give this or batchNumberId. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch to remove, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `lines[].serials` | array of object | no | The units to remove, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. Every unit named must already exist. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the unit being removed. Give this or serialNumber. | | `lines[].serials[].serialNumber` | string | no | The serial number stamped on the unit, matched against the units of this line's item. Give this or serialNumberId. | | `lines[].binLocations` | array of object | no | The bins the quantity comes out of, in a bin-tracked warehouse. An entry may name a batchNumber or serialNumber of its own line. | | `lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin to take the quantity from. See id on GET inventory/bins/:id. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `lines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. A number the line does not have is rejected rather than reinterpreted. | | `lines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | Example: ```json { "locationId": "e1d84c07-5b39-4a26-9f70-2c8b613ad495", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "documentDate": "2026-08-14", "reasonCodeId": "6f2b58c1-9d47-4e03-a815-70c9d2e46b38", "notes": "Forklift damage, aisle 4", "lines": [ { "itemId": "5f21c8b0-9d34-4a76-8e12-04b93c7de5a8", "unitId": "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "quantity": "12.00", "notes": "Crushed by a forklift", "batches": [ { "batchNumber": "LOT-2026-08-14", "quantity": "12.00" } ], "binLocations": [ { "binLocationId": "0c7e91d3-4a58-4b26-9e10-63f8a2c5d704", "quantity": "12.00", "batchNumber": "LOT-2026-08-14" } ] } ] } ``` ## Response `200` Success Under `data` (createInventoryDecreaseOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the inventory decrease. | | `documentNumber` | string | no | The inventory decrease's document number, for example 'DEC-42'. Absent while none has been assigned. | | `documentStatus` | string | yes | The status of the inventory decrease. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `documentDate` | string (date) | yes | The date the adjustment is dated. It drives the posting period, so it is not the same as when the record was created. Format YYYY-MM-DD. | | `entityId` | string | yes | The unique identifier for the legal entity the document belongs to. See id on GET base/entities. | | `entityName` | string | no | The display name of that legal entity. | | `locationId` | string | no | The unique identifier for the warehouse location. One location for the whole document, and every line uses it. Absent only on a document with no lines. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The short code of that warehouse location. | | `locationName` | string | no | The display name of that warehouse location. | | `reasonCodeId` | string | no | The unique identifier for the reason code, when one was given. See id on GET inventory/reason-codes/:id. | | `reasonCode` | string | no | The short code of that reason code. | | `reasonCodeDescription` | string | no | The description of that reason code. | | `currency` | string | no | The currency the inventory decrease is valued in, the legal entity's functional currency. Format ISO 4217, for example 'EUR'. | | `notes` | string | no | Free text on the inventory decrease. | | `createdDate` | string (date-time) | no | The date and time the inventory decrease was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the inventory decrease record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the inventory decrease. An inventory decrease always has at least one. | | `lines[].id` | string | yes | The unique identifier for the inventory decrease line. | | `lines[].itemId` | string | yes | The unique identifier for the item whose stock this line removes. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The short code of that item. | | `lines[].itemName` | string | no | The display name of that item. | | `lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `lines[].unitCode` | string | no | The short code of that unit, for example 'BOX'. | | `lines[].quantity` | string (decimal) | yes | The quantity removed, in the line's unit. A positive number. | | `lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].unitPrice` | string (decimal) | no | The value of one unit, in the inventory decrease's currency, when one was recorded. Usually absent, since stock leaving is valued at the cost it already has. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches making up this line's quantity, for a batch-tracked item. Absent when the item is not batch-tracked. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record. | | `lines[].batches[].batchNumber` | string | yes | The batch number as printed, for example 'LOT-2026-08-14'. | | `lines[].batches[].manufacturerBatchNumber` | string | no | The maker's own number for the same batch, when one was recorded. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch removed by the line, in the line's unit. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].serials` | array of object | no | The serialized units making up this line's quantity, one entry per unit. Absent when the item is not serialized. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record. | | `lines[].serials[].serialNumber` | string | yes | The serial number stamped on the unit. | | `lines[].serials[].manufacturerSerialNumber` | string | no | The maker's own number for the same unit, when one was recorded. | | `lines[].binLocations` | array of object | no | The bins the quantity came out of, in a bin-tracked warehouse. Each entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the quantity came out of. See id on GET inventory/bins/:id. | | `lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `UNKNOWN_TRACKING_NUMBER` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, UNKNOWN_TRACKING_NUMBER, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Get Inventory Decrease `GET /inventory/inventory-decreases/{id}` Group: Inventory › Inventory Decreases · Operation ID: `getInventoryDecrease` Returns the inventory decrease with the given identifier, with its lines. This returns the inventory decrease with its lines and their batch, serial and bin detail. The same item schema is returned by POST inventory/inventory-decreases/search. The collection GET inventory/inventory-decreases returns the header only, without the lines. documentStatus says what you may do next: a draft can be edited, committed or deleted, a committed document can only be canceled, and a canceled one can only be deleted. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getInventoryDecrease) ## 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`. | | `id` | path | string | yes | The unique identifier for the inventory decrease to return. | ## Response `200` Success Under `data` (getInventoryDecreaseOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the inventory decrease. | | `documentNumber` | string | no | The inventory decrease's document number, for example 'DEC-42'. Absent while none has been assigned. | | `documentStatus` | string | yes | The status of the inventory decrease. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `documentDate` | string (date) | yes | The date the adjustment is dated. It drives the posting period, so it is not the same as when the record was created. Format YYYY-MM-DD. | | `entityId` | string | yes | The unique identifier for the legal entity the document belongs to. See id on GET base/entities. | | `entityName` | string | no | The display name of that legal entity. | | `locationId` | string | no | The unique identifier for the warehouse location. One location for the whole document, and every line uses it. Absent only on a document with no lines. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The short code of that warehouse location. | | `locationName` | string | no | The display name of that warehouse location. | | `reasonCodeId` | string | no | The unique identifier for the reason code, when one was given. See id on GET inventory/reason-codes/:id. | | `reasonCode` | string | no | The short code of that reason code. | | `reasonCodeDescription` | string | no | The description of that reason code. | | `currency` | string | no | The currency the inventory decrease is valued in, the legal entity's functional currency. Format ISO 4217, for example 'EUR'. | | `notes` | string | no | Free text on the inventory decrease. | | `createdDate` | string (date-time) | no | The date and time the inventory decrease was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the inventory decrease record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the inventory decrease. An inventory decrease always has at least one. | | `lines[].id` | string | yes | The unique identifier for the inventory decrease line. | | `lines[].itemId` | string | yes | The unique identifier for the item whose stock this line removes. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The short code of that item. | | `lines[].itemName` | string | no | The display name of that item. | | `lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `lines[].unitCode` | string | no | The short code of that unit, for example 'BOX'. | | `lines[].quantity` | string (decimal) | yes | The quantity removed, in the line's unit. A positive number. | | `lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].unitPrice` | string (decimal) | no | The value of one unit, in the inventory decrease's currency, when one was recorded. Usually absent, since stock leaving is valued at the cost it already has. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches making up this line's quantity, for a batch-tracked item. Absent when the item is not batch-tracked. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record. | | `lines[].batches[].batchNumber` | string | yes | The batch number as printed, for example 'LOT-2026-08-14'. | | `lines[].batches[].manufacturerBatchNumber` | string | no | The maker's own number for the same batch, when one was recorded. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch removed by the line, in the line's unit. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].serials` | array of object | no | The serialized units making up this line's quantity, one entry per unit. Absent when the item is not serialized. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record. | | `lines[].serials[].serialNumber` | string | yes | The serial number stamped on the unit. | | `lines[].serials[].manufacturerSerialNumber` | string | no | The maker's own number for the same unit, when one was recorded. | | `lines[].binLocations` | array of object | no | The bins the quantity came out of, in a bin-tracked warehouse. Each entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the quantity came out of. See id on GET inventory/bins/:id. | | `lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Inventory Decrease `PATCH /inventory/inventory-decreases/{id}` Group: Inventory › Inventory Decreases · Operation ID: `updateInventoryDecrease` Updates the inventory decrease with the given identifier. Only a draft can be changed, and only the fields in the request body change. This updates a draft, and only a draft. Omitted fields are left alone. Lines change through three arrays, deleteLines, updateLines and createLines, applied in that order, so one call can remove a line and add its replacement. On an updateLines entry, a tracking array replaces what that line had, and an omitted one leaves it untouched. The location cannot be changed, since it belongs to the whole document and every line. A line id belonging to another document is rejected, not skipped. The response is the updated document in full. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateInventoryDecrease) ## 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. | | `id` | path | string | yes | The unique identifier for the inventory decrease to update. | ## Request body Required, `application/json`: updateInventoryDecreaseInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `documentDate` | string (date) | no | A new document date. Changing it moves the posting period. Use format YYYY-MM-DD. | | `reasonCodeId` | string | no | A new reason code. See id on GET inventory/reason-codes/:id. | | `notes` | string | no | Free text on the inventory decrease. Pass an empty string to clear it. | | `deleteLines` | array of string | no | The unique identifiers of lines to remove. Applied before updateLines and createLines. | | `updateLines` | array of object | no | The lines to change, addressed by id. Applied after deleteLines and before createLines. | | `updateLines[].id` | string | yes | The unique identifier for the line to change. A line belonging to another document is rejected, not skipped. | | `updateLines[].itemId` | string | no | A new item for the line. See id on GET inventory/items/:id. | | `updateLines[].unitId` | string | no | A new unit for the line's quantity. See id on GET inventory/unit-of-measures/:id. | | `updateLines[].quantity` | string (decimal) | no | A new quantity, in the line's unit. Must be greater than zero when given. | | `updateLines[].unitPrice` | string (decimal) | no | A new value of one unit, in the inventory decrease's currency. Must be greater than zero when given. Omit it to keep valuing the stock at the cost it already has. | | `updateLines[].notes` | string | no | Free text on the line. Pass an empty string to clear it. | | `updateLines[].batches` | array of object | no | The batches to take the quantity from. Required for a batch-tracked item, and the quantities must add up to the line's quantity. Every batch named must already exist for the item, since a decrease removes lots and does not create them. On update, this replaces the line's batches. | | `updateLines[].batches[].batchNumberId` | string | no | The unique identifier for the batch being removed. Give this or batchNumber. | | `updateLines[].batches[].batchNumber` | string | no | The batch number as printed, for example 'LOT-2026-08-14'. It is matched against the batches of this line's item, since the same number on another item is a different lot. Give this or batchNumberId. | | `updateLines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch to remove, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `updateLines[].serials` | array of object | no | The units to remove, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. Every unit named must already exist. On update, this replaces the line's serials. | | `updateLines[].serials[].serialNumberId` | string | no | The unique identifier for the unit being removed. Give this or serialNumber. | | `updateLines[].serials[].serialNumber` | string | no | The serial number stamped on the unit, matched against the units of this line's item. Give this or serialNumberId. | | `updateLines[].binLocations` | array of object | no | The bins the quantity comes out of, in a bin-tracked warehouse. An entry may name a batchNumber or serialNumber of its own line. On update, this replaces the line's bin entries. | | `updateLines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin to take the quantity from. See id on GET inventory/bins/:id. | | `updateLines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `updateLines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. A number the line does not have is rejected rather than reinterpreted. | | `updateLines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | | `createLines` | array of object | no | The lines to add. They use the document's existing location, so a document with no lines yet cannot take new ones. | | `createLines[].itemId` | string | yes | The unique identifier for the item whose stock this line removes. See id on GET inventory/items/:id. | | `createLines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `createLines[].quantity` | string (decimal) | yes | The quantity to remove, in that unit. A positive number greater than zero. | | `createLines[].unitPrice` | string (decimal) | no | The value of one unit, in the inventory decrease's currency. Optional: omit it to value the stock at the cost it already has, which is what a write-off ordinarily wants. A supplied value must be greater than zero. | | `createLines[].notes` | string | no | Free text on the line. | | `createLines[].batches` | array of object | no | The batches to take the quantity from. Required for a batch-tracked item, and the quantities must add up to the line's quantity. Every batch named must already exist for the item, since a decrease removes lots and does not create them. | | `createLines[].batches[].batchNumberId` | string | no | The unique identifier for the batch being removed. Give this or batchNumber. | | `createLines[].batches[].batchNumber` | string | no | The batch number as printed, for example 'LOT-2026-08-14'. It is matched against the batches of this line's item, since the same number on another item is a different lot. Give this or batchNumberId. | | `createLines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch to remove, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `createLines[].serials` | array of object | no | The units to remove, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. Every unit named must already exist. | | `createLines[].serials[].serialNumberId` | string | no | The unique identifier for the unit being removed. Give this or serialNumber. | | `createLines[].serials[].serialNumber` | string | no | The serial number stamped on the unit, matched against the units of this line's item. Give this or serialNumberId. | | `createLines[].binLocations` | array of object | no | The bins the quantity comes out of, in a bin-tracked warehouse. An entry may name a batchNumber or serialNumber of its own line. | | `createLines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin to take the quantity from. See id on GET inventory/bins/:id. | | `createLines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `createLines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. A number the line does not have is rejected rather than reinterpreted. | | `createLines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | Example: ```json { "documentDate": "2026-01-15", "reasonCodeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "notes": "Forklift damage, aisle 4 — two more found", "deleteLines": [ "<deleteLines>" ], "updateLines": [ { "id": "9b3f5a10-7c24-4e68-8d05-1f6a92b4c837", "quantity": "14.00", "batches": [ { "batchNumber": "LOT-2026-08-14", "quantity": "14.00" } ], "binLocations": [ { "binLocationId": "0c7e91d3-4a58-4b26-9e10-63f8a2c5d704", "quantity": "14.00", "batchNumber": "LOT-2026-08-14" } ] } ], "createLines": [ { "itemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "unitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "unitPrice": "19.99", "notes": "<notes>", "batches": [ { "batchNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "batchNumber": "<batchNumber>", "quantity": "19.99" } ], "serials": [ { "serialNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "serialNumber": "<serialNumber>" } ], "binLocations": [ { "binLocationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "batchNumber": "<batchNumber>", "serialNumber": "<serialNumber>" } ] } ] } ``` ## Response `200` Success Under `data` (updateInventoryDecreaseOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the inventory decrease. | | `documentNumber` | string | no | The inventory decrease's document number, for example 'DEC-42'. Absent while none has been assigned. | | `documentStatus` | string | yes | The status of the inventory decrease. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `documentDate` | string (date) | yes | The date the adjustment is dated. It drives the posting period, so it is not the same as when the record was created. Format YYYY-MM-DD. | | `entityId` | string | yes | The unique identifier for the legal entity the document belongs to. See id on GET base/entities. | | `entityName` | string | no | The display name of that legal entity. | | `locationId` | string | no | The unique identifier for the warehouse location. One location for the whole document, and every line uses it. Absent only on a document with no lines. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The short code of that warehouse location. | | `locationName` | string | no | The display name of that warehouse location. | | `reasonCodeId` | string | no | The unique identifier for the reason code, when one was given. See id on GET inventory/reason-codes/:id. | | `reasonCode` | string | no | The short code of that reason code. | | `reasonCodeDescription` | string | no | The description of that reason code. | | `currency` | string | no | The currency the inventory decrease is valued in, the legal entity's functional currency. Format ISO 4217, for example 'EUR'. | | `notes` | string | no | Free text on the inventory decrease. | | `createdDate` | string (date-time) | no | The date and time the inventory decrease was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the inventory decrease record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the inventory decrease. An inventory decrease always has at least one. | | `lines[].id` | string | yes | The unique identifier for the inventory decrease line. | | `lines[].itemId` | string | yes | The unique identifier for the item whose stock this line removes. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The short code of that item. | | `lines[].itemName` | string | no | The display name of that item. | | `lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `lines[].unitCode` | string | no | The short code of that unit, for example 'BOX'. | | `lines[].quantity` | string (decimal) | yes | The quantity removed, in the line's unit. A positive number. | | `lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].unitPrice` | string (decimal) | no | The value of one unit, in the inventory decrease's currency, when one was recorded. Usually absent, since stock leaving is valued at the cost it already has. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches making up this line's quantity, for a batch-tracked item. Absent when the item is not batch-tracked. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record. | | `lines[].batches[].batchNumber` | string | yes | The batch number as printed, for example 'LOT-2026-08-14'. | | `lines[].batches[].manufacturerBatchNumber` | string | no | The maker's own number for the same batch, when one was recorded. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch removed by the line, in the line's unit. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].serials` | array of object | no | The serialized units making up this line's quantity, one entry per unit. Absent when the item is not serialized. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record. | | `lines[].serials[].serialNumber` | string | yes | The serial number stamped on the unit. | | `lines[].serials[].manufacturerSerialNumber` | string | no | The maker's own number for the same unit, when one was recorded. | | `lines[].binLocations` | array of object | no | The bins the quantity came out of, in a bin-tracked warehouse. Each entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the quantity came out of. See id on GET inventory/bins/:id. | | `lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `UNKNOWN_TRACKING_NUMBER` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, UNKNOWN_TRACKING_NUMBER, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Delete Inventory Decrease `DELETE /inventory/inventory-decreases/{id}` Group: Inventory › Inventory Decreases · Operation ID: `deleteInventoryDecrease` Deletes the inventory decrease with the given identifier. Only a draft can be deleted. This deletes the inventory decrease and its lines, along with their batch, serial and bin rows. A committed document cannot be deleted, since it has posted stock and a journal entry. Cancel it first, and a canceled document can then be deleted. The batch and serial records the document named are untouched, because they belong to the item, not to this document. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteInventoryDecrease) ## 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. | | `id` | path | string | yes | The unique identifier for the inventory decrease to delete. | ## Response `200` Success Under `data` (deleteInventoryDecreaseOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted inventory decrease. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, POSTING_REJECTED - `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 --- # Commit Inventory Decrease `POST /inventory/inventory-decreases/{id}/commit` Group: Inventory › Inventory Decreases · Operation ID: `commitInventoryDecrease` Commits the draft inventory decrease, posting it: stock is relieved and a journal entry is written. This posts the draft: it relieves stock, writes the accounting journal entry, and moves the document to committed. Stock availability is checked here, against live levels at each line's item and location, not when the draft was built. Posting is one way: a committed document cannot be edited or deleted, and to undo it you cancel it, which posts the reversals. Anything the posting refuses, including insufficient stock, comes back as POSTING_REJECTED carrying the platform's own message. [Open in the interactive reference](https://docs.api.lensing.ai/?op=commitInventoryDecrease) ## 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. | | `id` | path | string | yes | The unique identifier for the draft inventory decrease to commit. | ## Response `200` Success Under `data` (commitInventoryDecreaseOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the committed inventory decrease. | | `documentStatus` | string | yes | The status of the inventory decrease, read back from the document. It is 'COMMITTED' on success. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `journalEntryId` | string | no | The unique identifier for the journal entry the commit posted. Absent when posting produced none. See id on GET accounting/journal-entries/:id. | | `warnings` | array of string | yes | Non-fatal advisories from the posting, in practice soft bin-capacity breaches. An empty array on a clean commit. The commit succeeded regardless of what is listed here. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, POSTING_REJECTED - `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 --- # Creates several inventory decreases in one call, each as a draft. `POST /inventory/inventory-decreases/bulk` Group: Inventory › Inventory Decreases · Operation ID: `bulkCreateInventoryDecreases` All or nothing: if any inventory decrease is rejected, nothing is written, not even the entries before it. index on the error says which, counting from zero. Each entry is exactly what POST inventory/inventory-decreases takes, and the results come back in input order. Nothing is posted, since committing is per document. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateInventoryDecreases) ## 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`: bulkCreateInventoryDecreasesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `decreases` | array of object | yes | The inventory decreases to create. At least one, and each is exactly what POST inventory/inventory-decreases takes. | | `decreases[].locationId` | string | yes | The unique identifier for the warehouse location. One location for the whole document, and every line comes out of it. Required. See id on GET inventory/warehouses/:id. | | `decreases[].entityId` | string | no | The unique identifier for the legal entity. Omit it to use the location's owning entity. See id on GET base/entities. | | `decreases[].documentDate` | string (date) | no | The date the adjustment is dated. It drives the posting period. Defaults to today. Use format YYYY-MM-DD. | | `decreases[].reasonCodeId` | string | no | The unique identifier for a reason code explaining the adjustment. See id on GET inventory/reason-codes/:id. | | `decreases[].notes` | string | no | Free text on the inventory decrease. | | `decreases[].lines` | array of object | yes | The lines of the inventory decrease. At least one is required. | | `decreases[].lines[].itemId` | string | yes | The unique identifier for the item whose stock this line removes. See id on GET inventory/items/:id. | | `decreases[].lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `decreases[].lines[].quantity` | string (decimal) | yes | The quantity to remove, in that unit. A positive number greater than zero. | | `decreases[].lines[].unitPrice` | string (decimal) | no | The value of one unit, in the inventory decrease's currency. Optional: omit it to value the stock at the cost it already has, which is what a write-off ordinarily wants. A supplied value must be greater than zero. | | `decreases[].lines[].notes` | string | no | Free text on the line. | | `decreases[].lines[].batches` | array of object | no | The batches to take the quantity from. Required for a batch-tracked item, and the quantities must add up to the line's quantity. Every batch named must already exist for the item, since a decrease removes lots and does not create them. | | `decreases[].lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch being removed. Give this or batchNumber. | | `decreases[].lines[].batches[].batchNumber` | string | no | The batch number as printed, for example 'LOT-2026-08-14'. It is matched against the batches of this line's item, since the same number on another item is a different lot. Give this or batchNumberId. | | `decreases[].lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch to remove, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `decreases[].lines[].serials` | array of object | no | The units to remove, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. Every unit named must already exist. | | `decreases[].lines[].serials[].serialNumberId` | string | no | The unique identifier for the unit being removed. Give this or serialNumber. | | `decreases[].lines[].serials[].serialNumber` | string | no | The serial number stamped on the unit, matched against the units of this line's item. Give this or serialNumberId. | | `decreases[].lines[].binLocations` | array of object | no | The bins the quantity comes out of, in a bin-tracked warehouse. An entry may name a batchNumber or serialNumber of its own line. | | `decreases[].lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin to take the quantity from. See id on GET inventory/bins/:id. | | `decreases[].lines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `decreases[].lines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. A number the line does not have is rejected rather than reinterpreted. | | `decreases[].lines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | Example: ```json { "decreases": [ { "locationId": "e1d84c07-5b39-4a26-9f70-2c8b613ad495", "documentDate": "2026-08-14", "reasonCodeId": "6f2b58c1-9d47-4e03-a815-70c9d2e46b38", "notes": "Forklift damage, aisle 4", "lines": [ { "itemId": "5f21c8b0-9d34-4a76-8e12-04b93c7de5a8", "unitId": "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "quantity": "12.00", "notes": "Crushed by a forklift", "batches": [ { "batchNumber": "LOT-2026-08-14", "quantity": "12.00" } ], "binLocations": [ { "binLocationId": "0c7e91d3-4a58-4b26-9e10-63f8a2c5d704", "quantity": "12.00", "batchNumber": "LOT-2026-08-14" } ] } ] } ] } ``` ## Response `200` Success Under `data` (bulkCreateInventoryDecreasesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The created inventory decreases, in input order. Match them to inputs by position. | | `items[].id` | string | yes | The unique identifier for the inventory decrease. | | `items[].documentNumber` | string | no | The inventory decrease's document number, for example 'DEC-42'. Absent while none has been assigned. | | `items[].documentStatus` | string | yes | The status of the inventory decrease. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `items[].documentDate` | string (date) | yes | The date the adjustment is dated. It drives the posting period, so it is not the same as when the record was created. Format YYYY-MM-DD. | | `items[].entityId` | string | yes | The unique identifier for the legal entity the document belongs to. See id on GET base/entities. | | `items[].entityName` | string | no | The display name of that legal entity. | | `items[].locationId` | string | no | The unique identifier for the warehouse location. One location for the whole document, and every line uses it. Absent only on a document with no lines. See id on GET inventory/warehouses/:id. | | `items[].locationCode` | string | no | The short code of that warehouse location. | | `items[].locationName` | string | no | The display name of that warehouse location. | | `items[].reasonCodeId` | string | no | The unique identifier for the reason code, when one was given. See id on GET inventory/reason-codes/:id. | | `items[].reasonCode` | string | no | The short code of that reason code. | | `items[].reasonCodeDescription` | string | no | The description of that reason code. | | `items[].currency` | string | no | The currency the inventory decrease is valued in, the legal entity's functional currency. Format ISO 4217, for example 'EUR'. | | `items[].notes` | string | no | Free text on the inventory decrease. | | `items[].createdDate` | string (date-time) | no | The date and time the inventory decrease was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the inventory decrease record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | yes | The lines of the inventory decrease. An inventory decrease always has at least one. | | `items[].lines[].id` | string | yes | The unique identifier for the inventory decrease line. | | `items[].lines[].itemId` | string | yes | The unique identifier for the item whose stock this line removes. See id on GET inventory/items/:id. | | `items[].lines[].itemCode` | string | no | The short code of that item. | | `items[].lines[].itemName` | string | no | The display name of that item. | | `items[].lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `items[].lines[].unitCode` | string | no | The short code of that unit, for example 'BOX'. | | `items[].lines[].quantity` | string (decimal) | yes | The quantity removed, in the line's unit. A positive number. | | `items[].lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].unitPrice` | string (decimal) | no | The value of one unit, in the inventory decrease's currency, when one was recorded. Usually absent, since stock leaving is valued at the cost it already has. | | `items[].lines[].notes` | string | no | Free text on the line. | | `items[].lines[].batches` | array of object | no | The batches making up this line's quantity, for a batch-tracked item. Absent when the item is not batch-tracked. | | `items[].lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record. | | `items[].lines[].batches[].batchNumber` | string | yes | The batch number as printed, for example 'LOT-2026-08-14'. | | `items[].lines[].batches[].manufacturerBatchNumber` | string | no | The maker's own number for the same batch, when one was recorded. | | `items[].lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `items[].lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch removed by the line, in the line's unit. | | `items[].lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].serials` | array of object | no | The serialized units making up this line's quantity, one entry per unit. Absent when the item is not serialized. | | `items[].lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record. | | `items[].lines[].serials[].serialNumber` | string | yes | The serial number stamped on the unit. | | `items[].lines[].serials[].manufacturerSerialNumber` | string | no | The maker's own number for the same unit, when one was recorded. | | `items[].lines[].binLocations` | array of object | no | The bins the quantity came out of, in a bin-tracked warehouse. Each entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `items[].lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the quantity came out of. See id on GET inventory/bins/:id. | | `items[].lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `items[].lines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `items[].lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `items[].lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | ## 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 | | --- | --- | | `NOTHING_TO_CREATE` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `UNKNOWN_TRACKING_NUMBER` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOTHING_TO_CREATE, REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, UNKNOWN_TRACKING_NUMBER, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Bulk Delete Inventory Decreases `POST /inventory/inventory-decreases/bulk-delete` Group: Inventory › Inventory Decreases · Operation ID: `bulkDeleteInventoryDecreases` Deletes several inventory decreases in one call. Only drafts can be deleted. All or nothing: if any identifier matches no inventory decrease, or names one past draft, nothing is deleted. index on the error says which, counting from zero. Only a draft can be deleted. A committed decrease is corrected with an offsetting document. Delete one at a time with DELETE inventory/inventory-decreases/:id. The identifiers come back in input order. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteInventoryDecreases) ## 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`: bulkDeleteInventoryDecreasesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the inventory decreases to delete. All or nothing: if any is missing or committed, none is deleted. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteInventoryDecreasesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the deleted inventory decreases, in input order. | ## 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 | | --- | --- | | `NOTHING_TO_DELETE` | 400 | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOTHING_TO_DELETE, NOT_FOUND, WRONG_STATUS, POSTING_REJECTED - `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 --- # Search Inventory Decreases `POST /inventory/inventory-decreases/search` Group: Inventory › Inventory Decreases · Operation ID: `searchInventoryDecreases` Returns the inventory decreases that match the given filters, each with its lines. This returns the inventory decreases that match, with their full header and lines, the same item schema GET inventory/inventory-decreases/:id returns. It is the read endpoint that returns lines, since the collection is header-only. itemId matches documents that have a line for that item, which the collection cannot answer. Every criterion is an exact match or a range, never a substring, and criteria combine with AND. An empty filters object returns everything, page by page. No match is an empty items array, not an error. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchInventoryDecreases) ## 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`. | ## Request body Required, `application/json`: searchInventoryDecreasesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The search criteria, combined with AND. An empty object returns every inventory decrease, page by page. | | `filters.documentNumber` | string | no | Limits results to the inventory decrease with this document number, for example 'DEC-42'. The match is exact. | | `filters.documentStatus` | string | no | Limits results to inventory decreases with this status. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. The match is exact. | | `filters.entityId` | string | no | Limits results to inventory decreases for this legal entity. See id on GET base/entities. | | `filters.locationId` | string | no | Limits results to inventory decreases at this warehouse location. See id on GET inventory/warehouses/:id. | | `filters.reasonCodeId` | string | no | Limits results to inventory decreases with this reason code. See id on GET inventory/reason-codes/:id. | | `filters.itemId` | string | no | Limits results to inventory decreases that have a line for this item. The collection cannot filter by item, because lines are not on it. See id on GET inventory/items/:id. | | `filters.fromDate` | string (date) | no | Limits results to documentDate on or after this date. Omit for an open-ended start. Use format YYYY-MM-DD. | | `filters.toDate` | string (date) | no | Limits results to documentDate on or before this date. Omit for an open-ended end. Use format YYYY-MM-DD. | | `filters.modifiedSince` | string (date-time) | no | Limits results to inventory decreases changed at or after this time, in UTC. This is separate from the documentDate window. Use format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of inventory decreases per page. Defaults to 50. A value above 200 is treated as 200. | Example: ```json { "filters": { "documentStatus": "DRAFT", "locationId": "e1d84c07-5b39-4a26-9f70-2c8b613ad495" }, "page": { "cursor": "<cursor>", "size": 0 } } ``` ## Response `200` Success Under `data` (searchInventoryDecreasesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The inventory decreases that match, oldest first, each with its lines. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the inventory decrease. | | `items[].documentNumber` | string | no | The inventory decrease's document number, for example 'DEC-42'. Absent while none has been assigned. | | `items[].documentStatus` | string | yes | The status of the inventory decrease. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `items[].documentDate` | string (date) | yes | The date the adjustment is dated. It drives the posting period, so it is not the same as when the record was created. Format YYYY-MM-DD. | | `items[].entityId` | string | yes | The unique identifier for the legal entity the document belongs to. See id on GET base/entities. | | `items[].entityName` | string | no | The display name of that legal entity. | | `items[].locationId` | string | no | The unique identifier for the warehouse location. One location for the whole document, and every line uses it. Absent only on a document with no lines. See id on GET inventory/warehouses/:id. | | `items[].locationCode` | string | no | The short code of that warehouse location. | | `items[].locationName` | string | no | The display name of that warehouse location. | | `items[].reasonCodeId` | string | no | The unique identifier for the reason code, when one was given. See id on GET inventory/reason-codes/:id. | | `items[].reasonCode` | string | no | The short code of that reason code. | | `items[].reasonCodeDescription` | string | no | The description of that reason code. | | `items[].currency` | string | no | The currency the inventory decrease is valued in, the legal entity's functional currency. Format ISO 4217, for example 'EUR'. | | `items[].notes` | string | no | Free text on the inventory decrease. | | `items[].createdDate` | string (date-time) | no | The date and time the inventory decrease was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the inventory decrease record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | yes | The lines of the inventory decrease. An inventory decrease always has at least one. | | `items[].lines[].id` | string | yes | The unique identifier for the inventory decrease line. | | `items[].lines[].itemId` | string | yes | The unique identifier for the item whose stock this line removes. See id on GET inventory/items/:id. | | `items[].lines[].itemCode` | string | no | The short code of that item. | | `items[].lines[].itemName` | string | no | The display name of that item. | | `items[].lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `items[].lines[].unitCode` | string | no | The short code of that unit, for example 'BOX'. | | `items[].lines[].quantity` | string (decimal) | yes | The quantity removed, in the line's unit. A positive number. | | `items[].lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].unitPrice` | string (decimal) | no | The value of one unit, in the inventory decrease's currency, when one was recorded. Usually absent, since stock leaving is valued at the cost it already has. | | `items[].lines[].notes` | string | no | Free text on the line. | | `items[].lines[].batches` | array of object | no | The batches making up this line's quantity, for a batch-tracked item. Absent when the item is not batch-tracked. | | `items[].lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record. | | `items[].lines[].batches[].batchNumber` | string | yes | The batch number as printed, for example 'LOT-2026-08-14'. | | `items[].lines[].batches[].manufacturerBatchNumber` | string | no | The maker's own number for the same batch, when one was recorded. | | `items[].lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `items[].lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch removed by the line, in the line's unit. | | `items[].lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].serials` | array of object | no | The serialized units making up this line's quantity, one entry per unit. Absent when the item is not serialized. | | `items[].lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record. | | `items[].lines[].serials[].serialNumber` | string | yes | The serial number stamped on the unit. | | `items[].lines[].serials[].manufacturerSerialNumber` | string | no | The maker's own number for the same unit, when one was recorded. | | `items[].lines[].binLocations` | array of object | no | The bins the quantity came out of, in a bin-tracked warehouse. Each entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `items[].lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the quantity came out of. See id on GET inventory/bins/:id. | | `items[].lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `items[].lines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `items[].lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `items[].lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether more results remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Inventory Increases `GET /inventory/inventory-increases` Group: Inventory › Inventory Increases · Operation ID: `inventoryIncreases` Returns a list of inventory increases. Each row is a header with lineCount. To get the lines, or to filter by item or date, use POST inventory/inventory-increases/search. Returns the inventory increase collection. It returns headers only, without the lines. GET inventory/inventory-increases/:id and POST inventory/inventory-increases/search return the lines, and search takes the same filters, so listing documents with their lines means searching. lineCount stands in for the rows, saying whether a document is one correction or forty. Filtering by item is not possible here, because items are on the lines. POST inventory/inventory-increases/search takes itemId and answers it. [Open in the interactive reference](https://docs.api.lensing.ai/?op=inventoryIncreases) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `reasonCodeId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `reasonCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, documentNumber, documentDate, documentStatus, createdDate, lastModifiedDate - Selectable: id, documentNumber, documentStatus, documentDate, entityId, entityName, locationId, locationCode, locationName, reasonCodeId, reasonCode, reasonCodeDescription, currency, lineCount, notes, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of inventoryIncreasesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the inventory increase. | | `documentNumber` | string or null | no | The inventory increase's document number, for example 'INC-42'. Absent while none has been assigned. | | `documentStatus` | string | no | The status of the inventory increase. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `documentDate` | string (date) | no | The date the inventory increase is dated. It sets the posting period. Format YYYY-MM-DD. | | `entityId` | string | no | The unique identifier for the legal entity. See id on GET base/entities. | | `entityName` | string or null | no | The display name of that legal entity. | | `locationId` | string or null | no | The unique identifier for the warehouse the stock is added to. See id on GET inventory/warehouses/:id. | | `locationCode` | string or null | no | The short code of that warehouse. | | `locationName` | string or null | no | The display name of that warehouse. | | `reasonCodeId` | string or null | no | The unique identifier for the reason code. See id on GET inventory/reason-codes/:id. | | `reasonCode` | string or null | no | The short code of that reason code. | | `reasonCodeDescription` | string or null | no | The description of that reason code. | | `currency` | string or null | no | The currency the inventory increase is valued in. Format ISO 4217, for example 'EUR'. | | `lineCount` | number or null | no | The number of lines the inventory increase has. The collection does not return the lines themselves. | | `notes` | string or null | no | Free text on the inventory increase. | | `createdDate` | string (date-time) or null | no | The date and time the inventory increase was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the inventory increase record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates an inventory increase as a draft. `POST /inventory/inventory-increases` Group: Inventory › Inventory Increases · Operation ID: `createInventoryIncrease` Creates one inventory increase in DRAFT. Nothing is posted: no stock moves and no journal entry is written until POST inventory/inventory-increases/:id/commit. The warehouse is a header field and every line is added to it. A batch-tracked item needs batches whose quantities add up to the line's, a serialized item needs one serials entry per unit, and a bin-tracked warehouse needs binLocations. A batch or serial number new to the item creates its record. The whole call is one transaction, so a rejected request writes nothing. The response is the created document, in the same shape GET inventory/inventory-increases/:id returns. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createInventoryIncrease) ## 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`: createInventoryIncreaseInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `locationId` | string | yes | The unique identifier for the warehouse the stock is added to. One warehouse for the whole inventory increase, and every line is added to it. Required. See id on GET inventory/warehouses/:id. | | `entityId` | string | no | The unique identifier for the legal entity. Omit it to use the warehouse's owning entity. See id on GET base/entities. | | `documentDate` | string (date) | no | The date the inventory increase is dated. Defaults to today. It sets the posting period. Use format YYYY-MM-DD. | | `reasonCodeId` | string | no | The unique identifier for a reason code explaining the adjustment. See id on GET inventory/reason-codes/:id. | | `notes` | string | no | Free text on the inventory increase. | | `lines` | array of object | yes | The lines of the inventory increase. At least one is required. | | `lines[].itemId` | string | yes | The unique identifier for the item this line adds to stock. See id on GET inventory/items/:id. | | `lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `lines[].quantity` | string (decimal) | yes | The quantity added, in that unit. Greater than zero. | | `lines[].unitPrice` | string (decimal) | yes | The value of one unit, in the inventory increase's currency. Greater than zero. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches making up this line's quantity. Required for a batch-tracked item, and the quantities must add up to the line's quantity. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch. Omit it and give batchNumber to receive a new lot, which creates the batch record when the number is new to the item. | | `lines[].batches[].batchNumber` | string | no | The batch number as printed. Required unless batchNumberId is given. | | `lines[].batches[].manufacturerBatchNumber` | string | no | The maker's own number for the batch. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Use format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the inventory increase's currency. Defaults to the line's unitPrice. | | `lines[].serials` | array of object | no | The serialized units, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial record. Omit it and give serialNumber for a unit new to the system. | | `lines[].serials[].serialNumber` | string | no | The serial number stamped on the unit. Required unless serialNumberId is given. | | `lines[].serials[].manufacturerSerialNumber` | string | no | The maker's own number for the unit. | | `lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the inventory increase's currency. Defaults to the line's unitPrice. | | `lines[].binLocations` | array of object | no | The bins the quantity lands in, in a bin-tracked warehouse. An entry may name a batchNumber or serialNumber of its own line. | | `lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the quantity lands in. See id on GET inventory/bins/:id. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity landing in this bin, in the line's unit. | | `lines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. A number not on the line is rejected. | | `lines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | Example: ```json { "locationId": "e1d84c07-5b39-4a26-9f70-2c8b613ad495", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "documentDate": "2026-08-14", "reasonCodeId": "6f2b58c1-9d47-4e03-a815-70c9d2e46b38", "notes": "August cycle count", "lines": [ { "itemId": "5f21c8b0-9d34-4a76-8e12-04b93c7de5a8", "unitId": "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "quantity": "120.00", "unitPrice": "3.75", "notes": "Found during the August count", "batches": [ { "batchNumber": "LOT-2026-08-14", "expirationDate": "2027-08-14", "quantity": "120.00" } ], "binLocations": [ { "binLocationId": "0c7e91d3-4a58-4b26-9e10-63f8a2c5d704", "quantity": "120.00", "batchNumber": "LOT-2026-08-14" } ] } ] } ``` ## Response `200` Success Under `data` (createInventoryIncreaseOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the inventory increase. | | `documentNumber` | string | no | The inventory increase's document number, for example 'INC-42'. Absent while none has been assigned. | | `documentStatus` | string | yes | The status of the inventory increase. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `documentDate` | string (date) | yes | The date the inventory increase is dated. It sets the posting period. Format YYYY-MM-DD. | | `entityId` | string | yes | The unique identifier for the legal entity the inventory increase belongs to. See id on GET base/entities. | | `entityName` | string | no | The display name of that legal entity. | | `locationId` | string | no | The unique identifier for the warehouse the stock is added to. One warehouse for the whole inventory increase. Absent only on a document with no lines. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The short code of that warehouse. | | `locationName` | string | no | The display name of that warehouse. | | `reasonCodeId` | string | no | The unique identifier for the reason code, when one was given. See id on GET inventory/reason-codes/:id. | | `reasonCode` | string | no | The short code of that reason code. | | `reasonCodeDescription` | string | no | The description of that reason code. | | `currency` | string | no | The currency of every amount on the inventory increase. Format ISO 4217, for example 'EUR'. | | `notes` | string | no | Free text on the inventory increase. | | `createdDate` | string (date-time) | no | The date and time the inventory increase was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the inventory increase record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the inventory increase. An inventory increase always has at least one. | | `lines[].id` | string | yes | The unique identifier for the inventory increase line. | | `lines[].itemId` | string | yes | The unique identifier for the item this line adds to stock. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The short code of that item. | | `lines[].itemName` | string | no | The display name of that item. | | `lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `lines[].unitCode` | string | no | The short code of that unit, for example 'BOX'. | | `lines[].quantity` | string (decimal) | yes | The quantity added, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].unitPrice` | string (decimal) | no | The value of one unit, in the inventory increase's currency. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches making up this line's quantity, for a batch-tracked item. Absent when the item is not batch-tracked. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record this entry created or matched. | | `lines[].batches[].batchNumber` | string | yes | The batch number as printed, for example 'LOT-2026-08-14'. | | `lines[].batches[].manufacturerBatchNumber` | string | no | The maker's own number for the same batch, when one was recorded. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch on the line, in the line's unit. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the inventory increase's currency. | | `lines[].serials` | array of object | no | The serialized units making up this line's quantity, one entry per unit. Absent when the item is not serialized. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record this entry created or matched. | | `lines[].serials[].serialNumber` | string | yes | The serial number stamped on the unit. | | `lines[].serials[].manufacturerSerialNumber` | string | no | The maker's own number for the same unit, when one was recorded. | | `lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the inventory increase's currency. | | `lines[].binLocations` | array of object | no | The bins the quantity lands in, in a bin-tracked warehouse. Each entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the quantity landed in. See id on GET inventory/bins/:id. | | `lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity landing in this bin, in the line's unit. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Returns the inventory increase with the given identifier. `GET /inventory/inventory-increases/{id}` Group: Inventory › Inventory Increases · Operation ID: `getInventoryIncrease` Returns one inventory increase with all of its lines and their batch, serial and bin detail. The item schema is the same here and on POST inventory/inventory-increases/search. The collection GET inventory/inventory-increases returns the header only, without the lines. documentStatus says what you may do next: a draft can be edited, committed or deleted, a committed one can only be canceled, and a canceled one is final but can be deleted. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getInventoryIncrease) ## 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`. | | `id` | path | string | yes | The unique identifier for the inventory increase to return. | ## Response `200` Success Under `data` (getInventoryIncreaseOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the inventory increase. | | `documentNumber` | string | no | The inventory increase's document number, for example 'INC-42'. Absent while none has been assigned. | | `documentStatus` | string | yes | The status of the inventory increase. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `documentDate` | string (date) | yes | The date the inventory increase is dated. It sets the posting period. Format YYYY-MM-DD. | | `entityId` | string | yes | The unique identifier for the legal entity the inventory increase belongs to. See id on GET base/entities. | | `entityName` | string | no | The display name of that legal entity. | | `locationId` | string | no | The unique identifier for the warehouse the stock is added to. One warehouse for the whole inventory increase. Absent only on a document with no lines. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The short code of that warehouse. | | `locationName` | string | no | The display name of that warehouse. | | `reasonCodeId` | string | no | The unique identifier for the reason code, when one was given. See id on GET inventory/reason-codes/:id. | | `reasonCode` | string | no | The short code of that reason code. | | `reasonCodeDescription` | string | no | The description of that reason code. | | `currency` | string | no | The currency of every amount on the inventory increase. Format ISO 4217, for example 'EUR'. | | `notes` | string | no | Free text on the inventory increase. | | `createdDate` | string (date-time) | no | The date and time the inventory increase was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the inventory increase record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the inventory increase. An inventory increase always has at least one. | | `lines[].id` | string | yes | The unique identifier for the inventory increase line. | | `lines[].itemId` | string | yes | The unique identifier for the item this line adds to stock. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The short code of that item. | | `lines[].itemName` | string | no | The display name of that item. | | `lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `lines[].unitCode` | string | no | The short code of that unit, for example 'BOX'. | | `lines[].quantity` | string (decimal) | yes | The quantity added, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].unitPrice` | string (decimal) | no | The value of one unit, in the inventory increase's currency. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches making up this line's quantity, for a batch-tracked item. Absent when the item is not batch-tracked. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record this entry created or matched. | | `lines[].batches[].batchNumber` | string | yes | The batch number as printed, for example 'LOT-2026-08-14'. | | `lines[].batches[].manufacturerBatchNumber` | string | no | The maker's own number for the same batch, when one was recorded. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch on the line, in the line's unit. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the inventory increase's currency. | | `lines[].serials` | array of object | no | The serialized units making up this line's quantity, one entry per unit. Absent when the item is not serialized. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record this entry created or matched. | | `lines[].serials[].serialNumber` | string | yes | The serial number stamped on the unit. | | `lines[].serials[].manufacturerSerialNumber` | string | no | The maker's own number for the same unit, when one was recorded. | | `lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the inventory increase's currency. | | `lines[].binLocations` | array of object | no | The bins the quantity lands in, in a bin-tracked warehouse. Each entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the quantity landed in. See id on GET inventory/bins/:id. | | `lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity landing in this bin, in the line's unit. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Inventory Increase `PATCH /inventory/inventory-increases/{id}` Group: Inventory › Inventory Increases · Operation ID: `updateInventoryIncrease` Updates the inventory increase with the given identifier. Only the fields in the request body change. Updates one draft inventory increase. Only the fields in the request body change. Only a draft can be changed: a committed document has already moved stock and posted a journal entry, so cancel it and post a new one instead. Lines change through three arrays, deleteLines, updateLines and createLines, applied in that order, so one call can retire a line and add its replacement. Sending a line's batches, serials or binLocations replaces what that line had. The warehouse cannot be changed. Replace it by deleting the document and posting a new one. A line id belonging to another inventory increase is rejected, not skipped. The response is the updated document in full. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateInventoryIncrease) ## 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. | | `id` | path | string | yes | The unique identifier for the inventory increase to update. | ## Request body Required, `application/json`: updateInventoryIncreaseInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `documentDate` | string (date) | no | A new document date. It sets the posting period. Use format YYYY-MM-DD. | | `reasonCodeId` | string | no | A new reason code. See id on GET inventory/reason-codes/:id. | | `notes` | string | no | New free text. Pass an empty string to clear it. | | `deleteLines` | array of string | no | The unique identifiers of lines to remove. Applied before updateLines and createLines. | | `updateLines` | array of object | no | The lines to change, each addressed by id. Applied after deleteLines and before createLines. | | `updateLines[].id` | string | yes | The unique identifier for the line to change. A line belonging to another inventory increase is rejected, not skipped. | | `updateLines[].itemId` | string | no | A new item for the line. See id on GET inventory/items/:id. | | `updateLines[].unitId` | string | no | A new unit for the line's quantity. See id on GET inventory/unit-of-measures/:id. | | `updateLines[].quantity` | string (decimal) | no | A new quantity, in the line's unit. Greater than zero when given. | | `updateLines[].unitPrice` | string (decimal) | no | A new unit value, in the inventory increase's currency. Greater than zero when given. | | `updateLines[].notes` | string | no | New free text. Pass an empty string to clear it. | | `updateLines[].batches` | array of object | no | The batches making up this line's quantity. Required for a batch-tracked item, and the quantities must add up to the line's quantity. Sending this array replaces the line's batches. | | `updateLines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch. Omit it and give batchNumber to receive a new lot, which creates the batch record when the number is new to the item. | | `updateLines[].batches[].batchNumber` | string | no | The batch number as printed. Required unless batchNumberId is given. | | `updateLines[].batches[].manufacturerBatchNumber` | string | no | The maker's own number for the batch. | | `updateLines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Use format YYYY-MM-DD. | | `updateLines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `updateLines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the inventory increase's currency. Defaults to the line's unitPrice. | | `updateLines[].serials` | array of object | no | The serialized units, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. Sending this array replaces the line's serials. | | `updateLines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial record. Omit it and give serialNumber for a unit new to the system. | | `updateLines[].serials[].serialNumber` | string | no | The serial number stamped on the unit. Required unless serialNumberId is given. | | `updateLines[].serials[].manufacturerSerialNumber` | string | no | The maker's own number for the unit. | | `updateLines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the inventory increase's currency. Defaults to the line's unitPrice. | | `updateLines[].binLocations` | array of object | no | The bins the quantity lands in, in a bin-tracked warehouse. An entry may name a batchNumber or serialNumber of its own line. Sending this array replaces the line's bin entries. | | `updateLines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the quantity lands in. See id on GET inventory/bins/:id. | | `updateLines[].binLocations[].quantity` | string (decimal) | yes | The quantity landing in this bin, in the line's unit. | | `updateLines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. A number not on the line is rejected. | | `updateLines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | | `createLines` | array of object | no | The lines to add. They land at the inventory increase's existing warehouse. A document with no lines cannot take new ones. | | `createLines[].itemId` | string | yes | The unique identifier for the item this line adds to stock. See id on GET inventory/items/:id. | | `createLines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `createLines[].quantity` | string (decimal) | yes | The quantity added, in that unit. Greater than zero. | | `createLines[].unitPrice` | string (decimal) | yes | The value of one unit, in the inventory increase's currency. Greater than zero. | | `createLines[].notes` | string | no | Free text on the line. | | `createLines[].batches` | array of object | no | The batches making up this line's quantity. Required for a batch-tracked item, and the quantities must add up to the line's quantity. | | `createLines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch. Omit it and give batchNumber to receive a new lot, which creates the batch record when the number is new to the item. | | `createLines[].batches[].batchNumber` | string | no | The batch number as printed. Required unless batchNumberId is given. | | `createLines[].batches[].manufacturerBatchNumber` | string | no | The maker's own number for the batch. | | `createLines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Use format YYYY-MM-DD. | | `createLines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `createLines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the inventory increase's currency. Defaults to the line's unitPrice. | | `createLines[].serials` | array of object | no | The serialized units, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. | | `createLines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial record. Omit it and give serialNumber for a unit new to the system. | | `createLines[].serials[].serialNumber` | string | no | The serial number stamped on the unit. Required unless serialNumberId is given. | | `createLines[].serials[].manufacturerSerialNumber` | string | no | The maker's own number for the unit. | | `createLines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the inventory increase's currency. Defaults to the line's unitPrice. | | `createLines[].binLocations` | array of object | no | The bins the quantity lands in, in a bin-tracked warehouse. An entry may name a batchNumber or serialNumber of its own line. | | `createLines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the quantity lands in. See id on GET inventory/bins/:id. | | `createLines[].binLocations[].quantity` | string (decimal) | yes | The quantity landing in this bin, in the line's unit. | | `createLines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. A number not on the line is rejected. | | `createLines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | Example: ```json { "documentDate": "2026-01-15", "reasonCodeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "notes": "August cycle count, recounted", "deleteLines": [ "<deleteLines>" ], "updateLines": [ { "id": "9b3f5a10-7c24-4e68-8d05-1f6a92b4c837", "quantity": "150.00", "batches": [ { "batchNumber": "LOT-2026-08-14", "expirationDate": "2027-08-14", "quantity": "150.00" } ], "binLocations": [ { "binLocationId": "0c7e91d3-4a58-4b26-9e10-63f8a2c5d704", "quantity": "150.00", "batchNumber": "LOT-2026-08-14" } ] } ], "createLines": [ { "itemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "unitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "unitPrice": "19.99", "notes": "<notes>", "batches": [ { "batchNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "batchNumber": "<batchNumber>", "manufacturerBatchNumber": "<manufacturerBatchNumber>", "expirationDate": "2026-01-15", "quantity": "19.99", "unitCost": "19.99" } ], "serials": [ { "serialNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "serialNumber": "<serialNumber>", "manufacturerSerialNumber": "<manufacturerSerialNumber>", "unitCost": "19.99" } ], "binLocations": [ { "binLocationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "batchNumber": "<batchNumber>", "serialNumber": "<serialNumber>" } ] } ] } ``` ## Response `200` Success Under `data` (updateInventoryIncreaseOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the inventory increase. | | `documentNumber` | string | no | The inventory increase's document number, for example 'INC-42'. Absent while none has been assigned. | | `documentStatus` | string | yes | The status of the inventory increase. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `documentDate` | string (date) | yes | The date the inventory increase is dated. It sets the posting period. Format YYYY-MM-DD. | | `entityId` | string | yes | The unique identifier for the legal entity the inventory increase belongs to. See id on GET base/entities. | | `entityName` | string | no | The display name of that legal entity. | | `locationId` | string | no | The unique identifier for the warehouse the stock is added to. One warehouse for the whole inventory increase. Absent only on a document with no lines. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The short code of that warehouse. | | `locationName` | string | no | The display name of that warehouse. | | `reasonCodeId` | string | no | The unique identifier for the reason code, when one was given. See id on GET inventory/reason-codes/:id. | | `reasonCode` | string | no | The short code of that reason code. | | `reasonCodeDescription` | string | no | The description of that reason code. | | `currency` | string | no | The currency of every amount on the inventory increase. Format ISO 4217, for example 'EUR'. | | `notes` | string | no | Free text on the inventory increase. | | `createdDate` | string (date-time) | no | The date and time the inventory increase was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the inventory increase record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the inventory increase. An inventory increase always has at least one. | | `lines[].id` | string | yes | The unique identifier for the inventory increase line. | | `lines[].itemId` | string | yes | The unique identifier for the item this line adds to stock. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The short code of that item. | | `lines[].itemName` | string | no | The display name of that item. | | `lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `lines[].unitCode` | string | no | The short code of that unit, for example 'BOX'. | | `lines[].quantity` | string (decimal) | yes | The quantity added, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].unitPrice` | string (decimal) | no | The value of one unit, in the inventory increase's currency. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches making up this line's quantity, for a batch-tracked item. Absent when the item is not batch-tracked. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record this entry created or matched. | | `lines[].batches[].batchNumber` | string | yes | The batch number as printed, for example 'LOT-2026-08-14'. | | `lines[].batches[].manufacturerBatchNumber` | string | no | The maker's own number for the same batch, when one was recorded. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch on the line, in the line's unit. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the inventory increase's currency. | | `lines[].serials` | array of object | no | The serialized units making up this line's quantity, one entry per unit. Absent when the item is not serialized. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record this entry created or matched. | | `lines[].serials[].serialNumber` | string | yes | The serial number stamped on the unit. | | `lines[].serials[].manufacturerSerialNumber` | string | no | The maker's own number for the same unit, when one was recorded. | | `lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the inventory increase's currency. | | `lines[].binLocations` | array of object | no | The bins the quantity lands in, in a bin-tracked warehouse. Each entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the quantity landed in. See id on GET inventory/bins/:id. | | `lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity landing in this bin, in the line's unit. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Deletes the inventory increase. `DELETE /inventory/inventory-increases/{id}` Group: Inventory › Inventory Increases · Operation ID: `deleteInventoryIncrease` Deletes one inventory increase and everything under it: its lines and their batch, serial and bin rows. A committed inventory increase cannot be deleted, because it has posted stock and a journal entry. Cancel it first, then delete the canceled document. The batch and serial records the document created are not deleted, because they belong to the item and other movements may reference them. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteInventoryIncrease) ## 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. | | `id` | path | string | yes | The unique identifier for the inventory increase to delete. | ## Response `200` Success Under `data` (deleteInventoryIncreaseOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted inventory increase. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, POSTING_REJECTED - `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 --- # Commits the inventory increase, posting the stock and the journal entry. `POST /inventory/inventory-increases/{id}/commit` Group: Inventory › Inventory Increases · Operation ID: `commitInventoryIncrease` Commits a draft inventory increase. It validates the document, posts the inventory transactions that raise stock, flips the status to COMMITTED, and writes the journal entry. This is the step that moves stock. Everything before it is a draft. A committed document cannot be edited or deleted, so to undo it, cancel it, which posts the reversals. Anything the posting refuses, such as insufficient stock coverage, a bin over its hard limit, a failed validation or unresolved account determination, comes back as POSTING_REJECTED with the platform's own message. warnings contains what did not stop the commit, in practice soft bin-capacity breaches. [Open in the interactive reference](https://docs.api.lensing.ai/?op=commitInventoryIncrease) ## 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. | | `id` | path | string | yes | The unique identifier for the inventory increase to commit. | ## Response `200` Success Under `data` (commitInventoryIncreaseOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the committed inventory increase. | | `documentStatus` | string | yes | The status of the inventory increase after the commit, read back from the document. It is 'COMMITTED' on success. | | `journalEntryId` | string | no | The unique identifier for the journal entry the commit wrote. Absent when the posting produced none. See id on GET accounting/journal-entries/:id. | | `warnings` | array of string | yes | Non-fatal advisories from the posting, in practice soft bin-capacity breaches. An empty array on a clean commit. The commit succeeded regardless of what is listed here. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, POSTING_REJECTED - `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 --- # Creates several inventory increases in one call, each as a draft. `POST /inventory/inventory-increases/bulk` Group: Inventory › Inventory Increases · Operation ID: `bulkCreateInventoryIncreases` Creates several inventory increases in one call, each in DRAFT. All or nothing: every entry is validated before any is written, so a rejected entry leaves nothing behind. index on the error says which, counting from zero. Each entry is exactly what POST inventory/inventory-increases takes, and the results come back in input order. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateInventoryIncreases) ## 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`: bulkCreateInventoryIncreasesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `increases` | array of object | yes | The inventory increases to create. At least one, and each is exactly what POST inventory/inventory-increases takes. | | `increases[].locationId` | string | yes | The unique identifier for the warehouse the stock is added to. One warehouse for the whole inventory increase, and every line is added to it. Required. See id on GET inventory/warehouses/:id. | | `increases[].entityId` | string | no | The unique identifier for the legal entity. Omit it to use the warehouse's owning entity. See id on GET base/entities. | | `increases[].documentDate` | string (date) | no | The date the inventory increase is dated. Defaults to today. It sets the posting period. Use format YYYY-MM-DD. | | `increases[].reasonCodeId` | string | no | The unique identifier for a reason code explaining the adjustment. See id on GET inventory/reason-codes/:id. | | `increases[].notes` | string | no | Free text on the inventory increase. | | `increases[].lines` | array of object | yes | The lines of the inventory increase. At least one is required. | | `increases[].lines[].itemId` | string | yes | The unique identifier for the item this line adds to stock. See id on GET inventory/items/:id. | | `increases[].lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `increases[].lines[].quantity` | string (decimal) | yes | The quantity added, in that unit. Greater than zero. | | `increases[].lines[].unitPrice` | string (decimal) | yes | The value of one unit, in the inventory increase's currency. Greater than zero. | | `increases[].lines[].notes` | string | no | Free text on the line. | | `increases[].lines[].batches` | array of object | no | The batches making up this line's quantity. Required for a batch-tracked item, and the quantities must add up to the line's quantity. | | `increases[].lines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch. Omit it and give batchNumber to receive a new lot, which creates the batch record when the number is new to the item. | | `increases[].lines[].batches[].batchNumber` | string | no | The batch number as printed. Required unless batchNumberId is given. | | `increases[].lines[].batches[].manufacturerBatchNumber` | string | no | The maker's own number for the batch. | | `increases[].lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Use format YYYY-MM-DD. | | `increases[].lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `increases[].lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the inventory increase's currency. Defaults to the line's unitPrice. | | `increases[].lines[].serials` | array of object | no | The serialized units, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. | | `increases[].lines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial record. Omit it and give serialNumber for a unit new to the system. | | `increases[].lines[].serials[].serialNumber` | string | no | The serial number stamped on the unit. Required unless serialNumberId is given. | | `increases[].lines[].serials[].manufacturerSerialNumber` | string | no | The maker's own number for the unit. | | `increases[].lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the inventory increase's currency. Defaults to the line's unitPrice. | | `increases[].lines[].binLocations` | array of object | no | The bins the quantity lands in, in a bin-tracked warehouse. An entry may name a batchNumber or serialNumber of its own line. | | `increases[].lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the quantity lands in. See id on GET inventory/bins/:id. | | `increases[].lines[].binLocations[].quantity` | string (decimal) | yes | The quantity landing in this bin, in the line's unit. | | `increases[].lines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. A number not on the line is rejected. | | `increases[].lines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | Example: ```json { "increases": [ { "locationId": "e1d84c07-5b39-4a26-9f70-2c8b613ad495", "documentDate": "2026-08-14", "reasonCodeId": "6f2b58c1-9d47-4e03-a815-70c9d2e46b38", "notes": "August cycle count", "lines": [ { "itemId": "5f21c8b0-9d34-4a76-8e12-04b93c7de5a8", "unitId": "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "quantity": "120.00", "unitPrice": "3.75", "notes": "Found during the August count", "batches": [ { "batchNumber": "LOT-2026-08-14", "expirationDate": "2027-08-14", "quantity": "120.00" } ], "binLocations": [ { "binLocationId": "0c7e91d3-4a58-4b26-9e10-63f8a2c5d704", "quantity": "120.00", "batchNumber": "LOT-2026-08-14" } ] } ] } ] } ``` ## Response `200` Success Under `data` (bulkCreateInventoryIncreasesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The created inventory increases, in input order. Match them to inputs by position. | | `items[].id` | string | yes | The unique identifier for the inventory increase. | | `items[].documentNumber` | string | no | The inventory increase's document number, for example 'INC-42'. Absent while none has been assigned. | | `items[].documentStatus` | string | yes | The status of the inventory increase. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `items[].documentDate` | string (date) | yes | The date the inventory increase is dated. It sets the posting period. Format YYYY-MM-DD. | | `items[].entityId` | string | yes | The unique identifier for the legal entity the inventory increase belongs to. See id on GET base/entities. | | `items[].entityName` | string | no | The display name of that legal entity. | | `items[].locationId` | string | no | The unique identifier for the warehouse the stock is added to. One warehouse for the whole inventory increase. Absent only on a document with no lines. See id on GET inventory/warehouses/:id. | | `items[].locationCode` | string | no | The short code of that warehouse. | | `items[].locationName` | string | no | The display name of that warehouse. | | `items[].reasonCodeId` | string | no | The unique identifier for the reason code, when one was given. See id on GET inventory/reason-codes/:id. | | `items[].reasonCode` | string | no | The short code of that reason code. | | `items[].reasonCodeDescription` | string | no | The description of that reason code. | | `items[].currency` | string | no | The currency of every amount on the inventory increase. Format ISO 4217, for example 'EUR'. | | `items[].notes` | string | no | Free text on the inventory increase. | | `items[].createdDate` | string (date-time) | no | The date and time the inventory increase was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the inventory increase record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | yes | The lines of the inventory increase. An inventory increase always has at least one. | | `items[].lines[].id` | string | yes | The unique identifier for the inventory increase line. | | `items[].lines[].itemId` | string | yes | The unique identifier for the item this line adds to stock. See id on GET inventory/items/:id. | | `items[].lines[].itemCode` | string | no | The short code of that item. | | `items[].lines[].itemName` | string | no | The display name of that item. | | `items[].lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `items[].lines[].unitCode` | string | no | The short code of that unit, for example 'BOX'. | | `items[].lines[].quantity` | string (decimal) | yes | The quantity added, in the line's unit. | | `items[].lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].unitPrice` | string (decimal) | no | The value of one unit, in the inventory increase's currency. | | `items[].lines[].notes` | string | no | Free text on the line. | | `items[].lines[].batches` | array of object | no | The batches making up this line's quantity, for a batch-tracked item. Absent when the item is not batch-tracked. | | `items[].lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record this entry created or matched. | | `items[].lines[].batches[].batchNumber` | string | yes | The batch number as printed, for example 'LOT-2026-08-14'. | | `items[].lines[].batches[].manufacturerBatchNumber` | string | no | The maker's own number for the same batch, when one was recorded. | | `items[].lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `items[].lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch on the line, in the line's unit. | | `items[].lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the inventory increase's currency. | | `items[].lines[].serials` | array of object | no | The serialized units making up this line's quantity, one entry per unit. Absent when the item is not serialized. | | `items[].lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record this entry created or matched. | | `items[].lines[].serials[].serialNumber` | string | yes | The serial number stamped on the unit. | | `items[].lines[].serials[].manufacturerSerialNumber` | string | no | The maker's own number for the same unit, when one was recorded. | | `items[].lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the inventory increase's currency. | | `items[].lines[].binLocations` | array of object | no | The bins the quantity lands in, in a bin-tracked warehouse. Each entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `items[].lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the quantity landed in. See id on GET inventory/bins/:id. | | `items[].lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `items[].lines[].binLocations[].quantity` | string (decimal) | yes | The quantity landing in this bin, in the line's unit. | | `items[].lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `items[].lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | ## 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 | | --- | --- | | `NOTHING_TO_CREATE` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOTHING_TO_CREATE, REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Deletes several inventory increases in one call. `POST /inventory/inventory-increases/bulk-delete` Group: Inventory › Inventory Increases · Operation ID: `bulkDeleteInventoryIncreases` Deletes several draft inventory increases in one call. All or nothing: if any identifier matches no document, or names one past draft, nothing is deleted. index on the error says which, counting from zero. Delete one at a time with DELETE inventory/inventory-increases/:id. The identifiers come back in input order. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteInventoryIncreases) ## 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`: bulkDeleteInventoryIncreasesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the inventory increases to delete. All or nothing: if any is missing or committed, none is deleted. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteInventoryIncreasesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the deleted inventory increases, in input order. | ## 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 | | --- | --- | | `NOTHING_TO_DELETE` | 400 | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOTHING_TO_DELETE, NOT_FOUND, WRONG_STATUS, POSTING_REJECTED - `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 --- # Returns the inventory increases that match the given criteria. `POST /inventory/inventory-increases/search` Group: Inventory › Inventory Increases · Operation ID: `searchInventoryIncreases` Searches inventory increases by criteria in a POST body and returns the full item schema, header and lines, the same one GET inventory/inventory-increases/:id returns. This is the endpoint that returns the lines, which the header-only collection cannot. itemId is a criterion only here, matching documents that have a line for the item. Every criterion is exact or a range, and criteria combine with AND. An empty filters object returns everything, page by page. Results are oldest first. No match is an empty items array, not an error. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchInventoryIncreases) ## 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`. | ## Request body Required, `application/json`: searchInventoryIncreasesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The search criteria. An inventory increase must match every criterion you send. Omit to return every inventory increase. | | `filters.documentNumber` | string | no | Limits results to the inventory increase with this document number, for example 'INC-42'. The match is exact. | | `filters.documentStatus` | string | no | Limits results to inventory increases with this status. Accepted values: 'DRAFT', 'COMMITTED', 'CANCELLED'. Any other value matches no inventory increase. | | `filters.entityId` | string | no | Limits results to inventory increases for this legal entity. See id on GET base/entities. | | `filters.locationId` | string | no | Limits results to inventory increases at this warehouse. See id on GET inventory/warehouses/:id. | | `filters.reasonCodeId` | string | no | Limits results to inventory increases with this reason code. See id on GET inventory/reason-codes/:id. | | `filters.itemId` | string | no | Limits results to inventory increases that have a line for this item. See id on GET inventory/items/:id. | | `filters.fromDate` | string (date) | no | Limits results to inventory increases dated on or after this date. Omit for an open-ended start. Format YYYY-MM-DD. | | `filters.toDate` | string (date) | no | Limits results to inventory increases dated on or before this date. Omit for an open-ended end. Format YYYY-MM-DD. | | `filters.modifiedSince` | string (date-time) | no | Limits results to inventory increases changed on or after this time, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of inventory increases per page, between 1 and 200. Defaults to 50. A value above 200 is treated as 200. | Example: ```json { "filters": { "documentStatus": "DRAFT", "locationId": "e1d84c07-5b39-4a26-9f70-2c8b613ad495" }, "page": { "cursor": "<cursor>", "size": 0 } } ``` ## Response `200` Success Under `data` (searchInventoryIncreasesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The inventory increases on this page, oldest first, each with its lines. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the inventory increase. | | `items[].documentNumber` | string | no | The inventory increase's document number, for example 'INC-42'. Absent while none has been assigned. | | `items[].documentStatus` | string | yes | The status of the inventory increase. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `items[].documentDate` | string (date) | yes | The date the inventory increase is dated. It sets the posting period. Format YYYY-MM-DD. | | `items[].entityId` | string | yes | The unique identifier for the legal entity the inventory increase belongs to. See id on GET base/entities. | | `items[].entityName` | string | no | The display name of that legal entity. | | `items[].locationId` | string | no | The unique identifier for the warehouse the stock is added to. One warehouse for the whole inventory increase. Absent only on a document with no lines. See id on GET inventory/warehouses/:id. | | `items[].locationCode` | string | no | The short code of that warehouse. | | `items[].locationName` | string | no | The display name of that warehouse. | | `items[].reasonCodeId` | string | no | The unique identifier for the reason code, when one was given. See id on GET inventory/reason-codes/:id. | | `items[].reasonCode` | string | no | The short code of that reason code. | | `items[].reasonCodeDescription` | string | no | The description of that reason code. | | `items[].currency` | string | no | The currency of every amount on the inventory increase. Format ISO 4217, for example 'EUR'. | | `items[].notes` | string | no | Free text on the inventory increase. | | `items[].createdDate` | string (date-time) | no | The date and time the inventory increase was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the inventory increase record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | yes | The lines of the inventory increase. An inventory increase always has at least one. | | `items[].lines[].id` | string | yes | The unique identifier for the inventory increase line. | | `items[].lines[].itemId` | string | yes | The unique identifier for the item this line adds to stock. See id on GET inventory/items/:id. | | `items[].lines[].itemCode` | string | no | The short code of that item. | | `items[].lines[].itemName` | string | no | The display name of that item. | | `items[].lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `items[].lines[].unitCode` | string | no | The short code of that unit, for example 'BOX'. | | `items[].lines[].quantity` | string (decimal) | yes | The quantity added, in the line's unit. | | `items[].lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].unitPrice` | string (decimal) | no | The value of one unit, in the inventory increase's currency. | | `items[].lines[].notes` | string | no | Free text on the line. | | `items[].lines[].batches` | array of object | no | The batches making up this line's quantity, for a batch-tracked item. Absent when the item is not batch-tracked. | | `items[].lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record this entry created or matched. | | `items[].lines[].batches[].batchNumber` | string | yes | The batch number as printed, for example 'LOT-2026-08-14'. | | `items[].lines[].batches[].manufacturerBatchNumber` | string | no | The maker's own number for the same batch, when one was recorded. | | `items[].lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `items[].lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch on the line, in the line's unit. | | `items[].lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the inventory increase's currency. | | `items[].lines[].serials` | array of object | no | The serialized units making up this line's quantity, one entry per unit. Absent when the item is not serialized. | | `items[].lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record this entry created or matched. | | `items[].lines[].serials[].serialNumber` | string | yes | The serial number stamped on the unit. | | `items[].lines[].serials[].manufacturerSerialNumber` | string | no | The maker's own number for the same unit, when one was recorded. | | `items[].lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the inventory increase's currency. | | `items[].lines[].binLocations` | array of object | no | The bins the quantity lands in, in a bin-tracked warehouse. Each entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `items[].lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the quantity landed in. See id on GET inventory/bins/:id. | | `items[].lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `items[].lines[].binLocations[].quantity` | string (decimal) | yes | The quantity landing in this bin, in the line's unit. | | `items[].lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `items[].lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | | `pagination` | object | yes | The pagination for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether more results remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Inventory Levels `GET /inventory/inventory-levels` Group: Inventory › Inventory Levels · Operation ID: `inventoryLevels` Returns a list of inventory levels. Filter by itemId and locationId together to read the stock of one item at one location. Each row resolves its item and location, so each row includes the identifier, code and name of both. Page size defaults to 25 and caps at 100. A page size above 100 is reduced to 100, not rejected. [Open in the interactive reference](https://docs.api.lensing.ai/?op=inventoryLevels) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `itemId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `itemCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, itemCode, locationCode, onHandQuantity, availableQuantity, onHandValue, lastTransactionTimestamp, lastModifiedDate - Selectable: id, itemId, itemCode, itemName, locationId, locationCode, locationName, onHandQuantity, availableQuantity, allocatedQuantity, reservedQuantity, inTransitQuantity, damagedQuantity, qualityHoldQuantity, averageCost, onHandValue, currency, lastTransactionTimestamp, lastModifiedDate ## Response `200` Success Under `data`, an array of inventoryLevelsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the inventory level. | | `itemId` | string | no | The unique identifier for the item this level is for. See id on GET inventory/items/:id. | | `itemCode` | string or null | no | The item's short code. | | `itemName` | string or null | no | The item's name. | | `locationId` | string | no | The unique identifier for the location this level is at. See id on GET inventory/warehouses/:id. | | `locationCode` | string or null | no | The location's short code. | | `locationName` | string or null | no | The location's name. | | `onHandQuantity` | string (decimal) or null | no | The total physical quantity on hand at this location, in the item's stocking unit of measure. | | `availableQuantity` | string (decimal) or null | no | The quantity free to allocate: onHandQuantity minus allocatedQuantity and reservedQuantity. | | `allocatedQuantity` | string (decimal) or null | no | The quantity allocated to outbound demand. | | `reservedQuantity` | string (decimal) or null | no | The quantity reserved. | | `inTransitQuantity` | string (decimal) or null | no | The quantity in transit to this location. | | `damagedQuantity` | string (decimal) or null | no | The quantity flagged as damaged. | | `qualityHoldQuantity` | string (decimal) or null | no | The quantity on quality hold. | | `averageCost` | string (decimal) or null | no | The moving-average unit cost, in the currency given by currency. | | `onHandValue` | string (decimal) or null | no | The value of the stock on hand, in the currency given by currency. | | `currency` | string or null | no | The currency of averageCost and onHandValue. Format ISO 4217, for example 'EUR'. Absent when the level has no valuation yet. | | `lastTransactionTimestamp` | string (date-time) or null | no | The date and time a stock movement last touched this level. Format YYYY-MM-DDTHH:mm:ss. Unlike lastModifiedDate, it changes only when stock moves. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the inventory-level record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the inventory level with the given identifier. `GET /inventory/inventory-levels/{id}` Group: Inventory › Inventory Levels · Operation ID: `getInventoryLevel` The same fields come back here, from GET inventory/inventory-levels, and from POST inventory/inventory-levels/search. A level's own identifier is rarely at hand, so the usual way to reach one is to filter the collection or the search endpoint by itemId and locationId, which together identify at most one level. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getInventoryLevel) ## 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`. | | `id` | path | string | yes | The unique identifier for the inventory level to return. | ## Response `200` Success Under `data` (getInventoryLevelOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the inventory level. | | `itemId` | string | yes | The unique identifier for the item this level is for. See id on GET inventory/items/:id. | | `itemCode` | string | no | The item's short code. | | `itemName` | string | no | The item's name. | | `locationId` | string | yes | The unique identifier for the location this level is at. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The location's short code. | | `locationName` | string | no | The location's name. | | `onHandQuantity` | string (decimal) | no | The total physical quantity on hand at this location, in the item's stocking unit of measure. | | `availableQuantity` | string (decimal) | yes | The quantity free to allocate: onHandQuantity minus allocatedQuantity and reservedQuantity. | | `allocatedQuantity` | string (decimal) | no | The quantity allocated to outbound demand. | | `reservedQuantity` | string (decimal) | no | The quantity reserved. | | `inTransitQuantity` | string (decimal) | no | The quantity in transit to this location. | | `damagedQuantity` | string (decimal) | no | The quantity flagged as damaged. | | `qualityHoldQuantity` | string (decimal) | no | The quantity on quality hold. | | `averageCost` | string (decimal) | no | The moving-average unit cost, in the currency given by currency. | | `onHandValue` | string (decimal) | no | The value of the stock on hand, in the currency given by currency. | | `currency` | string | no | The currency of averageCost and onHandValue. Format ISO 4217, for example 'EUR'. Absent when the level has no valuation yet. | | `lastTransactionTimestamp` | string (date-time) | no | The date and time a stock movement last touched this level. Format YYYY-MM-DDTHH:mm:ss. Unlike lastModifiedDate, it changes only when stock moves. | | `lastModifiedDate` | string (date-time) | no | The date and time the inventory-level record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the inventory levels that match the given filters. `POST /inventory/inventory-levels/search` Group: Inventory › Inventory Levels · Operation ID: `searchInventoryLevels` The same fields come back here as from GET inventory/inventory-levels/:id and GET inventory/inventory-levels. An itemId or locationId that matches no record returns an empty page rather than an error. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchInventoryLevels) ## 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`. | ## Request body Required, `application/json`: searchInventoryLevelsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. A level must match every filter you send. Omit to return all inventory levels. | | `filters.itemId` | string | no | Limits results to levels for the item with this identifier. An identifier that matches no item returns an empty page. See id on GET inventory/items/:id. | | `filters.locationId` | string | no | Limits results to levels at the location with this identifier. Combine with itemId to identify at most one level. An identifier that matches no location returns an empty page. See id on GET inventory/warehouses/:id. | | `filters.modifiedSince` | string (date-time) | no | Limits results to levels with lastModifiedDate at or after this time, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The maximum number of inventory levels to return. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "itemId": "5f21c8b0-9d34-4a76-8e12-04b93c7de5a8" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchInventoryLevelsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The inventory levels that match, ordered by id. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the inventory level. | | `items[].itemId` | string | yes | The unique identifier for the item this level is for. See id on GET inventory/items/:id. | | `items[].itemCode` | string | no | The item's short code. | | `items[].itemName` | string | no | The item's name. | | `items[].locationId` | string | yes | The unique identifier for the location this level is at. See id on GET inventory/warehouses/:id. | | `items[].locationCode` | string | no | The location's short code. | | `items[].locationName` | string | no | The location's name. | | `items[].onHandQuantity` | string (decimal) | no | The total physical quantity on hand at this location, in the item's stocking unit of measure. | | `items[].availableQuantity` | string (decimal) | yes | The quantity free to allocate: onHandQuantity minus allocatedQuantity and reservedQuantity. | | `items[].allocatedQuantity` | string (decimal) | no | The quantity allocated to outbound demand. | | `items[].reservedQuantity` | string (decimal) | no | The quantity reserved. | | `items[].inTransitQuantity` | string (decimal) | no | The quantity in transit to this location. | | `items[].damagedQuantity` | string (decimal) | no | The quantity flagged as damaged. | | `items[].qualityHoldQuantity` | string (decimal) | no | The quantity on quality hold. | | `items[].averageCost` | string (decimal) | no | The moving-average unit cost, in the currency given by currency. | | `items[].onHandValue` | string (decimal) | no | The value of the stock on hand, in the currency given by currency. | | `items[].currency` | string | no | The currency of averageCost and onHandValue. Format ISO 4217, for example 'EUR'. Absent when the level has no valuation yet. | | `items[].lastTransactionTimestamp` | string (date-time) | no | The date and time a stock movement last touched this level. Format YYYY-MM-DDTHH:mm:ss. Unlike lastModifiedDate, it changes only when stock moves. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the inventory-level record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more matches remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Inventory Revaluations `GET /inventory/inventory-revaluations` Group: Inventory › Inventory Revaluations · Operation ID: `inventoryRevaluations` Returns a list of inventory revaluations, headers only. To filter by item or warehouse, use POST inventory/inventory-revaluations/search. The inventory revaluation collection returns headers only. It does not return lines, so it publishes lineCount and totalRevaluationAmount in their place. totalRevaluationAmount is what the document moves, which is usually what a list of them is read for, and it can be ordered, so the largest write-downs in a period are one call. Filtering by item or warehouse needs the lines, so both live on POST inventory/inventory-revaluations/search. entityId can be filtered, so a collection can be scoped to one legal entity, and lastModifiedDate can be filtered and ordered, which makes it a sync cursor. [Open in the interactive reference](https://docs.api.lensing.ai/?op=inventoryRevaluations) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `costingValuationType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `reasonCodeId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `active` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, documentNumber, documentDate, documentStatus, costingValuationType, totalRevaluationAmount, createdDate, lastModifiedDate - Selectable: id, documentNumber, documentStatus, documentDate, entityId, entityName, costingValuationType, capitalisationExpenseAccountId, costingElementTypeId, reasonCodeId, reasonCode, reasonCodeDescription, currency, totalRevaluationAmount, active, notes, lineCount, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of inventoryRevaluationsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the revaluation. | | `documentNumber` | string or null | no | The revaluation's document number, for example 'REV-1187'. | | `documentStatus` | string | no | The status of the revaluation. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `documentDate` | string (date) or null | no | The date the revaluation is dated. Format YYYY-MM-DD. | | `entityId` | string or null | no | The unique identifier for the legal entity whose books this moves. See id on GET base/entities. | | `entityName` | string or null | no | The display name of that legal entity. | | `costingValuationType` | string or null | no | How the value change is recorded. Possible values: 'revaluation', 'directCostCapitalisation'. 'revaluation' restates the cost outright, and 'directCostCapitalisation' adds a cost that was expensed elsewhere onto the stock. | | `capitalisationExpenseAccountId` | string or null | no | The unique identifier for the GL account a direct cost capitalization takes the expense off. Absent on an ordinary revaluation. See id on GET accounting/chart-of-accounts/:id. | | `costingElementTypeId` | string or null | no | The unique identifier for the costing element type the cost breakdown rows are recorded against. Required to commit. | | `reasonCodeId` | string or null | no | The unique identifier for the reason code, when one was given. See id on GET inventory/reason-codes/:id. | | `reasonCode` | string or null | no | The code of that reason code. | | `reasonCodeDescription` | string or null | no | The description of that reason code. | | `currency` | string or null | no | The currency of every amount on the revaluation, the legal entity's functional currency. Format ISO 4217, for example 'EUR'. | | `totalRevaluationAmount` | string (decimal) or null | no | The sum of every line's revaluationAmount, in the revaluation's currency. This is what the document moves on the balance sheet. | | `active` | boolean or null | no | Whether the document is active. | | `notes` | string or null | no | Free text on the revaluation. | | `lineCount` | number or null | no | How many lines the revaluation has. | | `createdDate` | string (date-time) or null | no | The date and time the revaluation was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the revaluation record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates an inventory revaluation as a draft. `POST /inventory/inventory-revaluations` Group: Inventory › Inventory Revaluations · Operation ID: `createInventoryRevaluation` Creates one inventory revaluation as a draft. No value moves here: the stock is still carried at its old cost until POST inventory/inventory-revaluations/:id/commit posts the document. Read currentUnitCost before you send it. Every line states what the stock is carried at today and what it should be carried at instead. The platform checks currentUnitCost but never looks it up, so read it from GET inventory/inventory-levels for the same item and warehouse. A revaluation built on a guessed current cost posts the wrong difference, and nothing here catches that. The whole call is one transaction, so a rejected request writes nothing. The response is the created revaluation, in the same shape GET inventory/inventory-revaluations/:id returns. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createInventoryRevaluation) ## 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`: createInventoryRevaluationInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `costingValuationType` | string | yes | How the value change is recorded. Accepted values: 'revaluation', 'directCostCapitalisation'. 'revaluation' restates the cost outright, and 'directCostCapitalisation' adds a cost that was expensed elsewhere onto the stock. | | `costingElementTypeId` | string | yes | The unique identifier for the costing element type the cost breakdown rows are recorded against. Required, because the commit refuses a document without one. | | `entityId` | string | no | The unique identifier for the legal entity. Defaults to the entity that owns the first line's warehouse. See id on GET base/entities. | | `documentDate` | string (date) | no | The date to date the revaluation. Defaults to today. Use format YYYY-MM-DD. | | `capitalisationExpenseAccountId` | string | no | The unique identifier for the GL account the expense comes off. Required when costingValuationType is 'directCostCapitalisation'. See id on GET accounting/chart-of-accounts/:id. | | `reasonCodeId` | string | no | The unique identifier for a reason code. See id on GET inventory/reason-codes/:id. | | `notes` | string | no | Free text on the revaluation. | | `lines` | array of object | yes | The lines of the revaluation. At least one is required. | | `lines[].itemId` | string | yes | The unique identifier for the item to revalue. See id on GET inventory/items/:id. | | `lines[].locationId` | string | yes | The unique identifier for the warehouse whose stock is being restated. There is no document-level warehouse, so every line names its own. See id on GET inventory/warehouses/:id. | | `lines[].unitId` | string | yes | The unique identifier for the unit the quantity is in. Required: without it the platform falls back to whichever unit is first in the tenant. See id on GET inventory/unit-of-measures/:id. | | `lines[].quantity` | string (decimal) | yes | How much stock this line restates. Must be greater than zero. | | `lines[].currentUnitCost` | string (decimal) | yes | What the stock is carried at today, in the legal entity's functional currency. Read it from GET inventory/inventory-levels for the same item and warehouse, since it is checked and never looked up, and a guess posts the wrong difference. Zero is accepted and a negative cost is not. | | `lines[].newUnitCost` | string (decimal) | yes | What it should be carried at after this posts, in the legal entity's functional currency. | | `lines[].previousAfterSaleCost` | string (decimal) | no | The accrued after-sale cost before this document. Moving-average direct cost capitalization only. | | `lines[].newAfterSaleCost` | string (decimal) | no | The after-sale cost to capitalize. Moving-average direct cost capitalization only. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches being restated. Their quantities must add up to the line's quantity. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch number. | | `lines[].batches[].batchNumber` | string | no | The batch number itself. | | `lines[].batches[].quantity` | string (decimal) | yes | How much of the line this batch accounts for. | | `lines[].batches[].currentUnitCost` | string (decimal) | no | What this batch is carried at today. | | `lines[].batches[].newUnitCost` | string (decimal) | no | What it should be carried at. | | `lines[].serials` | array of object | no | The serialized units being restated, one entry per unit. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial number. | | `lines[].serials[].serialNumber` | string | no | The serial number itself. | | `lines[].serials[].currentUnitCost` | string (decimal) | no | What this unit is carried at today. | | `lines[].serials[].newUnitCost` | string (decimal) | no | What it should be carried at. | | `lines[].serials[].previousAfterSaleCost` | string (decimal) | no | The unit's accrued after-sale cost before this document. | | `lines[].serials[].newAfterSaleCost` | string (decimal) | no | The after-sale cost to capitalize on it. | Example: ```json { "costingValuationType": "revaluation", "costingElementTypeId": "f2a58c31-9d47-4e06-8b25-3c7f1a94e5d8", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "documentDate": "2026-01-15", "capitalisationExpenseAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "reasonCodeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "notes": "<notes>", "lines": [ { "itemId": "517a93e4-cbb4-4571-9e3d-2343e56cfd84", "locationId": "b6f0d2a4-77c1-4e39-8a52-0c4e1f9b73d5", "unitId": "d5a71e93-4c08-4b26-95f7-2e6a8c30b41f", "quantity": "150", "currentUnitCost": "12", "newUnitCost": "9" } ] } ``` ## Response `200` Success Under `data` (createInventoryRevaluationOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the revaluation. | | `documentNumber` | string | no | The revaluation's document number, for example 'REV-1187'. | | `documentStatus` | string | yes | The status of the revaluation. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `documentDate` | string (date) | no | The date the revaluation is dated. Format YYYY-MM-DD. | | `entityId` | string | no | The unique identifier for the legal entity whose books this moves. See id on GET base/entities. | | `entityName` | string | no | The display name of that legal entity. | | `costingValuationType` | string | no | How the value change is recorded. Possible values: 'revaluation', 'directCostCapitalisation'. 'revaluation' restates the cost outright, and 'directCostCapitalisation' adds a cost that was expensed elsewhere onto the stock. | | `capitalisationExpenseAccountId` | string | no | The unique identifier for the GL account a direct cost capitalization takes the expense off. Absent on an ordinary revaluation. See id on GET accounting/chart-of-accounts/:id. | | `costingElementTypeId` | string | no | The unique identifier for the costing element type the cost breakdown rows are recorded against. Required to commit. | | `reasonCodeId` | string | no | The unique identifier for the reason code, when one was given. See id on GET inventory/reason-codes/:id. | | `reasonCode` | string | no | The code of that reason code. | | `reasonCodeDescription` | string | no | The description of that reason code. | | `currency` | string | no | The currency of every amount on the revaluation, the legal entity's functional currency. Format ISO 4217, for example 'EUR'. | | `totalRevaluationAmount` | string (decimal) | no | The sum of every line's revaluationAmount, in the revaluation's currency. This is what the document moves on the balance sheet. | | `active` | boolean | no | Whether the document is active. | | `notes` | string | no | Free text on the revaluation. | | `lineCount` | number | no | How many lines the revaluation has. | | `createdDate` | string (date-time) | no | The date and time the revaluation was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the revaluation record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | no | The lines of the created revaluation. | | `lines[].id` | string | yes | The unique identifier for the revaluation line. Pass it in updateLines or deleteLines on PATCH inventory/inventory-revaluations/:id to change or remove the line. | | `lines[].itemId` | string | no | The unique identifier for the item being revalued. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The code of that item. | | `lines[].itemName` | string | no | The display name of that item. | | `lines[].locationId` | string | no | The unique identifier for the warehouse whose stock this line restates. The same item may be carried at a different cost in each warehouse. See id on GET inventory/warehouses/:id. | | `lines[].locationCode` | string | no | The code of that warehouse. | | `lines[].locationName` | string | no | The display name of that warehouse. | | `lines[].unitId` | string | no | The unique identifier for the unit the quantity is in. See id on GET inventory/unit-of-measures/:id. | | `lines[].unitCode` | string | no | The code of that unit. | | `lines[].quantity` | string (decimal) | no | How much stock this line restates the cost of, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit. | | `lines[].currentUnitCost` | string (decimal) | no | What the stock is carried at today, in the revaluation's currency. You supply it and the platform checks it rather than deriving it. Read it from GET inventory/inventory-levels. | | `lines[].newUnitCost` | string (decimal) | no | What it should be carried at after this posts, in the revaluation's currency. | | `lines[].previousAfterSaleCost` | string (decimal) | no | The accrued after-sale cost before this document, in the revaluation's currency. Moving-average direct cost capitalization only. | | `lines[].newAfterSaleCost` | string (decimal) | no | The after-sale cost to capitalize, in the revaluation's currency. Moving-average direct cost capitalization only. | | `lines[].revaluationAmount` | string (decimal) | no | The value this line moves on the balance sheet, in the revaluation's currency: (newUnitCost - currentUnitCost) x quantity. | | `lines[].status` | string | no | Where the line sits in the platform's own process. This is free text with no fixed set of values. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches being restated. Their quantities must add up to the line's quantity. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch number. | | `lines[].batches[].batchNumber` | string | no | The batch number itself. | | `lines[].batches[].quantity` | string (decimal) | no | How much of the line this batch accounts for. | | `lines[].batches[].currentUnitCost` | string (decimal) | no | What this batch is carried at today, in the revaluation's currency. | | `lines[].batches[].newUnitCost` | string (decimal) | no | What it should be carried at. | | `lines[].serials` | array of object | no | The serialized units being restated, one entry per unit, so there must be exactly as many as the quantity says. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial number. | | `lines[].serials[].serialNumber` | string | no | The serial number itself. | | `lines[].serials[].currentUnitCost` | string (decimal) | no | What this unit is carried at today, in the revaluation's currency. | | `lines[].serials[].newUnitCost` | string (decimal) | no | What it should be carried at. | | `lines[].serials[].previousAfterSaleCost` | string (decimal) | no | The unit's accrued after-sale cost before this document. Direct cost capitalization on a sold serial only. | | `lines[].serials[].newAfterSaleCost` | string (decimal) | no | The after-sale cost to capitalize on it. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `MISSING_CAPITALISATION_ACCOUNT` | 400 | | `TRACKING_MISMATCH` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, MISSING_CAPITALISATION_ACCOUNT, TRACKING_MISMATCH, POSTING_REJECTED - `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 --- # Returns the inventory revaluation with the given identifier. `GET /inventory/inventory-revaluations/{id}` Group: Inventory › Inventory Revaluations · Operation ID: `getInventoryRevaluation` Returns one inventory revaluation with its lines. Each line's revaluationAmount is what that line moves, and totalRevaluationAmount what the whole document moves: (newUnitCost - currentUnitCost) x quantity, in the legal entity's functional currency. A negative total is a write-down. The warehouse is on the line, not the header, so one revaluation can restate the same item in several warehouses. This is the same schema as the collection endpoint, plus the lines the collection endpoint does not carry. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getInventoryRevaluation) ## 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`. | | `id` | path | string | yes | The unique identifier for the revaluation to return. | ## Response `200` Success Under `data` (getInventoryRevaluationOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the revaluation. | | `documentNumber` | string | no | The revaluation's document number, for example 'REV-1187'. | | `documentStatus` | string | yes | The status of the revaluation. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `documentDate` | string (date) | no | The date the revaluation is dated. Format YYYY-MM-DD. | | `entityId` | string | no | The unique identifier for the legal entity whose books this moves. See id on GET base/entities. | | `entityName` | string | no | The display name of that legal entity. | | `costingValuationType` | string | no | How the value change is recorded. Possible values: 'revaluation', 'directCostCapitalisation'. 'revaluation' restates the cost outright, and 'directCostCapitalisation' adds a cost that was expensed elsewhere onto the stock. | | `capitalisationExpenseAccountId` | string | no | The unique identifier for the GL account a direct cost capitalization takes the expense off. Absent on an ordinary revaluation. See id on GET accounting/chart-of-accounts/:id. | | `costingElementTypeId` | string | no | The unique identifier for the costing element type the cost breakdown rows are recorded against. Required to commit. | | `reasonCodeId` | string | no | The unique identifier for the reason code, when one was given. See id on GET inventory/reason-codes/:id. | | `reasonCode` | string | no | The code of that reason code. | | `reasonCodeDescription` | string | no | The description of that reason code. | | `currency` | string | no | The currency of every amount on the revaluation, the legal entity's functional currency. Format ISO 4217, for example 'EUR'. | | `totalRevaluationAmount` | string (decimal) | no | The sum of every line's revaluationAmount, in the revaluation's currency. This is what the document moves on the balance sheet. | | `active` | boolean | no | Whether the document is active. | | `notes` | string | no | Free text on the revaluation. | | `lineCount` | number | no | How many lines the revaluation has. | | `createdDate` | string (date-time) | no | The date and time the revaluation was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the revaluation record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | no | The lines of the revaluation. Absent from the collection endpoint, which publishes lineCount instead. | | `lines[].id` | string | yes | The unique identifier for the revaluation line. Pass it in updateLines or deleteLines on PATCH inventory/inventory-revaluations/:id to change or remove the line. | | `lines[].itemId` | string | no | The unique identifier for the item being revalued. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The code of that item. | | `lines[].itemName` | string | no | The display name of that item. | | `lines[].locationId` | string | no | The unique identifier for the warehouse whose stock this line restates. The same item may be carried at a different cost in each warehouse. See id on GET inventory/warehouses/:id. | | `lines[].locationCode` | string | no | The code of that warehouse. | | `lines[].locationName` | string | no | The display name of that warehouse. | | `lines[].unitId` | string | no | The unique identifier for the unit the quantity is in. See id on GET inventory/unit-of-measures/:id. | | `lines[].unitCode` | string | no | The code of that unit. | | `lines[].quantity` | string (decimal) | no | How much stock this line restates the cost of, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit. | | `lines[].currentUnitCost` | string (decimal) | no | What the stock is carried at today, in the revaluation's currency. You supply it and the platform checks it rather than deriving it. Read it from GET inventory/inventory-levels. | | `lines[].newUnitCost` | string (decimal) | no | What it should be carried at after this posts, in the revaluation's currency. | | `lines[].previousAfterSaleCost` | string (decimal) | no | The accrued after-sale cost before this document, in the revaluation's currency. Moving-average direct cost capitalization only. | | `lines[].newAfterSaleCost` | string (decimal) | no | The after-sale cost to capitalize, in the revaluation's currency. Moving-average direct cost capitalization only. | | `lines[].revaluationAmount` | string (decimal) | no | The value this line moves on the balance sheet, in the revaluation's currency: (newUnitCost - currentUnitCost) x quantity. | | `lines[].status` | string | no | Where the line sits in the platform's own process. This is free text with no fixed set of values. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches being restated. Their quantities must add up to the line's quantity. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch number. | | `lines[].batches[].batchNumber` | string | no | The batch number itself. | | `lines[].batches[].quantity` | string (decimal) | no | How much of the line this batch accounts for. | | `lines[].batches[].currentUnitCost` | string (decimal) | no | What this batch is carried at today, in the revaluation's currency. | | `lines[].batches[].newUnitCost` | string (decimal) | no | What it should be carried at. | | `lines[].serials` | array of object | no | The serialized units being restated, one entry per unit, so there must be exactly as many as the quantity says. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial number. | | `lines[].serials[].serialNumber` | string | no | The serial number itself. | | `lines[].serials[].currentUnitCost` | string (decimal) | no | What this unit is carried at today, in the revaluation's currency. | | `lines[].serials[].newUnitCost` | string (decimal) | no | What it should be carried at. | | `lines[].serials[].previousAfterSaleCost` | string (decimal) | no | The unit's accrued after-sale cost before this document. Direct cost capitalization on a sold serial only. | | `lines[].serials[].newAfterSaleCost` | string (decimal) | no | The after-sale cost to capitalize on it. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Inventory Revaluation `PATCH /inventory/inventory-revaluations/{id}` Group: Inventory › Inventory Revaluations · Operation ID: `updateInventoryRevaluation` Updates the draft inventory revaluation with the given identifier. Only the fields in the request body change. Updates a draft inventory revaluation. A committed one has already moved value on the balance sheet, so reverse it rather than editing it. Everything omitted from the request body is left as it was. Lines change through three arrays, applied as deletes, then updates, then creates, so one call can retire a line and add its replacement. updateLines and deleteLines address a line by the id the document publishes, and a line id belonging to another revaluation is refused. A line's tracking arrays replace what that line had. The response is the updated revaluation, in the same shape GET inventory/inventory-revaluations/:id returns. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateInventoryRevaluation) ## 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. | | `id` | path | string | yes | The unique identifier for the revaluation to update. | ## Request body Required, `application/json`: updateInventoryRevaluationInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `documentDate` | string (date) | no | The date the revaluation is dated. Use format YYYY-MM-DD. | | `costingValuationType` | string | no | How the value change is recorded. Accepted values: 'revaluation', 'directCostCapitalisation'. 'revaluation' restates the cost outright, and 'directCostCapitalisation' adds a cost that was expensed elsewhere onto the stock. | | `capitalisationExpenseAccountId` | string | no | The unique identifier for the GL account a direct cost capitalization takes the expense off. See id on GET accounting/chart-of-accounts/:id. | | `costingElementTypeId` | string | no | The unique identifier for the costing element type the cost breakdown rows are recorded against. | | `reasonCodeId` | string | no | The unique identifier for a reason code. See id on GET inventory/reason-codes/:id. | | `notes` | string | no | Free text on the revaluation. | | `createLines` | array of object | no | The lines to add, each the same shape as a line on create. | | `createLines[].itemId` | string | yes | The unique identifier for the item to revalue. See id on GET inventory/items/:id. | | `createLines[].locationId` | string | yes | The unique identifier for the warehouse whose stock is being restated. There is no document-level warehouse, so every line names its own. See id on GET inventory/warehouses/:id. | | `createLines[].unitId` | string | yes | The unique identifier for the unit the quantity is in. Required: without it the platform falls back to whichever unit is first in the tenant. See id on GET inventory/unit-of-measures/:id. | | `createLines[].quantity` | string (decimal) | yes | How much stock this line restates. Must be greater than zero. | | `createLines[].currentUnitCost` | string (decimal) | yes | What the stock is carried at today, in the legal entity's functional currency. Read it from GET inventory/inventory-levels for the same item and warehouse, since it is checked and never looked up, and a guess posts the wrong difference. Zero is accepted and a negative cost is not. | | `createLines[].newUnitCost` | string (decimal) | yes | What it should be carried at after this posts, in the legal entity's functional currency. | | `createLines[].previousAfterSaleCost` | string (decimal) | no | The accrued after-sale cost before this document. Moving-average direct cost capitalization only. | | `createLines[].newAfterSaleCost` | string (decimal) | no | The after-sale cost to capitalize. Moving-average direct cost capitalization only. | | `createLines[].notes` | string | no | Free text on the line. | | `createLines[].batches` | array of object | no | The batches being restated. Their quantities must add up to the line's quantity. | | `createLines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch number. | | `createLines[].batches[].batchNumber` | string | no | The batch number itself. | | `createLines[].batches[].quantity` | string (decimal) | yes | How much of the line this batch accounts for. | | `createLines[].batches[].currentUnitCost` | string (decimal) | no | What this batch is carried at today. | | `createLines[].batches[].newUnitCost` | string (decimal) | no | What it should be carried at. | | `createLines[].serials` | array of object | no | The serialized units being restated, one entry per unit. | | `createLines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial number. | | `createLines[].serials[].serialNumber` | string | no | The serial number itself. | | `createLines[].serials[].currentUnitCost` | string (decimal) | no | What this unit is carried at today. | | `createLines[].serials[].newUnitCost` | string (decimal) | no | What it should be carried at. | | `createLines[].serials[].previousAfterSaleCost` | string (decimal) | no | The unit's accrued after-sale cost before this document. | | `createLines[].serials[].newAfterSaleCost` | string (decimal) | no | The after-sale cost to capitalize on it. | | `updateLines` | array of object | no | The lines to change, addressed by the id this endpoint publishes. Everything but the id is optional, and a tracking array replaces what that line had. | | `updateLines[].id` | string | yes | The unique identifier for the line to change. | | `updateLines[].itemId` | string | no | The unique identifier for a different item. See id on GET inventory/items/:id. | | `updateLines[].locationId` | string | no | The unique identifier for the warehouse whose stock is being restated. There is no document-level warehouse, so every line names its own. See id on GET inventory/warehouses/:id. | | `updateLines[].unitId` | string | no | The unique identifier for the unit the quantity is in. Required: without it the platform falls back to whichever unit is first in the tenant. See id on GET inventory/unit-of-measures/:id. | | `updateLines[].quantity` | string (decimal) | no | How much stock this line restates. Must be greater than zero. | | `updateLines[].currentUnitCost` | string (decimal) | no | What the stock is carried at today, in the legal entity's functional currency. Read it from GET inventory/inventory-levels for the same item and warehouse, since it is checked and never looked up, and a guess posts the wrong difference. Zero is accepted and a negative cost is not. | | `updateLines[].newUnitCost` | string (decimal) | no | What it should be carried at after this posts, in the legal entity's functional currency. | | `updateLines[].previousAfterSaleCost` | string (decimal) | no | The accrued after-sale cost before this document. Moving-average direct cost capitalization only. | | `updateLines[].newAfterSaleCost` | string (decimal) | no | The after-sale cost to capitalize. Moving-average direct cost capitalization only. | | `updateLines[].notes` | string | no | Free text on the line. | | `updateLines[].batches` | array of object | no | The batches being restated. Their quantities must add up to the line's quantity. | | `updateLines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch number. | | `updateLines[].batches[].batchNumber` | string | no | The batch number itself. | | `updateLines[].batches[].quantity` | string (decimal) | yes | How much of the line this batch accounts for. | | `updateLines[].batches[].currentUnitCost` | string (decimal) | no | What this batch is carried at today. | | `updateLines[].batches[].newUnitCost` | string (decimal) | no | What it should be carried at. | | `updateLines[].serials` | array of object | no | The serialized units being restated, one entry per unit. | | `updateLines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial number. | | `updateLines[].serials[].serialNumber` | string | no | The serial number itself. | | `updateLines[].serials[].currentUnitCost` | string (decimal) | no | What this unit is carried at today. | | `updateLines[].serials[].newUnitCost` | string (decimal) | no | What it should be carried at. | | `updateLines[].serials[].previousAfterSaleCost` | string (decimal) | no | The unit's accrued after-sale cost before this document. | | `updateLines[].serials[].newAfterSaleCost` | string (decimal) | no | The after-sale cost to capitalize on it. | | `deleteLines` | array of string | no | The unique identifiers of the lines to remove. They are applied before the updates and the creates. | Example: ```json { "documentDate": "2026-01-15", "costingValuationType": "<costingValuationType>", "capitalisationExpenseAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "costingElementTypeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "reasonCodeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "notes": "Reviewed with finance", "createLines": [ { "itemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "locationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "unitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "currentUnitCost": "19.99", "newUnitCost": "19.99", "previousAfterSaleCost": "19.99", "newAfterSaleCost": "19.99", "notes": "<notes>", "batches": [ { "batchNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "batchNumber": "<batchNumber>", "quantity": "19.99", "currentUnitCost": "19.99", "newUnitCost": "19.99" } ], "serials": [ { "serialNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "serialNumber": "<serialNumber>", "currentUnitCost": "19.99", "newUnitCost": "19.99", "previousAfterSaleCost": "19.99", "newAfterSaleCost": "19.99" } ] } ], "updateLines": [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "itemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "locationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "unitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "currentUnitCost": "19.99", "newUnitCost": "19.99", "previousAfterSaleCost": "19.99", "newAfterSaleCost": "19.99", "notes": "<notes>", "batches": [ { "batchNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "batchNumber": "<batchNumber>", "quantity": "19.99", "currentUnitCost": "19.99", "newUnitCost": "19.99" } ], "serials": [ { "serialNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "serialNumber": "<serialNumber>", "currentUnitCost": "19.99", "newUnitCost": "19.99", "previousAfterSaleCost": "19.99", "newAfterSaleCost": "19.99" } ] } ], "deleteLines": [ "<deleteLines>" ] } ``` ## Response `200` Success Under `data` (updateInventoryRevaluationOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the revaluation. | | `documentNumber` | string | no | The revaluation's document number, for example 'REV-1187'. | | `documentStatus` | string | yes | The status of the revaluation. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `documentDate` | string (date) | no | The date the revaluation is dated. Format YYYY-MM-DD. | | `entityId` | string | no | The unique identifier for the legal entity whose books this moves. See id on GET base/entities. | | `entityName` | string | no | The display name of that legal entity. | | `costingValuationType` | string | no | How the value change is recorded. Possible values: 'revaluation', 'directCostCapitalisation'. 'revaluation' restates the cost outright, and 'directCostCapitalisation' adds a cost that was expensed elsewhere onto the stock. | | `capitalisationExpenseAccountId` | string | no | The unique identifier for the GL account a direct cost capitalization takes the expense off. Absent on an ordinary revaluation. See id on GET accounting/chart-of-accounts/:id. | | `costingElementTypeId` | string | no | The unique identifier for the costing element type the cost breakdown rows are recorded against. Required to commit. | | `reasonCodeId` | string | no | The unique identifier for the reason code, when one was given. See id on GET inventory/reason-codes/:id. | | `reasonCode` | string | no | The code of that reason code. | | `reasonCodeDescription` | string | no | The description of that reason code. | | `currency` | string | no | The currency of every amount on the revaluation, the legal entity's functional currency. Format ISO 4217, for example 'EUR'. | | `totalRevaluationAmount` | string (decimal) | no | The sum of every line's revaluationAmount, in the revaluation's currency. This is what the document moves on the balance sheet. | | `active` | boolean | no | Whether the document is active. | | `notes` | string | no | Free text on the revaluation. | | `lineCount` | number | no | How many lines the revaluation has. | | `createdDate` | string (date-time) | no | The date and time the revaluation was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the revaluation record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | no | The lines of the revaluation as it now stands. | | `lines[].id` | string | yes | The unique identifier for the revaluation line. Pass it in updateLines or deleteLines on PATCH inventory/inventory-revaluations/:id to change or remove the line. | | `lines[].itemId` | string | no | The unique identifier for the item being revalued. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The code of that item. | | `lines[].itemName` | string | no | The display name of that item. | | `lines[].locationId` | string | no | The unique identifier for the warehouse whose stock this line restates. The same item may be carried at a different cost in each warehouse. See id on GET inventory/warehouses/:id. | | `lines[].locationCode` | string | no | The code of that warehouse. | | `lines[].locationName` | string | no | The display name of that warehouse. | | `lines[].unitId` | string | no | The unique identifier for the unit the quantity is in. See id on GET inventory/unit-of-measures/:id. | | `lines[].unitCode` | string | no | The code of that unit. | | `lines[].quantity` | string (decimal) | no | How much stock this line restates the cost of, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit. | | `lines[].currentUnitCost` | string (decimal) | no | What the stock is carried at today, in the revaluation's currency. You supply it and the platform checks it rather than deriving it. Read it from GET inventory/inventory-levels. | | `lines[].newUnitCost` | string (decimal) | no | What it should be carried at after this posts, in the revaluation's currency. | | `lines[].previousAfterSaleCost` | string (decimal) | no | The accrued after-sale cost before this document, in the revaluation's currency. Moving-average direct cost capitalization only. | | `lines[].newAfterSaleCost` | string (decimal) | no | The after-sale cost to capitalize, in the revaluation's currency. Moving-average direct cost capitalization only. | | `lines[].revaluationAmount` | string (decimal) | no | The value this line moves on the balance sheet, in the revaluation's currency: (newUnitCost - currentUnitCost) x quantity. | | `lines[].status` | string | no | Where the line sits in the platform's own process. This is free text with no fixed set of values. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches being restated. Their quantities must add up to the line's quantity. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch number. | | `lines[].batches[].batchNumber` | string | no | The batch number itself. | | `lines[].batches[].quantity` | string (decimal) | no | How much of the line this batch accounts for. | | `lines[].batches[].currentUnitCost` | string (decimal) | no | What this batch is carried at today, in the revaluation's currency. | | `lines[].batches[].newUnitCost` | string (decimal) | no | What it should be carried at. | | `lines[].serials` | array of object | no | The serialized units being restated, one entry per unit, so there must be exactly as many as the quantity says. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial number. | | `lines[].serials[].serialNumber` | string | no | The serial number itself. | | `lines[].serials[].currentUnitCost` | string (decimal) | no | What this unit is carried at today, in the revaluation's currency. | | `lines[].serials[].newUnitCost` | string (decimal) | no | What it should be carried at. | | `lines[].serials[].previousAfterSaleCost` | string (decimal) | no | The unit's accrued after-sale cost before this document. Direct cost capitalization on a sold serial only. | | `lines[].serials[].newAfterSaleCost` | string (decimal) | no | The after-sale cost to capitalize on it. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `MISSING_CAPITALISATION_ACCOUNT` | 400 | | `TRACKING_MISMATCH` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, MISSING_CAPITALISATION_ACCOUNT, TRACKING_MISMATCH, POSTING_REJECTED - `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 --- # Deletes a draft inventory revaluation. `DELETE /inventory/inventory-revaluations/{id}` Group: Inventory › Inventory Revaluations · Operation ID: `deleteInventoryRevaluation` Deletes a draft inventory revaluation and its lines. Only a draft can be deleted. A committed revaluation has moved value on the balance sheet and written a journal entry, so reversing it is a cancellation, which stays auditable, not a deletion. The response is the id it removed. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteInventoryRevaluation) ## 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. | | `id` | path | string | yes | The unique identifier for the revaluation to delete. | ## Response `200` Success Under `data` (deleteInventoryRevaluationOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted revaluation. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS - `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 --- # Commit Inventory Revaluation `POST /inventory/inventory-revaluations/{id}/commit` Group: Inventory › Inventory Revaluations · Operation ID: `commitInventoryRevaluation` Posts an inventory revaluation, so the stock value is restated and the difference moves to the balance sheet. Posts the revaluation, so the cost adjustments are written, the inventory levels revalue, and a journal entry moves the difference on the balance sheet. The document becomes committed. The platform's own checks run first and report every reason at once. A revaluation that cannot post answers VALIDATION_FAILED with errors, every reason and not just the first, so a caller with four things to fix is told all four. Nothing is written when it does. The checks cover that the document is still a draft and has lines, that every cost is in the legal entity's functional currency, that batch quantities and serial entries match their lines, and that a costing element type is named. totalRevaluationAmount on the response is what moved, negative for a write-down. [Open in the interactive reference](https://docs.api.lensing.ai/?op=commitInventoryRevaluation) ## 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. | | `id` | path | string | yes | The unique identifier for the revaluation to post. | ## Response `200` Success Under `data` (commitInventoryRevaluationOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the revaluation. | | `documentStatus` | string | yes | The status the revaluation is now in, read back from the document. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `journalEntryId` | string | no | The unique identifier for the journal entry the commit wrote. Absent when the posting reported none. See id on GET accounting/journal-entries/:id. | | `totalRevaluationAmount` | string (decimal) | no | What the document moved on the balance sheet, in the revaluation's currency. Negative for a write-down. | | `warnings` | array of string | yes | Non-fatal advisories from the posting. An empty array on a clean run. The commit succeeded regardless of what is listed here. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `VALIDATION_FAILED` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, VALIDATION_FAILED, POSTING_REJECTED - `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 --- # Creates several inventory revaluations in one call, each as a draft. `POST /inventory/inventory-revaluations/bulk` Group: Inventory › Inventory Revaluations · Operation ID: `bulkCreateInventoryRevaluations` All or nothing: if any revaluation is rejected, nothing is written, not even the entries before it. index on the error says which, counting from zero. Each entry is exactly what POST inventory/inventory-revaluations takes, and the results come back in input order, headers only. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateInventoryRevaluations) ## 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`: bulkCreateInventoryRevaluationsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `inventoryRevaluations` | array of object | yes | The revaluations to create. All or nothing: if any is rejected, none is written. | | `inventoryRevaluations[].costingValuationType` | string | yes | How the value change is recorded. Accepted values: 'revaluation', 'directCostCapitalisation'. 'revaluation' restates the cost outright, and 'directCostCapitalisation' adds a cost that was expensed elsewhere onto the stock. | | `inventoryRevaluations[].costingElementTypeId` | string | yes | The unique identifier for the costing element type the cost breakdown rows are recorded against. Required, because the commit refuses a document without one. | | `inventoryRevaluations[].entityId` | string | no | The unique identifier for the legal entity. Defaults to the entity that owns the first line's warehouse. See id on GET base/entities. | | `inventoryRevaluations[].documentDate` | string (date) | no | The date to date the revaluation. Defaults to today. Use format YYYY-MM-DD. | | `inventoryRevaluations[].capitalisationExpenseAccountId` | string | no | The unique identifier for the GL account the expense comes off. Required when costingValuationType is 'directCostCapitalisation'. See id on GET accounting/chart-of-accounts/:id. | | `inventoryRevaluations[].reasonCodeId` | string | no | The unique identifier for a reason code. See id on GET inventory/reason-codes/:id. | | `inventoryRevaluations[].notes` | string | no | Free text on the revaluation. | | `inventoryRevaluations[].lines` | array of object | yes | The lines of the revaluation. At least one is required. | | `inventoryRevaluations[].lines[].itemId` | string | yes | The unique identifier for the item to revalue. See id on GET inventory/items/:id. | | `inventoryRevaluations[].lines[].locationId` | string | yes | The unique identifier for the warehouse whose stock is being restated. There is no document-level warehouse, so every line names its own. See id on GET inventory/warehouses/:id. | | `inventoryRevaluations[].lines[].unitId` | string | yes | The unique identifier for the unit the quantity is in. Required: without it the platform falls back to whichever unit is first in the tenant. See id on GET inventory/unit-of-measures/:id. | | `inventoryRevaluations[].lines[].quantity` | string (decimal) | yes | How much stock this line restates. Must be greater than zero. | | `inventoryRevaluations[].lines[].currentUnitCost` | string (decimal) | yes | What the stock is carried at today, in the legal entity's functional currency. Read it from GET inventory/inventory-levels for the same item and warehouse, since it is checked and never looked up, and a guess posts the wrong difference. Zero is accepted and a negative cost is not. | | `inventoryRevaluations[].lines[].newUnitCost` | string (decimal) | yes | What it should be carried at after this posts, in the legal entity's functional currency. | | `inventoryRevaluations[].lines[].previousAfterSaleCost` | string (decimal) | no | The accrued after-sale cost before this document. Moving-average direct cost capitalization only. | | `inventoryRevaluations[].lines[].newAfterSaleCost` | string (decimal) | no | The after-sale cost to capitalize. Moving-average direct cost capitalization only. | | `inventoryRevaluations[].lines[].notes` | string | no | Free text on the line. | | `inventoryRevaluations[].lines[].batches` | array of object | no | The batches being restated. Their quantities must add up to the line's quantity. | | `inventoryRevaluations[].lines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch number. | | `inventoryRevaluations[].lines[].batches[].batchNumber` | string | no | The batch number itself. | | `inventoryRevaluations[].lines[].batches[].quantity` | string (decimal) | yes | How much of the line this batch accounts for. | | `inventoryRevaluations[].lines[].batches[].currentUnitCost` | string (decimal) | no | What this batch is carried at today. | | `inventoryRevaluations[].lines[].batches[].newUnitCost` | string (decimal) | no | What it should be carried at. | | `inventoryRevaluations[].lines[].serials` | array of object | no | The serialized units being restated, one entry per unit. | | `inventoryRevaluations[].lines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial number. | | `inventoryRevaluations[].lines[].serials[].serialNumber` | string | no | The serial number itself. | | `inventoryRevaluations[].lines[].serials[].currentUnitCost` | string (decimal) | no | What this unit is carried at today. | | `inventoryRevaluations[].lines[].serials[].newUnitCost` | string (decimal) | no | What it should be carried at. | | `inventoryRevaluations[].lines[].serials[].previousAfterSaleCost` | string (decimal) | no | The unit's accrued after-sale cost before this document. | | `inventoryRevaluations[].lines[].serials[].newAfterSaleCost` | string (decimal) | no | The after-sale cost to capitalize on it. | Example: ```json { "inventoryRevaluations": [ { "costingValuationType": "revaluation", "costingElementTypeId": "f2a58c31-9d47-4e06-8b25-3c7f1a94e5d8", "lines": [ { "itemId": "517a93e4-cbb4-4571-9e3d-2343e56cfd84", "locationId": "b6f0d2a4-77c1-4e39-8a52-0c4e1f9b73d5", "unitId": "d5a71e93-4c08-4b26-95f7-2e6a8c30b41f", "quantity": "150", "currentUnitCost": "12", "newUnitCost": "9" } ] } ] } ``` ## Response `200` Success Under `data` (bulkCreateInventoryRevaluationsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `inventoryRevaluations` | array of object | yes | The created revaluations, in input order, headers only. Read one in full on its own endpoint. | | `inventoryRevaluations[].id` | string | yes | The unique identifier for the revaluation. | | `inventoryRevaluations[].documentNumber` | string | no | The revaluation's document number, for example 'REV-1187'. | | `inventoryRevaluations[].documentStatus` | string | yes | The status of the revaluation. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `inventoryRevaluations[].documentDate` | string (date) | no | The date the revaluation is dated. Format YYYY-MM-DD. | | `inventoryRevaluations[].entityId` | string | no | The unique identifier for the legal entity whose books this moves. See id on GET base/entities. | | `inventoryRevaluations[].entityName` | string | no | The display name of that legal entity. | | `inventoryRevaluations[].costingValuationType` | string | no | How the value change is recorded. Possible values: 'revaluation', 'directCostCapitalisation'. 'revaluation' restates the cost outright, and 'directCostCapitalisation' adds a cost that was expensed elsewhere onto the stock. | | `inventoryRevaluations[].capitalisationExpenseAccountId` | string | no | The unique identifier for the GL account a direct cost capitalization takes the expense off. Absent on an ordinary revaluation. See id on GET accounting/chart-of-accounts/:id. | | `inventoryRevaluations[].costingElementTypeId` | string | no | The unique identifier for the costing element type the cost breakdown rows are recorded against. Required to commit. | | `inventoryRevaluations[].reasonCodeId` | string | no | The unique identifier for the reason code, when one was given. See id on GET inventory/reason-codes/:id. | | `inventoryRevaluations[].reasonCode` | string | no | The code of that reason code. | | `inventoryRevaluations[].reasonCodeDescription` | string | no | The description of that reason code. | | `inventoryRevaluations[].currency` | string | no | The currency of every amount on the revaluation, the legal entity's functional currency. Format ISO 4217, for example 'EUR'. | | `inventoryRevaluations[].totalRevaluationAmount` | string (decimal) | no | The sum of every line's revaluationAmount, in the revaluation's currency. This is what the document moves on the balance sheet. | | `inventoryRevaluations[].active` | boolean | no | Whether the document is active. | | `inventoryRevaluations[].notes` | string | no | Free text on the revaluation. | | `inventoryRevaluations[].lineCount` | number | no | How many lines the revaluation has. | | `inventoryRevaluations[].createdDate` | string (date-time) | no | The date and time the revaluation was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `inventoryRevaluations[].lastModifiedDate` | string (date-time) | no | The date and time the revaluation record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## 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 | | --- | --- | | `NOTHING_TO_CREATE` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `MISSING_CAPITALISATION_ACCOUNT` | 400 | | `TRACKING_MISMATCH` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOTHING_TO_CREATE, REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, MISSING_CAPITALISATION_ACCOUNT, TRACKING_MISMATCH, POSTING_REJECTED - `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 --- # Deletes several draft inventory revaluations in one call. `POST /inventory/inventory-revaluations/bulk-delete` Group: Inventory › Inventory Revaluations · Operation ID: `bulkDeleteInventoryRevaluations` All or nothing: if any identifier matches no revaluation, or names one past draft, nothing is deleted, not even the entries before it. index on the error says which, counting from zero. Only a draft can be deleted, because a committed revaluation has already moved the stock value and posted the journal. Delete one at a time with DELETE inventory/inventory-revaluations/:id. The identifiers come back in input order. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteInventoryRevaluations) ## 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`: bulkDeleteInventoryRevaluationsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the revaluations to delete. All or nothing: if any is missing or past draft, none is deleted. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteInventoryRevaluationsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the deleted revaluations, in input order. | ## 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 | | --- | --- | | `NOTHING_TO_DELETE` | 400 | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOTHING_TO_DELETE, NOT_FOUND, WRONG_STATUS - `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 --- # Search Inventory Revaluations `POST /inventory/inventory-revaluations/search` Group: Inventory › Inventory Revaluations · Operation ID: `searchInventoryRevaluations` Returns the inventory revaluations that match the given criteria, each with its lines. Returns the inventory revaluations that match the criteria in the request body, with the same full schema as GET inventory/inventory-revaluations/:id, lines included. This is a query, not a creation: it answers 200 and writes nothing. itemId and locationId can be searched only here, because both are line fields and the collection endpoint does not carry the lines. An identifier that matches nothing returns an empty page, not an error. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchInventoryRevaluations) ## 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`. | ## Request body Required, `application/json`: searchInventoryRevaluationsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The search criteria, combined with AND. An empty object returns every revaluation, page by page. | | `filters.documentNumber` | string | no | The exact document number, for example 'REV-1187'. The match is exact, not a substring. | | `filters.documentStatus` | string | no | Limits results to revaluations with this status. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. The match is exact. | | `filters.costingValuationType` | string | no | Limits results to revaluations of this kind. Possible values: 'revaluation', 'directCostCapitalisation'. The match is exact. | | `filters.entityId` | string | no | Limits results to revaluations for this legal entity. See id on GET base/entities. | | `filters.locationId` | string | no | Limits results to revaluations that have a line for this warehouse. The warehouse is a line field, so the collection endpoint cannot answer this. See id on GET inventory/warehouses/:id. | | `filters.itemId` | string | no | Limits results to revaluations that have a line for this item. See id on GET inventory/items/:id. | | `filters.reasonCodeId` | string | no | Limits results to revaluations with this reason code. See id on GET inventory/reason-codes/:id. | | `filters.fromDate` | string (date) | no | The inclusive lower bound on documentDate. Omit for an open-ended start. Use format YYYY-MM-DD. | | `filters.toDate` | string (date) | no | The inclusive upper bound on documentDate. Omit for an open-ended end. Use format YYYY-MM-DD. | | `filters.modifiedSince` | string (date-time) | no | The inclusive lower bound on lastModifiedDate, the incremental-sync filter, distinct from the documentDate window. Use format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of revaluations per page. Defaults to 50. The maximum is 200. | Example: ```json { "filters": { "itemId": "517a93e4-cbb4-4571-9e3d-2343e56cfd84", "documentStatus": "COMMITTED" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchInventoryRevaluationsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The revaluations on this page, oldest first, each with its lines. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the revaluation. | | `items[].documentNumber` | string | no | The revaluation's document number, for example 'REV-1187'. | | `items[].documentStatus` | string | yes | The status of the revaluation. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `items[].documentDate` | string (date) | no | The date the revaluation is dated. Format YYYY-MM-DD. | | `items[].entityId` | string | no | The unique identifier for the legal entity whose books this moves. See id on GET base/entities. | | `items[].entityName` | string | no | The display name of that legal entity. | | `items[].costingValuationType` | string | no | How the value change is recorded. Possible values: 'revaluation', 'directCostCapitalisation'. 'revaluation' restates the cost outright, and 'directCostCapitalisation' adds a cost that was expensed elsewhere onto the stock. | | `items[].capitalisationExpenseAccountId` | string | no | The unique identifier for the GL account a direct cost capitalization takes the expense off. Absent on an ordinary revaluation. See id on GET accounting/chart-of-accounts/:id. | | `items[].costingElementTypeId` | string | no | The unique identifier for the costing element type the cost breakdown rows are recorded against. Required to commit. | | `items[].reasonCodeId` | string | no | The unique identifier for the reason code, when one was given. See id on GET inventory/reason-codes/:id. | | `items[].reasonCode` | string | no | The code of that reason code. | | `items[].reasonCodeDescription` | string | no | The description of that reason code. | | `items[].currency` | string | no | The currency of every amount on the revaluation, the legal entity's functional currency. Format ISO 4217, for example 'EUR'. | | `items[].totalRevaluationAmount` | string (decimal) | no | The sum of every line's revaluationAmount, in the revaluation's currency. This is what the document moves on the balance sheet. | | `items[].active` | boolean | no | Whether the document is active. | | `items[].notes` | string | no | Free text on the revaluation. | | `items[].lineCount` | number | no | How many lines the revaluation has. | | `items[].createdDate` | string (date-time) | no | The date and time the revaluation was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the revaluation record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | no | The lines of the revaluation. | | `items[].lines[].id` | string | yes | The unique identifier for the revaluation line. Pass it in updateLines or deleteLines on PATCH inventory/inventory-revaluations/:id to change or remove the line. | | `items[].lines[].itemId` | string | no | The unique identifier for the item being revalued. See id on GET inventory/items/:id. | | `items[].lines[].itemCode` | string | no | The code of that item. | | `items[].lines[].itemName` | string | no | The display name of that item. | | `items[].lines[].locationId` | string | no | The unique identifier for the warehouse whose stock this line restates. The same item may be carried at a different cost in each warehouse. See id on GET inventory/warehouses/:id. | | `items[].lines[].locationCode` | string | no | The code of that warehouse. | | `items[].lines[].locationName` | string | no | The display name of that warehouse. | | `items[].lines[].unitId` | string | no | The unique identifier for the unit the quantity is in. See id on GET inventory/unit-of-measures/:id. | | `items[].lines[].unitCode` | string | no | The code of that unit. | | `items[].lines[].quantity` | string (decimal) | no | How much stock this line restates the cost of, in the line's unit. | | `items[].lines[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit. | | `items[].lines[].currentUnitCost` | string (decimal) | no | What the stock is carried at today, in the revaluation's currency. You supply it and the platform checks it rather than deriving it. Read it from GET inventory/inventory-levels. | | `items[].lines[].newUnitCost` | string (decimal) | no | What it should be carried at after this posts, in the revaluation's currency. | | `items[].lines[].previousAfterSaleCost` | string (decimal) | no | The accrued after-sale cost before this document, in the revaluation's currency. Moving-average direct cost capitalization only. | | `items[].lines[].newAfterSaleCost` | string (decimal) | no | The after-sale cost to capitalize, in the revaluation's currency. Moving-average direct cost capitalization only. | | `items[].lines[].revaluationAmount` | string (decimal) | no | The value this line moves on the balance sheet, in the revaluation's currency: (newUnitCost - currentUnitCost) x quantity. | | `items[].lines[].status` | string | no | Where the line sits in the platform's own process. This is free text with no fixed set of values. | | `items[].lines[].notes` | string | no | Free text on the line. | | `items[].lines[].batches` | array of object | no | The batches being restated. Their quantities must add up to the line's quantity. | | `items[].lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch number. | | `items[].lines[].batches[].batchNumber` | string | no | The batch number itself. | | `items[].lines[].batches[].quantity` | string (decimal) | no | How much of the line this batch accounts for. | | `items[].lines[].batches[].currentUnitCost` | string (decimal) | no | What this batch is carried at today, in the revaluation's currency. | | `items[].lines[].batches[].newUnitCost` | string (decimal) | no | What it should be carried at. | | `items[].lines[].serials` | array of object | no | The serialized units being restated, one entry per unit, so there must be exactly as many as the quantity says. | | `items[].lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial number. | | `items[].lines[].serials[].serialNumber` | string | no | The serial number itself. | | `items[].lines[].serials[].currentUnitCost` | string (decimal) | no | What this unit is carried at today, in the revaluation's currency. | | `items[].lines[].serials[].newUnitCost` | string (decimal) | no | What it should be carried at. | | `items[].lines[].serials[].previousAfterSaleCost` | string (decimal) | no | The unit's accrued after-sale cost before this document. Direct cost capitalization on a sold serial only. | | `items[].lines[].serials[].newAfterSaleCost` | string (decimal) | no | The after-sale cost to capitalize on it. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor to pass as page.cursor for the next page. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether more results remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of inventory transactions. `GET /inventory/inventory-transactions` Group: Inventory › Inventory Transactions · Operation ID: `inventoryTransactions` Returns the same fields as GET inventory/inventory-transactions/:id and POST inventory/inventory-transactions/search. A ledger is unbounded, so filter by a transactionDate window, on its own or with itemId or locationId, to turn it into a report. [Open in the interactive reference](https://docs.api.lensing.ai/?op=inventoryTransactions) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `itemId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `itemCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `transactionDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `sourceDocumentLineType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isReturn` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isPriceDiff` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, transactionDate, inQuantity, outQuantity, totalCost, itemCode, locationCode, lastModifiedDate - Selectable: id, itemId, itemCode, itemName, locationId, locationCode, locationName, unitId, unitCode, unitName, transactionDate, inQuantity, outQuantity, unitInQuantity, unitOutQuantity, unitCost, totalCost, salesAmount, currency, sourceDocumentLineType, isReturn, isPriceDiff, lastModifiedDate ## Response `200` Success Under `data`, an array of inventoryTransactionsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the ledger row. | | `itemId` | string | no | The unique identifier for the item that moved. See id on GET inventory/items/:id. | | `itemCode` | string or null | no | The code of that item. | | `itemName` | string or null | no | The display name of that item. | | `locationId` | string | no | The unique identifier for the location the movement happened at. See id on GET inventory/warehouses/:id. | | `locationCode` | string or null | no | The code of that location. | | `locationName` | string or null | no | The display name of that location. | | `unitId` | string or null | no | The unique identifier for the unit the movement was recorded in. See id on GET inventory/unit-of-measures/:id. | | `unitCode` | string or null | no | The code of that unit, for example 'BOX'. | | `unitName` | string or null | no | The display name of that unit. | | `transactionDate` | string (date) | no | The date the movement was posted. Format YYYY-MM-DD. | | `inQuantity` | string (decimal) | no | The quantity that moved into stock, in the item's stocking unit. Zero on an outbound movement. | | `outQuantity` | string (decimal) or null | no | The quantity that moved out of stock, in the item's stocking unit. Zero on an inbound movement. | | `unitInQuantity` | string (decimal) or null | no | The same inbound movement in the unit the document used, which unitId names. It differs from inQuantity when the document used a unit other than the item's stocking unit. | | `unitOutQuantity` | string (decimal) or null | no | The same outbound movement in the unit the document used, which unitId names. | | `unitCost` | string (decimal) or null | no | The unit cost at the time of the movement, in the currency named by currency. | | `totalCost` | string (decimal) or null | no | The total cost of the movement, in the currency named by currency. | | `salesAmount` | string (decimal) or null | no | The sales amount of the movement, where it has one, in the currency named by currency. | | `currency` | string or null | no | The currency that unitCost, totalCost and salesAmount are stated in. Format ISO 4217, for example 'USD'. | | `sourceDocumentLineType` | string or null | no | The kind of document line that produced this movement. Possible values: 'IMInventorySalesOrderLine', 'IMInventorySalesDeliveryLine', 'IMSalesOrderLine', 'IMInventoryPurchaseReturnLine', 'IMInventorySalesReturnLine', 'IMWarehouseTransferLine', 'IMInventoryDecreaseLine', 'IMInventoryIncreaseLine', 'IMInventoryRevaluationLine', 'IMInventoryTransferLine', 'IMInventoryAdjustmentLine', 'IMInventoryCountLine', 'IMInventoryPutAwayTask', 'IMInventoryPurchaseReturnReversal', 'PurchaseOrderLine', 'IMLandedCostAllocationLine', 'SalesOrderProductItem', 'IMWarehouseTransferOrderLine', 'VendorBillItemBase', 'InvPurchaseDeliveryLine'. | | `isReturn` | boolean or null | no | Whether the movement is a return. | | `isPriceDiff` | boolean or null | no | Whether the movement is a price-difference adjustment rather than a physical movement of goods. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the ledger row last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the inventory transaction with the given identifier. `GET /inventory/inventory-transactions/{id}` Group: Inventory › Inventory Transactions · Operation ID: `getInventoryTransaction` Returns the same fields as GET inventory/inventory-transactions and POST inventory/inventory-transactions/search. A ledger row cannot be changed once written, so a NOT_FOUND means the identifier was never one, not that the row was removed. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getInventoryTransaction) ## 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`. | | `id` | path | string | yes | The unique identifier for the inventory transaction to return. | ## Response `200` Success Under `data` (getInventoryTransactionOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the ledger row. | | `itemId` | string | yes | The unique identifier for the item that moved. See id on GET inventory/items/:id. | | `itemCode` | string | no | The code of that item. | | `itemName` | string | no | The display name of that item. | | `locationId` | string | yes | The unique identifier for the location the movement happened at. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The code of that location. | | `locationName` | string | no | The display name of that location. | | `unitId` | string | no | The unique identifier for the unit the movement was recorded in. See id on GET inventory/unit-of-measures/:id. | | `unitCode` | string | no | The code of that unit, for example 'BOX'. | | `unitName` | string | no | The display name of that unit. | | `transactionDate` | string (date) | yes | The date the movement was posted. Format YYYY-MM-DD. | | `inQuantity` | string (decimal) | yes | The quantity that moved into stock, in the item's stocking unit. Zero on an outbound movement. | | `outQuantity` | string (decimal) | no | The quantity that moved out of stock, in the item's stocking unit. Zero on an inbound movement. | | `unitInQuantity` | string (decimal) | no | The same inbound movement in the unit the document used, which unitId names. It differs from inQuantity when the document used a unit other than the item's stocking unit. | | `unitOutQuantity` | string (decimal) | no | The same outbound movement in the unit the document used, which unitId names. | | `unitCost` | string (decimal) | no | The unit cost at the time of the movement, in the currency named by currency. | | `totalCost` | string (decimal) | no | The total cost of the movement, in the currency named by currency. | | `salesAmount` | string (decimal) | no | The sales amount of the movement, where it has one, in the currency named by currency. | | `currency` | string | no | The currency that unitCost, totalCost and salesAmount are stated in. Format ISO 4217, for example 'USD'. | | `sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | The kind of document line that produced this movement. | | `isReturn` | boolean | no | Whether the movement is a return. | | `isPriceDiff` | boolean | no | Whether the movement is a price-difference adjustment rather than a physical movement of goods. | | `lastModifiedDate` | string (date-time) | no | The date and time the ledger row last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the inventory transactions that match the given filters. `POST /inventory/inventory-transactions/search` Group: Inventory › Inventory Transactions · Operation ID: `searchInventoryTransactions` Every filter you send must match and the filters combine. Send an empty filters object to return every inventory transaction. Use fromDate and toDate for a transactionDate window, inclusive on both sides, on its own or with itemId or locationId. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchInventoryTransactions) ## 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`. | ## Request body Required, `application/json`: searchInventoryTransactionsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. An inventory transaction must match every filter you send. Send an empty object to return all inventory transactions. | | `filters.itemId` | string | no | Limits results to movements of this item. An identifier that is not an item's matches no transaction. | | `filters.locationId` | string | no | Limits results to movements at this location. | | `filters.fromDate` | string (date) | no | Limits results to movements on or after this date. Format YYYY-MM-DD. | | `filters.toDate` | string (date) | no | Limits results to movements on or before this date. Format YYYY-MM-DD. | | `filters.sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | Limits results to movements caused by this kind of document line. | | `filters.isReturn` | boolean | no | Limits results to returns when true, and to all other movements when false. | | `filters.modifiedSince` | string (date-time) | no | Limits results to rows changed on or after this time. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of inventory transactions per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "itemId": "5f21c8b0-9d34-4a76-8e12-04b93c7de5a8", "fromDate": "2026-08-01", "toDate": "2026-08-31" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchInventoryTransactionsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The inventory transactions on this page, oldest first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the ledger row. | | `items[].itemId` | string | yes | The unique identifier for the item that moved. See id on GET inventory/items/:id. | | `items[].itemCode` | string | no | The code of that item. | | `items[].itemName` | string | no | The display name of that item. | | `items[].locationId` | string | yes | The unique identifier for the location the movement happened at. See id on GET inventory/warehouses/:id. | | `items[].locationCode` | string | no | The code of that location. | | `items[].locationName` | string | no | The display name of that location. | | `items[].unitId` | string | no | The unique identifier for the unit the movement was recorded in. See id on GET inventory/unit-of-measures/:id. | | `items[].unitCode` | string | no | The code of that unit, for example 'BOX'. | | `items[].unitName` | string | no | The display name of that unit. | | `items[].transactionDate` | string (date) | yes | The date the movement was posted. Format YYYY-MM-DD. | | `items[].inQuantity` | string (decimal) | yes | The quantity that moved into stock, in the item's stocking unit. Zero on an outbound movement. | | `items[].outQuantity` | string (decimal) | no | The quantity that moved out of stock, in the item's stocking unit. Zero on an inbound movement. | | `items[].unitInQuantity` | string (decimal) | no | The same inbound movement in the unit the document used, which unitId names. It differs from inQuantity when the document used a unit other than the item's stocking unit. | | `items[].unitOutQuantity` | string (decimal) | no | The same outbound movement in the unit the document used, which unitId names. | | `items[].unitCost` | string (decimal) | no | The unit cost at the time of the movement, in the currency named by currency. | | `items[].totalCost` | string (decimal) | no | The total cost of the movement, in the currency named by currency. | | `items[].salesAmount` | string (decimal) | no | The sales amount of the movement, where it has one, in the currency named by currency. | | `items[].currency` | string | no | The currency that unitCost, totalCost and salesAmount are stated in. Format ISO 4217, for example 'USD'. | | `items[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | The kind of document line that produced this movement. | | `items[].isReturn` | boolean | no | Whether the movement is a return. | | `items[].isPriceDiff` | boolean | no | Whether the movement is a price-difference adjustment rather than a physical movement of goods. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the ledger row last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more inventory transactions remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of item categories. `GET /inventory/item-categories` Group: Inventory › Item Categories · Operation ID: `itemCategories` The item category collection. Every field is scalar, so each row contains the same schema as GET inventory/item-categories/:id: nothing is withheld here. parentId is returned but is not filterable or orderable. To read the tree, use GET inventory/item-categories/tree, which returns every category with its parent, its ancestor path and its depth. Filtering by a category elsewhere in this API, such as the item collection or the aging and turnover reports, rolls up: it means that category or any category beneath it. Names are not unique, so a name filter can return several rows. There is no free-text or substring matching on any endpoint here. [Open in the interactive reference](https://docs.api.lensing.ai/?op=itemCategories) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `name` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `active` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, name, description, active, idealTurnoverRatio, lastModifiedDate - Selectable: id, name, description, active, idealTurnoverRatio, lastModifiedDate ## Response `200` Success Under `data`, an array of itemCategoriesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the item category. | | `name` | string | no | The display name of the category, for example 'Hardware'. Not unique: two categories can share a name, so use id to address one. | | `description` | string or null | no | A free-form description of what the category covers. Absent when none was set. | | `active` | boolean or null | no | Whether the category can be chosen on new items. When false the category is retired: existing items keep it and new ones cannot be assigned to it. | | `idealTurnoverRatio` | string (decimal) or null | no | The target inventory turnover for items in this category, in times per year. For analytics only. A ratio rather than an amount, so it has no currency. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the category last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates an item category. `POST /inventory/item-categories` Group: Inventory › Item Categories · Operation ID: `createItemCategory` Creates one item category and returns only its id. Read the record back with GET inventory/item-categories/:id if you need its stored state. Names are not unique, so you cannot reliably find the category again by name afterwards: keep the id. To create many at once, use POST inventory/item-categories/bulk, which is all-or-nothing over the whole batch. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createItemCategory) ## 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`: createItemCategoryInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The display name of the category, for example 'Hardware'. Not checked for uniqueness: creating a second category with an existing name succeeds. | | `description` | string | no | A free-form description of what the category covers. | | `active` | boolean | no | Whether the category can be chosen on new items. Defaults to true. | | `idealTurnoverRatio` | string (decimal) | no | The target inventory turnover for items in this category, in times per year. For analytics only. A ratio rather than an amount, so it has no currency. | | `parentId` | string | no | The identifier of the category this one sits under. Omit for a top-level category. See id on GET inventory/item-categories/:id. | Example: ```json { "name": "Hardware", "description": "Fasteners, fittings and tooling", "active": true, "idealTurnoverRatio": "6.50", "parentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` ## Response `200` Success Under `data` (createItemCategoryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created item category. | ## 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 | | --- | --- | | `PARENT_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: PARENT_NOT_FOUND - `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 --- # Returns the item category with the given identifier. `GET /inventory/item-categories/{id}` Group: Inventory › Item Categories · Operation ID: `getItemCategory` Returns one item category by id. The response uses the same schema as GET inventory/item-categories and POST inventory/item-categories/search. Names are not unique, so there is no name-keyed variant of this endpoint. To look one up by name, filter the collection with GET inventory/item-categories?filter[name][eq]=Hardware, which returns the same schema in an array. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getItemCategory) ## 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`. | | `id` | path | string | yes | The unique identifier for the item category to return. | ## Response `200` Success Under `data` (getItemCategoryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the item category. | | `name` | string | yes | The display name of the category, for example 'Hardware'. Not unique: two categories can share a name, so use id to address one. | | `description` | string | no | A free-form description of what the category covers. Absent when none was set. | | `active` | boolean | no | Whether the category can be chosen on new items. When false the category is retired: existing items keep it and new ones cannot be assigned to it. | | `idealTurnoverRatio` | string (decimal) | no | The target inventory turnover for items in this category, in times per year. For analytics only. A ratio rather than an amount, so it has no currency. | | `lastModifiedDate` | string (date-time) | no | The date and time the category last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `parentId` | string | no | The identifier of the category this one sits under. Absent for a top-level category. See id on GET inventory/item-categories/:id. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Item Category `PATCH /inventory/item-categories/{id}` Group: Inventory › Item Categories · Operation ID: `updateItemCategory` Updates the item category with the given identifier. Only the fields in the request body change. Updates one item category. Every mutable field is optional and flat on the body: a field you omit is left unchanged. name cannot be cleared, and it stays non-unique, so renaming onto a name another category already has is allowed. Setting active to false retires a category without deleting it. parentId re-parents the category and moves its whole subtree without touching any item. Be aware that integrations may match categories by name. A rename is a change the API permits and downstream systems may not expect, and because names are not unique a rename can merge two categories from a name-matching consumer's point of view. The response contains only the id. Read the record back with GET inventory/item-categories/:id if you need its state after the update. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateItemCategory) ## 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. | | `id` | path | string | yes | The unique identifier for the item category to update. | ## Request body Required, `application/json`: updateItemCategoryInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The new display name of the category, for example 'Hardware'. Cannot be cleared, and is not checked for uniqueness: renaming onto a name another category already has is allowed. | | `description` | string | no | A new free-form description of what the category covers. | | `active` | boolean | no | Whether the category can be chosen on new items. Set false to retire the category without deleting it: existing items keep their assignment and new ones cannot choose it. | | `idealTurnoverRatio` | string (decimal) | no | A new target inventory turnover for items in this category, in times per year. For analytics only. A ratio rather than an amount, so it has no currency. | | `parentId` | string or null | no | The identifier of the category this one sits under. This is how a category is re-parented. Pass null to make it a top-level category. Re-parenting moves the whole subtree and touches no item: items stay assigned to the categories they already had. See id on GET inventory/item-categories/:id. | Example: ```json { "name": "<name>", "description": "<description>", "active": true, "idealTurnoverRatio": "8.00", "parentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` ## Response `200` Success Under `data` (updateItemCategoryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the updated item category. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `PARENT_NOT_FOUND` | 400 | | `CATEGORY_CYCLE` | 400 | | `CATEGORY_TOO_DEEP` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, PARENT_NOT_FOUND, CATEGORY_CYCLE, CATEGORY_TOO_DEEP - `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 --- # Deletes an item category. `DELETE /inventory/item-categories/{id}` Group: Inventory › Item Categories · Operation ID: `deleteItemCategory` Deletes one item category. This is a hard delete: the record is removed, not flagged. Deleting is not how you retire a category. A category that any item or inventory transaction still references cannot be deleted, and the call fails with ITEM_CATEGORY_IN_USE. An item's category is a required field, so a delete succeeds only for a category nothing has used yet. To take a category with history out of use, set active to false with PATCH inventory/item-categories/:id instead: existing items keep their assignment and new ones cannot choose it. The response echoes the deleted category's id and the name it held. To delete many at once, use POST inventory/item-categories/bulk-delete, which is all-or-nothing over the whole batch. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteItemCategory) ## 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. | | `id` | path | string | yes | The unique identifier for the item category to delete. | ## Response `200` Success Under `data` (deleteItemCategoryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted item category. | | `name` | string | yes | The display name the deleted category held, echoed so you can log the removal without a prior read. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `ITEM_CATEGORY_IN_USE` | 400 | | `ITEM_CATEGORY_HAS_CHILDREN` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, ITEM_CATEGORY_IN_USE, ITEM_CATEGORY_HAS_CHILDREN - `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 --- # Creates many item categories in one call. `POST /inventory/item-categories/bulk` Group: Inventory › Item Categories · Operation ID: `bulkCreateItemCategories` Creates many item categories in one call, all or nothing: if any row is rejected, nothing is created and there is no partial state to reconcile. Each row has the same shape as the single create body. A row's only precondition is its parentId: that category must already exist, either in your tenant or earlier in this same batch. Rows are processed in order, so a parent listed before its children seeds a whole tree in one call, and a row naming a parent that appears later rejects the batch. Names are not unique, so nothing else about a row's content is rejected. The response echoes the created categories in input order, each with only its id. Read a record back with GET inventory/item-categories/:id if you need its stored state. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateItemCategories) ## 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`: bulkCreateItemCategoriesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `itemCategories` | array of object | yes | The item categories to create. All or nothing: if any row fails, nothing is created. | | `itemCategories[].name` | string | yes | The display name of the category, for example 'Hardware'. Not checked for uniqueness, within this batch or against existing records. | | `itemCategories[].description` | string | no | A free-form description of what the category covers. | | `itemCategories[].active` | boolean | no | Whether the category can be chosen on new items. Defaults to true. | | `itemCategories[].idealTurnoverRatio` | string (decimal) | no | The target inventory turnover for items in this category, in times per year. For analytics only. A ratio rather than an amount, so it has no currency. | | `itemCategories[].parentId` | string | no | The identifier of the category this one sits under. Omit for a top-level category. A row can name a parent created earlier in the same batch, which is how a whole tree is seeded in one call, but only in that order: a row naming a parent that appears later is rejected, and with it the whole batch. See id on GET inventory/item-categories/:id. | Example: ```json { "itemCategories": [ { "name": "Hardware", "description": "Fasteners, fittings and tooling", "idealTurnoverRatio": "6.50" }, { "name": "Consumables", "idealTurnoverRatio": "12.00" } ] } ``` ## Response `200` Success Under `data` (bulkCreateItemCategoriesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `created` | array of object | yes | The created item categories, in input order. Match rows to inputs by index. | | `created[].id` | string | yes | The unique identifier for the created item category. | ## 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 | | --- | --- | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure - `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 --- # Deletes many item categories in one call. `POST /inventory/item-categories/bulk-delete` Group: Inventory › Item Categories · Operation ID: `bulkDeleteItemCategories` Deletes many item categories in one call, all or nothing. Every id is resolved and reference-checked before the first delete: an id that matches nothing fails the call with NOT_FOUND, and a category still referenced by items or inventory transactions fails it with ITEM_CATEGORY_IN_USE. Both name the offending row's index, and nothing is deleted in either case. The response echoes the deleted categories in input order. To delete one at a time, use DELETE inventory/item-categories/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteItemCategories) ## 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`: bulkDeleteItemCategoriesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the item categories to delete. If any id is unknown or its category is still referenced, the whole call is rejected and nothing is deleted. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteItemCategoriesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `deleted` | array of object | yes | The deleted item categories, in input order. Match rows to inputs by index. | | `deleted[].id` | string | yes | The unique identifier for the deleted item category. | | `deleted[].name` | string | yes | The display name the deleted category held, echoed so you can log the removal without a prior read. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `ITEM_CATEGORY_IN_USE` | 400 | | `ITEM_CATEGORY_HAS_CHILDREN` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, ITEM_CATEGORY_IN_USE, ITEM_CATEGORY_HAS_CHILDREN - `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 --- # Returns the item categories that match the given criteria. `POST /inventory/item-categories/search` Group: Inventory › Item Categories · Operation ID: `searchItemCategories` Searches item categories by exact criteria in a request body, returning the same schema as GET inventory/item-categories/:id and GET inventory/item-categories. Every field on an item category is a plain scalar, so the collection can already express every filter offered here. This endpoint adds a request body, for a query assembled programmatically or one that would push a URL past a proxy's length limit, and a larger page size. There is no free-text or substring matching, which is why description is not a criterion. Nothing matching is an empty result, never an error. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchItemCategories) ## 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`. | ## Request body Required, `application/json`: searchItemCategoriesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. Omit to return all item categories. | | `filters.name` | string | no | Limits results to item categories with this exact name, for example 'Hardware'. Names are not unique, so this can match several. | | `filters.active` | boolean | no | Limits results to categories that can be chosen on new items (true) or to retired ones (false). Omit to match both. | | `filters.modifiedSince` | string (date-time) | no | Limits results to categories changed at or after this time, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of item categories per page. Defaults to 50, capped at 200. | Example: ```json { "filters": { "active": true, "modifiedSince": "2026-08-01T00:00:00.000Z" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchItemCategoriesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The item categories on this page, ordered by id. The same schema as GET inventory/item-categories/:id. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the item category. | | `items[].name` | string | yes | The display name of the category, for example 'Hardware'. Not unique: two categories can share a name, so use id to address one. | | `items[].description` | string | no | A free-form description of what the category covers. Absent when none was set. | | `items[].active` | boolean | no | Whether the category can be chosen on new items. When false the category is retired: existing items keep it and new ones cannot be assigned to it. | | `items[].idealTurnoverRatio` | string (decimal) | no | The target inventory turnover for items in this category, in times per year. For analytics only. A ratio rather than an amount, so it has no currency. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the category last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].parentId` | string | no | The identifier of the category this one sits under. Absent for a top-level category. See id on GET inventory/item-categories/:id. | | `pagination` | object | yes | The pagination details for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether more results remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns every item category with its position in the category tree. `GET /inventory/item-categories/tree` Group: Inventory › Item Categories · Operation ID: `readItemCategoryTree` Returns every item category with its position in the category tree, its parent, its ancestor path and its depth, in one request. It computes server-side the derived fields the collection cannot express: path, the ancestor names ending with the category's own, and level, its depth. Every category appears, including a flat one that has never been placed in the tree: it comes back as a root, with parentId absent, level 0 and path its own name alone. Categories are ordered by name, not by tree position. The tree is capped at 6 levels deep, and hasChildren says whether a category has anything beneath it. The result is an array, empty only when your tenant has no categories at all. [Open in the interactive reference](https://docs.api.lensing.ai/?op=readItemCategoryTree) ## 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`. | ## Response `200` Success Under `data` (readItemCategoryTreeOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | Every item category, each with its position in the tree, ordered by name. | | `data[].id` | string | yes | The unique identifier for the item category. | | `data[].name` | string | yes | The display name of the category, for example 'Hardware'. Not unique: two categories can share a name, so use id to address one. | | `data[].description` | string | no | A free-form description of what the category covers. Absent when none was set. | | `data[].parentId` | string | no | The identifier of the category this one sits under. Absent for a top-level category. See id on GET inventory/item-categories/:id. | | `data[].level` | number | yes | The depth in the tree: 0 for a top-level category, 1 for its children, and so on, up to 5. | | `data[].path` | array of string | yes | The ancestor names from the root down, ending with this category's own name. A root's path is its own name alone. | | `data[].hasChildren` | boolean | yes | Whether any category sits beneath this one. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of item types. `GET /inventory/item-types` Group: Inventory › Item Types · Operation ID: `itemTypes` Use this collection to find a valid itemTypeId before creating an item. It does not include itemCount, the number of items that name each type. Read that from GET inventory/item-types/:id or POST inventory/item-types/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=itemTypes) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `name` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `procurable` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `stockable` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `active` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, name, active, createdDate, lastModifiedDate - Selectable: id, name, description, procurable, stockable, active, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of itemTypesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the item type. | | `name` | string or null | no | The display name of the item type, for example 'Stocked good'. Two item types cannot share a name. | | `description` | string or null | no | A free-form description of the item type. | | `procurable` | boolean or null | no | Whether items of this type can appear on purchase documents. | | `stockable` | boolean or null | no | Whether items of this type hold stock. | | `active` | boolean or null | no | Whether the item type can be chosen for new items. | | `createdDate` | string (date-time) or null | no | The date and time the item type was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the item type record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the item type with the given identifier. `GET /inventory/item-types/{id}` Group: Inventory › Item Types · Operation ID: `getItemType` Returns the same fields as POST inventory/item-types/search. The collection GET inventory/item-types omits itemCount. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getItemType) ## 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`. | | `id` | path | string | yes | The unique identifier for the item type to return. | ## Response `200` Success Under `data` (getItemTypeOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the item type. | | `name` | string | no | The display name of the item type, for example 'Stocked good'. Two item types cannot share a name. | | `description` | string | no | A free-form description of the item type. | | `procurable` | boolean | no | Whether items of this type can appear on purchase documents. | | `stockable` | boolean | no | Whether items of this type hold stock. | | `active` | boolean | no | Whether the item type can be chosen for new items. | | `itemCount` | number | no | The number of items that name this item type. | | `createdDate` | string (date-time) | no | The date and time the item type was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the item type record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the item types that match the given filters. `POST /inventory/item-types/search` Group: Inventory › Item Types · Operation ID: `searchItemTypes` Every criterion is exact and criteria combine with AND. Send an empty filters object to return every item type. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchItemTypes) ## 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`. | ## Request body Required, `application/json`: searchItemTypesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. An item type must match every filter you send. Send an empty object to return all item types. | | `filters.name` | string | no | Limits results to the item type with this name. The match is exact. Names are unique, so this matches at most one item type. | | `filters.procurable` | boolean | no | Limits results to item types with the given procurable value. | | `filters.stockable` | boolean | no | Limits results to item types with the given stockable value. | | `filters.active` | boolean | no | Limits results to item types with the given active value. | | `filters.modifiedSince` | string (date-time) | no | Limits results to item types changed on or after this time. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of item types per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "stockable": false }, "page": { "cursor": "<cursor>", "size": 0 } } ``` ## Response `200` Success Under `data` (searchItemTypesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The item types on this page, oldest first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the item type. | | `items[].name` | string | no | The display name of the item type, for example 'Stocked good'. Two item types cannot share a name. | | `items[].description` | string | no | A free-form description of the item type. | | `items[].procurable` | boolean | no | Whether items of this type can appear on purchase documents. | | `items[].stockable` | boolean | no | Whether items of this type hold stock. | | `items[].active` | boolean | no | Whether the item type can be chosen for new items. | | `items[].itemCount` | number | no | The number of items that name this item type. | | `items[].createdDate` | string (date-time) | no | The date and time the item type was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the item type record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | no | True when more item types remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of items. `GET /inventory/items` Group: Inventory › Items · Operation ID: `items` Returns a list of items. It cannot filter on missing weight or missing dimensions. Use POST inventory/items/search for those. It also omits standardCost, itemPurchasePrice and currency, which GET inventory/items/:id and POST inventory/items/search return. [Open in the interactive reference](https://docs.api.lensing.ai/?op=items) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `itemCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `externalItemId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `brandName` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `itemTypeId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `categoryId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `unitGroupId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `valuationMethod` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isBatch` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isSerial` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isInventory` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isSalesItem` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isPurchaseItem` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `active` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `countryOfOrigin` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isPerishable` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, itemCode, valuationMethod, active, createdDate, lastModifiedDate - Selectable: id, itemCode, itemName, externalItemId, brandName, notes, imageAvatar, itemTypeId, itemTypeName, categoryId, categoryName, inventoryUnitId, inventoryUnitCode, salesUnitId, salesUnitCode, purchaseUnitId, purchaseUnitCode, unitGroupId, unitGroupName, valuationMethod, isBatch, isSerial, isInventory, isSalesItem, isPurchaseItem, active, eccnNumber, htsCode, countryOfOrigin, minStockLevel, minimumTransactionQuantity, leadTime, weight, weightUnit, grossWeight, netWeight, length, width, height, volumePerUnit, volumeUnit, defaultAllocationMode, isPerishable, shelfLifeDays, minShelfLifeDaysOnReceipt, minShelfLifeDaysOnDispatch, bestBeforeOffsetDays, storageTempMin, storageTempMax, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of itemsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the item. | | `itemCode` | string or null | no | The item code, for example 'WIDGET-01'. Unique within the tenant. It is set when the item is created and cannot be changed. | | `itemName` | string or null | no | The display name of the item. | | `externalItemId` | string or null | no | Your own identifier for this item in an external system. | | `brandName` | string or null | no | The display name of the brand. | | `notes` | string or null | no | Free text on the item. | | `imageAvatar` | string or null | no | The URL of the item's avatar image. Absent when none is set. | | `itemTypeId` | string or null | no | The unique identifier of the item's type. It is set when the item is created and cannot be changed. See id on GET inventory/item-types/:id. | | `itemTypeName` | string or null | no | The display name of the item's type. | | `categoryId` | string or null | no | The unique identifier of the item's category. See id on GET inventory/item-categories/:id. | | `categoryName` | string or null | no | The display name of the item's category. | | `inventoryUnitId` | string or null | no | The unique identifier of the unit stock is counted in. Every quantity on an inventory level or transaction is in this unit. See id on GET inventory/unit-of-measures/:id. | | `inventoryUnitCode` | string or null | no | The code of the inventory unit. | | `salesUnitId` | string or null | no | The unique identifier of the unit used on sales documents. Absent when it is the same as the inventory unit. See id on GET inventory/unit-of-measures/:id. | | `salesUnitCode` | string or null | no | The code of the sales unit. | | `purchaseUnitId` | string or null | no | The unique identifier of the unit used on purchase documents. Absent when it is the same as the inventory unit. See id on GET inventory/unit-of-measures/:id. | | `purchaseUnitCode` | string or null | no | The code of the purchase unit. | | `unitGroupId` | string or null | no | The unique identifier of the unit of measure group that has the conversions between the item's units. See id on GET inventory/unit-of-measure-groups/:id. | | `unitGroupName` | string or null | no | The display name of the unit of measure group. | | `valuationMethod` | string or null | no | How the item's stock is valued. Possible values: 'movingAverage', 'batch', 'serials', 'none'. 'none' is for an item that is not stocked. 'fifo' can be returned but cannot be written, because it is under development. | | `isBatch` | boolean or null | no | Whether the item is tracked by batch. Receipts and shipments of it name batches. | | `isSerial` | boolean or null | no | Whether the item is tracked by serial number. Receipts and shipments of it name one serial per unit. | | `isInventory` | boolean or null | no | Whether stock levels are kept for the item. False for a service or charge item. | | `isSalesItem` | boolean or null | no | Whether the item can appear on sales documents. | | `isPurchaseItem` | boolean or null | no | Whether the item can appear on purchase documents. | | `active` | boolean or null | no | Whether the item can be selected on new documents. False means it is retired, and historical records keep it. | | `eccnNumber` | string or null | no | The Export Control Classification Number. | | `htsCode` | string or null | no | The Harmonized Tariff Schedule code. | | `countryOfOrigin` | string or null | no | The country the item originates from. | | `minStockLevel` | string (decimal) or null | no | The stock level below which the item should be reordered, in the inventory unit. | | `minimumTransactionQuantity` | string (decimal) or null | no | The smallest quantity any document can move. Absent when there is no restriction. | | `leadTime` | string (decimal) or null | no | The number of days between ordering the item and receiving it. | | `weight` | string (decimal) or null | no | The weight of one item, in weightUnit. A landed cost Weight allocation fails rather than falling back when an item on the receipt has none. | | `weightUnit` | string or null | no | The unit weight is expressed in. Possible values: 'G', 'Kg', 'Lbs'. | | `grossWeight` | string (decimal) or null | no | The weight including packaging, in weightUnit. | | `netWeight` | string (decimal) or null | no | The weight excluding packaging, in weightUnit. | | `length` | string (decimal) or null | no | The length of one item. A landed cost Volume allocation needs length, width and height together, and fails when the set is incomplete. | | `width` | string (decimal) or null | no | The width of one item. | | `height` | string (decimal) or null | no | The height of one item. | | `volumePerUnit` | string (decimal) or null | no | The volume of one item, in volumeUnit. | | `volumeUnit` | string or null | no | The unit volumePerUnit is expressed in. Possible values: 'L'. | | `defaultAllocationMode` | string or null | no | The allocation mode a landed cost charge for this item falls back to when it names none. Possible values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. | | `isPerishable` | boolean or null | no | Whether the item expires. | | `shelfLifeDays` | number or null | no | The number of days the item keeps from production. | | `minShelfLifeDaysOnReceipt` | number or null | no | The least remaining shelf life a receipt of this item will accept. | | `minShelfLifeDaysOnDispatch` | number or null | no | The least remaining shelf life a dispatch of this item will accept. | | `bestBeforeOffsetDays` | number or null | no | The number of days between the expiry date and the best-before date printed on it. | | `storageTempMin` | string (decimal) or null | no | The lowest temperature the item can be stored at, in degrees Celsius. | | `storageTempMax` | string (decimal) or null | no | The highest temperature the item can be stored at, in degrees Celsius. | | `createdDate` | string (date-time) or null | no | The date and time the item was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the item record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates an item. `POST /inventory/items` Group: Inventory › Items · Operation ID: `createItem` Creates an item. Five fields are required: itemCode, itemTypeId, categoryId, inventoryUnitId and unitGroupId. The rest have defaults. itemCode and itemTypeId cannot be changed after the item is created. Set a weight if the item will ever meet a landed cost. The Weight and Volume allocation modes fail rather than falling back when an item on the receipt has no measurement. POST inventory/items/search with missingWeight finds the items already in that state. A valuation method must match its tracking: 'batch' requires isBatch and 'serials' requires isSerial. Every unit named on the item must belong to unitGroupId. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createItem) ## 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`: createItemInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `itemCode` | string | yes | The item code, for example 'WIDGET-01'. Must be unique within the tenant, including against retired items whose codes are never released. It cannot be changed after the item is created. | | `itemName` | string | no | The display name of the item. | | `itemTypeId` | string | yes | The unique identifier of the item's type. It cannot be changed after the item is created. See id on GET inventory/item-types/:id. | | `categoryId` | string | yes | The unique identifier of the item's category. See id on GET inventory/item-categories/:id. | | `inventoryUnitId` | string | yes | The unique identifier of the unit stock is counted in. Every quantity on an inventory level or transaction will be in this unit. It must be one of the units in unitGroupId. See id on GET inventory/unit-of-measures/:id. | | `unitGroupId` | string | yes | The unique identifier of the unit of measure group that has the conversions between the item's units. inventoryUnitId, salesUnitId and purchaseUnitId must all be units in this group, and a unit outside it fails with UNIT_NOT_IN_UNIT_GROUP. See id on GET inventory/unit-of-measure-groups/:id. | | `salesUnitId` | string | no | The unique identifier of the unit used on sales documents. Omit when it is the same as the inventory unit. It must be one of the units in unitGroupId. See id on GET inventory/unit-of-measures/:id. | | `purchaseUnitId` | string | no | The unique identifier of the unit used on purchase documents. Omit when it is the same as the inventory unit. It must be one of the units in unitGroupId. See id on GET inventory/unit-of-measures/:id. | | `valuationMethod` | string | no | How the item's stock is valued. Accepted values: 'movingAverage', 'batch', 'serials', 'none'. Use 'none' for an item that is not stocked. Defaults to 'movingAverage'. 'batch' requires isBatch, 'serials' requires isSerial, and 'none' requires isInventory false, and each contradiction fails with TRACKING_MISMATCH. 'fifo' is under development and fails with VALUATION_METHOD_UNAVAILABLE. | | `isBatch` | boolean | no | Whether the item is tracked by batch. Defaults to false. | | `isSerial` | boolean | no | Whether the item is tracked by serial number. Defaults to false. | | `isInventory` | boolean | no | Whether stock levels are kept for the item. Defaults to true. Set false for a service or charge item. | | `isSalesItem` | boolean | no | Whether the item can appear on sales documents. Defaults to true. | | `isPurchaseItem` | boolean | no | Whether the item can appear on purchase documents. Defaults to true. | | `active` | boolean | no | Whether the item can be selected on new documents. Defaults to true. Setting it false retires the item without deleting it. | | `notes` | string | no | Free text on the item. | | `externalItemId` | string | no | Your own identifier for this item in an external system. | | `brandName` | string | no | The display name of the brand. | | `imageAvatar` | string | no | The URL of the item's avatar image. | | `eccnNumber` | string | no | The Export Control Classification Number. | | `htsCode` | string | no | The Harmonized Tariff Schedule code. | | `countryOfOrigin` | string | no | The country the item originates from. | | `minStockLevel` | string (decimal) | no | The stock level below which the item should be reordered, in the inventory unit. | | `minimumTransactionQuantity` | string (decimal) | no | The smallest quantity any document can move. Omit for no restriction. Omitting it is not the same as sending 1. | | `leadTime` | string (decimal) | no | The number of days between ordering the item and receiving it. | | `standardCost` | string (decimal) | no | The standard cost carried for planning. State its currency in currency. | | `itemPurchasePrice` | string (decimal) | no | The expected purchase price. State its currency in currency. | | `currency` | string | no | The currency of standardCost and itemPurchasePrice. Required when either is given. Format ISO 4217, for example 'USD'. | | `weight` | string (decimal) | no | The weight of one item, in weightUnit. Set it if the item may be received under a landed cost that uses Weight allocation, because that mode fails rather than falling back when an item has no weight. | | `weightUnit` | string | no | The unit weight is expressed in. Accepted values: 'G', 'Kg', 'Lbs'. | | `grossWeight` | string (decimal) | no | The weight including packaging, in weightUnit. | | `netWeight` | string (decimal) | no | The weight excluding packaging, in weightUnit. | | `length` | string (decimal) | no | The length of one item. A landed cost Volume allocation needs length, width and height together, and fails when the set is incomplete. | | `width` | string (decimal) | no | The width of one item. | | `height` | string (decimal) | no | The height of one item. | | `volumePerUnit` | string (decimal) | no | The volume of one item, in volumeUnit. | | `volumeUnit` | string | no | The unit volumePerUnit is expressed in. Accepted values: 'L'. | | `defaultAllocationMode` | string | no | The allocation mode a landed cost charge billed as this item falls back to when it names none. Accepted values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. | | `isPerishable` | boolean | no | Whether the item expires. | | `shelfLifeDays` | number | no | The number of days the item keeps from production. | | `minShelfLifeDaysOnReceipt` | number | no | The least remaining shelf life a receipt of this item will accept. | | `minShelfLifeDaysOnDispatch` | number | no | The least remaining shelf life a dispatch of this item will accept. | | `bestBeforeOffsetDays` | number | no | The number of days between the expiry date and the best-before date printed on it. | | `storageTempMin` | string (decimal) | no | The lowest storage temperature, in degrees Celsius. | | `storageTempMax` | string (decimal) | no | The highest storage temperature, in degrees Celsius. | Example: ```json { "itemCode": "WIDGET-01", "itemName": "Standard widget", "itemTypeId": "66aea173-07b8-4c4e-bc4a-5c35dedfdc16", "categoryId": "6709e2bf-d6cf-4800-9ffa-d5a496ec4d0b", "inventoryUnitId": "77bd4c16-e1f0-426d-a98f-07b063df2d9c", "unitGroupId": "38b5454a-c3fe-452b-9754-61f9ead49e87", "salesUnitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "purchaseUnitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "valuationMethod": "movingAverage", "isBatch": true, "isSerial": true, "isInventory": true, "isSalesItem": true, "isPurchaseItem": true, "active": true, "notes": "<notes>", "externalItemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "brandName": "<brandName>", "imageAvatar": "<imageAvatar>", "eccnNumber": "<eccnNumber>", "htsCode": "<htsCode>", "countryOfOrigin": "<countryOfOrigin>", "minStockLevel": "19.99", "minimumTransactionQuantity": "19.99", "leadTime": "19.99", "standardCost": "19.99", "itemPurchasePrice": "19.99", "currency": "<currency>", "weight": "1.4", "weightUnit": "Kg", "grossWeight": "19.99", "netWeight": "19.99", "length": "19.99", "width": "19.99", "height": "19.99", "volumePerUnit": "19.99", "volumeUnit": "<volumeUnit>", "defaultAllocationMode": "<defaultAllocationMode>", "isPerishable": true, "shelfLifeDays": 0, "minShelfLifeDaysOnReceipt": 0, "minShelfLifeDaysOnDispatch": 0, "bestBeforeOffsetDays": 0, "storageTempMin": "19.99", "storageTempMax": "19.99" } ``` ## Response `200` Success Under `data` (createItemOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created item. | | `itemCode` | string | no | The item code of the created item. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `DUPLICATE_CODE` | 400 | | `UNKNOWN_VALUE` | 400 | | `TRACKING_MISMATCH` | 400 | | `ITEM_TYPE_NOT_STOCKABLE` | 400 | | `VALUATION_METHOD_UNAVAILABLE` | 400 | | `UNIT_NOT_IN_UNIT_GROUP` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, DUPLICATE_CODE, UNKNOWN_VALUE, TRACKING_MISMATCH, ITEM_TYPE_NOT_STOCKABLE, VALUATION_METHOD_UNAVAILABLE, UNIT_NOT_IN_UNIT_GROUP - `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 --- # Returns the item with the given identifier. `GET /inventory/items/{id}` Group: Inventory › Items · Operation ID: `getItem` Returns the item with the given identifier. POST inventory/items/search returns the same fields. The collection GET inventory/items omits standardCost, itemPurchasePrice and currency. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getItem) ## 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`. | | `id` | path | string | yes | The unique identifier for the item to return. | ## Response `200` Success Under `data` (getItemOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the item. | | `itemCode` | string | no | The item code, for example 'WIDGET-01'. Unique within the tenant. It is set when the item is created and cannot be changed. | | `itemName` | string | no | The display name of the item. | | `externalItemId` | string | no | Your own identifier for this item in an external system. | | `brandName` | string | no | The display name of the brand. | | `notes` | string | no | Free text on the item. | | `imageAvatar` | string | no | The URL of the item's avatar image. Absent when none is set. | | `itemTypeId` | string | no | The unique identifier of the item's type. It is set when the item is created and cannot be changed. See id on GET inventory/item-types/:id. | | `itemTypeName` | string | no | The display name of the item's type. | | `categoryId` | string | no | The unique identifier of the item's category. See id on GET inventory/item-categories/:id. | | `categoryName` | string | no | The display name of the item's category. | | `inventoryUnitId` | string | no | The unique identifier of the unit stock is counted in. Every quantity on an inventory level or transaction is in this unit. See id on GET inventory/unit-of-measures/:id. | | `inventoryUnitCode` | string | no | The code of the inventory unit. | | `salesUnitId` | string | no | The unique identifier of the unit used on sales documents. Absent when it is the same as the inventory unit. See id on GET inventory/unit-of-measures/:id. | | `salesUnitCode` | string | no | The code of the sales unit. | | `purchaseUnitId` | string | no | The unique identifier of the unit used on purchase documents. Absent when it is the same as the inventory unit. See id on GET inventory/unit-of-measures/:id. | | `purchaseUnitCode` | string | no | The code of the purchase unit. | | `unitGroupId` | string | no | The unique identifier of the unit of measure group that has the conversions between the item's units. See id on GET inventory/unit-of-measure-groups/:id. | | `unitGroupName` | string | no | The display name of the unit of measure group. | | `valuationMethod` | string | no | How the item's stock is valued. Possible values: 'movingAverage', 'batch', 'serials', 'none'. 'none' is for an item that is not stocked. 'fifo' can be returned but cannot be written, because it is under development. | | `isBatch` | boolean | no | Whether the item is tracked by batch. Receipts and shipments of it name batches. | | `isSerial` | boolean | no | Whether the item is tracked by serial number. Receipts and shipments of it name one serial per unit. | | `isInventory` | boolean | no | Whether stock levels are kept for the item. False for a service or charge item. | | `isSalesItem` | boolean | no | Whether the item can appear on sales documents. | | `isPurchaseItem` | boolean | no | Whether the item can appear on purchase documents. | | `active` | boolean | no | Whether the item can be selected on new documents. False means it is retired, and historical records keep it. | | `eccnNumber` | string | no | The Export Control Classification Number. | | `htsCode` | string | no | The Harmonized Tariff Schedule code. | | `countryOfOrigin` | string | no | The country the item originates from. | | `minStockLevel` | string (decimal) | no | The stock level below which the item should be reordered, in the inventory unit. | | `minimumTransactionQuantity` | string (decimal) | no | The smallest quantity any document can move. Absent when there is no restriction. | | `leadTime` | string (decimal) | no | The number of days between ordering the item and receiving it. | | `standardCost` | string (decimal) | no | The standard cost carried for planning, in the item's currency. It is not the value of the stock on hand. Read that from GET inventory/inventory-levels. | | `itemPurchasePrice` | string (decimal) | no | The expected purchase price, in the item's currency. | | `currency` | string | no | The currency of standardCost and itemPurchasePrice. Format ISO 4217, for example 'USD'. | | `weight` | string (decimal) | no | The weight of one item, in weightUnit. A landed cost Weight allocation fails rather than falling back when an item on the receipt has none. | | `weightUnit` | string | no | The unit weight is expressed in. Possible values: 'G', 'Kg', 'Lbs'. | | `grossWeight` | string (decimal) | no | The weight including packaging, in weightUnit. | | `netWeight` | string (decimal) | no | The weight excluding packaging, in weightUnit. | | `length` | string (decimal) | no | The length of one item. A landed cost Volume allocation needs length, width and height together, and fails when the set is incomplete. | | `width` | string (decimal) | no | The width of one item. | | `height` | string (decimal) | no | The height of one item. | | `volumePerUnit` | string (decimal) | no | The volume of one item, in volumeUnit. | | `volumeUnit` | string | no | The unit volumePerUnit is expressed in. Possible values: 'L'. | | `defaultAllocationMode` | string | no | The allocation mode a landed cost charge for this item falls back to when it names none. Possible values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. | | `isPerishable` | boolean | no | Whether the item expires. | | `shelfLifeDays` | number | no | The number of days the item keeps from production. | | `minShelfLifeDaysOnReceipt` | number | no | The least remaining shelf life a receipt of this item will accept. | | `minShelfLifeDaysOnDispatch` | number | no | The least remaining shelf life a dispatch of this item will accept. | | `bestBeforeOffsetDays` | number | no | The number of days between the expiry date and the best-before date printed on it. | | `storageTempMin` | string (decimal) | no | The lowest temperature the item can be stored at, in degrees Celsius. | | `storageTempMax` | string (decimal) | no | The highest temperature the item can be stored at, in degrees Celsius. | | `createdDate` | string (date-time) | no | The date and time the item was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the item record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Item `PATCH /inventory/items/{id}` Group: Inventory › Items · Operation ID: `updateItem` Updates the item with the given identifier. Only the fields in the request body change. Updates the item with the given identifier. Only the fields in the request body change. itemCode and itemTypeId cannot be changed and are not accepted here. Changing valuationMethod is checked against the tracking flags the item already has, not only the ones in this request, so switching an untracked item to 'batch' valuation fails. Setting active to false retires the item. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateItem) ## 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. | | `id` | path | string | yes | The unique identifier for the item to update. | ## Request body Required, `application/json`: updateItemInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `itemName` | string | no | The display name of the item. | | `categoryId` | string | no | The unique identifier of the item's category. See id on GET inventory/item-categories/:id. | | `inventoryUnitId` | string | no | The unique identifier of the unit stock is counted in. Every quantity on an inventory level or transaction will be in this unit. It must be one of the units in unitGroupId. See id on GET inventory/unit-of-measures/:id. | | `unitGroupId` | string | no | The unique identifier of the unit of measure group that has the conversions between the item's units. Moving the item to another group re-checks the units it keeps as well as any sent here, because their conversions live in the group, and a unit with no conversion fails with UNIT_NOT_IN_UNIT_GROUP. See id on GET inventory/unit-of-measure-groups/:id. | | `salesUnitId` | string | no | The unique identifier of the unit used on sales documents. It must be one of the units in unitGroupId. See id on GET inventory/unit-of-measures/:id. | | `purchaseUnitId` | string | no | The unique identifier of the unit used on purchase documents. It must be one of the units in unitGroupId. See id on GET inventory/unit-of-measures/:id. | | `valuationMethod` | string | no | How the item's stock is valued. Accepted values: 'movingAverage', 'batch', 'serials', 'none'. Use 'none' for an item that is not stocked. 'batch' requires isBatch, 'serials' requires isSerial, and 'none' requires isInventory false, and each contradiction fails with TRACKING_MISMATCH. Changing it is checked against the tracking flags the item already has, not only the ones in this request. 'fifo' is under development and fails with VALUATION_METHOD_UNAVAILABLE. | | `isBatch` | boolean | no | Whether the item is tracked by batch. | | `isSerial` | boolean | no | Whether the item is tracked by serial number. | | `isInventory` | boolean | no | Whether stock levels are kept for the item. Set false for a service or charge item. | | `isSalesItem` | boolean | no | Whether the item can appear on sales documents. | | `isPurchaseItem` | boolean | no | Whether the item can appear on purchase documents. | | `active` | boolean | no | Whether the item can be selected on new documents. Setting it false retires the item without deleting it. | | `notes` | string | no | Free text on the item. | | `externalItemId` | string | no | Your own identifier for this item in an external system. | | `brandName` | string | no | The display name of the brand. | | `imageAvatar` | string | no | The URL of the item's avatar image. | | `eccnNumber` | string | no | The Export Control Classification Number. | | `htsCode` | string | no | The Harmonized Tariff Schedule code. | | `countryOfOrigin` | string | no | The country the item originates from. | | `minStockLevel` | string (decimal) | no | The stock level below which the item should be reordered, in the inventory unit. | | `minimumTransactionQuantity` | string (decimal) | no | The smallest quantity any document can move. Omit for no restriction. Omitting it is not the same as sending 1. | | `leadTime` | string (decimal) | no | The number of days between ordering the item and receiving it. | | `standardCost` | string (decimal) | no | The standard cost carried for planning. State its currency in currency. | | `itemPurchasePrice` | string (decimal) | no | The expected purchase price. State its currency in currency. | | `currency` | string | no | The currency of standardCost and itemPurchasePrice. Required when either is given. Format ISO 4217, for example 'USD'. | | `weight` | string (decimal) | no | The weight of one item, in weightUnit. Set it if the item may be received under a landed cost that uses Weight allocation, because that mode fails rather than falling back when an item has no weight. | | `weightUnit` | string | no | The unit weight is expressed in. Accepted values: 'G', 'Kg', 'Lbs'. | | `grossWeight` | string (decimal) | no | The weight including packaging, in weightUnit. | | `netWeight` | string (decimal) | no | The weight excluding packaging, in weightUnit. | | `length` | string (decimal) | no | The length of one item. A landed cost Volume allocation needs length, width and height together, and fails when the set is incomplete. | | `width` | string (decimal) | no | The width of one item. | | `height` | string (decimal) | no | The height of one item. | | `volumePerUnit` | string (decimal) | no | The volume of one item, in volumeUnit. | | `volumeUnit` | string | no | The unit volumePerUnit is expressed in. Accepted values: 'L'. | | `defaultAllocationMode` | string | no | The allocation mode a landed cost charge billed as this item falls back to when it names none. Accepted values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. | | `isPerishable` | boolean | no | Whether the item expires. | | `shelfLifeDays` | number | no | The number of days the item keeps from production. | | `minShelfLifeDaysOnReceipt` | number | no | The least remaining shelf life a receipt of this item will accept. | | `minShelfLifeDaysOnDispatch` | number | no | The least remaining shelf life a dispatch of this item will accept. | | `bestBeforeOffsetDays` | number | no | The number of days between the expiry date and the best-before date printed on it. | | `storageTempMin` | string (decimal) | no | The lowest storage temperature, in degrees Celsius. | | `storageTempMax` | string (decimal) | no | The highest storage temperature, in degrees Celsius. | Example: ```json { "itemName": "<itemName>", "categoryId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "inventoryUnitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "unitGroupId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "salesUnitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "purchaseUnitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "valuationMethod": "<valuationMethod>", "isBatch": true, "isSerial": true, "isInventory": true, "isSalesItem": true, "isPurchaseItem": true, "active": true, "notes": "<notes>", "externalItemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "brandName": "<brandName>", "imageAvatar": "<imageAvatar>", "eccnNumber": "<eccnNumber>", "htsCode": "<htsCode>", "countryOfOrigin": "<countryOfOrigin>", "minStockLevel": "19.99", "minimumTransactionQuantity": "19.99", "leadTime": "19.99", "standardCost": "19.99", "itemPurchasePrice": "19.99", "currency": "<currency>", "weight": "1.6", "weightUnit": "Kg", "grossWeight": "19.99", "netWeight": "19.99", "length": "19.99", "width": "19.99", "height": "19.99", "volumePerUnit": "19.99", "volumeUnit": "<volumeUnit>", "defaultAllocationMode": "<defaultAllocationMode>", "isPerishable": true, "shelfLifeDays": 0, "minShelfLifeDaysOnReceipt": 0, "minShelfLifeDaysOnDispatch": 0, "bestBeforeOffsetDays": 0, "storageTempMin": "19.99", "storageTempMax": "19.99" } ``` ## Response `200` Success Under `data` (updateItemOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the item. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `DUPLICATE_CODE` | 400 | | `UNKNOWN_VALUE` | 400 | | `TRACKING_MISMATCH` | 400 | | `ITEM_TYPE_NOT_STOCKABLE` | 400 | | `VALUATION_METHOD_UNAVAILABLE` | 400 | | `UNIT_NOT_IN_UNIT_GROUP` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND, DUPLICATE_CODE, UNKNOWN_VALUE, TRACKING_MISMATCH, ITEM_TYPE_NOT_STOCKABLE, VALUATION_METHOD_UNAVAILABLE, UNIT_NOT_IN_UNIT_GROUP - `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 --- # Deletes the item with the given identifier. `DELETE /inventory/items/{id}` Group: Inventory › Items · Operation ID: `deleteItem` Deletes an item that has no stock and no history. An item with stock on hand or any inventory transaction against it fails with IN_USE. Setting active to false with PATCH inventory/items/:id is the non-destructive alternative: the item stops being selectable on new documents while every historical record keeps it. It does not release the item code, which stays reserved within the tenant. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteItem) ## 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. | | `id` | path | string | yes | The unique identifier for the item to delete. | ## Response `200` Success Under `data` (deleteItemOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the item. | | `itemCode` | string | no | The item code, for example 'WIDGET-01'. Unique within the tenant. It is set when the item is created and cannot be changed. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, IN_USE - `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 --- # Creates several items in one call. All or nothing. `POST /inventory/items/bulk` Group: Inventory › Items · Operation ID: `bulkCreateItems` Creates several items in one call, all or nothing. If any row is rejected, nothing is created and the error names the offending row's index. Rows are checked against each other as well as against stored items, so two rows claiming the same item code collide just as a row colliding with a stored one does. For per-row independence, call POST inventory/items once per row. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateItems) ## 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`: bulkCreateItemsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The items to create. All or nothing: if any row is invalid, the whole call fails and nothing is created. | | `items[].itemCode` | string | yes | The item code, for example 'WIDGET-01'. Must be unique within the tenant, including against retired items whose codes are never released. It cannot be changed after the item is created. | | `items[].itemName` | string | no | The display name of the item. | | `items[].itemTypeId` | string | yes | The unique identifier of the item's type. It cannot be changed after the item is created. See id on GET inventory/item-types/:id. | | `items[].categoryId` | string | yes | The unique identifier of the item's category. See id on GET inventory/item-categories/:id. | | `items[].inventoryUnitId` | string | yes | The unique identifier of the unit stock is counted in. Every quantity on an inventory level or transaction will be in this unit. It must be one of the units in unitGroupId. See id on GET inventory/unit-of-measures/:id. | | `items[].unitGroupId` | string | yes | The unique identifier of the unit of measure group that has the conversions between the item's units. inventoryUnitId, salesUnitId and purchaseUnitId must all be units in this group, and a unit outside it fails with UNIT_NOT_IN_UNIT_GROUP. See id on GET inventory/unit-of-measure-groups/:id. | | `items[].salesUnitId` | string | no | The unique identifier of the unit used on sales documents. Omit when it is the same as the inventory unit. It must be one of the units in unitGroupId. See id on GET inventory/unit-of-measures/:id. | | `items[].purchaseUnitId` | string | no | The unique identifier of the unit used on purchase documents. Omit when it is the same as the inventory unit. It must be one of the units in unitGroupId. See id on GET inventory/unit-of-measures/:id. | | `items[].valuationMethod` | string | no | How the item's stock is valued. Accepted values: 'movingAverage', 'batch', 'serials', 'none'. Use 'none' for an item that is not stocked. Defaults to 'movingAverage'. 'batch' requires isBatch, 'serials' requires isSerial, and 'none' requires isInventory false, and each contradiction fails with TRACKING_MISMATCH. 'fifo' is under development and fails with VALUATION_METHOD_UNAVAILABLE. | | `items[].isBatch` | boolean | no | Whether the item is tracked by batch. Defaults to false. | | `items[].isSerial` | boolean | no | Whether the item is tracked by serial number. Defaults to false. | | `items[].isInventory` | boolean | no | Whether stock levels are kept for the item. Defaults to true. Set false for a service or charge item. | | `items[].isSalesItem` | boolean | no | Whether the item can appear on sales documents. Defaults to true. | | `items[].isPurchaseItem` | boolean | no | Whether the item can appear on purchase documents. Defaults to true. | | `items[].active` | boolean | no | Whether the item can be selected on new documents. Defaults to true. Setting it false retires the item without deleting it. | | `items[].notes` | string | no | Free text on the item. | | `items[].externalItemId` | string | no | Your own identifier for this item in an external system. | | `items[].brandName` | string | no | The display name of the brand. | | `items[].imageAvatar` | string | no | The URL of the item's avatar image. | | `items[].eccnNumber` | string | no | The Export Control Classification Number. | | `items[].htsCode` | string | no | The Harmonized Tariff Schedule code. | | `items[].countryOfOrigin` | string | no | The country the item originates from. | | `items[].minStockLevel` | string (decimal) | no | The stock level below which the item should be reordered, in the inventory unit. | | `items[].minimumTransactionQuantity` | string (decimal) | no | The smallest quantity any document can move. Omit for no restriction. Omitting it is not the same as sending 1. | | `items[].leadTime` | string (decimal) | no | The number of days between ordering the item and receiving it. | | `items[].standardCost` | string (decimal) | no | The standard cost carried for planning. State its currency in currency. | | `items[].itemPurchasePrice` | string (decimal) | no | The expected purchase price. State its currency in currency. | | `items[].currency` | string | no | The currency of standardCost and itemPurchasePrice. Required when either is given. Format ISO 4217, for example 'USD'. | | `items[].weight` | string (decimal) | no | The weight of one item, in weightUnit. Set it if the item may be received under a landed cost that uses Weight allocation, because that mode fails rather than falling back when an item has no weight. | | `items[].weightUnit` | string | no | The unit weight is expressed in. Accepted values: 'G', 'Kg', 'Lbs'. | | `items[].grossWeight` | string (decimal) | no | The weight including packaging, in weightUnit. | | `items[].netWeight` | string (decimal) | no | The weight excluding packaging, in weightUnit. | | `items[].length` | string (decimal) | no | The length of one item. A landed cost Volume allocation needs length, width and height together, and fails when the set is incomplete. | | `items[].width` | string (decimal) | no | The width of one item. | | `items[].height` | string (decimal) | no | The height of one item. | | `items[].volumePerUnit` | string (decimal) | no | The volume of one item, in volumeUnit. | | `items[].volumeUnit` | string | no | The unit volumePerUnit is expressed in. Accepted values: 'L'. | | `items[].defaultAllocationMode` | string | no | The allocation mode a landed cost charge billed as this item falls back to when it names none. Accepted values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. | | `items[].isPerishable` | boolean | no | Whether the item expires. | | `items[].shelfLifeDays` | number | no | The number of days the item keeps from production. | | `items[].minShelfLifeDaysOnReceipt` | number | no | The least remaining shelf life a receipt of this item will accept. | | `items[].minShelfLifeDaysOnDispatch` | number | no | The least remaining shelf life a dispatch of this item will accept. | | `items[].bestBeforeOffsetDays` | number | no | The number of days between the expiry date and the best-before date printed on it. | | `items[].storageTempMin` | string (decimal) | no | The lowest storage temperature, in degrees Celsius. | | `items[].storageTempMax` | string (decimal) | no | The highest storage temperature, in degrees Celsius. | Example: ```json { "items": [ { "itemCode": "WIDGET-01", "itemName": "Standard widget", "itemTypeId": "66aea173-07b8-4c4e-bc4a-5c35dedfdc16", "categoryId": "6709e2bf-d6cf-4800-9ffa-d5a496ec4d0b", "inventoryUnitId": "77bd4c16-e1f0-426d-a98f-07b063df2d9c", "unitGroupId": "38b5454a-c3fe-452b-9754-61f9ead49e87" } ] } ``` ## Response `200` Success Under `data` (bulkCreateItemsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `created` | array of object | yes | The created items, in the same order as the input. | | `created[].id` | string | yes | The unique identifier for the created item. | | `created[].itemCode` | string | no | The item code of the created item. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `DUPLICATE_CODE` | 400 | | `UNKNOWN_VALUE` | 400 | | `TRACKING_MISMATCH` | 400 | | `ITEM_TYPE_NOT_STOCKABLE` | 400 | | `VALUATION_METHOD_UNAVAILABLE` | 400 | | `UNIT_NOT_IN_UNIT_GROUP` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, DUPLICATE_CODE, UNKNOWN_VALUE, TRACKING_MISMATCH, ITEM_TYPE_NOT_STOCKABLE, VALUATION_METHOD_UNAVAILABLE, UNIT_NOT_IN_UNIT_GROUP - `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 --- # Deletes several items in one call. All or nothing. `POST /inventory/items/bulk-delete` Group: Inventory › Items · Operation ID: `bulkDeleteItems` Deletes several items in one call, all or nothing. If any item still has stock or history, or any id matches no item, nothing is deleted. The error's index says which row, counting from zero. Delete one at a time with DELETE inventory/items/:id. The deleted items come back in input order. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteItems) ## 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`: bulkDeleteItemsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the items to delete. If any id is unknown, or its item still has stock or history, the whole call fails and nothing is deleted. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteItemsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `deleted` | array of object | yes | The deleted items, in the same order as the input. | | `deleted[].id` | string | yes | The unique identifier for the deleted item. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, IN_USE - `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 --- # Returns the items that match the given filters. `POST /inventory/items/search` Group: Inventory › Items · Operation ID: `searchItems` Returns the items that match the given filters. Every criterion is exact and criteria combine with AND. Send an empty filters object to return every item. missingWeight and missingDimensions find the items a landed cost Weight or Volume allocation would fail on, because those modes refuse the whole allocation rather than falling back. itemCode is unique, so filtering on it returns at most one item. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchItems) ## 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`. | ## Request body Required, `application/json`: searchItemsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. An item must match every filter you send. Send an empty object to return all items. | | `filters.itemCode` | string | no | Limits results to the item with this item code. The match is exact. Item codes are unique, so this matches at most one item. | | `filters.itemTypeId` | string | no | Limits results to items with this item type. See id on GET inventory/item-types/:id. | | `filters.categoryId` | string | no | Limits results to items in this category. See id on GET inventory/item-categories/:id. | | `filters.unitGroupId` | string | no | Limits results to items in this unit of measure group. See id on GET inventory/unit-of-measure-groups/:id. | | `filters.valuationMethod` | string | no | Limits results to items with this valuation method. Accepted values: 'movingAverage', 'batch', 'serials', 'none', 'fifo'. 'fifo' can be matched on read but cannot be written. | | `filters.isBatch` | boolean | no | Limits results to items with the given isBatch value. | | `filters.isSerial` | boolean | no | Limits results to items with the given isSerial value. | | `filters.isInventory` | boolean | no | Limits results to items with the given isInventory value. False finds service and charge items. | | `filters.isSalesItem` | boolean | no | Limits results to items with the given isSalesItem value. | | `filters.isPurchaseItem` | boolean | no | Limits results to items with the given isPurchaseItem value. | | `filters.isPerishable` | boolean | no | Limits results to items with the given isPerishable value. | | `filters.active` | boolean | no | Limits results to items with the given active value. Omit to return both. | | `filters.externalItemId` | string | no | Limits results to the item with this external identifier. The match is exact. | | `filters.brandName` | string | no | Limits results to items with this brand name. The match is exact. | | `filters.countryOfOrigin` | string | no | Limits results to items with this country of origin. The match is exact. | | `filters.missingWeight` | boolean | no | True to return only items that have no weight, the items a landed cost Weight allocation would fail on. | | `filters.missingDimensions` | boolean | no | True to return only items missing length, width or height, the items a landed cost Volume allocation would fail on. | | `filters.modifiedSince` | string (date-time) | no | Limits results to items changed on or after this time. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of items per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "missingWeight": true }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchItemsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The items on this page, oldest first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the item. | | `items[].itemCode` | string | no | The item code, for example 'WIDGET-01'. Unique within the tenant. It is set when the item is created and cannot be changed. | | `items[].itemName` | string | no | The display name of the item. | | `items[].externalItemId` | string | no | Your own identifier for this item in an external system. | | `items[].brandName` | string | no | The display name of the brand. | | `items[].notes` | string | no | Free text on the item. | | `items[].imageAvatar` | string | no | The URL of the item's avatar image. Absent when none is set. | | `items[].itemTypeId` | string | no | The unique identifier of the item's type. It is set when the item is created and cannot be changed. See id on GET inventory/item-types/:id. | | `items[].itemTypeName` | string | no | The display name of the item's type. | | `items[].categoryId` | string | no | The unique identifier of the item's category. See id on GET inventory/item-categories/:id. | | `items[].categoryName` | string | no | The display name of the item's category. | | `items[].inventoryUnitId` | string | no | The unique identifier of the unit stock is counted in. Every quantity on an inventory level or transaction is in this unit. See id on GET inventory/unit-of-measures/:id. | | `items[].inventoryUnitCode` | string | no | The code of the inventory unit. | | `items[].salesUnitId` | string | no | The unique identifier of the unit used on sales documents. Absent when it is the same as the inventory unit. See id on GET inventory/unit-of-measures/:id. | | `items[].salesUnitCode` | string | no | The code of the sales unit. | | `items[].purchaseUnitId` | string | no | The unique identifier of the unit used on purchase documents. Absent when it is the same as the inventory unit. See id on GET inventory/unit-of-measures/:id. | | `items[].purchaseUnitCode` | string | no | The code of the purchase unit. | | `items[].unitGroupId` | string | no | The unique identifier of the unit of measure group that has the conversions between the item's units. See id on GET inventory/unit-of-measure-groups/:id. | | `items[].unitGroupName` | string | no | The display name of the unit of measure group. | | `items[].valuationMethod` | string | no | How the item's stock is valued. Possible values: 'movingAverage', 'batch', 'serials', 'none'. 'none' is for an item that is not stocked. 'fifo' can be returned but cannot be written, because it is under development. | | `items[].isBatch` | boolean | no | Whether the item is tracked by batch. Receipts and shipments of it name batches. | | `items[].isSerial` | boolean | no | Whether the item is tracked by serial number. Receipts and shipments of it name one serial per unit. | | `items[].isInventory` | boolean | no | Whether stock levels are kept for the item. False for a service or charge item. | | `items[].isSalesItem` | boolean | no | Whether the item can appear on sales documents. | | `items[].isPurchaseItem` | boolean | no | Whether the item can appear on purchase documents. | | `items[].active` | boolean | no | Whether the item can be selected on new documents. False means it is retired, and historical records keep it. | | `items[].eccnNumber` | string | no | The Export Control Classification Number. | | `items[].htsCode` | string | no | The Harmonized Tariff Schedule code. | | `items[].countryOfOrigin` | string | no | The country the item originates from. | | `items[].minStockLevel` | string (decimal) | no | The stock level below which the item should be reordered, in the inventory unit. | | `items[].minimumTransactionQuantity` | string (decimal) | no | The smallest quantity any document can move. Absent when there is no restriction. | | `items[].leadTime` | string (decimal) | no | The number of days between ordering the item and receiving it. | | `items[].standardCost` | string (decimal) | no | The standard cost carried for planning, in the item's currency. It is not the value of the stock on hand. Read that from GET inventory/inventory-levels. | | `items[].itemPurchasePrice` | string (decimal) | no | The expected purchase price, in the item's currency. | | `items[].currency` | string | no | The currency of standardCost and itemPurchasePrice. Format ISO 4217, for example 'USD'. | | `items[].weight` | string (decimal) | no | The weight of one item, in weightUnit. A landed cost Weight allocation fails rather than falling back when an item on the receipt has none. | | `items[].weightUnit` | string | no | The unit weight is expressed in. Possible values: 'G', 'Kg', 'Lbs'. | | `items[].grossWeight` | string (decimal) | no | The weight including packaging, in weightUnit. | | `items[].netWeight` | string (decimal) | no | The weight excluding packaging, in weightUnit. | | `items[].length` | string (decimal) | no | The length of one item. A landed cost Volume allocation needs length, width and height together, and fails when the set is incomplete. | | `items[].width` | string (decimal) | no | The width of one item. | | `items[].height` | string (decimal) | no | The height of one item. | | `items[].volumePerUnit` | string (decimal) | no | The volume of one item, in volumeUnit. | | `items[].volumeUnit` | string | no | The unit volumePerUnit is expressed in. Possible values: 'L'. | | `items[].defaultAllocationMode` | string | no | The allocation mode a landed cost charge for this item falls back to when it names none. Possible values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. | | `items[].isPerishable` | boolean | no | Whether the item expires. | | `items[].shelfLifeDays` | number | no | The number of days the item keeps from production. | | `items[].minShelfLifeDaysOnReceipt` | number | no | The least remaining shelf life a receipt of this item will accept. | | `items[].minShelfLifeDaysOnDispatch` | number | no | The least remaining shelf life a dispatch of this item will accept. | | `items[].bestBeforeOffsetDays` | number | no | The number of days between the expiry date and the best-before date printed on it. | | `items[].storageTempMin` | string (decimal) | no | The lowest temperature the item can be stored at, in degrees Celsius. | | `items[].storageTempMax` | string (decimal) | no | The highest temperature the item can be stored at, in degrees Celsius. | | `items[].createdDate` | string (date-time) | no | The date and time the item was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the item record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | no | True when more items remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of landed cost items. `GET /inventory/landed-cost-items` Group: Inventory › Landed Cost Items · Operation ID: `landedCostItems` The landed cost item collection, everything configured as capitalizable. Read this before building a landed cost. GET inventory/landed-costs/vendor-bill-lines returns only the charges that match a row here, so an empty collection explains an empty selector. [Open in the interactive reference](https://docs.api.lensing.ai/?op=landedCostItems) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `itemId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `itemCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `accountId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `allocationMode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `costingElementTypeId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `active` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, itemCode, accountNumber, allocationMode, active, lastModifiedDate - Selectable: id, itemId, itemCode, itemName, accountId, accountNumber, accountName, allocationMode, costingElementTypeId, costingElementTypeCode, costingElementTypeName, active, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of landedCostItemsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the landed cost item. | | `itemId` | string or null | no | The service item a vendor bill line must reference to be caught by this row. Absent when the row selects by account instead. See id on GET inventory/items/:id. | | `itemCode` | string or null | no | The short code of that item. | | `itemName` | string or null | no | The name of that item. | | `accountId` | string or null | no | The GL account a vendor bill line must post to to be caught by this row. Absent when the row selects by item instead. See id on GET accounting/accounts/:id. | | `accountNumber` | string or null | no | The number of that account. | | `accountName` | string or null | no | The name of that account. | | `allocationMode` | string or null | no | How a charge caught by this row is spread over the receipt lines it lands on. Possible values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. 'Weight' and 'Volume' require every item on the receipt to carry the measurement, and a missing one fails the allocation rather than falling back to an even split. A landed cost may override this per vendor bill line. | | `costingElementTypeId` | string or null | no | The costing element type carried onto the inventory revaluation the landed cost commit raises. Absent when the row sets none. There is no endpoint for it. | | `costingElementTypeCode` | string or null | no | The code of that costing element type. | | `costingElementTypeName` | string or null | no | The name of that costing element type. | | `active` | boolean or null | no | Whether the row is in force. When false, the charges it used to catch are no longer eligible, and existing landed costs keep theirs. | | `createdDate` | string (date-time) or null | no | The date and time the row was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the row last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Landed Cost Item `POST /inventory/landed-cost-items` Group: Inventory › Landed Cost Items · Operation ID: `createLandedCostItem` Creates a landed cost item. The item or account it selects on must not already be configured. Configures a charge as a landed cost item. From now on, approved vendor bill lines matching this row's selector may be capitalized onto received stock. Name a selector. itemId catches a charge billed as a service item, and accountId catches one coded straight to an account. At least one is required. The underlying record allows a row with neither, and such a row would match nothing ever after. One selector, one row. Configuring the same item or account twice is refused with DUPLICATE, because two matching rows would give one vendor bill line two allocation modes, and which one won would depend on query order. Change the existing row instead. The response is the new landed cost item's id. Read it back with GET inventory/landed-cost-items/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createLandedCostItem) ## 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`: createLandedCostItemInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `itemId` | string | no | The service item to select on, a freight, handling or insurance item, not the goods themselves. Required unless accountId is given, and both may be given. See id on GET inventory/items/:id. | | `accountId` | string | no | The GL account to select on. Required unless itemId is given, and both may be given. See id on GET accounting/accounts/:id. | | `allocationMode` | string | yes | How a charge caught by this row is spread over the receipt lines it lands on. Accepted values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. 'Weight' and 'Volume' require every item on the receipt to carry the measurement, and a missing weight, or an incomplete length, width or height, fails the allocation rather than falling back to an even split. | | `costingElementTypeId` | string | no | The costing element type carried onto the inventory revaluation the landed cost commit raises, so the added cost lands in the right bucket of the cost breakdown. There is no endpoint for it. | | `active` | boolean | no | Whether the row is in force. Defaults to true. | Example: ```json { "itemId": "3d81e07c-95a2-4f63-b1c8-27e5a94d0b36", "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "allocationMode": "Weight", "costingElementTypeId": "c4a90e21-7b35-4d18-9e02-6f13a85c7d40", "active": true } ``` ## Response `200` Success Under `data` (createLandedCostItemOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created landed cost item. | ## 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 | | --- | --- | | `NO_SELECTOR` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `UNKNOWN_ALLOCATION_MODE` | 400 | | `DUPLICATE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NO_SELECTOR, REFERENCE_NOT_FOUND, UNKNOWN_ALLOCATION_MODE, DUPLICATE - `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 --- # Returns the landed cost item with the given identifier. `GET /inventory/landed-cost-items/{id}` Group: Inventory › Landed Cost Items · Operation ID: `getLandedCostItem` Returns one landed cost item whole. A row names a selector, itemId, accountId, or both, and an allocation mode. The selector is what an approved vendor bill line is matched against to decide whether it may be capitalized onto received stock. This is the same item schema the collection and the search endpoint return. The codes and names beside each id, itemCode, accountNumber, costingElementTypeName, are resolved at read time and are never stored on the row, so they follow the record they came from. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getLandedCostItem) ## 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`. | | `id` | path | string | yes | The unique identifier for the landed cost item to return. | ## Response `200` Success Under `data` (getLandedCostItemOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the landed cost item. | | `itemId` | string | no | The service item a vendor bill line must reference to be caught by this row. Absent when the row selects by account instead. See id on GET inventory/items/:id. | | `itemCode` | string | no | The short code of that item. | | `itemName` | string | no | The name of that item. | | `accountId` | string | no | The GL account a vendor bill line must post to to be caught by this row. Absent when the row selects by item instead. See id on GET accounting/accounts/:id. | | `accountNumber` | string | no | The number of that account. | | `accountName` | string | no | The name of that account. | | `allocationMode` | string | no | How a charge caught by this row is spread over the receipt lines it lands on. Possible values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. 'Weight' and 'Volume' require every item on the receipt to carry the measurement, and a missing one fails the allocation rather than falling back to an even split. A landed cost may override this per vendor bill line. | | `costingElementTypeId` | string | no | The costing element type carried onto the inventory revaluation the landed cost commit raises. Absent when the row sets none. There is no endpoint for it. | | `costingElementTypeCode` | string | no | The code of that costing element type. | | `costingElementTypeName` | string | no | The name of that costing element type. | | `active` | boolean | no | Whether the row is in force. When false, the charges it used to catch are no longer eligible, and existing landed costs keep theirs. | | `createdDate` | string (date-time) | no | The date and time the row was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the row last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Landed Cost Item `PATCH /inventory/landed-cost-items/{id}` Group: Inventory › Landed Cost Items · Operation ID: `updateLandedCostItem` Updates the landed cost item with the given identifier. Only the fields in the request body change. Updates part of a landed cost item. Only the fields in the request body change, and anything omitted is left as it was. Changing allocationMode changes the default offered for future charges. Landed costs already created keep the mode they were built with, and committed ones keep their allocation outright. A selector cannot be removed here. Passing itemId or accountId replaces it, and there is no way to end up with a row that selects on neither. Set active to false to take a landed cost item out of force without deleting it. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateLandedCostItem) ## 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. | | `id` | path | string | yes | The unique identifier for the landed cost item to update. | ## Request body Required, `application/json`: updateLandedCostItemInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `itemId` | string | no | A replacement service item to select on. Omit to leave the selector as it is. There is no way to clear it. See id on GET inventory/items/:id. | | `accountId` | string | no | A replacement GL account to select on. Omit to leave the selector as it is. There is no way to clear it. See id on GET accounting/accounts/:id. | | `allocationMode` | string | no | A replacement allocation mode. Accepted values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. It changes the default offered for future charges only. | | `costingElementTypeId` | string | no | A replacement costing element type. There is no endpoint for it. | | `active` | boolean | no | Whether the row is in force. Set it to false to retire a landed cost item without deleting it. | Example: ```json { "itemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "allocationMode": "Quantity", "costingElementTypeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "active": true } ``` ## Response `200` Success Under `data` (updateLandedCostItemOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the landed cost item. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `UNKNOWN_ALLOCATION_MODE` | 400 | | `DUPLICATE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND, UNKNOWN_ALLOCATION_MODE, DUPLICATE - `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 --- # Deletes the landed cost item with the given identifier. `DELETE /inventory/landed-cost-items/{id}` Group: Inventory › Landed Cost Items · Operation ID: `deleteLandedCostItem` Deletes a landed cost item. Charges its selector used to catch stop being eligible, so they no longer appear in GET inventory/landed-costs/vendor-bill-lines. Landed costs are not touched. Drafts keep the vendor bill lines already chosen, and committed ones keep their allocation outright, because this row is consulted when charges are offered, not when they are posted. Nothing here reverses a capitalization. Setting active to false through PATCH is the reversible alternative, and it reads the same way to the selector. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteLandedCostItem) ## 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. | | `id` | path | string | yes | The unique identifier for the landed cost item to delete. | ## Response `200` Success Under `data` (deleteLandedCostItemOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted landed cost item. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Creates several landed cost items in one call. `POST /inventory/landed-cost-items/bulk` Group: Inventory › Landed Cost Items · Operation ID: `bulkCreateLandedCostItems` Configures several charges as landed cost items in one all-or-nothing call. This is the usual way to set the collection up, since it is normally populated once rather than a row at a time. If any row is rejected, nothing is created, and the error names the offending row's index. Rows are checked against each other as well as against what already exists, so two rows in one batch selecting the same item collide exactly as a row colliding with a stored one does. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateLandedCostItems) ## 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`: bulkCreateLandedCostItemsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `landedCostItems` | array of object | yes | The landed cost items to create. All or nothing: if any row is invalid, nothing is created. | | `landedCostItems[].itemId` | string | no | The service item to select on, a freight, handling or insurance item, not the goods themselves. Required unless accountId is given, and both may be given. See id on GET inventory/items/:id. | | `landedCostItems[].accountId` | string | no | The GL account to select on. Required unless itemId is given, and both may be given. See id on GET accounting/accounts/:id. | | `landedCostItems[].allocationMode` | string | yes | How a charge caught by this row is spread over the receipt lines it lands on. Accepted values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. 'Weight' and 'Volume' require every item on the receipt to carry the measurement, and a missing weight, or an incomplete length, width or height, fails the allocation rather than falling back to an even split. | | `landedCostItems[].costingElementTypeId` | string | no | The costing element type carried onto the inventory revaluation the landed cost commit raises, so the added cost lands in the right bucket of the cost breakdown. There is no endpoint for it. | | `landedCostItems[].active` | boolean | no | Whether the row is in force. Defaults to true. | Example: ```json { "landedCostItems": [ { "itemId": "3d81e07c-95a2-4f63-b1c8-27e5a94d0b36", "allocationMode": "Weight" }, { "accountId": "7e21b508-4a93-4c67-b0d5-19f6a3e84c72", "allocationMode": "LineTotal" } ] } ``` ## Response `200` Success Under `data` (bulkCreateLandedCostItemsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `created` | array of object | yes | The created landed cost items, in input order. Match them to inputs by index. | | `created[].id` | string | yes | The unique identifier for the created landed cost item. | ## 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 | | --- | --- | | `NO_SELECTOR` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `UNKNOWN_ALLOCATION_MODE` | 400 | | `DUPLICATE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NO_SELECTOR, REFERENCE_NOT_FOUND, UNKNOWN_ALLOCATION_MODE, DUPLICATE - `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 --- # Deletes several landed cost items in one call. `POST /inventory/landed-cost-items/bulk-delete` Group: Inventory › Landed Cost Items · Operation ID: `bulkDeleteLandedCostItems` Deletes several landed cost items in one call. All or nothing: if any id matches no landed cost item, nothing is deleted, and index on the error says which, counting from zero. Delete a single landed cost item with DELETE inventory/landed-cost-items/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteLandedCostItems) ## 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`: bulkDeleteLandedCostItemsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the landed cost items to delete. All or nothing: if any id is unknown, nothing is deleted. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteLandedCostItemsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `deleted` | array of object | yes | The deleted landed cost items, in input order. Match them to inputs by index. | | `deleted[].id` | string | yes | The unique identifier for the deleted landed cost item. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Returns the landed cost items that match the given filters. `POST /inventory/landed-cost-items/search` Group: Inventory › Landed Cost Items · Operation ID: `searchLandedCostItems` Searches landed cost items by exact criteria in a request body, returning the same item schema as get and list. Every criterion is an exact match and they combine with AND. An empty filters object returns everything, page by page. There is no substring or free-text matching on any endpoint of this resource. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchLandedCostItems) ## 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`. | ## Request body Required, `application/json`: searchLandedCostItemsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The search criteria, combined with AND. An empty object returns every landed cost item, page by page. | | `filters.itemId` | string | no | Limits results to landed cost items that select on this service item. An id that is not an item matches nothing. | | `filters.accountId` | string | no | Limits results to landed cost items that select on this GL account. An id that is not an account matches nothing. | | `filters.allocationMode` | string | no | Limits results to landed cost items with this allocation mode. Accepted values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. | | `filters.costingElementTypeId` | string | no | Limits results to landed cost items with this costing element type. | | `filters.active` | boolean | no | Limits results to landed cost items that are in force when true, or out of force when false. Omit for both. | | `filters.modifiedSince` | string (date-time) | no | Limits results to landed cost items changed on or after this time, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of landed cost items per page, between 1 and 200. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "allocationMode": "Weight", "active": true }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchLandedCostItemsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The landed cost items that match, oldest first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the landed cost item. | | `items[].itemId` | string | no | The service item a vendor bill line must reference to be caught by this row. Absent when the row selects by account instead. See id on GET inventory/items/:id. | | `items[].itemCode` | string | no | The short code of that item. | | `items[].itemName` | string | no | The name of that item. | | `items[].accountId` | string | no | The GL account a vendor bill line must post to to be caught by this row. Absent when the row selects by item instead. See id on GET accounting/accounts/:id. | | `items[].accountNumber` | string | no | The number of that account. | | `items[].accountName` | string | no | The name of that account. | | `items[].allocationMode` | string | no | How a charge caught by this row is spread over the receipt lines it lands on. Possible values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. 'Weight' and 'Volume' require every item on the receipt to carry the measurement, and a missing one fails the allocation rather than falling back to an even split. A landed cost may override this per vendor bill line. | | `items[].costingElementTypeId` | string | no | The costing element type carried onto the inventory revaluation the landed cost commit raises. Absent when the row sets none. There is no endpoint for it. | | `items[].costingElementTypeCode` | string | no | The code of that costing element type. | | `items[].costingElementTypeName` | string | no | The name of that costing element type. | | `items[].active` | boolean | no | Whether the row is in force. When false, the charges it used to catch are no longer eligible, and existing landed costs keep theirs. | | `items[].createdDate` | string (date-time) | no | The date and time the row was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the row last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | no | Whether more results remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Landed Costs `GET /inventory/landed-costs` Group: Inventory › Landed Costs · Operation ID: `landedCosts` Returns a list of landed costs. This endpoint returns the header fields only. Use POST inventory/landed-costs/search to also read the receipts, charges and allocation. The landed cost collection. This endpoint returns the header fields only. The receipts, the charges and the allocation lines are arrays of objects, which the collection cannot express, so read one landed cost with GET inventory/landed-costs/:id or search with POST inventory/landed-costs/search, both of which return the whole thing. The questions about a landed cost's contents belong to the search endpoint: which landed costs cover a given receipt, draw on a given vendor bill, or touch a given item. None of them can be asked here. [Open in the interactive reference](https://docs.api.lensing.ai/?op=landedCosts) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `active` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, documentNumber, documentStatus, documentDate, entityId, lastModifiedDate - Selectable: id, documentNumber, documentStatus, documentDate, entityId, entityName, notes, active, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of landedCostsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the landed cost. | | `documentNumber` | string or null | no | The landed cost number, for example 'LC-1007'. Assigned when the landed cost is created. | | `documentStatus` | string or null | no | The status of the landed cost. It is DRAFT while it is being built and COMMITTED once you commit it with POST inventory/landed-costs/:id/commit. | | `documentDate` | string (date) or null | no | The date of the landed cost. Format YYYY-MM-DD. | | `entityId` | string or null | no | The legal entity the landed cost belongs to. It decides the functional currency that every amount is in. | | `entityName` | string or null | no | The name of that legal entity. | | `notes` | string or null | no | Free text on the landed cost. | | `active` | boolean or null | no | Whether the record is active. | | `createdDate` | string (date-time) or null | no | The date and time the landed cost was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the landed cost last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Landed Cost `POST /inventory/landed-costs` Group: Inventory › Landed Costs · Operation ID: `createLandedCost` Creates a landed cost as a draft. Nothing is capitalized until you commit it. Creates a landed cost as a draft over committed receipts and approved vendor bill charges. Nothing is capitalized until you commit it. Read GET inventory/landed-costs/vendor-bill-lines?entityId=<id> first. Its id is a charge's chargeId, its functionalLineTotal is what to send as amount, and its allocationMode is what to pass back. Without it you cannot build the payload. Send only the charge line, never the vendor bill with it. The vendor bill is derived from the line. The allocation is computed as part of the create. Read the landed cost back and allocationLines says what each receipt line's unit cost becomes. That is the preview, and it is always current because it is recomputed on every write. Every amount is in the entity's functional currency. Nothing converts them, so the selector's functionalLineTotal is the right figure and the bill's own lineTotal is not. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createLandedCost) ## 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`: createLandedCostInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `entityId` | string | yes | The legal entity. It decides the functional currency that every amount on the landed cost is in, and it cannot be changed after create. | | `documentDate` | string (date) | no | The date of the landed cost. Use format YYYY-MM-DD. Defaults to today. | | `notes` | string | no | Free text on the landed cost. | | `receipts` | array of object | yes | The receipts whose stock the charges are capitalized onto. At least one, and every one must be committed. | | `receipts[].receiptId` | string | yes | A committed receipt. See id on GET inventory/receipts/:id. | | `receipts[].selectedLineIds` | array of string | no | The receipt lines to allocate onto, by their id under lines on GET inventory/receipts/:id. Omit to use all of them, which is the usual case. Every id must belong to this receipt, and an id from another one fails with REFERENCE_NOT_FOUND. | | `charges` | array of object | yes | The charges to capitalize, read from GET inventory/landed-costs/vendor-bill-lines. At least one, and every one from an approved vendor bill. | | `charges[].chargeId` | string | yes | The vendor bill line the charge comes from, by its id on GET inventory/landed-costs/vendor-bill-lines. The vendor bill is derived from it, so there is no second id to send. | | `charges[].allocationMode` | string | no | How to spread this charge over the receipt lines. Accepted values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. Pass back what GET inventory/landed-costs/vendor-bill-lines returned. Omitting it does not fall through to the landed cost item configuration. It falls through to the item's own default, which may be a different mode. | | `charges[].amount` | string (decimal) | yes | How much of the charge to capitalize, in the entity's functional currency. Send functionalLineTotal from GET inventory/landed-costs/vendor-bill-lines, less anything already allocated, not the bill's own lineTotal. Nothing converts this amount. | | `charges[].isSelected` | boolean | no | Whether to include the charge. Defaults to true. A charge set to false is kept on the landed cost and allocated nothing. | Example: ```json { "entityId": "7039b992-696a-40c7-a181-51759304ef83", "documentDate": "2026-01-15", "notes": "<notes>", "receipts": [ { "receiptId": "b1a7c930-5e46-4f82-8d05-71c3e9a24f68" } ], "charges": [ { "chargeId": "9c47e1b8-2a63-4d05-b7f9-04e28d5a1c37", "allocationMode": "Quantity", "amount": "240" } ] } ``` ## Response `200` Success Under `data` (createLandedCostOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created landed cost. | | `documentNumber` | string | no | The landed cost number, for example 'LC-1007'. Assigned when the landed cost is created. | | `documentStatus` | string | yes | Always DRAFT. Nothing is capitalized until you commit the landed cost. | | `allocationLineCount` | number | no | The number of allocation lines, one per selected receipt line. Zero means the charges landed on nothing, which is worth checking before you commit. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `NO_RECEIPTS` | 400 | | `NO_CHARGES` | 400 | | `RECEIPT_NOT_COMMITTED` | 400 | | `VENDOR_BILL_NOT_APPROVED` | 400 | | `INVALID_AMOUNT` | 400 | | `UNKNOWN_ALLOCATION_MODE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, NO_RECEIPTS, NO_CHARGES, RECEIPT_NOT_COMMITTED, VENDOR_BILL_NOT_APPROVED, INVALID_AMOUNT, UNKNOWN_ALLOCATION_MODE - `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 --- # Returns the landed cost with the given identifier. `GET /inventory/landed-costs/{id}` Group: Inventory › Landed Costs · Operation ID: `getLandedCost` Returns one landed cost whole: the header, the receipts in scope with their lines, the charges being capitalized, and the allocation computed from them. allocationLines is the answer to the question the landed cost exists to ask, what each receipt line's unit cost becomes. It is recomputed on every create and update, so what you read is always current. There is no separate preview. On a committed landed cost, each allocation line also names the inventory revaluations it produced, under revaluationDocuments. Those are the documents that moved the value, and they are what a reversal would have to address. A draft's are empty. This is the same item schema the search endpoint returns. The collection endpoint returns the header fields only. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getLandedCost) ## 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`. | | `id` | path | string | yes | The unique identifier for the landed cost to return. | ## Response `200` Success Under `data` (getLandedCostOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the landed cost. | | `documentNumber` | string | no | The landed cost number, for example 'LC-1007'. Assigned when the landed cost is created. | | `documentStatus` | string | yes | The status of the landed cost. It is DRAFT while it is being built and COMMITTED once you commit it with POST inventory/landed-costs/:id/commit. | | `documentDate` | string (date) | no | The date of the landed cost. Format YYYY-MM-DD. | | `entityId` | string | no | The legal entity the landed cost belongs to. It decides the functional currency that every amount is in. | | `entityName` | string | no | The name of that legal entity. | | `currency` | string | no | The currency of the landed cost's amounts. Format ISO 4217, for example 'GBP'. This is the entity's functional currency, and nothing converts the amounts. | | `notes` | string | no | Free text on the landed cost. | | `totalChargeAmount` | string (decimal) | no | The total of the selected charges, in the landed cost's currency. | | `receiptCount` | number | no | The number of receipts in scope. | | `chargeCount` | number | no | The number of charges on the landed cost. | | `allocationLineCount` | number | no | The number of allocation lines, one per selected receipt line. | | `createdDate` | string (date-time) | no | The date and time the landed cost was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the landed cost last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `receipts` | array of object | no | The receipts whose stock the charges are capitalized onto, each with the lines in scope. | | `receipts[].id` | string | yes | The receipt this scope entry is for. See id on GET inventory/receipts/:id. | | `receipts[].receiptNumber` | string | no | The receipt's number. | | `receipts[].documentDate` | string (date) | no | The date of the receipt. Format YYYY-MM-DD. | | `receipts[].documentStatus` | string | no | The receipt's status. Always COMMITTED, because a landed cost cannot name a receipt that has not been committed. | | `receipts[].lines` | array of object | no | Every line of that receipt, each showing whether this landed cost allocates onto it. A line that is not selected takes none of the charge. | | `receipts[].lines[].id` | string | yes | The receipt line. This is what selectedLineIds names on create and update. See id under lines on GET inventory/receipts/:id. | | `receipts[].lines[].isSelected` | boolean | no | Whether this landed cost allocates onto the line. | | `receipts[].lines[].itemId` | string | no | The item received on the line. | | `receipts[].lines[].itemCode` | string | no | The short code of that item. | | `receipts[].lines[].itemName` | string | no | The name of that item. | | `receipts[].lines[].locationId` | string | no | The warehouse it was received into. | | `receipts[].lines[].quantity` | string (decimal) | no | The quantity received, in the line's unit. | | `receipts[].lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `receipts[].lines[].unitPrice` | string (decimal) | no | The price the line was received at, per unit, in the landed cost's currency. | | `receipts[].lines[].lineTotal` | string (decimal) | no | The total for the line, in the landed cost's currency. | | `charges` | array of object | no | The charges being capitalized, one per vendor bill line. | | `charges[].id` | string | yes | The vendor bill line the charge comes from. See id on GET inventory/landed-costs/vendor-bill-lines. | | `charges[].vendorBillId` | string | no | The vendor bill the charge belongs to. Derived from the line, never supplied. See id on GET accounting/vendor-bills/:id. | | `charges[].vendorBillNumber` | string | no | The vendor bill's number. | | `charges[].itemId` | string | no | The item the charge is billed as, where it is billed as one. | | `charges[].itemCode` | string | no | The short code of that item. | | `charges[].itemName` | string | no | The name of that item. | | `charges[].allocationMode` | string | no | How this charge is spread over the receipt lines. Possible values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. | | `charges[].amount` | string (decimal) | no | The amount of the charge this landed cost takes, in the landed cost's currency. | | `charges[].allocatedAmount` | string (decimal) | no | The amount of the charge that has actually been allocated. Filled in when you commit the landed cost. | | `charges[].isSelected` | boolean | no | Whether this charge is included. An excluded charge is kept on the landed cost and allocated nothing. | | `allocationLines` | array of object | no | The computed allocation: one line per selected receipt line, saying what its unit cost becomes. Recomputed on every write, so it is always current. | | `allocationLines[].id` | string | yes | The unique identifier for the allocation line within the landed cost. | | `allocationLines[].receiptId` | string | no | The receipt this allocation line belongs to. See id on GET inventory/receipts/:id. | | `allocationLines[].receiptLineId` | string | no | The receipt line this allocation lands on. See id under lines on GET inventory/receipts/:id. | | `allocationLines[].itemId` | string | no | The item. | | `allocationLines[].itemCode` | string | no | The short code of that item. | | `allocationLines[].itemName` | string | no | The name of that item. | | `allocationLines[].locationId` | string | no | The warehouse. | | `allocationLines[].locationCode` | string | no | The short code of that warehouse. | | `allocationLines[].quantity` | string (decimal) | no | The quantity the allocation is spread over. | | `allocationLines[].currentUnitCost` | string (decimal) | no | The unit cost of the stock before the landed cost commits. | | `allocationLines[].additionalCostAmount` | string (decimal) | no | The share of the charges this line takes, in total, not per unit. | | `allocationLines[].newUnitCost` | string (decimal) | no | The unit cost of the stock after the landed cost commits. | | `allocationLines[].revaluationDocuments` | array of object | no | The inventory revaluations this allocation line produced. An empty array on a draft, and more than one where the charges post to different expense accounts. | | `allocationLines[].revaluationDocuments[].id` | string | yes | The inventory revaluation. See id on GET inventory/inventory-revaluations/:id. | | `allocationLines[].revaluationDocuments[].documentNumber` | string | no | The revaluation's number. | | `allocationLines[].revaluationDocuments[].documentStatus` | string | no | The revaluation's status. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Landed Cost `PATCH /inventory/landed-costs/{id}` Group: Inventory › Landed Costs · Operation ID: `updateLandedCost` Updates the landed cost with the given identifier. Only the fields in the request body change. Only a draft can be changed. Changes a draft landed cost. A committed one cannot be changed, because it has already revalued the stock. receipts and charges are replaced wholesale, not merged. There are no createLines, updateLines or deleteLines arrays, because neither is a line with an identity of its own. A receipt selection points at another document and a charge points at another document's vendor bill line, so there is nothing stable to address a partial update to. Send the whole set you want, or omit the field to leave it alone. The allocation is recomputed whenever either set changes, so reading the landed cost back after a change shows the new unit costs. The entity cannot be changed. Every amount on the landed cost is in its functional currency, so changing the entity would reinterpret every charge at once. Delete the landed cost and create another instead. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateLandedCost) ## 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. | | `id` | path | string | yes | The unique identifier for the landed cost to update. | ## Request body Required, `application/json`: updateLandedCostInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `documentDate` | string (date) | no | The date of the landed cost. Use format YYYY-MM-DD. | | `notes` | string | no | Free text on the landed cost. | | `receipts` | array of object | no | The receipts whose stock the charges are capitalized onto. At least one, and every one must be committed. Omit to leave the current set. To change it, send the whole set, because it replaces rather than merges. | | `receipts[].receiptId` | string | yes | A committed receipt. See id on GET inventory/receipts/:id. | | `receipts[].selectedLineIds` | array of string | no | The receipt lines to allocate onto, by their id under lines on GET inventory/receipts/:id. Omit to use all of them, which is the usual case. Every id must belong to this receipt, and an id from another one fails with REFERENCE_NOT_FOUND. | | `charges` | array of object | no | The charges to capitalize, read from GET inventory/landed-costs/vendor-bill-lines. At least one, and every one from an approved vendor bill. Omit to leave the current set. To change it, send the whole set, because it replaces rather than merges. | | `charges[].chargeId` | string | yes | The vendor bill line the charge comes from, by its id on GET inventory/landed-costs/vendor-bill-lines. The vendor bill is derived from it, so there is no second id to send. | | `charges[].allocationMode` | string | no | How to spread this charge over the receipt lines. Accepted values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. Pass back what GET inventory/landed-costs/vendor-bill-lines returned. Omitting it does not fall through to the landed cost item configuration. It falls through to the item's own default, which may be a different mode. | | `charges[].amount` | string (decimal) | yes | How much of the charge to capitalize, in the entity's functional currency. Send functionalLineTotal from GET inventory/landed-costs/vendor-bill-lines, less anything already allocated, not the bill's own lineTotal. Nothing converts this amount. | | `charges[].isSelected` | boolean | no | Whether to include the charge. Defaults to true. A charge set to false is kept on the landed cost and allocated nothing. | Example: ```json { "documentDate": "2026-01-15", "notes": "<notes>", "receipts": [ { "receiptId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "selectedLineIds": [ "<selectedLineIds>" ] } ], "charges": [ { "chargeId": "9c47e1b8-2a63-4d05-b7f9-04e28d5a1c37", "allocationMode": "LineTotal", "amount": "300" } ] } ``` ## Response `200` Success Under `data` (updateLandedCostOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the landed cost. | | `allocationLineCount` | number | no | The number of allocation lines after the change, one per selected receipt line. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_RECEIPTS` | 400 | | `NO_CHARGES` | 400 | | `RECEIPT_NOT_COMMITTED` | 400 | | `VENDOR_BILL_NOT_APPROVED` | 400 | | `INVALID_AMOUNT` | 400 | | `UNKNOWN_ALLOCATION_MODE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, REFERENCE_NOT_FOUND, NO_RECEIPTS, NO_CHARGES, RECEIPT_NOT_COMMITTED, VENDOR_BILL_NOT_APPROVED, INVALID_AMOUNT, UNKNOWN_ALLOCATION_MODE - `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 --- # Delete Landed Cost `DELETE /inventory/landed-costs/{id}` Group: Inventory › Landed Costs · Operation ID: `deleteLandedCost` Deletes the landed cost with the given identifier. Only a draft can be deleted. Deletes a draft landed cost, and the receipt selections, charges and allocation lines that hang off it. A committed one cannot be deleted, because it has already capitalized the charges onto the stock, and the inventory revaluations it raised are what a reversal would have to address. Nothing here reverses one. The receipts and the vendor bills are untouched, so the charges it had taken become available again on GET inventory/landed-costs/vendor-bill-lines. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteLandedCost) ## 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. | | `id` | path | string | yes | The unique identifier for the landed cost to delete. | ## Response `200` Success Under `data` (deleteLandedCostOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted landed cost. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS - `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 --- # Commit Landed Cost `POST /inventory/landed-costs/{id}/commit` Group: Inventory › Landed Costs · Operation ID: `commitLandedCost` Commits the landed cost, capitalizing the charges onto the received stock. Commits a draft landed cost: the charges are capitalized onto the received stock and the inventory is worth more, in one call. This is the only workflow verb this resource has. The landed cost is a draft until you commit it and committed afterwards. There is no separate validate endpoint, because the commit runs the platform's dry run first and reports what it would have refused. After this the landed cost can be neither edited nor deleted. It posts by raising inventory revaluations. The charges are grouped by expense account and costing element type, and one revaluation is raised and committed per group. Their ids come back in revaluationIds and are also visible per allocation line on the read. Two things follow: several charges can produce several revaluations from one commit, and a rejection naming an account is that revaluation's, not the landed cost's, so check the entity's account determination rule. warnings is non-fatal. The commit succeeded regardless of what is listed there. [Open in the interactive reference](https://docs.api.lensing.ai/?op=commitLandedCost) ## 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. | | `id` | path | string | yes | The unique identifier for the landed cost to commit. | ## Response `200` Success Under `data` (commitLandedCostOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the landed cost. | | `documentStatus` | string | yes | The status of the landed cost after the commit. It is read back from the document, so a commit that ends in an unexpected status says so. | | `totalChargeAmount` | string (decimal) | no | The total of the charges capitalized, in the landed cost's currency. | | `revaluationIds` | array of string | yes | The inventory revaluations this commit raised, one per expense account and costing element type. They are the documents that moved the value. See id on GET inventory/inventory-revaluations/:id. | | `warnings` | array of string | yes | Non-fatal advisories from the commit. An empty array on a clean run. The commit succeeded regardless of what is listed here. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `VALIDATION_FAILED` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, VALIDATION_FAILED, POSTING_REJECTED - `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 --- # Creates several landed costs in one call, each as a draft. `POST /inventory/landed-costs/bulk` Group: Inventory › Landed Costs · Operation ID: `bulkCreateLandedCosts` Creates several landed costs as drafts in one all-or-nothing call, the shape a period-end run takes, where a month of freight invoices is capitalized onto the receipts they belong to in one pass. If any landed cost is rejected, none is created, and the error names the offending document's index. Every precondition of the single create applies to every entry. Nothing is committed. Each landed cost comes back as a draft and has to be committed on its own. A commit revalues stock, and one that failed partway through a batch would leave some of the batch's stock restated and the rest not. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateLandedCosts) ## 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`: bulkCreateLandedCostsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `landedCosts` | array of object | yes | The landed costs to create, each as a draft. All or nothing: if any is invalid, nothing is created. | | `landedCosts[].entityId` | string | yes | The legal entity. It decides the functional currency that every amount on the landed cost is in, and it cannot be changed after create. | | `landedCosts[].documentDate` | string (date) | no | The date of the landed cost. Use format YYYY-MM-DD. Defaults to today. | | `landedCosts[].notes` | string | no | Free text on the landed cost. | | `landedCosts[].receipts` | array of object | yes | The receipts whose stock the charges are capitalized onto. At least one, and every one must be committed. | | `landedCosts[].receipts[].receiptId` | string | yes | A committed receipt. See id on GET inventory/receipts/:id. | | `landedCosts[].receipts[].selectedLineIds` | array of string | no | The receipt lines to allocate onto, by their id under lines on GET inventory/receipts/:id. Omit to use all of them, which is the usual case. Every id must belong to this receipt, and an id from another one fails with REFERENCE_NOT_FOUND. | | `landedCosts[].charges` | array of object | yes | The charges to capitalize, read from GET inventory/landed-costs/vendor-bill-lines. At least one, and every one from an approved vendor bill. | | `landedCosts[].charges[].chargeId` | string | yes | The vendor bill line the charge comes from, by its id on GET inventory/landed-costs/vendor-bill-lines. The vendor bill is derived from it, so there is no second id to send. | | `landedCosts[].charges[].allocationMode` | string | no | How to spread this charge over the receipt lines. Accepted values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. Pass back what GET inventory/landed-costs/vendor-bill-lines returned. Omitting it does not fall through to the landed cost item configuration. It falls through to the item's own default, which may be a different mode. | | `landedCosts[].charges[].amount` | string (decimal) | yes | How much of the charge to capitalize, in the entity's functional currency. Send functionalLineTotal from GET inventory/landed-costs/vendor-bill-lines, less anything already allocated, not the bill's own lineTotal. Nothing converts this amount. | | `landedCosts[].charges[].isSelected` | boolean | no | Whether to include the charge. Defaults to true. A charge set to false is kept on the landed cost and allocated nothing. | Example: ```json { "landedCosts": [ { "entityId": "7039b992-696a-40c7-a181-51759304ef83", "receipts": [ { "receiptId": "b1a7c930-5e46-4f82-8d05-71c3e9a24f68" } ], "charges": [ { "chargeId": "9c47e1b8-2a63-4d05-b7f9-04e28d5a1c37", "allocationMode": "Quantity", "amount": "240" } ] } ] } ``` ## Response `200` Success Under `data` (bulkCreateLandedCostsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `created` | array of object | yes | The created landed costs, in input order, each a draft. Match them to inputs by index. | | `created[].id` | string | yes | The unique identifier for the created landed cost. | | `created[].documentNumber` | string | no | The landed cost number, for example 'LC-1007'. Assigned when the landed cost is created. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `NO_RECEIPTS` | 400 | | `NO_CHARGES` | 400 | | `RECEIPT_NOT_COMMITTED` | 400 | | `VENDOR_BILL_NOT_APPROVED` | 400 | | `INVALID_AMOUNT` | 400 | | `UNKNOWN_ALLOCATION_MODE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, NO_RECEIPTS, NO_CHARGES, RECEIPT_NOT_COMMITTED, VENDOR_BILL_NOT_APPROVED, INVALID_AMOUNT, UNKNOWN_ALLOCATION_MODE - `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 --- # Deletes several landed costs in one call. `POST /inventory/landed-costs/bulk-delete` Group: Inventory › Landed Costs · Operation ID: `bulkDeleteLandedCosts` Deletes several landed costs in one call. All or nothing: if any id matches no landed cost, or names one that cannot be deleted, nothing is deleted, and 'index' on the error says which, counting from zero. Delete a single landed cost with DELETE inventory/landed-costs/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteLandedCosts) ## 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`: bulkDeleteLandedCostsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the landed costs to delete. All or nothing: if any id is unknown or its landed cost is not a draft, nothing is deleted. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteLandedCostsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `deleted` | array of object | yes | The deleted landed costs, in input order. Match them to inputs by index. | | `deleted[].id` | string | yes | The unique identifier for the deleted landed cost. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS - `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 --- # Returns the landed costs that match the given criteria. `POST /inventory/landed-costs/search` Group: Inventory › Landed Costs · Operation ID: `searchLandedCosts` Searches landed costs by exact criteria in a request body, and returns the same item schema as get, the whole landed cost with its receipts, charges and allocation. Three criteria here cannot be asked of the collection endpoint, because they are questions about the landed cost's children. receiptId finds the landed costs covering a given receipt, vendorBillId those drawing on a given vendor bill, and itemId those whose allocation touches a given item. Each resolves to the set of landed costs that satisfy it, and they intersect. Every criterion is exact and they combine with AND. An empty filters object returns everything, page by page. There is no substring or free-text matching on any endpoint of this resource. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchLandedCosts) ## 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`. | ## Request body Required, `application/json`: searchLandedCostsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The search criteria, combined with AND. An empty object returns every landed cost, page by page. | | `filters.documentNumber` | string | no | Limits results to the landed cost with this number, for example 'LC-1007'. The match is exact. | | `filters.documentStatus` | string | no | Limits results to landed costs with this status. The two statuses are DRAFT and COMMITTED. The match is exact. | | `filters.entityId` | string | no | Limits results to landed costs of this legal entity. | | `filters.receiptId` | string | no | Limits results to the landed costs that name this receipt. See id on GET inventory/receipts/:id. | | `filters.vendorBillId` | string | no | Limits results to the landed costs that draw a charge from this vendor bill. See id on GET accounting/vendor-bills/:id. | | `filters.itemId` | string | no | Limits results to the landed costs whose allocation touches this item. | | `filters.fromDate` | string (date) | no | Limits results to landed costs dated on or after this date. Format YYYY-MM-DD. | | `filters.toDate` | string (date) | no | Limits results to landed costs dated on or before this date. Format YYYY-MM-DD. | | `filters.modifiedSince` | string (date-time) | no | Limits results to landed costs changed on or after this time, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of landed costs per page, between 1 and 200. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "documentStatus": "DRAFT" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchLandedCostsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The landed costs that match, oldest first, each whole. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the landed cost. | | `items[].documentNumber` | string | no | The landed cost number, for example 'LC-1007'. Assigned when the landed cost is created. | | `items[].documentStatus` | string | yes | The status of the landed cost. It is DRAFT while it is being built and COMMITTED once you commit it with POST inventory/landed-costs/:id/commit. | | `items[].documentDate` | string (date) | no | The date of the landed cost. Format YYYY-MM-DD. | | `items[].entityId` | string | no | The legal entity the landed cost belongs to. It decides the functional currency that every amount is in. | | `items[].entityName` | string | no | The name of that legal entity. | | `items[].currency` | string | no | The currency of the landed cost's amounts. Format ISO 4217, for example 'GBP'. This is the entity's functional currency, and nothing converts the amounts. | | `items[].notes` | string | no | Free text on the landed cost. | | `items[].totalChargeAmount` | string (decimal) | no | The total of the selected charges, in the landed cost's currency. | | `items[].receiptCount` | number | no | The number of receipts in scope. | | `items[].chargeCount` | number | no | The number of charges on the landed cost. | | `items[].allocationLineCount` | number | no | The number of allocation lines, one per selected receipt line. | | `items[].createdDate` | string (date-time) | no | The date and time the landed cost was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the landed cost last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].receipts` | array of object | no | The receipts whose stock the charges are capitalized onto, each with the lines in scope. | | `items[].receipts[].id` | string | yes | The receipt this scope entry is for. See id on GET inventory/receipts/:id. | | `items[].receipts[].receiptNumber` | string | no | The receipt's number. | | `items[].receipts[].documentDate` | string (date) | no | The date of the receipt. Format YYYY-MM-DD. | | `items[].receipts[].documentStatus` | string | no | The receipt's status. Always COMMITTED, because a landed cost cannot name a receipt that has not been committed. | | `items[].receipts[].lines` | array of object | no | Every line of that receipt, each showing whether this landed cost allocates onto it. A line that is not selected takes none of the charge. | | `items[].receipts[].lines[].id` | string | yes | The receipt line. This is what selectedLineIds names on create and update. See id under lines on GET inventory/receipts/:id. | | `items[].receipts[].lines[].isSelected` | boolean | no | Whether this landed cost allocates onto the line. | | `items[].receipts[].lines[].itemId` | string | no | The item received on the line. | | `items[].receipts[].lines[].itemCode` | string | no | The short code of that item. | | `items[].receipts[].lines[].itemName` | string | no | The name of that item. | | `items[].receipts[].lines[].locationId` | string | no | The warehouse it was received into. | | `items[].receipts[].lines[].quantity` | string (decimal) | no | The quantity received, in the line's unit. | | `items[].receipts[].lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].receipts[].lines[].unitPrice` | string (decimal) | no | The price the line was received at, per unit, in the landed cost's currency. | | `items[].receipts[].lines[].lineTotal` | string (decimal) | no | The total for the line, in the landed cost's currency. | | `items[].charges` | array of object | no | The charges being capitalized, one per vendor bill line. | | `items[].charges[].id` | string | yes | The vendor bill line the charge comes from. See id on GET inventory/landed-costs/vendor-bill-lines. | | `items[].charges[].vendorBillId` | string | no | The vendor bill the charge belongs to. Derived from the line, never supplied. See id on GET accounting/vendor-bills/:id. | | `items[].charges[].vendorBillNumber` | string | no | The vendor bill's number. | | `items[].charges[].itemId` | string | no | The item the charge is billed as, where it is billed as one. | | `items[].charges[].itemCode` | string | no | The short code of that item. | | `items[].charges[].itemName` | string | no | The name of that item. | | `items[].charges[].allocationMode` | string | no | How this charge is spread over the receipt lines. Possible values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. | | `items[].charges[].amount` | string (decimal) | no | The amount of the charge this landed cost takes, in the landed cost's currency. | | `items[].charges[].allocatedAmount` | string (decimal) | no | The amount of the charge that has actually been allocated. Filled in when you commit the landed cost. | | `items[].charges[].isSelected` | boolean | no | Whether this charge is included. An excluded charge is kept on the landed cost and allocated nothing. | | `items[].allocationLines` | array of object | no | The computed allocation: one line per selected receipt line, saying what its unit cost becomes. Recomputed on every write, so it is always current. | | `items[].allocationLines[].id` | string | yes | The unique identifier for the allocation line within the landed cost. | | `items[].allocationLines[].receiptId` | string | no | The receipt this allocation line belongs to. See id on GET inventory/receipts/:id. | | `items[].allocationLines[].receiptLineId` | string | no | The receipt line this allocation lands on. See id under lines on GET inventory/receipts/:id. | | `items[].allocationLines[].itemId` | string | no | The item. | | `items[].allocationLines[].itemCode` | string | no | The short code of that item. | | `items[].allocationLines[].itemName` | string | no | The name of that item. | | `items[].allocationLines[].locationId` | string | no | The warehouse. | | `items[].allocationLines[].locationCode` | string | no | The short code of that warehouse. | | `items[].allocationLines[].quantity` | string (decimal) | no | The quantity the allocation is spread over. | | `items[].allocationLines[].currentUnitCost` | string (decimal) | no | The unit cost of the stock before the landed cost commits. | | `items[].allocationLines[].additionalCostAmount` | string (decimal) | no | The share of the charges this line takes, in total, not per unit. | | `items[].allocationLines[].newUnitCost` | string (decimal) | no | The unit cost of the stock after the landed cost commits. | | `items[].allocationLines[].revaluationDocuments` | array of object | no | The inventory revaluations this allocation line produced. An empty array on a draft, and more than one where the charges post to different expense accounts. | | `items[].allocationLines[].revaluationDocuments[].id` | string | yes | The inventory revaluation. See id on GET inventory/inventory-revaluations/:id. | | `items[].allocationLines[].revaluationDocuments[].documentNumber` | string | no | The revaluation's number. | | `items[].allocationLines[].revaluationDocuments[].documentStatus` | string | no | The revaluation's status. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | no | Whether more results remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Get Available Charges `GET /inventory/landed-costs/vendor-bill-lines` Group: Inventory › Landed Costs · Operation ID: `getAvailableCharges` Returns the vendor bill charges available to capitalize for a legal entity. Returns the vendor bill charges available to capitalize for a legal entity, the endpoint to call before building a landed cost, because you cannot write the create payload without it. Each row is a line of an approved vendor bill that a landed cost item configuration makes eligible. Its id is what goes into a charge's chargeId. Its functionalLineTotal is what is left to allocate in the entity's functional currency, and that is the figure to send as amount, because a landed cost is consumed with no conversion, so a foreign-currency bill's own lineTotal is the wrong number. allocationMode is what the configuration recommends for the charge. Pass it back. Omitting it does not fall through to the configuration, it falls through to the item's own default, which may be a different mode. An empty array almost always means nothing is configured, not that there are no bills. A vendor bill line is eligible only when it matches a row in inventory/landed-cost-items, by item or by GL account. With that configuration empty, nothing matches. Check it before looking at the bills. excludeFullyAllocated defaults to true, which leaves out charges with nothing left. Pass false to see everything, including what previous landed costs have taken. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getAvailableCharges) ## 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`. | | `entityId` | query | string | yes | The legal entity whose bills to look at. Required, because eligibility, the functional currency and the bills are all per entity. | | `excludeFullyAllocated` | query | boolean | no | Whether to leave out charges with nothing left to allocate. Defaults to true. Pass false to see what previous landed costs have already taken. | ## Response `200` Success Under `data` (getAvailableChargesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `charges` | array of object | yes | The charges available, newest bills first. An empty array when nothing matches, which usually means nothing is configured in inventory/landed-cost-items rather than that there are no bills. | | `charges[].id` | string | yes | The vendor bill line. This is what a landed cost charge's chargeId takes. | | `charges[].vendorBillId` | string | no | The vendor bill the line belongs to. A landed cost derives it and does not ask for it. See id on GET accounting/vendor-bills/:id. | | `charges[].vendorBillNumber` | string | no | The vendor bill's number. | | `charges[].description` | string | no | The line's own description, for example 'Sea freight, Shanghai to Felixstowe'. | | `charges[].poNumber` | string | no | The number of the purchase order the line is matched to, where it is matched to one. | | `charges[].billStatus` | string | no | The vendor bill's status. Always approved, because an unapproved bill's lines are not eligible. | | `charges[].billDate` | string (date) | no | The date of the vendor bill. It is also the date the currency conversion below is taken at. | | `charges[].dueDate` | string (date) | no | The vendor bill's due date. Format YYYY-MM-DD. | | `charges[].vendorName` | string | no | The name of the vendor who billed it. | | `charges[].entityId` | string | no | The legal entity the bill belongs to. | | `charges[].itemId` | string | no | The item the charge is billed as, where it is billed as one rather than coded to an account. | | `charges[].itemCode` | string | no | The short code of that item. | | `charges[].itemName` | string | no | The name of that item, falling back to the line's description. | | `charges[].allocationMode` | string | no | The allocation mode the landed cost item configuration recommends for this charge. Possible values: 'Equal', 'Quantity', 'LineTotal', 'Weight', 'Volume'. Pass it back on create. Omitting it falls through to the item's own default, which is a different lookup and may be a different mode. | | `charges[].lineTotal` | string (decimal) | no | The line's total, in the vendor bill's currency. | | `charges[].allocatedAmount` | string (decimal) | no | The amount previous landed costs have already taken, in the vendor bill's currency. | | `charges[].currency` | string | no | The vendor bill's currency, which need not be the entity's functional currency. Format ISO 4217, for example 'GBP'. | | `charges[].functionalLineTotal` | string (decimal) | no | The line's total in the entity's functional currency, converted at billDate. This is the figure to allocate, because a landed cost is stated in the functional currency and nothing converts it afterwards. | | `charges[].functionalAllocatedAmount` | string (decimal) | no | The amount already taken, in the entity's functional currency. Subtract it from functionalLineTotal for what is left. | | `charges[].functionalCurrency` | string | no | The entity's functional currency. It is the currency of the two functional figures above, and of every amount on a landed cost. Format ISO 4217, for example 'GBP'. | ## Errors | Code | Status | | --- | --- | | `ENTITY_NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: ENTITY_NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Reason Codes `GET /inventory/reason-codes` Group: Inventory › Reason Codes · Operation ID: `reasonCodes` Returns a list of reason codes. To filter by document type, use POST inventory/reason-codes/search. This collection returns the same fields as GET inventory/reason-codes/:id and POST inventory/reason-codes/search, except documentTypes, which it does not return and cannot filter by. Read document types, or filter by one, with GET inventory/reason-codes/:id or POST inventory/reason-codes/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=reasonCodes) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `code` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isActive` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `requiresApproval` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, code, description, isActive, requiresApproval, lastModifiedDate - Selectable: id, code, description, isActive, requiresApproval, lastModifiedDate ## Response `200` Success Under `data`, an array of reasonCodesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the reason code. | | `code` | string | no | The business code of the reason code, for example 'DAMAGE'. It is unique within the tenant. | | `description` | string or null | no | A free-form description of what the reason code means. Absent when none was given. | | `isActive` | boolean or null | no | Whether the reason code can be selected on new documents. When false, existing documents keep it and new ones cannot select it. | | `requiresApproval` | boolean or null | no | Whether documents using this reason code require approval before they take effect. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the reason code record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Adding or removing a document type does not change it. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a reason code. `POST /inventory/reason-codes` Group: Inventory › Reason Codes · Operation ID: `createReasonCode` The response is only the created reason code's id. Read the record with GET inventory/reason-codes/:id when you need its stored state. Use POST inventory/reason-codes/bulk to create several at once. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createReasonCode) ## 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`: createReasonCodeInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `code` | string | yes | The business code of the reason code, for example 'DAMAGE'. Required. It must be unique within the tenant. | | `description` | string | no | A free-form description of what the reason code means. Omit for none. | | `isActive` | boolean | no | Whether the reason code can be selected on new documents. Defaults to true. Set it to false to stage a reason code before it goes into use. | | `requiresApproval` | boolean | no | Whether documents using this reason code require approval before they take effect. Defaults to false. | | `documentTypes` | array of string, one of `"InventoryIncrease"`, `"InventoryDecrease"`, `"SalesReturn"`, `"PurchaseReturn"`, `"Disposal"` | yes | The document types this reason code may be selected on. Required, but may be empty. An empty array creates a reason code that is not yet selectable anywhere. An unrecognized value fails the call with UNKNOWN_DOCUMENT_TYPES, which names every value it did not recognize. Repeated values are collapsed. | Example: ```json { "code": "DAMAGE", "description": "Stock damaged in the warehouse", "isActive": true, "requiresApproval": true, "documentTypes": [ "InventoryDecrease", "SalesReturn" ] } ``` ## Response `200` Success Under `data` (createReasonCodeOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created reason code. | ## 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 | | --- | --- | | `DUPLICATE_CODE` | 400 | | `UNKNOWN_DOCUMENT_TYPES` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: DUPLICATE_CODE, UNKNOWN_DOCUMENT_TYPES - `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 --- # Returns the reason code with the given identifier. `GET /inventory/reason-codes/{id}` Group: Inventory › Reason Codes · Operation ID: `getReasonCode` The response has the same fields as POST inventory/reason-codes/search. The collection GET inventory/reason-codes returns the same fields except documentTypes, which it cannot express. To look a reason code up by its code instead of its id, filter the collection GET inventory/reason-codes on code. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getReasonCode) ## 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`. | | `id` | path | string | yes | The unique identifier for the reason code to return. | ## Response `200` Success Under `data` (getReasonCodeOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the reason code. | | `code` | string | yes | The business code of the reason code, for example 'DAMAGE'. It is unique within the tenant. | | `description` | string | no | A free-form description of what the reason code means. Absent when none was given. | | `isActive` | boolean | no | Whether the reason code can be selected on new documents. When false, existing documents keep it and new ones cannot select it. | | `requiresApproval` | boolean | no | Whether documents using this reason code require approval before they take effect. | | `documentTypes` | array of string, one of `"InventoryIncrease"`, `"InventoryDecrease"`, `"SalesReturn"`, `"PurchaseReturn"`, `"Disposal"` | yes | The document types this reason code may be selected on, in a stable order. An empty array when it is linked to none. It is not returned by the collection GET inventory/reason-codes. | | `lastModifiedDate` | string (date-time) | no | The date and time the reason code record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Adding or removing a document type does not change it. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Reason Code `PATCH /inventory/reason-codes/{id}` Group: Inventory › Reason Codes · Operation ID: `updateReasonCode` Updates the reason code with the given identifier. Only the fields in the request body change. Everything omitted from the request body is left as it was. documentTypes replaces the whole set of linked document types rather than merging, so read the current set from GET inventory/reason-codes/:id first if you mean to add one. The response is only the reason code's id. Read the updated record with GET inventory/reason-codes/:id when you need its state after the update. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateReasonCode) ## 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. | | `id` | path | string | yes | The unique identifier for the reason code to update. | ## Request body Required, `application/json`: updateReasonCodeInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `code` | string | no | The business code of the reason code, for example 'DAMAGE'. It must stay unique within the tenant. Reusing the reason code's own current code is not a collision. | | `description` | string | no | A free-form description of what the reason code means. | | `isActive` | boolean | no | Whether the reason code can be selected on new documents. Set it to false to retire the reason code without deleting it: existing documents keep it and new ones cannot select it. | | `requiresApproval` | boolean | no | Whether documents using this reason code require approval before they take effect. | | `documentTypes` | array of string, one of `"InventoryIncrease"`, `"InventoryDecrease"`, `"SalesReturn"`, `"PurchaseReturn"`, `"Disposal"` | no | The complete set of document types this reason code may be selected on. It replaces the current set. Anything not listed is unlinked, and an empty array unlinks everything. Omit to leave the links unchanged. An unrecognized value fails the call with UNKNOWN_DOCUMENT_TYPES, which names every value it did not recognize. | Example: ```json { "code": "<code>", "description": "<description>", "isActive": true, "requiresApproval": true, "documentTypes": [ "InventoryDecrease", "SalesReturn", "Disposal" ] } ``` ## Response `200` Success Under `data` (updateReasonCodeOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the reason code. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `DUPLICATE_CODE` | 400 | | `UNKNOWN_DOCUMENT_TYPES` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, DUPLICATE_CODE, UNKNOWN_DOCUMENT_TYPES - `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 --- # Deletes the reason code with the given identifier. `DELETE /inventory/reason-codes/{id}` Group: Inventory › Reason Codes · Operation ID: `deleteReasonCode` A reason code that inventory documents still name cannot be deleted, because deleting would strip the reason from those historical documents. Set isActive to false with PATCH inventory/reason-codes/:id to retire the reason code instead. A delete succeeds only for a reason code nothing has used yet. Use POST inventory/reason-codes/bulk-delete to delete several at once. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteReasonCode) ## 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. | | `id` | path | string | yes | The unique identifier for the reason code to delete. | ## Response `200` Success Under `data` (deleteReasonCodeOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted reason code. | | `code` | string | yes | The business code the deleted reason code held. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REASON_CODE_IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REASON_CODE_IN_USE - `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 --- # Creates several reason codes in one call. `POST /inventory/reason-codes/bulk` Group: Inventory › Reason Codes · Operation ID: `bulkCreateReasonCodes` All or nothing. Every entry is checked before any is written, so one rejected entry leaves the store untouched and the whole call fails. The response is the created reason codes in the same order as the input. Use POST inventory/reason-codes to create a single reason code. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateReasonCodes) ## 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`: bulkCreateReasonCodesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `reasonCodes` | array of object | yes | The reason codes to create. If any entry is invalid, nothing is created and the whole call fails. | | `reasonCodes[].code` | string | yes | The business code of the reason code, for example 'DAMAGE'. Required. It must be unique within the tenant and within this request. | | `reasonCodes[].description` | string | no | A free-form description of what the reason code means. Omit for none. | | `reasonCodes[].isActive` | boolean | no | Whether the reason code can be selected on new documents. Defaults to true. | | `reasonCodes[].requiresApproval` | boolean | no | Whether documents using this reason code require approval before they take effect. Defaults to false. | | `reasonCodes[].documentTypes` | array of string, one of `"InventoryIncrease"`, `"InventoryDecrease"`, `"SalesReturn"`, `"PurchaseReturn"`, `"Disposal"` | yes | The document types this reason code may be selected on. Required, but may be empty. An unrecognized value fails the whole call with UNKNOWN_DOCUMENT_TYPES, which names this entry's index. | Example: ```json { "reasonCodes": [ { "code": "DAMAGE", "description": "Stock damaged in the warehouse", "requiresApproval": true, "documentTypes": [ "InventoryDecrease", "SalesReturn" ] }, { "code": "CYCLECOUNT", "description": "Correction from a cycle count", "documentTypes": [ "InventoryIncrease", "InventoryDecrease" ] } ] } ``` ## Response `200` Success Under `data` (bulkCreateReasonCodesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `created` | array of object | yes | The created reason codes, in the same order as the input. Match entries to inputs by index. | | `created[].id` | string | yes | The unique identifier for the created reason code. | ## 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 | | --- | --- | | `DUPLICATE_CODE` | 400 | | `DUPLICATE_CODE_IN_REQUEST` | 400 | | `UNKNOWN_DOCUMENT_TYPES` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: DUPLICATE_CODE, DUPLICATE_CODE_IN_REQUEST, UNKNOWN_DOCUMENT_TYPES - `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 --- # Deletes several reason codes in one call. `POST /inventory/reason-codes/bulk-delete` Group: Inventory › Reason Codes · Operation ID: `bulkDeleteReasonCodes` All or nothing. Every identifier is resolved and checked for references before anything is deleted, so one unknown identifier or one reason code still named on a document fails the whole call and nothing is deleted. Use DELETE inventory/reason-codes/:id to delete a single reason code. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteReasonCodes) ## 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`: bulkDeleteReasonCodesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers for the reason codes to delete. If any identifier matches nothing or its reason code is still named on a document, nothing is deleted and the whole call fails. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteReasonCodesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `deleted` | array of object | yes | The deleted reason codes, in the same order as the input. Match entries to inputs by index. | | `deleted[].id` | string | yes | The unique identifier for the deleted reason code. | | `deleted[].code` | string | yes | The business code the deleted reason code held. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REASON_CODE_IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REASON_CODE_IN_USE - `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 --- # Returns the reason codes that match the given filters. `POST /inventory/reason-codes/search` Group: Inventory › Reason Codes · Operation ID: `searchReasonCodes` Every criterion is exact and criteria combine with AND. Send an empty filters object to return every reason code. There is no free-text search. This endpoint returns documentTypes and is the only one that can filter by a document type, which the collection cannot. The other filters share the names they have on GET inventory/reason-codes. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchReasonCodes) ## 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`. | ## Request body Required, `application/json`: searchReasonCodesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. A reason code must match every filter you send. Send an empty object to return all reason codes. | | `filters.code` | string | no | Limits results to the reason code with this code. The match is exact. Codes are unique, so this matches at most one reason code. | | `filters.isActive` | boolean | no | Limits results to reason codes that can be selected on new documents when true, or to retired ones when false. Omit to match both. | | `filters.requiresApproval` | boolean | no | Limits results to reason codes that require approval when true, or that do not when false. Omit to match both. | | `filters.documentType` | string, one of `"InventoryIncrease"`, `"InventoryDecrease"`, `"SalesReturn"`, `"PurchaseReturn"`, `"Disposal"` | no | Limits results to the reason codes linked to this document type. This filter is available only here, not on the collection. A document type nothing is linked to returns an empty page, not an error. | | `filters.modifiedSince` | string (date-time) | no | Limits results to reason codes changed on or after this time. Format YYYY-MM-DDTHH:mm:ss.sssZ. Adding or removing a document type does not change lastModifiedDate. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of reason codes per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "documentType": "InventoryDecrease", "isActive": true }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchReasonCodesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The reason codes on this page, ordered by id. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the reason code. | | `items[].code` | string | yes | The business code of the reason code, for example 'DAMAGE'. It is unique within the tenant. | | `items[].description` | string | no | A free-form description of what the reason code means. Absent when none was given. | | `items[].isActive` | boolean | no | Whether the reason code can be selected on new documents. When false, existing documents keep it and new ones cannot select it. | | `items[].requiresApproval` | boolean | no | Whether documents using this reason code require approval before they take effect. | | `items[].documentTypes` | array of string, one of `"InventoryIncrease"`, `"InventoryDecrease"`, `"SalesReturn"`, `"PurchaseReturn"`, `"Disposal"` | yes | The document types this reason code may be selected on, in a stable order. An empty array when it is linked to none. This is the reason code's full set. Filtering by documentType narrows which reason codes are returned, not the set each one reports. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the reason code record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Adding or removing a document type does not change it. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more reason codes remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Receipts `GET /inventory/receipts` Group: Inventory › Receipts · Operation ID: `receipts` Returns a list of receipts. To filter by vendor, reference number, item or warehouse, use POST inventory/receipts/search. The receipt collection. This endpoint returns the header fields only. A nested array of lines cannot be returned here, so lineCount stands in for them, and GET inventory/receipts/:id or POST inventory/receipts/search returns the full document. The vendor, its reference number, and the item and warehouse on the lines are not filterable here. POST inventory/receipts/search takes them. [Open in the interactive reference](https://docs.api.lensing.ai/?op=receipts) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `deliveryDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, documentNumber, documentDate, deliveryDate, documentStatus, totalAmount, createdDate, lastModifiedDate - Selectable: id, documentNumber, documentStatus, documentDate, deliveryDate, entityId, entityName, businessPartnerId, businessPartnerName, referenceNumber, currency, totalAmount, lineCount, notes, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of receiptsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the receipt. | | `documentNumber` | string or null | no | The receipt number, for example 'GR-2291'. Assigned when the receipt is created. | | `documentStatus` | string | no | The status of the receipt. It is DRAFT when the receipt is created through this API and COMMITTED once you commit it with POST inventory/receipts/:id/commit. The receiving and invoicing processes set the other statuses. | | `documentDate` | string (date) | no | The date of the receipt. Format YYYY-MM-DD. | | `deliveryDate` | string (date) or null | no | The date the goods arrived or are expected. Format YYYY-MM-DD. | | `entityId` | string | no | The legal entity the receipt belongs to. | | `entityName` | string or null | no | The name of that legal entity. | | `businessPartnerId` | string | no | The vendor the goods came from. | | `businessPartnerName` | string or null | no | The name of that vendor. | | `referenceNumber` | string or null | no | The vendor's own reference, typically their delivery note number. | | `currency` | string or null | no | The currency of the receipt's amounts. Format ISO 4217, for example 'EUR'. This is the vendor's currency, which need not be the entity's functional currency. See functionalUnitPrice on the line for the functional-currency amounts. | | `totalAmount` | string (decimal) or null | no | The total value of the receipt, in the receipt's currency. | | `lineCount` | number or null | no | The number of lines on the receipt. | | `notes` | string or null | no | Free text on the receipt. | | `createdDate` | string (date-time) or null | no | The date and time the receipt was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the receipt last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a receipt in draft status. `POST /inventory/receipts` Group: Inventory › Receipts · Operation ID: `createReceipt` Creates a receipt in draft status. Nothing moves until you commit it: creating records what arrived, and POST inventory/receipts/:id/commit brings the stock in and posts what is owed. The warehouse is set per line, so one receipt can bring goods into several warehouses. A batch-tracked item needs batches adding up to the line's quantity, and a serialized item needs one serials entry per unit. The whole call is one transaction: a rejected request writes nothing. The response is the created receipt, in the same shape GET inventory/receipts/:id returns. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createReceipt) ## 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`: createReceiptInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `businessPartnerId` | string | yes | The vendor the goods came from. Required. | | `entityId` | string | no | The legal entity. Omit it and the first line's warehouse decides it, but a receipt of only service lines has no warehouse, so supply it there. | | `documentDate` | string (date) | no | The date of the receipt. Use format YYYY-MM-DD. Defaults to today. | | `deliveryDate` | string (date) | no | The date the goods arrived. Use format YYYY-MM-DD. | | `currency` | string | no | The currency for every amount on the receipt. Format ISO 4217, for example 'EUR'. Defaults to the vendor's or the entity's, as the platform resolves it. | | `referenceNumber` | string | no | The vendor's own reference, typically their delivery note number. | | `notes` | string | no | Free text on the receipt. | | `lines` | array of object | yes | The lines of the receipt. At least one is required. | | `lines[].lineType` | string, one of `"Inventory"`, `"Service"`, `"Expense"` | no | What this line receives. Defaults to Inventory. An Inventory line needs itemId, unitId, locationId and quantity, because it moves stock into a warehouse. A Service or Expense line needs serviceDescription and an amount, and has no item or warehouse. A line with a description and no item is taken as a Service line. | | `lines[].itemId` | string | no | The item received. Required on an Inventory line, and not used on a Service or Expense line. | | `lines[].serviceDescription` | string | no | What was bought. Required on a Service or Expense line, and not used on an Inventory line. | | `lines[].locationId` | string | no | The warehouse the goods go into. Required on an Inventory line. | | `lines[].unitId` | string | no | The unit the quantity is counted in. Required on an Inventory line. | | `lines[].quantity` | string (decimal) | no | The quantity received, in that unit. Required on an Inventory line, and greater than zero. | | `lines[].unitPrice` | string (decimal) | no | The price of one unit, in the receipt's currency. | | `lines[].lineTotal` | string (decimal) | no | The total for the line, in the receipt's currency. On a Service or Expense line, give this or a unitPrice. | | `lines[].expenseAccountId` | string | no | The GL account to charge. Required on a Service or Expense line, and not used on an Inventory line. Without it, the commit fails when it posts the journal entry. | | `lines[].sourceDocumentLineId` | string | no | The purchase order line this line receives against, as its id under lines on GET accounting/purchase-orders/:id. Its numeric id in string form is also accepted. The line type is set from it. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches received on this line. Required for a batch-tracked item, and the quantities must add up to the line's quantity. When you send it on update, it replaces the line's batches. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch. Omit it and give batchNumber for a batch arriving for the first time, and the batch record is created. | | `lines[].batches[].batchNumber` | string | no | The batch number as printed on the delivery. Required unless batchNumberId is given. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch received, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `lines[].serials` | array of object | no | The serialized units received, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. When you send it on update, it replaces the line's serials. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial record. Omit it and give serialNumber for a unit arriving for the first time. | | `lines[].serials[].serialNumber` | string | no | The serial number stamped on the unit. Required unless serialNumberId is given. | | `lines[].binLocations` | array of object | no | The bins the goods go into, in a bin-tracked warehouse. One flat array, where an entry may name a batchNumber or serialNumber of its own line. When you send it on update, it replaces the line's bin entries. | | `lines[].binLocations[].binLocationId` | string | yes | The bin the goods go into. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity going into this bin, in the line's unit. | | `lines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. | | `lines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | Example: ```json { "businessPartnerId": "b62e4f17-0c93-4a58-8d21-5f70e3b9a4c6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "documentDate": "2026-08-14", "deliveryDate": "2026-08-14", "currency": "EUR", "referenceNumber": "DN-77120", "notes": "Received at the back dock", "lines": [ { "lineType": "Inventory", "itemId": "5f21c8b0-9d34-4a76-8e12-04b93c7de5a8", "locationId": "e1d84c07-5b39-4a26-9f70-2c8b613ad495", "unitId": "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "quantity": "200.00", "unitPrice": "3.20", "sourceDocumentLineId": "551043", "notes": "Two pallets" }, { "lineType": "Service", "serviceDescription": "Inbound freight", "lineTotal": "85.00", "expenseAccountId": "4b7c0e29-6d15-4830-9f42-8a3e57c1d604" } ] } ``` ## Response `200` Success Under `data` (createReceiptOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the receipt. | | `documentNumber` | string | no | The receipt number, for example 'GR-2291'. Assigned when the receipt is created. | | `documentStatus` | string | yes | The status of the receipt. It is DRAFT when the receipt is created through this API and COMMITTED once you commit it with POST inventory/receipts/:id/commit. The receiving and invoicing processes set the other statuses. | | `documentDate` | string (date) | yes | The date of the receipt. Format YYYY-MM-DD. | | `deliveryDate` | string (date) | no | The date the goods arrived or are expected. Format YYYY-MM-DD. | | `entityId` | string | yes | The legal entity the receipt belongs to. | | `entityName` | string | no | The name of that legal entity. | | `businessPartnerId` | string | yes | The vendor the goods came from. | | `businessPartnerName` | string | no | The name of that vendor. | | `referenceNumber` | string | no | The vendor's own reference, typically their delivery note number. | | `currency` | string | no | The currency of the receipt's amounts. Format ISO 4217, for example 'EUR'. This is the vendor's currency, which need not be the entity's functional currency. See functionalUnitPrice on the line for the functional-currency amounts. | | `totalAmount` | string (decimal) | no | The total value of the receipt, in the receipt's currency. | | `notes` | string | no | Free text on the receipt. | | `createdDate` | string (date-time) | no | The date and time the receipt was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the receipt last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the receipt, oldest first. A receipt always has at least one line. | | `lines[].id` | string | yes | The unique identifier for the line. Use it in updateLines and deleteLines. | | `lines[].lineType` | string | no | The type of the line. Possible values: 'Inventory', 'Service', 'Expense'. Only an Inventory line moves stock. A Service or Expense line records something bought that does not enter a warehouse and has a description instead of an item. | | `lines[].itemId` | string | no | The item received. Absent on a Service or Expense line. | | `lines[].itemCode` | string | no | The short code of that item. | | `lines[].itemName` | string | no | The name of that item. | | `lines[].serviceDescription` | string | no | What a Service or Expense line is for. Absent on an Inventory line. | | `lines[].locationId` | string | no | The warehouse the goods went into. Absent on a Service or Expense line. | | `lines[].locationCode` | string | no | The short code of that warehouse. | | `lines[].locationName` | string | no | The name of that warehouse. | | `lines[].unitId` | string | no | The unit the quantity is counted in. | | `lines[].unitCode` | string | no | The short code of that unit. | | `lines[].quantity` | string (decimal) | no | The quantity received, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].receivedQuantity` | string (decimal) | no | The quantity actually received, where the receiving process records it separately. | | `lines[].openQuantity` | string (decimal) | no | The quantity still to be invoiced. | | `lines[].returnedQuantity` | string (decimal) | no | The quantity that has gone back on a purchase return. | | `lines[].unitPrice` | string (decimal) | no | The price of one unit, in the receipt's currency. | | `lines[].lineTotal` | string (decimal) | no | The total for the line, in the receipt's currency. | | `lines[].functionalUnitPrice` | string (decimal) | no | The unit price in the entity's functional currency. Present when the receipt is in another currency. | | `lines[].functionalLineTotal` | string (decimal) | no | The line total in the entity's functional currency. | | `lines[].expenseAccountId` | string | no | The GL account a Service or Expense line is charged to. Absent on an Inventory line. | | `lines[].expenseAccountNumber` | string | no | The number of that account. | | `lines[].expenseAccountName` | string | no | The name of that account. | | `lines[].status` | string | no | Where the line is in the receiving process. This is free text, with no fixed set of values. | | `lines[].sourceDocumentLineId` | string | no | The purchase order line this line was received against. It is a numeric id in string form, not a UUID. Purchase order lines are not individually addressable, so there is no endpoint that takes it. | | `lines[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | The kind of source document line this receipt line was created from. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches received on this line, for a batch-tracked item. Absent when the item is not batch-tracked. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record this entry created or matched. | | `lines[].batches[].batchNumber` | string | yes | The batch number as printed. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch received, in the line's unit. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].batches[].openQuantity` | string (decimal) | no | The quantity of this batch still to be invoiced. | | `lines[].batches[].returnedQuantity` | string (decimal) | no | The quantity of this batch that has gone back to the vendor. | | `lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the receipt's currency. | | `lines[].serials` | array of object | no | The serialized units received, one entry per unit. Absent when the item is not serialized. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record this entry created or matched. | | `lines[].serials[].serialNumber` | string | yes | The serial number as stamped on the unit. | | `lines[].serials[].openQuantity` | string (decimal) | no | The quantity of this unit still to be invoiced. | | `lines[].serials[].returnedQuantity` | string (decimal) | no | The quantity of this unit that has gone back to the vendor. | | `lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the receipt's currency. | | `lines[].binLocations` | array of object | no | The bins the goods went into, in a bin-tracked warehouse. One flat array per line: an entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `lines[].binLocations[].binLocationId` | string | yes | The bin the goods went into. | | `lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity put into this bin, in the line's unit. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `INCOMPLETE_LINE` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, INCOMPLETE_LINE, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Returns the receipt with the given identifier. `GET /inventory/receipts/{id}` Group: Inventory › Receipts · Operation ID: `getReceipt` Returns one receipt with all of its lines and their batch, serial and bin detail. POST inventory/receipts/search returns the same fields. The collection GET inventory/receipts returns the header only and gives lineCount in place of the lines. Read lineType before reading a line. An Inventory line has an item, a warehouse and a quantity. A Service or Expense line has a description and an amount and none of the rest. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getReceipt) ## 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`. | | `id` | path | string | yes | The unique identifier for the receipt to return. | ## Response `200` Success Under `data` (getReceiptOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the receipt. | | `documentNumber` | string | no | The receipt number, for example 'GR-2291'. Assigned when the receipt is created. | | `documentStatus` | string | yes | The status of the receipt. It is DRAFT when the receipt is created through this API and COMMITTED once you commit it with POST inventory/receipts/:id/commit. The receiving and invoicing processes set the other statuses. | | `documentDate` | string (date) | yes | The date of the receipt. Format YYYY-MM-DD. | | `deliveryDate` | string (date) | no | The date the goods arrived or are expected. Format YYYY-MM-DD. | | `entityId` | string | yes | The legal entity the receipt belongs to. | | `entityName` | string | no | The name of that legal entity. | | `businessPartnerId` | string | yes | The vendor the goods came from. | | `businessPartnerName` | string | no | The name of that vendor. | | `referenceNumber` | string | no | The vendor's own reference, typically their delivery note number. | | `currency` | string | no | The currency of the receipt's amounts. Format ISO 4217, for example 'EUR'. This is the vendor's currency, which need not be the entity's functional currency. See functionalUnitPrice on the line for the functional-currency amounts. | | `totalAmount` | string (decimal) | no | The total value of the receipt, in the receipt's currency. | | `notes` | string | no | Free text on the receipt. | | `createdDate` | string (date-time) | no | The date and time the receipt was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the receipt last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the receipt, oldest first. A receipt always has at least one line. | | `lines[].id` | string | yes | The unique identifier for the line. Use it in updateLines and deleteLines. | | `lines[].lineType` | string | no | The type of the line. Possible values: 'Inventory', 'Service', 'Expense'. Only an Inventory line moves stock. A Service or Expense line records something bought that does not enter a warehouse and has a description instead of an item. | | `lines[].itemId` | string | no | The item received. Absent on a Service or Expense line. | | `lines[].itemCode` | string | no | The short code of that item. | | `lines[].itemName` | string | no | The name of that item. | | `lines[].serviceDescription` | string | no | What a Service or Expense line is for. Absent on an Inventory line. | | `lines[].locationId` | string | no | The warehouse the goods went into. Absent on a Service or Expense line. | | `lines[].locationCode` | string | no | The short code of that warehouse. | | `lines[].locationName` | string | no | The name of that warehouse. | | `lines[].unitId` | string | no | The unit the quantity is counted in. | | `lines[].unitCode` | string | no | The short code of that unit. | | `lines[].quantity` | string (decimal) | no | The quantity received, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].receivedQuantity` | string (decimal) | no | The quantity actually received, where the receiving process records it separately. | | `lines[].openQuantity` | string (decimal) | no | The quantity still to be invoiced. | | `lines[].returnedQuantity` | string (decimal) | no | The quantity that has gone back on a purchase return. | | `lines[].unitPrice` | string (decimal) | no | The price of one unit, in the receipt's currency. | | `lines[].lineTotal` | string (decimal) | no | The total for the line, in the receipt's currency. | | `lines[].functionalUnitPrice` | string (decimal) | no | The unit price in the entity's functional currency. Present when the receipt is in another currency. | | `lines[].functionalLineTotal` | string (decimal) | no | The line total in the entity's functional currency. | | `lines[].expenseAccountId` | string | no | The GL account a Service or Expense line is charged to. Absent on an Inventory line. | | `lines[].expenseAccountNumber` | string | no | The number of that account. | | `lines[].expenseAccountName` | string | no | The name of that account. | | `lines[].status` | string | no | Where the line is in the receiving process. This is free text, with no fixed set of values. | | `lines[].sourceDocumentLineId` | string | no | The purchase order line this line was received against. It is a numeric id in string form, not a UUID. Purchase order lines are not individually addressable, so there is no endpoint that takes it. | | `lines[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | The kind of source document line this receipt line was created from. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches received on this line, for a batch-tracked item. Absent when the item is not batch-tracked. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record this entry created or matched. | | `lines[].batches[].batchNumber` | string | yes | The batch number as printed. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch received, in the line's unit. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].batches[].openQuantity` | string (decimal) | no | The quantity of this batch still to be invoiced. | | `lines[].batches[].returnedQuantity` | string (decimal) | no | The quantity of this batch that has gone back to the vendor. | | `lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the receipt's currency. | | `lines[].serials` | array of object | no | The serialized units received, one entry per unit. Absent when the item is not serialized. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record this entry created or matched. | | `lines[].serials[].serialNumber` | string | yes | The serial number as stamped on the unit. | | `lines[].serials[].openQuantity` | string (decimal) | no | The quantity of this unit still to be invoiced. | | `lines[].serials[].returnedQuantity` | string (decimal) | no | The quantity of this unit that has gone back to the vendor. | | `lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the receipt's currency. | | `lines[].binLocations` | array of object | no | The bins the goods went into, in a bin-tracked warehouse. One flat array per line: an entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `lines[].binLocations[].binLocationId` | string | yes | The bin the goods went into. | | `lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity put into this bin, in the line's unit. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Receipt `PATCH /inventory/receipts/{id}` Group: Inventory › Receipts · Operation ID: `updateReceipt` Updates the receipt with the given identifier. Only the fields in the request body change. Updates one draft receipt. Only the fields in the request body change. Only a draft can be edited. Once committed, a receipt is corrected with a purchase return or a credit instead. Lines change through three arrays, applied in order: deleteLines, then updateLines, then createLines, so one call can remove a line and add its replacement. A line's batches, serials and binLocations arrays each replace what the line had when you send them, and are left untouched when you omit them. A line id that belongs to another receipt is rejected. The response is the updated receipt in full. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateReceipt) ## 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. | | `id` | path | string | yes | The unique identifier for the receipt to update. | ## Request body Required, `application/json`: updateReceiptInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `businessPartnerId` | string | no | A new vendor for the receipt. | | `documentDate` | string (date) | no | A new date for the receipt. Use format YYYY-MM-DD. | | `deliveryDate` | string (date) | no | A new delivery date. Use format YYYY-MM-DD. | | `currency` | string | no | A new currency for the receipt's amounts. Format ISO 4217, for example 'EUR'. | | `referenceNumber` | string | no | A new vendor reference. | | `notes` | string | no | New free text. Send an empty string to clear it. | | `deleteLines` | array of string | no | The ids of lines to remove. Applied before updateLines and createLines. | | `updateLines` | array of object | no | The lines to change, addressed by id. Applied after deleteLines and before createLines. | | `updateLines[].id` | string | yes | The unique identifier for the line to change. A line that belongs to another receipt is rejected. | | `updateLines[].lineType` | string, one of `"Inventory"`, `"Service"`, `"Expense"` | no | A new type for the line. | | `updateLines[].itemId` | string | no | A new item for the line. | | `updateLines[].serviceDescription` | string | no | A new description, on a Service or Expense line. | | `updateLines[].locationId` | string | no | A new warehouse for the line. | | `updateLines[].unitId` | string | no | A new unit for the line's quantity. | | `updateLines[].quantity` | string (decimal) | no | A new quantity, in the line's unit. Greater than zero when given. | | `updateLines[].unitPrice` | string (decimal) | no | A new unit price, in the receipt's currency. | | `updateLines[].lineTotal` | string (decimal) | no | A new line total, in the receipt's currency. | | `updateLines[].expenseAccountId` | string | no | A new GL account for a Service or Expense line. | | `updateLines[].notes` | string | no | New free text. Send an empty string to clear it. | | `updateLines[].batches` | array of object | no | The batches received on this line. Required for a batch-tracked item, and the quantities must add up to the line's quantity. When you send it on update, it replaces the line's batches. | | `updateLines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch. Omit it and give batchNumber for a batch arriving for the first time, and the batch record is created. | | `updateLines[].batches[].batchNumber` | string | no | The batch number as printed on the delivery. Required unless batchNumberId is given. | | `updateLines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `updateLines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch received, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `updateLines[].serials` | array of object | no | The serialized units received, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. When you send it on update, it replaces the line's serials. | | `updateLines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial record. Omit it and give serialNumber for a unit arriving for the first time. | | `updateLines[].serials[].serialNumber` | string | no | The serial number stamped on the unit. Required unless serialNumberId is given. | | `updateLines[].binLocations` | array of object | no | The bins the goods go into, in a bin-tracked warehouse. One flat array, where an entry may name a batchNumber or serialNumber of its own line. When you send it on update, it replaces the line's bin entries. | | `updateLines[].binLocations[].binLocationId` | string | yes | The bin the goods go into. | | `updateLines[].binLocations[].quantity` | string (decimal) | yes | The quantity going into this bin, in the line's unit. | | `updateLines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. | | `updateLines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | | `createLines` | array of object | no | The lines to add. | | `createLines[].lineType` | string, one of `"Inventory"`, `"Service"`, `"Expense"` | no | What this line receives. Defaults to Inventory. An Inventory line needs itemId, unitId, locationId and quantity, because it moves stock into a warehouse. A Service or Expense line needs serviceDescription and an amount, and has no item or warehouse. A line with a description and no item is taken as a Service line. | | `createLines[].itemId` | string | no | The item received. Required on an Inventory line, and not used on a Service or Expense line. | | `createLines[].serviceDescription` | string | no | What was bought. Required on a Service or Expense line, and not used on an Inventory line. | | `createLines[].locationId` | string | no | The warehouse the goods go into. Required on an Inventory line. | | `createLines[].unitId` | string | no | The unit the quantity is counted in. Required on an Inventory line. | | `createLines[].quantity` | string (decimal) | no | The quantity received, in that unit. Required on an Inventory line, and greater than zero. | | `createLines[].unitPrice` | string (decimal) | no | The price of one unit, in the receipt's currency. | | `createLines[].lineTotal` | string (decimal) | no | The total for the line, in the receipt's currency. On a Service or Expense line, give this or a unitPrice. | | `createLines[].expenseAccountId` | string | no | The GL account to charge. Required on a Service or Expense line, and not used on an Inventory line. Without it, the commit fails when it posts the journal entry. | | `createLines[].sourceDocumentLineId` | string | no | The purchase order line this line receives against, as its id under lines on GET accounting/purchase-orders/:id. Its numeric id in string form is also accepted. The line type is set from it. | | `createLines[].notes` | string | no | Free text on the line. | | `createLines[].batches` | array of object | no | The batches received on this line. Required for a batch-tracked item, and the quantities must add up to the line's quantity. When you send it on update, it replaces the line's batches. | | `createLines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch. Omit it and give batchNumber for a batch arriving for the first time, and the batch record is created. | | `createLines[].batches[].batchNumber` | string | no | The batch number as printed on the delivery. Required unless batchNumberId is given. | | `createLines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `createLines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch received, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `createLines[].serials` | array of object | no | The serialized units received, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. When you send it on update, it replaces the line's serials. | | `createLines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial record. Omit it and give serialNumber for a unit arriving for the first time. | | `createLines[].serials[].serialNumber` | string | no | The serial number stamped on the unit. Required unless serialNumberId is given. | | `createLines[].binLocations` | array of object | no | The bins the goods go into, in a bin-tracked warehouse. One flat array, where an entry may name a batchNumber or serialNumber of its own line. When you send it on update, it replaces the line's bin entries. | | `createLines[].binLocations[].binLocationId` | string | yes | The bin the goods go into. | | `createLines[].binLocations[].quantity` | string (decimal) | yes | The quantity going into this bin, in the line's unit. | | `createLines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. | | `createLines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | Example: ```json { "businessPartnerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "documentDate": "2026-01-15", "deliveryDate": "2026-01-15", "currency": "<currency>", "referenceNumber": "DN-77121", "notes": "<notes>", "deleteLines": [ "<deleteLines>" ], "updateLines": [ { "id": "8c14a6f2-3b70-4d59-9e02-6f5a1d38b47c", "quantity": "196.00" } ], "createLines": [ { "lineType": "Inventory", "itemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "serviceDescription": "<serviceDescription>", "locationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "unitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "unitPrice": "19.99", "lineTotal": "19.99", "expenseAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "sourceDocumentLineId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "notes": "<notes>", "batches": [ { "batchNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "batchNumber": "<batchNumber>", "expirationDate": "2026-01-15", "quantity": "19.99" } ], "serials": [ { "serialNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "serialNumber": "<serialNumber>" } ], "binLocations": [ { "binLocationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "batchNumber": "<batchNumber>", "serialNumber": "<serialNumber>" } ] } ] } ``` ## Response `200` Success Under `data` (updateReceiptOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the receipt. | | `documentNumber` | string | no | The receipt number, for example 'GR-2291'. Assigned when the receipt is created. | | `documentStatus` | string | yes | The status of the receipt. It is DRAFT when the receipt is created through this API and COMMITTED once you commit it with POST inventory/receipts/:id/commit. The receiving and invoicing processes set the other statuses. | | `documentDate` | string (date) | yes | The date of the receipt. Format YYYY-MM-DD. | | `deliveryDate` | string (date) | no | The date the goods arrived or are expected. Format YYYY-MM-DD. | | `entityId` | string | yes | The legal entity the receipt belongs to. | | `entityName` | string | no | The name of that legal entity. | | `businessPartnerId` | string | yes | The vendor the goods came from. | | `businessPartnerName` | string | no | The name of that vendor. | | `referenceNumber` | string | no | The vendor's own reference, typically their delivery note number. | | `currency` | string | no | The currency of the receipt's amounts. Format ISO 4217, for example 'EUR'. This is the vendor's currency, which need not be the entity's functional currency. See functionalUnitPrice on the line for the functional-currency amounts. | | `totalAmount` | string (decimal) | no | The total value of the receipt, in the receipt's currency. | | `notes` | string | no | Free text on the receipt. | | `createdDate` | string (date-time) | no | The date and time the receipt was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the receipt last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the receipt, oldest first. A receipt always has at least one line. | | `lines[].id` | string | yes | The unique identifier for the line. Use it in updateLines and deleteLines. | | `lines[].lineType` | string | no | The type of the line. Possible values: 'Inventory', 'Service', 'Expense'. Only an Inventory line moves stock. A Service or Expense line records something bought that does not enter a warehouse and has a description instead of an item. | | `lines[].itemId` | string | no | The item received. Absent on a Service or Expense line. | | `lines[].itemCode` | string | no | The short code of that item. | | `lines[].itemName` | string | no | The name of that item. | | `lines[].serviceDescription` | string | no | What a Service or Expense line is for. Absent on an Inventory line. | | `lines[].locationId` | string | no | The warehouse the goods went into. Absent on a Service or Expense line. | | `lines[].locationCode` | string | no | The short code of that warehouse. | | `lines[].locationName` | string | no | The name of that warehouse. | | `lines[].unitId` | string | no | The unit the quantity is counted in. | | `lines[].unitCode` | string | no | The short code of that unit. | | `lines[].quantity` | string (decimal) | no | The quantity received, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].receivedQuantity` | string (decimal) | no | The quantity actually received, where the receiving process records it separately. | | `lines[].openQuantity` | string (decimal) | no | The quantity still to be invoiced. | | `lines[].returnedQuantity` | string (decimal) | no | The quantity that has gone back on a purchase return. | | `lines[].unitPrice` | string (decimal) | no | The price of one unit, in the receipt's currency. | | `lines[].lineTotal` | string (decimal) | no | The total for the line, in the receipt's currency. | | `lines[].functionalUnitPrice` | string (decimal) | no | The unit price in the entity's functional currency. Present when the receipt is in another currency. | | `lines[].functionalLineTotal` | string (decimal) | no | The line total in the entity's functional currency. | | `lines[].expenseAccountId` | string | no | The GL account a Service or Expense line is charged to. Absent on an Inventory line. | | `lines[].expenseAccountNumber` | string | no | The number of that account. | | `lines[].expenseAccountName` | string | no | The name of that account. | | `lines[].status` | string | no | Where the line is in the receiving process. This is free text, with no fixed set of values. | | `lines[].sourceDocumentLineId` | string | no | The purchase order line this line was received against. It is a numeric id in string form, not a UUID. Purchase order lines are not individually addressable, so there is no endpoint that takes it. | | `lines[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | The kind of source document line this receipt line was created from. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches received on this line, for a batch-tracked item. Absent when the item is not batch-tracked. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record this entry created or matched. | | `lines[].batches[].batchNumber` | string | yes | The batch number as printed. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch received, in the line's unit. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].batches[].openQuantity` | string (decimal) | no | The quantity of this batch still to be invoiced. | | `lines[].batches[].returnedQuantity` | string (decimal) | no | The quantity of this batch that has gone back to the vendor. | | `lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the receipt's currency. | | `lines[].serials` | array of object | no | The serialized units received, one entry per unit. Absent when the item is not serialized. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record this entry created or matched. | | `lines[].serials[].serialNumber` | string | yes | The serial number as stamped on the unit. | | `lines[].serials[].openQuantity` | string (decimal) | no | The quantity of this unit still to be invoiced. | | `lines[].serials[].returnedQuantity` | string (decimal) | no | The quantity of this unit that has gone back to the vendor. | | `lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the receipt's currency. | | `lines[].binLocations` | array of object | no | The bins the goods went into, in a bin-tracked warehouse. One flat array per line: an entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `lines[].binLocations[].binLocationId` | string | yes | The bin the goods went into. | | `lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity put into this bin, in the line's unit. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `INCOMPLETE_LINE` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, INCOMPLETE_LINE, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Deletes the receipt with the given identifier. `DELETE /inventory/receipts/{id}` Group: Inventory › Receipts · Operation ID: `deleteReceipt` Deletes one receipt and everything under it: its lines and their batch, serial and bin rows. Only a draft or a canceled receipt can be deleted. A committed receipt has brought the stock in and posted what is owed, and is corrected with a purchase return or a credit. Deleting an id that does not exist fails with NOT_FOUND. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteReceipt) ## 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. | | `id` | path | string | yes | The unique identifier for the receipt to delete. | ## Response `200` Success Under `data` (deleteReceiptOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted receipt. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, POSTING_REJECTED - `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 --- # Commit Receipt `POST /inventory/receipts/{id}/commit` Group: Inventory › Receipts · Operation ID: `commitReceipt` Commits the receipt with the given identifier, bringing the stock in and posting what is owed. Commits one draft receipt: it posts the inventory transactions that bring the stock in and the accrual for what is now owed to the vendor. This is the step that moves stock and money. Before it, the receipt is a draft that can be edited or deleted. After it, neither. Only Inventory lines produce inventory transactions. Service and Expense lines go straight to the accrual. A committed receipt is corrected with a purchase return or a credit, not by editing it. Checks that the platform runs here, such as the warehouse accepting receipts, tracking rows adding up, and account determination resolving, arrive as POSTING_REJECTED with the platform's own message. [Open in the interactive reference](https://docs.api.lensing.ai/?op=commitReceipt) ## 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. | | `id` | path | string | yes | The unique identifier for the receipt to commit. | ## Response `200` Success Under `data` (commitReceiptOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the receipt. | | `documentStatus` | string | yes | The status of the receipt after committing. | | `journalEntryId` | string | no | The journal entry the commit posted, where it posted one. | | `warnings` | array of string | yes | Non-fatal advisories from the commit. An empty array on a clean run. The commit succeeded regardless of what is listed here. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, POSTING_REJECTED - `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 --- # Creates several receipts in one call. `POST /inventory/receipts/bulk` Group: Inventory › Receipts · Operation ID: `bulkCreateReceipts` Creates several receipts in one call, each in draft status. All or nothing: if any receipt is rejected, none is written, and 'index' on the error says which, counting from zero. Each entry is exactly what POST inventory/receipts takes, and the results come back in input order. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateReceipts) ## 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`: bulkCreateReceiptsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `receipts` | array of object | yes | The receipts to create. At least one, and each is exactly what POST inventory/receipts takes. | | `receipts[].businessPartnerId` | string | yes | The vendor the goods came from. Required. | | `receipts[].entityId` | string | no | The legal entity. Omit it and the first line's warehouse decides it, but a receipt of only service lines has no warehouse, so supply it there. | | `receipts[].documentDate` | string (date) | no | The date of the receipt. Use format YYYY-MM-DD. Defaults to today. | | `receipts[].deliveryDate` | string (date) | no | The date the goods arrived. Use format YYYY-MM-DD. | | `receipts[].currency` | string | no | The currency for every amount on the receipt. Format ISO 4217, for example 'EUR'. Defaults to the vendor's or the entity's, as the platform resolves it. | | `receipts[].referenceNumber` | string | no | The vendor's own reference, typically their delivery note number. | | `receipts[].notes` | string | no | Free text on the receipt. | | `receipts[].lines` | array of object | yes | The lines of the receipt. At least one is required. | | `receipts[].lines[].lineType` | string, one of `"Inventory"`, `"Service"`, `"Expense"` | no | What this line receives. Defaults to Inventory. An Inventory line needs itemId, unitId, locationId and quantity, because it moves stock into a warehouse. A Service or Expense line needs serviceDescription and an amount, and has no item or warehouse. A line with a description and no item is taken as a Service line. | | `receipts[].lines[].itemId` | string | no | The item received. Required on an Inventory line, and not used on a Service or Expense line. | | `receipts[].lines[].serviceDescription` | string | no | What was bought. Required on a Service or Expense line, and not used on an Inventory line. | | `receipts[].lines[].locationId` | string | no | The warehouse the goods go into. Required on an Inventory line. | | `receipts[].lines[].unitId` | string | no | The unit the quantity is counted in. Required on an Inventory line. | | `receipts[].lines[].quantity` | string (decimal) | no | The quantity received, in that unit. Required on an Inventory line, and greater than zero. | | `receipts[].lines[].unitPrice` | string (decimal) | no | The price of one unit, in the receipt's currency. | | `receipts[].lines[].lineTotal` | string (decimal) | no | The total for the line, in the receipt's currency. On a Service or Expense line, give this or a unitPrice. | | `receipts[].lines[].expenseAccountId` | string | no | The GL account to charge. Required on a Service or Expense line, and not used on an Inventory line. Without it, the commit fails when it posts the journal entry. | | `receipts[].lines[].sourceDocumentLineId` | string | no | The purchase order line this line receives against, as its id under lines on GET accounting/purchase-orders/:id. Its numeric id in string form is also accepted. The line type is set from it. | | `receipts[].lines[].notes` | string | no | Free text on the line. | | `receipts[].lines[].batches` | array of object | no | The batches received on this line. Required for a batch-tracked item, and the quantities must add up to the line's quantity. When you send it on update, it replaces the line's batches. | | `receipts[].lines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch. Omit it and give batchNumber for a batch arriving for the first time, and the batch record is created. | | `receipts[].lines[].batches[].batchNumber` | string | no | The batch number as printed on the delivery. Required unless batchNumberId is given. | | `receipts[].lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `receipts[].lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch received, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `receipts[].lines[].serials` | array of object | no | The serialized units received, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. When you send it on update, it replaces the line's serials. | | `receipts[].lines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial record. Omit it and give serialNumber for a unit arriving for the first time. | | `receipts[].lines[].serials[].serialNumber` | string | no | The serial number stamped on the unit. Required unless serialNumberId is given. | | `receipts[].lines[].binLocations` | array of object | no | The bins the goods go into, in a bin-tracked warehouse. One flat array, where an entry may name a batchNumber or serialNumber of its own line. When you send it on update, it replaces the line's bin entries. | | `receipts[].lines[].binLocations[].binLocationId` | string | yes | The bin the goods go into. | | `receipts[].lines[].binLocations[].quantity` | string (decimal) | yes | The quantity going into this bin, in the line's unit. | | `receipts[].lines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. | | `receipts[].lines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | Example: ```json { "receipts": [ { "businessPartnerId": "b62e4f17-0c93-4a58-8d21-5f70e3b9a4c6", "documentDate": "2026-08-14", "deliveryDate": "2026-08-14", "referenceNumber": "DN-77120", "currency": "EUR", "notes": "Received at the back dock", "lines": [ { "lineType": "Inventory", "itemId": "5f21c8b0-9d34-4a76-8e12-04b93c7de5a8", "locationId": "e1d84c07-5b39-4a26-9f70-2c8b613ad495", "unitId": "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "quantity": "200.00", "unitPrice": "3.20", "sourceDocumentLineId": "551043", "notes": "Two pallets" }, { "lineType": "Service", "serviceDescription": "Inbound freight", "lineTotal": "85.00", "expenseAccountId": "4b7c0e29-6d15-4830-9f42-8a3e57c1d604" } ] } ] } ``` ## Response `200` Success Under `data` (bulkCreateReceiptsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The created receipts, in input order. Match them to inputs by index. | | `items[].id` | string | yes | The unique identifier for the receipt. | | `items[].documentNumber` | string | no | The receipt number, for example 'GR-2291'. Assigned when the receipt is created. | | `items[].documentStatus` | string | yes | The status of the receipt. It is DRAFT when the receipt is created through this API and COMMITTED once you commit it with POST inventory/receipts/:id/commit. The receiving and invoicing processes set the other statuses. | | `items[].documentDate` | string (date) | yes | The date of the receipt. Format YYYY-MM-DD. | | `items[].deliveryDate` | string (date) | no | The date the goods arrived or are expected. Format YYYY-MM-DD. | | `items[].entityId` | string | yes | The legal entity the receipt belongs to. | | `items[].entityName` | string | no | The name of that legal entity. | | `items[].businessPartnerId` | string | yes | The vendor the goods came from. | | `items[].businessPartnerName` | string | no | The name of that vendor. | | `items[].referenceNumber` | string | no | The vendor's own reference, typically their delivery note number. | | `items[].currency` | string | no | The currency of the receipt's amounts. Format ISO 4217, for example 'EUR'. This is the vendor's currency, which need not be the entity's functional currency. See functionalUnitPrice on the line for the functional-currency amounts. | | `items[].totalAmount` | string (decimal) | no | The total value of the receipt, in the receipt's currency. | | `items[].notes` | string | no | Free text on the receipt. | | `items[].createdDate` | string (date-time) | no | The date and time the receipt was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the receipt last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | yes | The lines of the receipt, oldest first. A receipt always has at least one line. | | `items[].lines[].id` | string | yes | The unique identifier for the line. Use it in updateLines and deleteLines. | | `items[].lines[].lineType` | string | no | The type of the line. Possible values: 'Inventory', 'Service', 'Expense'. Only an Inventory line moves stock. A Service or Expense line records something bought that does not enter a warehouse and has a description instead of an item. | | `items[].lines[].itemId` | string | no | The item received. Absent on a Service or Expense line. | | `items[].lines[].itemCode` | string | no | The short code of that item. | | `items[].lines[].itemName` | string | no | The name of that item. | | `items[].lines[].serviceDescription` | string | no | What a Service or Expense line is for. Absent on an Inventory line. | | `items[].lines[].locationId` | string | no | The warehouse the goods went into. Absent on a Service or Expense line. | | `items[].lines[].locationCode` | string | no | The short code of that warehouse. | | `items[].lines[].locationName` | string | no | The name of that warehouse. | | `items[].lines[].unitId` | string | no | The unit the quantity is counted in. | | `items[].lines[].unitCode` | string | no | The short code of that unit. | | `items[].lines[].quantity` | string (decimal) | no | The quantity received, in the line's unit. | | `items[].lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].receivedQuantity` | string (decimal) | no | The quantity actually received, where the receiving process records it separately. | | `items[].lines[].openQuantity` | string (decimal) | no | The quantity still to be invoiced. | | `items[].lines[].returnedQuantity` | string (decimal) | no | The quantity that has gone back on a purchase return. | | `items[].lines[].unitPrice` | string (decimal) | no | The price of one unit, in the receipt's currency. | | `items[].lines[].lineTotal` | string (decimal) | no | The total for the line, in the receipt's currency. | | `items[].lines[].functionalUnitPrice` | string (decimal) | no | The unit price in the entity's functional currency. Present when the receipt is in another currency. | | `items[].lines[].functionalLineTotal` | string (decimal) | no | The line total in the entity's functional currency. | | `items[].lines[].expenseAccountId` | string | no | The GL account a Service or Expense line is charged to. Absent on an Inventory line. | | `items[].lines[].expenseAccountNumber` | string | no | The number of that account. | | `items[].lines[].expenseAccountName` | string | no | The name of that account. | | `items[].lines[].status` | string | no | Where the line is in the receiving process. This is free text, with no fixed set of values. | | `items[].lines[].sourceDocumentLineId` | string | no | The purchase order line this line was received against. It is a numeric id in string form, not a UUID. Purchase order lines are not individually addressable, so there is no endpoint that takes it. | | `items[].lines[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | The kind of source document line this receipt line was created from. | | `items[].lines[].notes` | string | no | Free text on the line. | | `items[].lines[].batches` | array of object | no | The batches received on this line, for a batch-tracked item. Absent when the item is not batch-tracked. | | `items[].lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record this entry created or matched. | | `items[].lines[].batches[].batchNumber` | string | yes | The batch number as printed. | | `items[].lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `items[].lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch received, in the line's unit. | | `items[].lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].batches[].openQuantity` | string (decimal) | no | The quantity of this batch still to be invoiced. | | `items[].lines[].batches[].returnedQuantity` | string (decimal) | no | The quantity of this batch that has gone back to the vendor. | | `items[].lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the receipt's currency. | | `items[].lines[].serials` | array of object | no | The serialized units received, one entry per unit. Absent when the item is not serialized. | | `items[].lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record this entry created or matched. | | `items[].lines[].serials[].serialNumber` | string | yes | The serial number as stamped on the unit. | | `items[].lines[].serials[].openQuantity` | string (decimal) | no | The quantity of this unit still to be invoiced. | | `items[].lines[].serials[].returnedQuantity` | string (decimal) | no | The quantity of this unit that has gone back to the vendor. | | `items[].lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the receipt's currency. | | `items[].lines[].binLocations` | array of object | no | The bins the goods went into, in a bin-tracked warehouse. One flat array per line: an entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `items[].lines[].binLocations[].binLocationId` | string | yes | The bin the goods went into. | | `items[].lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `items[].lines[].binLocations[].quantity` | string (decimal) | yes | The quantity put into this bin, in the line's unit. | | `items[].lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `items[].lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | ## 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 | | --- | --- | | `NOTHING_TO_CREATE` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `INCOMPLETE_LINE` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOTHING_TO_CREATE, REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, INCOMPLETE_LINE, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Deletes several receipts in one call. `POST /inventory/receipts/bulk-delete` Group: Inventory › Receipts · Operation ID: `bulkDeleteReceipts` Deletes several draft receipts in one call. All or nothing: if any id matches no receipt, or names one past draft, nothing is deleted, and 'index' on the error says which, counting from zero. Delete a single receipt with DELETE inventory/receipts/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteReceipts) ## 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`: bulkDeleteReceiptsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the receipts to delete. All or nothing: if any is missing or past draft, none is deleted. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteReceiptsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the deleted receipts, in input order. | ## 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 | | --- | --- | | `NOTHING_TO_DELETE` | 400 | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOTHING_TO_DELETE, NOT_FOUND, WRONG_STATUS, POSTING_REJECTED - `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 --- # Returns the receipts that match the given filters. `POST /inventory/receipts/search` Group: Inventory › Receipts · Operation ID: `searchReceipts` Returns the receipts that match the given filters, each with its full lines and their batch, serial and bin detail, the same fields GET inventory/receipts/:id returns. This is the only endpoint that returns the lines, and the only one that filters by vendor, reference number, item or warehouse. Every criterion is exact or a range, and criteria combine with AND. Send an empty filters object to return every receipt. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchReceipts) ## 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`. | ## Request body Required, `application/json`: searchReceiptsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. A receipt must match every filter you send. Send an empty object to return all receipts. | | `filters.documentNumber` | string | no | Limits results to the receipt with this receipt number. The match is exact. | | `filters.documentStatus` | string | no | Limits results to receipts with this status. The match is exact. | | `filters.entityId` | string | no | Limits results to receipts for this legal entity. | | `filters.businessPartnerId` | string | no | Limits results to receipts from this vendor. | | `filters.locationId` | string | no | Limits results to receipts that have a line for this warehouse. | | `filters.itemId` | string | no | Limits results to receipts that have a line for this item. | | `filters.referenceNumber` | string | no | Limits results to receipts with this vendor reference. The match is exact. | | `filters.fromDate` | string (date) | no | Limits results to receipts dated on or after this date. Format YYYY-MM-DD. | | `filters.toDate` | string (date) | no | Limits results to receipts dated on or before this date. Format YYYY-MM-DD. | | `filters.modifiedSince` | string (date-time) | no | Limits results to receipts changed on or after this time. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of receipts per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "documentStatus": "DRAFT" }, "page": { "cursor": "<cursor>", "size": 0 } } ``` ## Response `200` Success Under `data` (searchReceiptsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The receipts on this page, oldest first, each with its lines. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the receipt. | | `items[].documentNumber` | string | no | The receipt number, for example 'GR-2291'. Assigned when the receipt is created. | | `items[].documentStatus` | string | yes | The status of the receipt. It is DRAFT when the receipt is created through this API and COMMITTED once you commit it with POST inventory/receipts/:id/commit. The receiving and invoicing processes set the other statuses. | | `items[].documentDate` | string (date) | yes | The date of the receipt. Format YYYY-MM-DD. | | `items[].deliveryDate` | string (date) | no | The date the goods arrived or are expected. Format YYYY-MM-DD. | | `items[].entityId` | string | yes | The legal entity the receipt belongs to. | | `items[].entityName` | string | no | The name of that legal entity. | | `items[].businessPartnerId` | string | yes | The vendor the goods came from. | | `items[].businessPartnerName` | string | no | The name of that vendor. | | `items[].referenceNumber` | string | no | The vendor's own reference, typically their delivery note number. | | `items[].currency` | string | no | The currency of the receipt's amounts. Format ISO 4217, for example 'EUR'. This is the vendor's currency, which need not be the entity's functional currency. See functionalUnitPrice on the line for the functional-currency amounts. | | `items[].totalAmount` | string (decimal) | no | The total value of the receipt, in the receipt's currency. | | `items[].notes` | string | no | Free text on the receipt. | | `items[].createdDate` | string (date-time) | no | The date and time the receipt was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the receipt last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | yes | The lines of the receipt, oldest first. A receipt always has at least one line. | | `items[].lines[].id` | string | yes | The unique identifier for the line. Use it in updateLines and deleteLines. | | `items[].lines[].lineType` | string | no | The type of the line. Possible values: 'Inventory', 'Service', 'Expense'. Only an Inventory line moves stock. A Service or Expense line records something bought that does not enter a warehouse and has a description instead of an item. | | `items[].lines[].itemId` | string | no | The item received. Absent on a Service or Expense line. | | `items[].lines[].itemCode` | string | no | The short code of that item. | | `items[].lines[].itemName` | string | no | The name of that item. | | `items[].lines[].serviceDescription` | string | no | What a Service or Expense line is for. Absent on an Inventory line. | | `items[].lines[].locationId` | string | no | The warehouse the goods went into. Absent on a Service or Expense line. | | `items[].lines[].locationCode` | string | no | The short code of that warehouse. | | `items[].lines[].locationName` | string | no | The name of that warehouse. | | `items[].lines[].unitId` | string | no | The unit the quantity is counted in. | | `items[].lines[].unitCode` | string | no | The short code of that unit. | | `items[].lines[].quantity` | string (decimal) | no | The quantity received, in the line's unit. | | `items[].lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].receivedQuantity` | string (decimal) | no | The quantity actually received, where the receiving process records it separately. | | `items[].lines[].openQuantity` | string (decimal) | no | The quantity still to be invoiced. | | `items[].lines[].returnedQuantity` | string (decimal) | no | The quantity that has gone back on a purchase return. | | `items[].lines[].unitPrice` | string (decimal) | no | The price of one unit, in the receipt's currency. | | `items[].lines[].lineTotal` | string (decimal) | no | The total for the line, in the receipt's currency. | | `items[].lines[].functionalUnitPrice` | string (decimal) | no | The unit price in the entity's functional currency. Present when the receipt is in another currency. | | `items[].lines[].functionalLineTotal` | string (decimal) | no | The line total in the entity's functional currency. | | `items[].lines[].expenseAccountId` | string | no | The GL account a Service or Expense line is charged to. Absent on an Inventory line. | | `items[].lines[].expenseAccountNumber` | string | no | The number of that account. | | `items[].lines[].expenseAccountName` | string | no | The name of that account. | | `items[].lines[].status` | string | no | Where the line is in the receiving process. This is free text, with no fixed set of values. | | `items[].lines[].sourceDocumentLineId` | string | no | The purchase order line this line was received against. It is a numeric id in string form, not a UUID. Purchase order lines are not individually addressable, so there is no endpoint that takes it. | | `items[].lines[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | The kind of source document line this receipt line was created from. | | `items[].lines[].notes` | string | no | Free text on the line. | | `items[].lines[].batches` | array of object | no | The batches received on this line, for a batch-tracked item. Absent when the item is not batch-tracked. | | `items[].lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record this entry created or matched. | | `items[].lines[].batches[].batchNumber` | string | yes | The batch number as printed. | | `items[].lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `items[].lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch received, in the line's unit. | | `items[].lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].batches[].openQuantity` | string (decimal) | no | The quantity of this batch still to be invoiced. | | `items[].lines[].batches[].returnedQuantity` | string (decimal) | no | The quantity of this batch that has gone back to the vendor. | | `items[].lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the receipt's currency. | | `items[].lines[].serials` | array of object | no | The serialized units received, one entry per unit. Absent when the item is not serialized. | | `items[].lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record this entry created or matched. | | `items[].lines[].serials[].serialNumber` | string | yes | The serial number as stamped on the unit. | | `items[].lines[].serials[].openQuantity` | string (decimal) | no | The quantity of this unit still to be invoiced. | | `items[].lines[].serials[].returnedQuantity` | string (decimal) | no | The quantity of this unit that has gone back to the vendor. | | `items[].lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the receipt's currency. | | `items[].lines[].binLocations` | array of object | no | The bins the goods went into, in a bin-tracked warehouse. One flat array per line: an entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `items[].lines[].binLocations[].binLocationId` | string | yes | The bin the goods went into. | | `items[].lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `items[].lines[].binLocations[].quantity` | string (decimal) | yes | The quantity put into this bin, in the line's unit. | | `items[].lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `items[].lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more receipts remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Sales Returns `GET /inventory/sales-returns` Group: Inventory › Sales Returns · Operation ID: `salesReturns` Returns a list of sales returns. To filter by customer, reference number, tracking number, item or warehouse, use POST inventory/sales-returns/search. The sales return collection. It returns headers only: a nested array has no representation here, so lineCount stands in for the lines. GET inventory/sales-returns/:id and POST inventory/sales-returns/search return the full document. The customer, their reference number and the carrier's tracking number are returned but not filterable here. They identify a person and their transaction, so they belong in a request body. POST inventory/sales-returns/search takes all three. entityId is filterable, so a collection can be scoped to one legal entity. Filtering by item or warehouse is not possible here because both live on the lines. Search takes them. [Open in the interactive reference](https://docs.api.lensing.ai/?op=salesReturns) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `deliveryDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `returnType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, documentNumber, documentDate, deliveryDate, documentStatus, totalAmount, createdDate, lastModifiedDate - Selectable: id, documentNumber, documentStatus, documentDate, deliveryDate, entityId, entityName, businessPartnerId, businessPartnerName, currency, totalAmount, returnFeeAmount, returnType, lineCount, referenceNumber, trackingNumber, sourceDocumentId, sourceDocumentType, notes, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of salesReturnsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the sales return. | | `documentNumber` | string or null | no | The sales return number, for example 'SR-1042'. Assigned on create. | | `documentStatus` | string | no | The status of the return. Possible values: 'DRAFT', 'OPEN', 'COMMITTED', 'RECEIVED', 'INSPECTED', 'PROCESSED', 'COMPLETED', 'CANCELLED'. | | `documentDate` | string (date) | no | The date the return is dated. Format YYYY-MM-DD. | | `deliveryDate` | string (date) or null | no | When the goods are expected back, or came back. Format YYYY-MM-DD. | | `entityId` | string | no | The unique identifier for the legal entity. See id on GET base/entities. | | `entityName` | string or null | no | The name of that legal entity. | | `businessPartnerId` | string | no | The unique identifier for the customer. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `businessPartnerName` | string or null | no | The name of that customer. | | `currency` | string or null | no | The currency the return is valued in. Format ISO 4217, for example 'EUR'. | | `totalAmount` | string (decimal) or null | no | The total value of the return, in the return's currency. | | `returnFeeAmount` | string (decimal) or null | no | The fee withheld across the return, in the document's currency. | | `returnType` | string or null | no | How the business classifies the return. | | `lineCount` | number or null | no | How many lines the return has. Published because the lines themselves are not. | | `referenceNumber` | string or null | no | The customer's own reference for the return, for example an RMA number. | | `trackingNumber` | string or null | no | The carrier's tracking number for the returned goods. | | `sourceDocumentId` | string or null | no | The document the goods went out on, where the platform recorded one, as its own numeric id in a string. It is not an identifier this API can look up. The usable link is sourceDocumentLineId on the line, which GET inventory/sales-returns/:id and POST inventory/sales-returns/search return. | | `sourceDocumentType` | string or null | no | What kind of document that id refers to, for example 'SalesDelivery'. | | `notes` | string or null | no | Free text on the document. | | `createdDate` | string (date-time) or null | no | The date and time the return was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the return record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a sales return. `POST /inventory/sales-returns` Group: Inventory › Sales Returns · Operation ID: `createSalesReturn` Creates one sales return, a draft. Nothing moves: no stock, no credit. The document records what the customer says is coming back. POST inventory/sales-returns/:id/commit is what accepts it, bringing the goods back into stock, writing the postings and raising the credit memo where one is due. condition and disposition on a line are expectations here, what the customer reported and what you intend to do. A batch-tracked item needs batches adding up to the line's quantity, and a serialized item needs one serials entry per unit. The whole call is one transaction: a rejected request writes nothing. The response is the created return in the shape GET inventory/sales-returns/:id returns. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createSalesReturn) ## 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`: createSalesReturnInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `businessPartnerId` | string | yes | The unique identifier for the customer returning the goods. Required, a return always comes from one. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `entityId` | string | no | The unique identifier for the legal entity. Omit it and the first line's warehouse decides it. See id on GET base/entities. | | `documentDate` | string (date) | no | The date the return is dated. Defaults to today. Use format YYYY-MM-DD. | | `deliveryDate` | string (date) | no | When the goods are expected back. Use format YYYY-MM-DD. | | `currency` | string | no | The currency for every amount on the return. Format ISO 4217, for example 'EUR'. Defaults to the legal entity's functional currency. Set it when the sale was in another. | | `referenceNumber` | string | no | The customer's own reference, for example an RMA number. | | `trackingNumber` | string | no | The carrier's tracking number for the goods coming back. | | `returnType` | string | no | How the business classifies the return. | | `returnFeeAmount` | string (decimal) | no | The fee withheld across the return, in the document's currency. | | `notes` | string | no | Free text on the document. | | `lines` | array of object | yes | The lines of the return. At least one is required. | | `lines[].itemId` | string | yes | The unique identifier for the item coming back. See id on GET inventory/items/:id. | | `lines[].locationId` | string | yes | The unique identifier for the warehouse the goods come back to. Each line of a return may use a different warehouse. See id on GET inventory/warehouses/:id. | | `lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `lines[].quantity` | string (decimal) | yes | The quantity the customer is returning, in that unit. Greater than zero. | | `lines[].unitPrice` | string (decimal) | no | The value of one unit, in the document's currency. Omit it to take the price from the sale. | | `lines[].condition` | string, one of `"NEW"`, `"GOOD"`, `"USED"`, `"OPENED"`, `"DAMAGED"`, `"DEFECTIVE"`, `"EXPIRED"`, `"REFURBISHED"` | no | The condition the goods are expected in. The inspection may find otherwise and restates it. | | `lines[].disposition` | string, one of `"RETURN_TO_STOCK"`, `"RETURN_TO_VENDOR"`, `"SCRAP"`, `"DONATE"`, `"REPAIR"`, `"REWORK"`, `"CREDIT_ONLY"` | no | What you intend to do with the goods. The inspection may change it. | | `lines[].reasonCode` | string | no | The customer-facing reason for the return, for example 'defectiveItem'. Reporting only. | | `lines[].reasonCodeId` | string | no | The unique identifier for an internal reason code. See id on GET inventory/reason-codes/:id. | | `lines[].returnFeeAmount` | string (decimal) | no | The fee withheld on this line, in the document's currency. | | `lines[].sourceDocumentLineId` | string | no | The shipment line the goods went out on, where the return follows a delivery. It is a line's id on GET inventory/shipments/:id. Naming it lets the commit value the return from the original shipment cost. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches coming back on this line. Required for a batch-tracked item, and the quantities must add up to the line's quantity. On PATCH inventory/sales-returns/:id this replaces the line's batches. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch. Omit it and give batchNumber for a lot the system has not seen. | | `lines[].batches[].batchNumber` | string | no | The batch number as printed. Required unless batchNumberId is given. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the lot. Format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch coming back, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `lines[].serials` | array of object | no | The serialized units coming back, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. On PATCH inventory/sales-returns/:id this replaces the line's serials. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial record. Omit it and give serialNumber. | | `lines[].serials[].serialNumber` | string | no | The serial number stamped on the unit. Required unless serialNumberId is given. | | `lines[].binLocations` | array of object | no | The bins the goods go into, in a bin-tracked warehouse. One flat array. An entry may name a batchNumber or serialNumber of its own line. On PATCH inventory/sales-returns/:id this replaces the line's bin entries. | | `lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the goods go into. See id on GET inventory/bins/:id. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity going into this bin, in the line's unit. | | `lines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. | | `lines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | Example: ```json { "businessPartnerId": "c73f2b81-5d94-4e60-a812-0f9b6d3e45c7", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "documentDate": "2026-08-14", "deliveryDate": "2026-08-18", "currency": "EUR", "referenceNumber": "RMA-2026-0087", "trackingNumber": "3SABCD1234567", "returnType": "<returnType>", "returnFeeAmount": "19.99", "notes": "Customer reports transit damage", "lines": [ { "itemId": "5f21c8b0-9d34-4a76-8e12-04b93c7de5a8", "locationId": "e1d84c07-5b39-4a26-9f70-2c8b613ad495", "unitId": "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "quantity": "6.00", "unitPrice": "24.00", "condition": "DAMAGED", "disposition": "SCRAP", "reasonCode": "damagedInTransit", "sourceDocumentLineId": "5c1d9a72-0e64-4a1f-8b39-2c7f4a6e15d8", "notes": "Carton crushed in transit" } ] } ``` ## Response `200` Success Under `data` (createSalesReturnOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the sales return. | | `documentNumber` | string | no | The sales return number, for example 'SR-1042'. Assigned on create and never supplied by you. | | `documentStatus` | string | yes | The status of the return. Possible values: 'DRAFT', 'OPEN', 'COMMITTED', 'RECEIVED', 'INSPECTED', 'PROCESSED', 'COMPLETED', 'CANCELLED'. | | `documentDate` | string (date) | yes | The date the return is dated. Format YYYY-MM-DD. | | `deliveryDate` | string (date) | no | When the goods are expected back, or came back. Format YYYY-MM-DD. | | `entityId` | string | yes | The unique identifier for the legal entity the return belongs to. See id on GET base/entities. | | `entityName` | string | no | The name of that legal entity. | | `businessPartnerId` | string | yes | The unique identifier for the customer returning the goods. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `businessPartnerName` | string | no | The name of that customer. | | `sourceDocumentId` | string | no | The document the goods went out on, where the platform recorded one, as its own numeric id in a string. It is not an identifier this API can look up. The usable link is sourceDocumentLineId on the line. | | `sourceDocumentType` | string | no | What kind of document that was, for example 'SalesDelivery'. | | `referenceNumber` | string | no | The customer's own reference for the return, for example an RMA number. | | `trackingNumber` | string | no | The carrier's tracking number for the goods coming back. | | `returnType` | string | no | How the business classifies the return. | | `currency` | string | no | The currency every amount on the document is in. Format ISO 4217, for example 'EUR'. A return is valued in the currency it was sold in, which need not be the legal entity's functional currency. | | `totalAmount` | string (decimal) | no | The total value of the return, in the document's currency. | | `returnFeeAmount` | string (decimal) | no | The fee withheld across the return, in the document's currency. | | `notes` | string | no | Free text on the document. | | `createdDate` | string (date-time) | no | The date and time the return was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the return record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the return, oldest first. A sales return always has at least one. | | `lines[].id` | string | yes | The unique identifier for the line. Use it in updateLines and deleteLines on PATCH inventory/sales-returns/:id. | | `lines[].itemId` | string | yes | The unique identifier for the item coming back. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The short code of that item. | | `lines[].itemName` | string | no | The name of that item. | | `lines[].locationId` | string | yes | The unique identifier for the warehouse the goods come back to. Each line of a return may use a different warehouse. See id on GET inventory/warehouses/:id. | | `lines[].locationCode` | string | no | The short code of that warehouse. | | `lines[].locationName` | string | no | The name of that warehouse. | | `lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `lines[].unitCode` | string | no | The short code of that unit. | | `lines[].quantity` | string (decimal) | yes | The quantity the customer is returning, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].openQuantity` | string (decimal) | no | How much of the line has not yet been received. | | `lines[].inspectedQuantity` | string (decimal) | no | How much of the line the inspection covered. | | `lines[].approvedQuantity` | string (decimal) | no | How much of the line was approved back into stock. | | `lines[].rejectedQuantity` | string (decimal) | no | How much of the line was rejected. Approved plus rejected equals inspected. | | `lines[].inspectedDate` | string (date) | no | When the inspection took place. Format YYYY-MM-DD. | | `lines[].condition` | string | no | The condition the goods arrived in, restated by the inspection. Possible values: 'NEW', 'GOOD', 'USED', 'OPENED', 'DAMAGED', 'DEFECTIVE', 'EXPIRED', 'REFURBISHED'. | | `lines[].disposition` | string | no | What is to be done with the goods. Possible values: 'RETURN_TO_STOCK', 'RETURN_TO_VENDOR', 'SCRAP', 'DONATE', 'REPAIR', 'REWORK', 'CREDIT_ONLY'. | | `lines[].qualityGrade` | string | no | The grade assigned at inspection. | | `lines[].inspectionNotes` | string | no | Free text from the inspector. | | `lines[].rejectionReason` | string | no | Why the rejected portion was rejected. | | `lines[].unitPrice` | string (decimal) | no | The value of one unit, in the document's currency. | | `lines[].returnFeeAmount` | string (decimal) | no | The fee withheld on this line, in the document's currency. | | `lines[].reasonCode` | string | no | The customer-facing reason for the return, for example 'defectiveItem'. Reporting only, it does not affect how the return is processed. | | `lines[].reasonCodeId` | string | no | The unique identifier for an internal reason code, where one was set. See id on GET inventory/reason-codes/:id. | | `lines[].sourceDocumentLineId` | string | no | The shipment line the goods went out on, where the return cites one. It is a line's id on GET inventory/shipments/:id. | | `lines[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | What kind of document line that was. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches coming back on this line, for a batch-tracked item. Absent when the item is not batch-tracked. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record. | | `lines[].batches[].batchNumber` | string | yes | The batch number as printed. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the lot. Format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch coming back, in the line's unit. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].batches[].openQuantity` | string (decimal) | no | How much of this batch has not yet been received. | | `lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the document's currency. | | `lines[].serials` | array of object | no | The serialized units coming back, one entry per unit. Absent when the item is not serialized. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record. | | `lines[].serials[].serialNumber` | string | yes | The serial number as stamped on the unit. | | `lines[].serials[].openQuantity` | string (decimal) | no | How much of this unit is still outstanding, one or zero. | | `lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the document's currency. | | `lines[].binLocations` | array of object | no | The bins the goods go into, in a bin-tracked warehouse. One flat array. An entry naming a batchNumber or serialNumber belongs to that batch or unit, and one naming neither belongs to the line. | | `lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the goods go into. See id on GET inventory/bins/:id. | | `lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity going into this bin, in the line's unit. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | Set when this bin entry belongs to one of the line's batches, naming it. | | `lines[].binLocations[].serialNumber` | string | no | Set when this bin entry belongs to one of the line's serialized units, naming it. | | `lines[].inspectionDetails` | array of object | no | What the inspection found, broken down by batch or unit. Absent before the inspection runs. The line has the totals and these give the detail. | | `lines[].inspectionDetails[].approvalStatus` | string | yes | What was decided about this portion, recorded as given. | | `lines[].inspectionDetails[].inspectedQuantity` | string (decimal) | yes | How much this finding covers. | | `lines[].inspectionDetails[].condition` | string | no | The condition of this portion. Possible values: 'NEW', 'GOOD', 'USED', 'OPENED', 'DAMAGED', 'DEFECTIVE', 'EXPIRED', 'REFURBISHED'. | | `lines[].inspectionDetails[].disposition` | string | no | What is to be done with this portion. Possible values: 'RETURN_TO_STOCK', 'RETURN_TO_VENDOR', 'SCRAP', 'DONATE', 'REPAIR', 'REWORK', 'CREDIT_ONLY'. | | `lines[].inspectionDetails[].qualityGrade` | string | no | The grade assigned at inspection, where the business uses one. | | `lines[].inspectionDetails[].inspectionNotes` | string | no | Free text from the inspector. | | `lines[].inspectionDetails[].rejectionReason` | string | no | Why this portion was rejected, where it was. | | `lines[].inspectionDetails[].batchNumber` | string | no | The batch this finding is about, where it is about one. | | `lines[].inspectionDetails[].serialNumber` | string | no | The serialized unit this finding is about, where it is about one. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Returns the sales return with the given identifier. `GET /inventory/sales-returns/{id}` Group: Inventory › Sales Returns · Operation ID: `getSalesReturn` Returns one sales return by its identifier, with all of its lines and their batch, serial, bin and inspection detail. The item schema is identical here and on POST inventory/sales-returns/search. The collection GET inventory/sales-returns returns the header only, so it cannot include the lines. documentStatus shows where the return is in its lifecycle: a draft can be updated, committed or deleted, and after commit the return is RECEIVED. The inspection detail appears once the goods have been inspected. Before that the lines show what was expected. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getSalesReturn) ## 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`. | | `id` | path | string | yes | The unique identifier for the sales return to return. | ## Response `200` Success Under `data` (getSalesReturnOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the sales return. | | `documentNumber` | string | no | The sales return number, for example 'SR-1042'. Assigned on create and never supplied by you. | | `documentStatus` | string | yes | The status of the return. Possible values: 'DRAFT', 'OPEN', 'COMMITTED', 'RECEIVED', 'INSPECTED', 'PROCESSED', 'COMPLETED', 'CANCELLED'. | | `documentDate` | string (date) | yes | The date the return is dated. Format YYYY-MM-DD. | | `deliveryDate` | string (date) | no | When the goods are expected back, or came back. Format YYYY-MM-DD. | | `entityId` | string | yes | The unique identifier for the legal entity the return belongs to. See id on GET base/entities. | | `entityName` | string | no | The name of that legal entity. | | `businessPartnerId` | string | yes | The unique identifier for the customer returning the goods. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `businessPartnerName` | string | no | The name of that customer. | | `sourceDocumentId` | string | no | The document the goods went out on, where the platform recorded one, as its own numeric id in a string. It is not an identifier this API can look up. The usable link is sourceDocumentLineId on the line. | | `sourceDocumentType` | string | no | What kind of document that was, for example 'SalesDelivery'. | | `referenceNumber` | string | no | The customer's own reference for the return, for example an RMA number. | | `trackingNumber` | string | no | The carrier's tracking number for the goods coming back. | | `returnType` | string | no | How the business classifies the return. | | `currency` | string | no | The currency every amount on the document is in. Format ISO 4217, for example 'EUR'. A return is valued in the currency it was sold in, which need not be the legal entity's functional currency. | | `totalAmount` | string (decimal) | no | The total value of the return, in the document's currency. | | `returnFeeAmount` | string (decimal) | no | The fee withheld across the return, in the document's currency. | | `notes` | string | no | Free text on the document. | | `createdDate` | string (date-time) | no | The date and time the return was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the return record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the return, oldest first. A sales return always has at least one. | | `lines[].id` | string | yes | The unique identifier for the line. Use it in updateLines and deleteLines on PATCH inventory/sales-returns/:id. | | `lines[].itemId` | string | yes | The unique identifier for the item coming back. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The short code of that item. | | `lines[].itemName` | string | no | The name of that item. | | `lines[].locationId` | string | yes | The unique identifier for the warehouse the goods come back to. Each line of a return may use a different warehouse. See id on GET inventory/warehouses/:id. | | `lines[].locationCode` | string | no | The short code of that warehouse. | | `lines[].locationName` | string | no | The name of that warehouse. | | `lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `lines[].unitCode` | string | no | The short code of that unit. | | `lines[].quantity` | string (decimal) | yes | The quantity the customer is returning, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].openQuantity` | string (decimal) | no | How much of the line has not yet been received. | | `lines[].inspectedQuantity` | string (decimal) | no | How much of the line the inspection covered. | | `lines[].approvedQuantity` | string (decimal) | no | How much of the line was approved back into stock. | | `lines[].rejectedQuantity` | string (decimal) | no | How much of the line was rejected. Approved plus rejected equals inspected. | | `lines[].inspectedDate` | string (date) | no | When the inspection took place. Format YYYY-MM-DD. | | `lines[].condition` | string | no | The condition the goods arrived in, restated by the inspection. Possible values: 'NEW', 'GOOD', 'USED', 'OPENED', 'DAMAGED', 'DEFECTIVE', 'EXPIRED', 'REFURBISHED'. | | `lines[].disposition` | string | no | What is to be done with the goods. Possible values: 'RETURN_TO_STOCK', 'RETURN_TO_VENDOR', 'SCRAP', 'DONATE', 'REPAIR', 'REWORK', 'CREDIT_ONLY'. | | `lines[].qualityGrade` | string | no | The grade assigned at inspection. | | `lines[].inspectionNotes` | string | no | Free text from the inspector. | | `lines[].rejectionReason` | string | no | Why the rejected portion was rejected. | | `lines[].unitPrice` | string (decimal) | no | The value of one unit, in the document's currency. | | `lines[].returnFeeAmount` | string (decimal) | no | The fee withheld on this line, in the document's currency. | | `lines[].reasonCode` | string | no | The customer-facing reason for the return, for example 'defectiveItem'. Reporting only, it does not affect how the return is processed. | | `lines[].reasonCodeId` | string | no | The unique identifier for an internal reason code, where one was set. See id on GET inventory/reason-codes/:id. | | `lines[].sourceDocumentLineId` | string | no | The shipment line the goods went out on, where the return cites one. It is a line's id on GET inventory/shipments/:id. | | `lines[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | What kind of document line that was. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches coming back on this line, for a batch-tracked item. Absent when the item is not batch-tracked. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record. | | `lines[].batches[].batchNumber` | string | yes | The batch number as printed. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the lot. Format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch coming back, in the line's unit. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].batches[].openQuantity` | string (decimal) | no | How much of this batch has not yet been received. | | `lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the document's currency. | | `lines[].serials` | array of object | no | The serialized units coming back, one entry per unit. Absent when the item is not serialized. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record. | | `lines[].serials[].serialNumber` | string | yes | The serial number as stamped on the unit. | | `lines[].serials[].openQuantity` | string (decimal) | no | How much of this unit is still outstanding, one or zero. | | `lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the document's currency. | | `lines[].binLocations` | array of object | no | The bins the goods go into, in a bin-tracked warehouse. One flat array. An entry naming a batchNumber or serialNumber belongs to that batch or unit, and one naming neither belongs to the line. | | `lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the goods go into. See id on GET inventory/bins/:id. | | `lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity going into this bin, in the line's unit. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | Set when this bin entry belongs to one of the line's batches, naming it. | | `lines[].binLocations[].serialNumber` | string | no | Set when this bin entry belongs to one of the line's serialized units, naming it. | | `lines[].inspectionDetails` | array of object | no | What the inspection found, broken down by batch or unit. Absent before the inspection runs. The line has the totals and these give the detail. | | `lines[].inspectionDetails[].approvalStatus` | string | yes | What was decided about this portion, recorded as given. | | `lines[].inspectionDetails[].inspectedQuantity` | string (decimal) | yes | How much this finding covers. | | `lines[].inspectionDetails[].condition` | string | no | The condition of this portion. Possible values: 'NEW', 'GOOD', 'USED', 'OPENED', 'DAMAGED', 'DEFECTIVE', 'EXPIRED', 'REFURBISHED'. | | `lines[].inspectionDetails[].disposition` | string | no | What is to be done with this portion. Possible values: 'RETURN_TO_STOCK', 'RETURN_TO_VENDOR', 'SCRAP', 'DONATE', 'REPAIR', 'REWORK', 'CREDIT_ONLY'. | | `lines[].inspectionDetails[].qualityGrade` | string | no | The grade assigned at inspection, where the business uses one. | | `lines[].inspectionDetails[].inspectionNotes` | string | no | Free text from the inspector. | | `lines[].inspectionDetails[].rejectionReason` | string | no | Why this portion was rejected, where it was. | | `lines[].inspectionDetails[].batchNumber` | string | no | The batch this finding is about, where it is about one. | | `lines[].inspectionDetails[].serialNumber` | string | no | The serialized unit this finding is about, where it is about one. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Sales Return `PATCH /inventory/sales-returns/{id}` Group: Inventory › Sales Returns · Operation ID: `updateSalesReturn` Updates the sales return with the given identifier. Only the fields in the request body change. Updates one draft sales return. Only the fields in the request body change. Only a draft can be edited. Once committed, a return has moved stock and can no longer be edited. Lines change through three arrays, deleteLines, updateLines and createLines, applied in that order, so one call can retire a line and add its replacement. A line's tracking arrays replace what the line had: sending batches replaces that line's batches, and omitting it leaves them untouched. The inspection fields are not writable here. A line id belonging to another return is rejected, not skipped. The response is the updated return in full. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateSalesReturn) ## 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. | | `id` | path | string | yes | The unique identifier for the sales return to update. | ## Request body Required, `application/json`: updateSalesReturnInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `businessPartnerId` | string | no | A new customer for the return. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `documentDate` | string (date) | no | A new document date. Use format YYYY-MM-DD. | | `deliveryDate` | string (date) | no | A new expected-back date. Use format YYYY-MM-DD. | | `currency` | string | no | A new currency for the return's amounts. Format ISO 4217, for example 'EUR'. | | `referenceNumber` | string | no | A new customer reference. | | `trackingNumber` | string | no | A new carrier tracking number. | | `returnType` | string | no | A new classification for the return. | | `returnFeeAmount` | string (decimal) | no | A new fee across the return, in the document's currency. | | `notes` | string | no | New free text. Pass an empty string to clear it. | | `deleteLines` | array of string | no | The identifiers of lines to remove. Applied before the other two arrays. | | `updateLines` | array of object | no | Lines to change, addressed by id. Applied after deleteLines and before createLines. | | `updateLines[].id` | string | yes | The unique identifier for the line to change. A line belonging to another return is rejected, not skipped. | | `updateLines[].itemId` | string | no | A new item for the line. See id on GET inventory/items/:id. | | `updateLines[].locationId` | string | no | A new warehouse for the line. See id on GET inventory/warehouses/:id. | | `updateLines[].unitId` | string | no | A new unit for the line's quantity. See id on GET inventory/unit-of-measures/:id. | | `updateLines[].quantity` | string (decimal) | no | A new quantity, in the line's unit. Greater than zero when given. | | `updateLines[].unitPrice` | string (decimal) | no | A new unit value, in the document's currency. | | `updateLines[].condition` | string, one of `"NEW"`, `"GOOD"`, `"USED"`, `"OPENED"`, `"DAMAGED"`, `"DEFECTIVE"`, `"EXPIRED"`, `"REFURBISHED"` | no | A new expected condition. | | `updateLines[].disposition` | string, one of `"RETURN_TO_STOCK"`, `"RETURN_TO_VENDOR"`, `"SCRAP"`, `"DONATE"`, `"REPAIR"`, `"REWORK"`, `"CREDIT_ONLY"` | no | A new intended disposition. | | `updateLines[].reasonCode` | string | no | A new customer-facing reason. | | `updateLines[].reasonCodeId` | string | no | A new internal reason code. See id on GET inventory/reason-codes/:id. | | `updateLines[].returnFeeAmount` | string (decimal) | no | A new fee for this line, in the document's currency. | | `updateLines[].notes` | string | no | New free text. Pass an empty string to clear it. | | `updateLines[].batches` | array of object | no | The batches coming back on this line. Required for a batch-tracked item, and the quantities must add up to the line's quantity. On PATCH inventory/sales-returns/:id this replaces the line's batches. | | `updateLines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch. Omit it and give batchNumber for a lot the system has not seen. | | `updateLines[].batches[].batchNumber` | string | no | The batch number as printed. Required unless batchNumberId is given. | | `updateLines[].batches[].expirationDate` | string (date) | no | The expiration date of the lot. Format YYYY-MM-DD. | | `updateLines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch coming back, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `updateLines[].serials` | array of object | no | The serialized units coming back, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. On PATCH inventory/sales-returns/:id this replaces the line's serials. | | `updateLines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial record. Omit it and give serialNumber. | | `updateLines[].serials[].serialNumber` | string | no | The serial number stamped on the unit. Required unless serialNumberId is given. | | `updateLines[].binLocations` | array of object | no | The bins the goods go into, in a bin-tracked warehouse. One flat array. An entry may name a batchNumber or serialNumber of its own line. On PATCH inventory/sales-returns/:id this replaces the line's bin entries. | | `updateLines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the goods go into. See id on GET inventory/bins/:id. | | `updateLines[].binLocations[].quantity` | string (decimal) | yes | The quantity going into this bin, in the line's unit. | | `updateLines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. | | `updateLines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | | `createLines` | array of object | no | Lines to add. | | `createLines[].itemId` | string | yes | The unique identifier for the item coming back. See id on GET inventory/items/:id. | | `createLines[].locationId` | string | yes | The unique identifier for the warehouse the goods come back to. Each line of a return may use a different warehouse. See id on GET inventory/warehouses/:id. | | `createLines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `createLines[].quantity` | string (decimal) | yes | The quantity the customer is returning, in that unit. Greater than zero. | | `createLines[].unitPrice` | string (decimal) | no | The value of one unit, in the document's currency. Omit it to take the price from the sale. | | `createLines[].condition` | string, one of `"NEW"`, `"GOOD"`, `"USED"`, `"OPENED"`, `"DAMAGED"`, `"DEFECTIVE"`, `"EXPIRED"`, `"REFURBISHED"` | no | The condition the goods are expected in. The inspection may find otherwise and restates it. | | `createLines[].disposition` | string, one of `"RETURN_TO_STOCK"`, `"RETURN_TO_VENDOR"`, `"SCRAP"`, `"DONATE"`, `"REPAIR"`, `"REWORK"`, `"CREDIT_ONLY"` | no | What you intend to do with the goods. The inspection may change it. | | `createLines[].reasonCode` | string | no | The customer-facing reason for the return, for example 'defectiveItem'. Reporting only. | | `createLines[].reasonCodeId` | string | no | The unique identifier for an internal reason code. See id on GET inventory/reason-codes/:id. | | `createLines[].returnFeeAmount` | string (decimal) | no | The fee withheld on this line, in the document's currency. | | `createLines[].sourceDocumentLineId` | string | no | The shipment line the goods went out on, where the return follows a delivery. It is a line's id on GET inventory/shipments/:id. Naming it lets the commit value the return from the original shipment cost. | | `createLines[].notes` | string | no | Free text on the line. | | `createLines[].batches` | array of object | no | The batches coming back on this line. Required for a batch-tracked item, and the quantities must add up to the line's quantity. On PATCH inventory/sales-returns/:id this replaces the line's batches. | | `createLines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch. Omit it and give batchNumber for a lot the system has not seen. | | `createLines[].batches[].batchNumber` | string | no | The batch number as printed. Required unless batchNumberId is given. | | `createLines[].batches[].expirationDate` | string (date) | no | The expiration date of the lot. Format YYYY-MM-DD. | | `createLines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch coming back, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `createLines[].serials` | array of object | no | The serialized units coming back, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. On PATCH inventory/sales-returns/:id this replaces the line's serials. | | `createLines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial record. Omit it and give serialNumber. | | `createLines[].serials[].serialNumber` | string | no | The serial number stamped on the unit. Required unless serialNumberId is given. | | `createLines[].binLocations` | array of object | no | The bins the goods go into, in a bin-tracked warehouse. One flat array. An entry may name a batchNumber or serialNumber of its own line. On PATCH inventory/sales-returns/:id this replaces the line's bin entries. | | `createLines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the goods go into. See id on GET inventory/bins/:id. | | `createLines[].binLocations[].quantity` | string (decimal) | yes | The quantity going into this bin, in the line's unit. | | `createLines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. | | `createLines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | Example: ```json { "businessPartnerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "documentDate": "2026-01-15", "deliveryDate": "2026-01-15", "currency": "<currency>", "referenceNumber": "<referenceNumber>", "trackingNumber": "3SABCD7654321", "returnType": "<returnType>", "returnFeeAmount": "19.99", "notes": "<notes>", "deleteLines": [ "<deleteLines>" ], "updateLines": [ { "id": "8c14a6f2-3b70-4d59-9e02-6f5a1d38b47c", "quantity": "4.00" } ], "createLines": [ { "itemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "locationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "unitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "unitPrice": "19.99", "condition": "NEW", "disposition": "RETURN_TO_STOCK", "reasonCode": "<reasonCode>", "reasonCodeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "returnFeeAmount": "19.99", "sourceDocumentLineId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "notes": "<notes>", "batches": [ { "batchNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "batchNumber": "<batchNumber>", "expirationDate": "2026-01-15", "quantity": "19.99" } ], "serials": [ { "serialNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "serialNumber": "<serialNumber>" } ], "binLocations": [ { "binLocationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "batchNumber": "<batchNumber>", "serialNumber": "<serialNumber>" } ] } ] } ``` ## Response `200` Success Under `data` (updateSalesReturnOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the sales return. | | `documentNumber` | string | no | The sales return number, for example 'SR-1042'. Assigned on create and never supplied by you. | | `documentStatus` | string | yes | The status of the return. Possible values: 'DRAFT', 'OPEN', 'COMMITTED', 'RECEIVED', 'INSPECTED', 'PROCESSED', 'COMPLETED', 'CANCELLED'. | | `documentDate` | string (date) | yes | The date the return is dated. Format YYYY-MM-DD. | | `deliveryDate` | string (date) | no | When the goods are expected back, or came back. Format YYYY-MM-DD. | | `entityId` | string | yes | The unique identifier for the legal entity the return belongs to. See id on GET base/entities. | | `entityName` | string | no | The name of that legal entity. | | `businessPartnerId` | string | yes | The unique identifier for the customer returning the goods. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `businessPartnerName` | string | no | The name of that customer. | | `sourceDocumentId` | string | no | The document the goods went out on, where the platform recorded one, as its own numeric id in a string. It is not an identifier this API can look up. The usable link is sourceDocumentLineId on the line. | | `sourceDocumentType` | string | no | What kind of document that was, for example 'SalesDelivery'. | | `referenceNumber` | string | no | The customer's own reference for the return, for example an RMA number. | | `trackingNumber` | string | no | The carrier's tracking number for the goods coming back. | | `returnType` | string | no | How the business classifies the return. | | `currency` | string | no | The currency every amount on the document is in. Format ISO 4217, for example 'EUR'. A return is valued in the currency it was sold in, which need not be the legal entity's functional currency. | | `totalAmount` | string (decimal) | no | The total value of the return, in the document's currency. | | `returnFeeAmount` | string (decimal) | no | The fee withheld across the return, in the document's currency. | | `notes` | string | no | Free text on the document. | | `createdDate` | string (date-time) | no | The date and time the return was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the return record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the return, oldest first. A sales return always has at least one. | | `lines[].id` | string | yes | The unique identifier for the line. Use it in updateLines and deleteLines on PATCH inventory/sales-returns/:id. | | `lines[].itemId` | string | yes | The unique identifier for the item coming back. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The short code of that item. | | `lines[].itemName` | string | no | The name of that item. | | `lines[].locationId` | string | yes | The unique identifier for the warehouse the goods come back to. Each line of a return may use a different warehouse. See id on GET inventory/warehouses/:id. | | `lines[].locationCode` | string | no | The short code of that warehouse. | | `lines[].locationName` | string | no | The name of that warehouse. | | `lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `lines[].unitCode` | string | no | The short code of that unit. | | `lines[].quantity` | string (decimal) | yes | The quantity the customer is returning, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].openQuantity` | string (decimal) | no | How much of the line has not yet been received. | | `lines[].inspectedQuantity` | string (decimal) | no | How much of the line the inspection covered. | | `lines[].approvedQuantity` | string (decimal) | no | How much of the line was approved back into stock. | | `lines[].rejectedQuantity` | string (decimal) | no | How much of the line was rejected. Approved plus rejected equals inspected. | | `lines[].inspectedDate` | string (date) | no | When the inspection took place. Format YYYY-MM-DD. | | `lines[].condition` | string | no | The condition the goods arrived in, restated by the inspection. Possible values: 'NEW', 'GOOD', 'USED', 'OPENED', 'DAMAGED', 'DEFECTIVE', 'EXPIRED', 'REFURBISHED'. | | `lines[].disposition` | string | no | What is to be done with the goods. Possible values: 'RETURN_TO_STOCK', 'RETURN_TO_VENDOR', 'SCRAP', 'DONATE', 'REPAIR', 'REWORK', 'CREDIT_ONLY'. | | `lines[].qualityGrade` | string | no | The grade assigned at inspection. | | `lines[].inspectionNotes` | string | no | Free text from the inspector. | | `lines[].rejectionReason` | string | no | Why the rejected portion was rejected. | | `lines[].unitPrice` | string (decimal) | no | The value of one unit, in the document's currency. | | `lines[].returnFeeAmount` | string (decimal) | no | The fee withheld on this line, in the document's currency. | | `lines[].reasonCode` | string | no | The customer-facing reason for the return, for example 'defectiveItem'. Reporting only, it does not affect how the return is processed. | | `lines[].reasonCodeId` | string | no | The unique identifier for an internal reason code, where one was set. See id on GET inventory/reason-codes/:id. | | `lines[].sourceDocumentLineId` | string | no | The shipment line the goods went out on, where the return cites one. It is a line's id on GET inventory/shipments/:id. | | `lines[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | What kind of document line that was. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches coming back on this line, for a batch-tracked item. Absent when the item is not batch-tracked. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record. | | `lines[].batches[].batchNumber` | string | yes | The batch number as printed. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the lot. Format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch coming back, in the line's unit. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].batches[].openQuantity` | string (decimal) | no | How much of this batch has not yet been received. | | `lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the document's currency. | | `lines[].serials` | array of object | no | The serialized units coming back, one entry per unit. Absent when the item is not serialized. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record. | | `lines[].serials[].serialNumber` | string | yes | The serial number as stamped on the unit. | | `lines[].serials[].openQuantity` | string (decimal) | no | How much of this unit is still outstanding, one or zero. | | `lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the document's currency. | | `lines[].binLocations` | array of object | no | The bins the goods go into, in a bin-tracked warehouse. One flat array. An entry naming a batchNumber or serialNumber belongs to that batch or unit, and one naming neither belongs to the line. | | `lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the goods go into. See id on GET inventory/bins/:id. | | `lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity going into this bin, in the line's unit. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | Set when this bin entry belongs to one of the line's batches, naming it. | | `lines[].binLocations[].serialNumber` | string | no | Set when this bin entry belongs to one of the line's serialized units, naming it. | | `lines[].inspectionDetails` | array of object | no | What the inspection found, broken down by batch or unit. Absent before the inspection runs. The line has the totals and these give the detail. | | `lines[].inspectionDetails[].approvalStatus` | string | yes | What was decided about this portion, recorded as given. | | `lines[].inspectionDetails[].inspectedQuantity` | string (decimal) | yes | How much this finding covers. | | `lines[].inspectionDetails[].condition` | string | no | The condition of this portion. Possible values: 'NEW', 'GOOD', 'USED', 'OPENED', 'DAMAGED', 'DEFECTIVE', 'EXPIRED', 'REFURBISHED'. | | `lines[].inspectionDetails[].disposition` | string | no | What is to be done with this portion. Possible values: 'RETURN_TO_STOCK', 'RETURN_TO_VENDOR', 'SCRAP', 'DONATE', 'REPAIR', 'REWORK', 'CREDIT_ONLY'. | | `lines[].inspectionDetails[].qualityGrade` | string | no | The grade assigned at inspection, where the business uses one. | | `lines[].inspectionDetails[].inspectionNotes` | string | no | Free text from the inspector. | | `lines[].inspectionDetails[].rejectionReason` | string | no | Why this portion was rejected, where it was. | | `lines[].inspectionDetails[].batchNumber` | string | no | The batch this finding is about, where it is about one. | | `lines[].inspectionDetails[].serialNumber` | string | no | The serialized unit this finding is about, where it is about one. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Deletes a sales return. `DELETE /inventory/sales-returns/{id}` Group: Inventory › Sales Returns · Operation ID: `deleteSalesReturn` Deletes one sales return and everything under it: its lines and their tracking and inspection rows. Only a draft or a canceled return can be deleted. A committed return has stock and postings attached and cannot be deleted here. Deleting an identifier that does not exist returns NOT_FOUND, not a silent success. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteSalesReturn) ## 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. | | `id` | path | string | yes | The unique identifier for the sales return to delete. | ## Response `200` Success Under `data` (deleteSalesReturnOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted return, echoed back so a batch of deletes can be matched to requests. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, POSTING_REJECTED - `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 --- # Commits a sales return. `POST /inventory/sales-returns/{id}/commit` Group: Inventory › Sales Returns · Operation ID: `commitSalesReturn` Commits a draft sales return, bringing the goods back into stock and posting the accounting, in one call. The platform commits the document, posts the inventory transactions that bring the stock back, writes the financial postings, and leaves the return RECEIVED. Where the return was raised as a credit, the credit memo is raised in the same transaction and its id is returned. This is the only workflow step this API publishes. After it, the return can no longer be edited or deleted. [Open in the interactive reference](https://docs.api.lensing.ai/?op=commitSalesReturn) ## 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. | | `id` | path | string | yes | The unique identifier for the sales return. | ## Request body Required, `application/json`: commitSalesReturnInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `approvalNotes` | string | no | Free text recorded with the commit, added to the return's notes for audit. | Example: ```json { "approvalNotes": "RMA verified against the delivery" } ``` ## Response `200` Success Under `data` (commitSalesReturnOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the sales return. | | `documentStatus` | string | yes | The status the return is now in, read back from the document. Possible values: 'DRAFT', 'OPEN', 'COMMITTED', 'RECEIVED', 'INSPECTED', 'PROCESSED', 'COMPLETED', 'CANCELLED'. | | `creditMemoId` | string | no | The unique identifier for the credit memo this commit raised, where it raised one. Absent for an inventory-only return, and its absence never means the commit failed. See id on GET accounting/credit-memos/:id. | | `warnings` | array of string | yes | Non-fatal advisories from this step. An empty array on a clean run. The step succeeded whatever is listed here. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, POSTING_REJECTED - `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 --- # Creates several sales returns in one call. `POST /inventory/sales-returns/bulk` Group: Inventory › Sales Returns · Operation ID: `bulkCreateSalesReturns` Creates several sales returns in one call, each a draft. All or nothing: if any return is rejected, nothing is written, not even the entries before it. 'index' on the error says which, counting from zero. Each entry is exactly what POST inventory/sales-returns takes, and the results come back in input order. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateSalesReturns) ## 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`: bulkCreateSalesReturnsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `returns` | array of object | yes | The returns to create. At least one, each exactly what POST inventory/sales-returns takes. | | `returns[].businessPartnerId` | string | yes | The unique identifier for the customer returning the goods. Required, a return always comes from one. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `returns[].entityId` | string | no | The unique identifier for the legal entity. Omit it and the first line's warehouse decides it. See id on GET base/entities. | | `returns[].documentDate` | string (date) | no | The date the return is dated. Defaults to today. Use format YYYY-MM-DD. | | `returns[].deliveryDate` | string (date) | no | When the goods are expected back. Use format YYYY-MM-DD. | | `returns[].currency` | string | no | The currency for every amount on the return. Format ISO 4217, for example 'EUR'. Defaults to the legal entity's functional currency. Set it when the sale was in another. | | `returns[].referenceNumber` | string | no | The customer's own reference, for example an RMA number. | | `returns[].trackingNumber` | string | no | The carrier's tracking number for the goods coming back. | | `returns[].returnType` | string | no | How the business classifies the return. | | `returns[].returnFeeAmount` | string (decimal) | no | The fee withheld across the return, in the document's currency. | | `returns[].notes` | string | no | Free text on the document. | | `returns[].lines` | array of object | yes | The lines of this return. At least one is required. | | `returns[].lines[].itemId` | string | yes | The unique identifier for the item coming back. See id on GET inventory/items/:id. | | `returns[].lines[].locationId` | string | yes | The unique identifier for the warehouse the goods come back to. Each line of a return may use a different warehouse. See id on GET inventory/warehouses/:id. | | `returns[].lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `returns[].lines[].quantity` | string (decimal) | yes | The quantity the customer is returning, in that unit. Greater than zero. | | `returns[].lines[].unitPrice` | string (decimal) | no | The value of one unit, in the document's currency. Omit it to take the price from the sale. | | `returns[].lines[].condition` | string, one of `"NEW"`, `"GOOD"`, `"USED"`, `"OPENED"`, `"DAMAGED"`, `"DEFECTIVE"`, `"EXPIRED"`, `"REFURBISHED"` | no | The condition the goods are expected in. The inspection may find otherwise and restates it. | | `returns[].lines[].disposition` | string, one of `"RETURN_TO_STOCK"`, `"RETURN_TO_VENDOR"`, `"SCRAP"`, `"DONATE"`, `"REPAIR"`, `"REWORK"`, `"CREDIT_ONLY"` | no | What you intend to do with the goods. The inspection may change it. | | `returns[].lines[].reasonCode` | string | no | The customer-facing reason for the return, for example 'defectiveItem'. Reporting only. | | `returns[].lines[].reasonCodeId` | string | no | The unique identifier for an internal reason code. See id on GET inventory/reason-codes/:id. | | `returns[].lines[].returnFeeAmount` | string (decimal) | no | The fee withheld on this line, in the document's currency. | | `returns[].lines[].sourceDocumentLineId` | string | no | The shipment line the goods went out on, where the return follows a delivery. It is a line's id on GET inventory/shipments/:id. Naming it lets the commit value the return from the original shipment cost. | | `returns[].lines[].notes` | string | no | Free text on the line. | | `returns[].lines[].batches` | array of object | no | The batches coming back on this line. Required for a batch-tracked item, and the quantities must add up to the line's quantity. On PATCH inventory/sales-returns/:id this replaces the line's batches. | | `returns[].lines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch. Omit it and give batchNumber for a lot the system has not seen. | | `returns[].lines[].batches[].batchNumber` | string | no | The batch number as printed. Required unless batchNumberId is given. | | `returns[].lines[].batches[].expirationDate` | string (date) | no | The expiration date of the lot. Format YYYY-MM-DD. | | `returns[].lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch coming back, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `returns[].lines[].serials` | array of object | no | The serialized units coming back, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. On PATCH inventory/sales-returns/:id this replaces the line's serials. | | `returns[].lines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial record. Omit it and give serialNumber. | | `returns[].lines[].serials[].serialNumber` | string | no | The serial number stamped on the unit. Required unless serialNumberId is given. | | `returns[].lines[].binLocations` | array of object | no | The bins the goods go into, in a bin-tracked warehouse. One flat array. An entry may name a batchNumber or serialNumber of its own line. On PATCH inventory/sales-returns/:id this replaces the line's bin entries. | | `returns[].lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the goods go into. See id on GET inventory/bins/:id. | | `returns[].lines[].binLocations[].quantity` | string (decimal) | yes | The quantity going into this bin, in the line's unit. | | `returns[].lines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. | | `returns[].lines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | Example: ```json { "returns": [ { "businessPartnerId": "c73f2b81-5d94-4e60-a812-0f9b6d3e45c7", "documentDate": "2026-08-14", "deliveryDate": "2026-08-18", "referenceNumber": "RMA-2026-0087", "trackingNumber": "3SABCD1234567", "currency": "EUR", "notes": "Customer reports transit damage", "lines": [ { "itemId": "5f21c8b0-9d34-4a76-8e12-04b93c7de5a8", "locationId": "e1d84c07-5b39-4a26-9f70-2c8b613ad495", "unitId": "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "quantity": "6.00", "unitPrice": "24.00", "condition": "DAMAGED", "disposition": "SCRAP", "reasonCode": "damagedInTransit", "sourceDocumentLineId": "5c1d9a72-0e64-4a1f-8b39-2c7f4a6e15d8", "notes": "Carton crushed in transit" } ] } ] } ``` ## Response `200` Success Under `data` (bulkCreateSalesReturnsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The created returns, in input order. Match them to inputs by index. | | `items[].id` | string | yes | The unique identifier for the sales return. | | `items[].documentNumber` | string | no | The sales return number, for example 'SR-1042'. Assigned on create and never supplied by you. | | `items[].documentStatus` | string | yes | The status of the return. Possible values: 'DRAFT', 'OPEN', 'COMMITTED', 'RECEIVED', 'INSPECTED', 'PROCESSED', 'COMPLETED', 'CANCELLED'. | | `items[].documentDate` | string (date) | yes | The date the return is dated. Format YYYY-MM-DD. | | `items[].deliveryDate` | string (date) | no | When the goods are expected back, or came back. Format YYYY-MM-DD. | | `items[].entityId` | string | yes | The unique identifier for the legal entity the return belongs to. See id on GET base/entities. | | `items[].entityName` | string | no | The name of that legal entity. | | `items[].businessPartnerId` | string | yes | The unique identifier for the customer returning the goods. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `items[].businessPartnerName` | string | no | The name of that customer. | | `items[].sourceDocumentId` | string | no | The document the goods went out on, where the platform recorded one, as its own numeric id in a string. It is not an identifier this API can look up. The usable link is sourceDocumentLineId on the line. | | `items[].sourceDocumentType` | string | no | What kind of document that was, for example 'SalesDelivery'. | | `items[].referenceNumber` | string | no | The customer's own reference for the return, for example an RMA number. | | `items[].trackingNumber` | string | no | The carrier's tracking number for the goods coming back. | | `items[].returnType` | string | no | How the business classifies the return. | | `items[].currency` | string | no | The currency every amount on the document is in. Format ISO 4217, for example 'EUR'. A return is valued in the currency it was sold in, which need not be the legal entity's functional currency. | | `items[].totalAmount` | string (decimal) | no | The total value of the return, in the document's currency. | | `items[].returnFeeAmount` | string (decimal) | no | The fee withheld across the return, in the document's currency. | | `items[].notes` | string | no | Free text on the document. | | `items[].createdDate` | string (date-time) | no | The date and time the return was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the return record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | yes | The lines of the return, oldest first. A sales return always has at least one. | | `items[].lines[].id` | string | yes | The unique identifier for the line. Use it in updateLines and deleteLines on PATCH inventory/sales-returns/:id. | | `items[].lines[].itemId` | string | yes | The unique identifier for the item coming back. See id on GET inventory/items/:id. | | `items[].lines[].itemCode` | string | no | The short code of that item. | | `items[].lines[].itemName` | string | no | The name of that item. | | `items[].lines[].locationId` | string | yes | The unique identifier for the warehouse the goods come back to. Each line of a return may use a different warehouse. See id on GET inventory/warehouses/:id. | | `items[].lines[].locationCode` | string | no | The short code of that warehouse. | | `items[].lines[].locationName` | string | no | The name of that warehouse. | | `items[].lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `items[].lines[].unitCode` | string | no | The short code of that unit. | | `items[].lines[].quantity` | string (decimal) | yes | The quantity the customer is returning, in the line's unit. | | `items[].lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].openQuantity` | string (decimal) | no | How much of the line has not yet been received. | | `items[].lines[].inspectedQuantity` | string (decimal) | no | How much of the line the inspection covered. | | `items[].lines[].approvedQuantity` | string (decimal) | no | How much of the line was approved back into stock. | | `items[].lines[].rejectedQuantity` | string (decimal) | no | How much of the line was rejected. Approved plus rejected equals inspected. | | `items[].lines[].inspectedDate` | string (date) | no | When the inspection took place. Format YYYY-MM-DD. | | `items[].lines[].condition` | string | no | The condition the goods arrived in, restated by the inspection. Possible values: 'NEW', 'GOOD', 'USED', 'OPENED', 'DAMAGED', 'DEFECTIVE', 'EXPIRED', 'REFURBISHED'. | | `items[].lines[].disposition` | string | no | What is to be done with the goods. Possible values: 'RETURN_TO_STOCK', 'RETURN_TO_VENDOR', 'SCRAP', 'DONATE', 'REPAIR', 'REWORK', 'CREDIT_ONLY'. | | `items[].lines[].qualityGrade` | string | no | The grade assigned at inspection. | | `items[].lines[].inspectionNotes` | string | no | Free text from the inspector. | | `items[].lines[].rejectionReason` | string | no | Why the rejected portion was rejected. | | `items[].lines[].unitPrice` | string (decimal) | no | The value of one unit, in the document's currency. | | `items[].lines[].returnFeeAmount` | string (decimal) | no | The fee withheld on this line, in the document's currency. | | `items[].lines[].reasonCode` | string | no | The customer-facing reason for the return, for example 'defectiveItem'. Reporting only, it does not affect how the return is processed. | | `items[].lines[].reasonCodeId` | string | no | The unique identifier for an internal reason code, where one was set. See id on GET inventory/reason-codes/:id. | | `items[].lines[].sourceDocumentLineId` | string | no | The shipment line the goods went out on, where the return cites one. It is a line's id on GET inventory/shipments/:id. | | `items[].lines[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | What kind of document line that was. | | `items[].lines[].notes` | string | no | Free text on the line. | | `items[].lines[].batches` | array of object | no | The batches coming back on this line, for a batch-tracked item. Absent when the item is not batch-tracked. | | `items[].lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record. | | `items[].lines[].batches[].batchNumber` | string | yes | The batch number as printed. | | `items[].lines[].batches[].expirationDate` | string (date) | no | The expiration date of the lot. Format YYYY-MM-DD. | | `items[].lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch coming back, in the line's unit. | | `items[].lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].batches[].openQuantity` | string (decimal) | no | How much of this batch has not yet been received. | | `items[].lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the document's currency. | | `items[].lines[].serials` | array of object | no | The serialized units coming back, one entry per unit. Absent when the item is not serialized. | | `items[].lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record. | | `items[].lines[].serials[].serialNumber` | string | yes | The serial number as stamped on the unit. | | `items[].lines[].serials[].openQuantity` | string (decimal) | no | How much of this unit is still outstanding, one or zero. | | `items[].lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the document's currency. | | `items[].lines[].binLocations` | array of object | no | The bins the goods go into, in a bin-tracked warehouse. One flat array. An entry naming a batchNumber or serialNumber belongs to that batch or unit, and one naming neither belongs to the line. | | `items[].lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the goods go into. See id on GET inventory/bins/:id. | | `items[].lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `items[].lines[].binLocations[].quantity` | string (decimal) | yes | The quantity going into this bin, in the line's unit. | | `items[].lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].binLocations[].batchNumber` | string | no | Set when this bin entry belongs to one of the line's batches, naming it. | | `items[].lines[].binLocations[].serialNumber` | string | no | Set when this bin entry belongs to one of the line's serialized units, naming it. | | `items[].lines[].inspectionDetails` | array of object | no | What the inspection found, broken down by batch or unit. Absent before the inspection runs. The line has the totals and these give the detail. | | `items[].lines[].inspectionDetails[].approvalStatus` | string | yes | What was decided about this portion, recorded as given. | | `items[].lines[].inspectionDetails[].inspectedQuantity` | string (decimal) | yes | How much this finding covers. | | `items[].lines[].inspectionDetails[].condition` | string | no | The condition of this portion. Possible values: 'NEW', 'GOOD', 'USED', 'OPENED', 'DAMAGED', 'DEFECTIVE', 'EXPIRED', 'REFURBISHED'. | | `items[].lines[].inspectionDetails[].disposition` | string | no | What is to be done with this portion. Possible values: 'RETURN_TO_STOCK', 'RETURN_TO_VENDOR', 'SCRAP', 'DONATE', 'REPAIR', 'REWORK', 'CREDIT_ONLY'. | | `items[].lines[].inspectionDetails[].qualityGrade` | string | no | The grade assigned at inspection, where the business uses one. | | `items[].lines[].inspectionDetails[].inspectionNotes` | string | no | Free text from the inspector. | | `items[].lines[].inspectionDetails[].rejectionReason` | string | no | Why this portion was rejected, where it was. | | `items[].lines[].inspectionDetails[].batchNumber` | string | no | The batch this finding is about, where it is about one. | | `items[].lines[].inspectionDetails[].serialNumber` | string | no | The serialized unit this finding is about, where it is about one. | ## 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 | | --- | --- | | `NOTHING_TO_CREATE` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOTHING_TO_CREATE, REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Deletes several sales returns in one call. `POST /inventory/sales-returns/bulk-delete` Group: Inventory › Sales Returns · Operation ID: `bulkDeleteSalesReturns` Deletes several draft sales returns in one call. All or nothing: if any identifier matches no return, or names one that is neither draft nor canceled, nothing is deleted, not even the entries before it. 'index' on the error says which, counting from zero. To remove a committed return, correct it with a further document rather than deleting it. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteSalesReturns) ## 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`: bulkDeleteSalesReturnsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The identifiers of the returns to delete. All or nothing: if any is missing, or is neither draft nor canceled, none is deleted. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteSalesReturnsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The identifiers of the deleted returns, in input order. | ## 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 | | --- | --- | | `NOTHING_TO_DELETE` | 400 | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOTHING_TO_DELETE, NOT_FOUND, WRONG_STATUS, POSTING_REJECTED - `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 --- # Returns the sales returns that match the given criteria. `POST /inventory/sales-returns/search` Group: Inventory › Sales Returns · Operation ID: `searchSalesReturns` Returns the sales returns that match the criteria in the request body, with the full item schema: header, lines, tracking and inspection detail, the same GET inventory/sales-returns/:id returns. This is the endpoint that returns the lines, and the only one that filters by customer, reference number or tracking number. Filtering by documentStatus is how you work the queue. Every criterion is exact or a range, and criteria combine with AND. An empty filters object returns everything, page by page. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchSalesReturns) ## 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`. | ## Request body Required, `application/json`: searchSalesReturnsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The criteria to apply, combined with AND. Omit to return all sales returns. | | `filters.documentNumber` | string | no | Limits results to the return with this document number. The match is exact. | | `filters.documentStatus` | string | no | Limits results to returns with this status. The match is exact. Possible values: 'DRAFT', 'OPEN', 'COMMITTED', 'RECEIVED', 'INSPECTED', 'PROCESSED', 'COMPLETED', 'CANCELLED'. | | `filters.entityId` | string | no | Limits results to returns for the legal entity with this identifier. | | `filters.businessPartnerId` | string | no | Limits results to returns for the customer with this identifier. Despite the name, this is a customer id. | | `filters.locationId` | string | no | Limits results to returns with a line for the warehouse with this identifier. | | `filters.itemId` | string | no | Limits results to returns with a line for the item with this identifier. | | `filters.referenceNumber` | string | no | Limits results to returns with this customer reference, for example an RMA number. The match is exact. | | `filters.trackingNumber` | string | no | Limits results to returns with this carrier tracking number. The match is exact. | | `filters.fromDate` | string (date) | no | Limits results to returns dated on or after this date. Use format YYYY-MM-DD. | | `filters.toDate` | string (date) | no | Limits results to returns dated on or before this date. Use format YYYY-MM-DD. | | `filters.modifiedSince` | string (date-time) | no | Limits results to returns changed on or after this time, in UTC. Use format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The maximum number of returns to return. Defaults to 50. The maximum is 200. | Example: ```json { "filters": { "documentStatus": "RECEIVED" }, "page": { "cursor": "<cursor>", "size": 0 } } ``` ## Response `200` Success Under `data` (searchSalesReturnsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The matching returns for this page, oldest first, each with its lines. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the sales return. | | `items[].documentNumber` | string | no | The sales return number, for example 'SR-1042'. Assigned on create and never supplied by you. | | `items[].documentStatus` | string | yes | The status of the return. Possible values: 'DRAFT', 'OPEN', 'COMMITTED', 'RECEIVED', 'INSPECTED', 'PROCESSED', 'COMPLETED', 'CANCELLED'. | | `items[].documentDate` | string (date) | yes | The date the return is dated. Format YYYY-MM-DD. | | `items[].deliveryDate` | string (date) | no | When the goods are expected back, or came back. Format YYYY-MM-DD. | | `items[].entityId` | string | yes | The unique identifier for the legal entity the return belongs to. See id on GET base/entities. | | `items[].entityName` | string | no | The name of that legal entity. | | `items[].businessPartnerId` | string | yes | The unique identifier for the customer returning the goods. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `items[].businessPartnerName` | string | no | The name of that customer. | | `items[].sourceDocumentId` | string | no | The document the goods went out on, where the platform recorded one, as its own numeric id in a string. It is not an identifier this API can look up. The usable link is sourceDocumentLineId on the line. | | `items[].sourceDocumentType` | string | no | What kind of document that was, for example 'SalesDelivery'. | | `items[].referenceNumber` | string | no | The customer's own reference for the return, for example an RMA number. | | `items[].trackingNumber` | string | no | The carrier's tracking number for the goods coming back. | | `items[].returnType` | string | no | How the business classifies the return. | | `items[].currency` | string | no | The currency every amount on the document is in. Format ISO 4217, for example 'EUR'. A return is valued in the currency it was sold in, which need not be the legal entity's functional currency. | | `items[].totalAmount` | string (decimal) | no | The total value of the return, in the document's currency. | | `items[].returnFeeAmount` | string (decimal) | no | The fee withheld across the return, in the document's currency. | | `items[].notes` | string | no | Free text on the document. | | `items[].createdDate` | string (date-time) | no | The date and time the return was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the return record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | yes | The lines of the return, oldest first. A sales return always has at least one. | | `items[].lines[].id` | string | yes | The unique identifier for the line. Use it in updateLines and deleteLines on PATCH inventory/sales-returns/:id. | | `items[].lines[].itemId` | string | yes | The unique identifier for the item coming back. See id on GET inventory/items/:id. | | `items[].lines[].itemCode` | string | no | The short code of that item. | | `items[].lines[].itemName` | string | no | The name of that item. | | `items[].lines[].locationId` | string | yes | The unique identifier for the warehouse the goods come back to. Each line of a return may use a different warehouse. See id on GET inventory/warehouses/:id. | | `items[].lines[].locationCode` | string | no | The short code of that warehouse. | | `items[].lines[].locationName` | string | no | The name of that warehouse. | | `items[].lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `items[].lines[].unitCode` | string | no | The short code of that unit. | | `items[].lines[].quantity` | string (decimal) | yes | The quantity the customer is returning, in the line's unit. | | `items[].lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].openQuantity` | string (decimal) | no | How much of the line has not yet been received. | | `items[].lines[].inspectedQuantity` | string (decimal) | no | How much of the line the inspection covered. | | `items[].lines[].approvedQuantity` | string (decimal) | no | How much of the line was approved back into stock. | | `items[].lines[].rejectedQuantity` | string (decimal) | no | How much of the line was rejected. Approved plus rejected equals inspected. | | `items[].lines[].inspectedDate` | string (date) | no | When the inspection took place. Format YYYY-MM-DD. | | `items[].lines[].condition` | string | no | The condition the goods arrived in, restated by the inspection. Possible values: 'NEW', 'GOOD', 'USED', 'OPENED', 'DAMAGED', 'DEFECTIVE', 'EXPIRED', 'REFURBISHED'. | | `items[].lines[].disposition` | string | no | What is to be done with the goods. Possible values: 'RETURN_TO_STOCK', 'RETURN_TO_VENDOR', 'SCRAP', 'DONATE', 'REPAIR', 'REWORK', 'CREDIT_ONLY'. | | `items[].lines[].qualityGrade` | string | no | The grade assigned at inspection. | | `items[].lines[].inspectionNotes` | string | no | Free text from the inspector. | | `items[].lines[].rejectionReason` | string | no | Why the rejected portion was rejected. | | `items[].lines[].unitPrice` | string (decimal) | no | The value of one unit, in the document's currency. | | `items[].lines[].returnFeeAmount` | string (decimal) | no | The fee withheld on this line, in the document's currency. | | `items[].lines[].reasonCode` | string | no | The customer-facing reason for the return, for example 'defectiveItem'. Reporting only, it does not affect how the return is processed. | | `items[].lines[].reasonCodeId` | string | no | The unique identifier for an internal reason code, where one was set. See id on GET inventory/reason-codes/:id. | | `items[].lines[].sourceDocumentLineId` | string | no | The shipment line the goods went out on, where the return cites one. It is a line's id on GET inventory/shipments/:id. | | `items[].lines[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | What kind of document line that was. | | `items[].lines[].notes` | string | no | Free text on the line. | | `items[].lines[].batches` | array of object | no | The batches coming back on this line, for a batch-tracked item. Absent when the item is not batch-tracked. | | `items[].lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record. | | `items[].lines[].batches[].batchNumber` | string | yes | The batch number as printed. | | `items[].lines[].batches[].expirationDate` | string (date) | no | The expiration date of the lot. Format YYYY-MM-DD. | | `items[].lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch coming back, in the line's unit. | | `items[].lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].batches[].openQuantity` | string (decimal) | no | How much of this batch has not yet been received. | | `items[].lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch, in the document's currency. | | `items[].lines[].serials` | array of object | no | The serialized units coming back, one entry per unit. Absent when the item is not serialized. | | `items[].lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record. | | `items[].lines[].serials[].serialNumber` | string | yes | The serial number as stamped on the unit. | | `items[].lines[].serials[].openQuantity` | string (decimal) | no | How much of this unit is still outstanding, one or zero. | | `items[].lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit, in the document's currency. | | `items[].lines[].binLocations` | array of object | no | The bins the goods go into, in a bin-tracked warehouse. One flat array. An entry naming a batchNumber or serialNumber belongs to that batch or unit, and one naming neither belongs to the line. | | `items[].lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the goods go into. See id on GET inventory/bins/:id. | | `items[].lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `items[].lines[].binLocations[].quantity` | string (decimal) | yes | The quantity going into this bin, in the line's unit. | | `items[].lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].binLocations[].batchNumber` | string | no | Set when this bin entry belongs to one of the line's batches, naming it. | | `items[].lines[].binLocations[].serialNumber` | string | no | Set when this bin entry belongs to one of the line's serialized units, naming it. | | `items[].lines[].inspectionDetails` | array of object | no | What the inspection found, broken down by batch or unit. Absent before the inspection runs. The line has the totals and these give the detail. | | `items[].lines[].inspectionDetails[].approvalStatus` | string | yes | What was decided about this portion, recorded as given. | | `items[].lines[].inspectionDetails[].inspectedQuantity` | string (decimal) | yes | How much this finding covers. | | `items[].lines[].inspectionDetails[].condition` | string | no | The condition of this portion. Possible values: 'NEW', 'GOOD', 'USED', 'OPENED', 'DAMAGED', 'DEFECTIVE', 'EXPIRED', 'REFURBISHED'. | | `items[].lines[].inspectionDetails[].disposition` | string | no | What is to be done with this portion. Possible values: 'RETURN_TO_STOCK', 'RETURN_TO_VENDOR', 'SCRAP', 'DONATE', 'REPAIR', 'REWORK', 'CREDIT_ONLY'. | | `items[].lines[].inspectionDetails[].qualityGrade` | string | no | The grade assigned at inspection, where the business uses one. | | `items[].lines[].inspectionDetails[].inspectionNotes` | string | no | Free text from the inspector. | | `items[].lines[].inspectionDetails[].rejectionReason` | string | no | Why this portion was rejected, where it was. | | `items[].lines[].inspectionDetails[].batchNumber` | string | no | The batch this finding is about, where it is about one. | | `items[].lines[].inspectionDetails[].serialNumber` | string | no | The serialized unit this finding is about, where it is about one. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more results remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of serial numbers. `GET /inventory/serial-numbers` Group: Inventory › Serial Numbers · Operation ID: `serialNumbers` This collection omits cost, originalCost, afterSaleCost and currency. Read those from GET inventory/serial-numbers/:id or POST inventory/serial-numbers/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=serialNumbers) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `serialNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `manufacturerSerialNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `itemId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isSold` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `expirationDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `active` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, serialNumber, expirationDate, lastModifiedDate - Selectable: id, serialNumber, manufacturerSerialNumber, itemId, itemCode, itemName, locationId, locationCode, locationName, entityId, inStock, isSold, lastCostChangeDate, expirationDate, entryDate, active, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of serialNumbersRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the serial number. | | `serialNumber` | string or null | no | The serial number assigned to the unit when it was received. It cannot be changed. | | `manufacturerSerialNumber` | string or null | no | The manufacturer's own serial number for the unit, where it differs from the assigned serial number. | | `itemId` | string or null | no | The unique identifier for the item this unit is of. See id on GET inventory/items/:id. | | `itemCode` | string or null | no | The code of that item. | | `itemName` | string or null | no | The display name of that item. | | `locationId` | string or null | no | The unique identifier for the warehouse holding the unit. See id on GET inventory/warehouses/:id. | | `locationCode` | string or null | no | The code of that warehouse. | | `locationName` | string or null | no | The display name of that warehouse. | | `entityId` | string or null | no | The unique identifier for the legal entity the unit belongs to. See id on GET base/entities. | | `inStock` | string (decimal) or null | no | 1 while the unit is on hand, 0 once it has gone. | | `isSold` | boolean or null | no | Whether the unit has been sold. | | `lastCostChangeDate` | string (date) or null | no | The date the unit's cost last changed. Format YYYY-MM-DD. | | `expirationDate` | string (date) or null | no | The date the unit expires. Format YYYY-MM-DD. | | `entryDate` | string (date) or null | no | The date the unit entered the warehouse. Format YYYY-MM-DD. | | `active` | boolean or null | no | Whether the serial number can be selected on new documents. | | `createdDate` | string (date-time) or null | no | The date and time the serial number record was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the serial number record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the serial number with the given identifier. `GET /inventory/serial-numbers/{id}` Group: Inventory › Serial Numbers · Operation ID: `getSerialNumber` Returns the same fields as POST inventory/serial-numbers/search. The collection GET inventory/serial-numbers omits cost, originalCost, afterSaleCost and currency. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getSerialNumber) ## 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`. | | `id` | path | string | yes | The unique identifier for the serial number to return. | ## Response `200` Success Under `data` (getSerialNumberOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the serial number. | | `serialNumber` | string | no | The serial number assigned to the unit when it was received. It cannot be changed. | | `manufacturerSerialNumber` | string | no | The manufacturer's own serial number for the unit, where it differs from the assigned serial number. | | `itemId` | string | no | The unique identifier for the item this unit is of. See id on GET inventory/items/:id. | | `itemCode` | string | no | The code of that item. | | `itemName` | string | no | The display name of that item. | | `locationId` | string | no | The unique identifier for the warehouse holding the unit. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The code of that warehouse. | | `locationName` | string | no | The display name of that warehouse. | | `entityId` | string | no | The unique identifier for the legal entity the unit belongs to. See id on GET base/entities. | | `inStock` | string (decimal) | no | 1 while the unit is on hand, 0 once it has gone. | | `isSold` | boolean | no | Whether the unit has been sold. | | `cost` | string (decimal) | no | The value the unit is carried at, in the currency named by currency. | | `originalCost` | string (decimal) | no | The value the unit was carried at when it arrived, in the currency named by currency. | | `afterSaleCost` | string (decimal) | no | The value of the unit after it was sold, in the currency named by currency. | | `currency` | string | no | The currency the cost fields are stated in. Format ISO 4217, for example 'USD'. | | `lastCostChangeDate` | string (date) | no | The date the unit's cost last changed. Format YYYY-MM-DD. | | `expirationDate` | string (date) | no | The date the unit expires. Format YYYY-MM-DD. | | `entryDate` | string (date) | no | The date the unit entered the warehouse. Format YYYY-MM-DD. | | `active` | boolean | no | Whether the serial number can be selected on new documents. | | `createdDate` | string (date-time) | no | The date and time the serial number record was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the serial number record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Serial Number `PATCH /inventory/serial-numbers/{id}` Group: Inventory › Serial Numbers · Operation ID: `updateSerialNumber` Updates the serial number with the given identifier. Only the manufacturer serial number, the expiry and entry dates, and the active flag can be changed. Only the fields present in the request are written. The cost fields, the stock state and the location are written by the documents that move and value the unit and cannot be sent here. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateSerialNumber) ## 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. | | `id` | path | string | yes | The unique identifier for the serial number to update. | ## Request body Required, `application/json`: updateSerialNumberInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `manufacturerSerialNumber` | string | no | The manufacturer's own serial number for the unit. | | `expirationDate` | string (date) | no | The date the unit expires. Use format YYYY-MM-DD. | | `entryDate` | string (date) | no | The date the unit entered the warehouse. Use format YYYY-MM-DD. | | `active` | boolean | no | Whether the serial number can be selected on new documents. Setting it to false retires the record. Its history stays readable. | Example: ```json { "manufacturerSerialNumber": "MFG-77-000418", "expirationDate": "2026-01-15", "entryDate": "2026-01-15", "active": true } ``` ## Response `200` Success Under `data` (updateSerialNumberOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the serial number. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Returns the serial numbers that match the given filters. `POST /inventory/serial-numbers/search` Group: Inventory › Serial Numbers · Operation ID: `searchSerialNumbers` Every criterion is exact and criteria combine with AND. Send an empty filters object to return every serial number. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchSerialNumbers) ## 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`. | ## Request body Required, `application/json`: searchSerialNumbersInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. A serial number must match every filter you send. Send an empty object to return all serial numbers. | | `filters.serialNumber` | string | no | Limits results to the serial number with this value. The match is exact. | | `filters.manufacturerSerialNumber` | string | no | Limits results to serial numbers with this manufacturer serial number. The match is exact. | | `filters.itemId` | string | no | Limits results to serial numbers of this item. An identifier that is not an item's matches no serial number. | | `filters.locationId` | string | no | Limits results to serial numbers in this warehouse. | | `filters.active` | boolean | no | Limits results to serial numbers with the given active value. | | `filters.inStock` | boolean | no | Limits results to serial numbers on hand when true, and to those already gone when false. | | `filters.isSold` | boolean | no | Limits results to serial numbers with the given isSold value. | | `filters.expiringBefore` | string (date) | no | Limits results to serial numbers expiring on or before this date. Format YYYY-MM-DD. | | `filters.expiringAfter` | string (date) | no | Limits results to serial numbers expiring on or after this date. Format YYYY-MM-DD. | | `filters.modifiedSince` | string (date-time) | no | Limits results to serial numbers changed on or after this time. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of serial numbers per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "itemId": "afb4492d-52fc-44c5-ab10-71ad4a615496", "inStock": true }, "page": { "cursor": "<cursor>", "size": 0 } } ``` ## Response `200` Success Under `data` (searchSerialNumbersOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The serial numbers on this page, oldest first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the serial number. | | `items[].serialNumber` | string | no | The serial number assigned to the unit when it was received. It cannot be changed. | | `items[].manufacturerSerialNumber` | string | no | The manufacturer's own serial number for the unit, where it differs from the assigned serial number. | | `items[].itemId` | string | no | The unique identifier for the item this unit is of. See id on GET inventory/items/:id. | | `items[].itemCode` | string | no | The code of that item. | | `items[].itemName` | string | no | The display name of that item. | | `items[].locationId` | string | no | The unique identifier for the warehouse holding the unit. See id on GET inventory/warehouses/:id. | | `items[].locationCode` | string | no | The code of that warehouse. | | `items[].locationName` | string | no | The display name of that warehouse. | | `items[].entityId` | string | no | The unique identifier for the legal entity the unit belongs to. See id on GET base/entities. | | `items[].inStock` | string (decimal) | no | 1 while the unit is on hand, 0 once it has gone. | | `items[].isSold` | boolean | no | Whether the unit has been sold. | | `items[].cost` | string (decimal) | no | The value the unit is carried at, in the currency named by currency. | | `items[].originalCost` | string (decimal) | no | The value the unit was carried at when it arrived, in the currency named by currency. | | `items[].afterSaleCost` | string (decimal) | no | The value of the unit after it was sold, in the currency named by currency. | | `items[].currency` | string | no | The currency the cost fields are stated in. Format ISO 4217, for example 'USD'. | | `items[].lastCostChangeDate` | string (date) | no | The date the unit's cost last changed. Format YYYY-MM-DD. | | `items[].expirationDate` | string (date) | no | The date the unit expires. Format YYYY-MM-DD. | | `items[].entryDate` | string (date) | no | The date the unit entered the warehouse. Format YYYY-MM-DD. | | `items[].active` | boolean | no | Whether the serial number can be selected on new documents. | | `items[].createdDate` | string (date-time) | no | The date and time the serial number record was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the serial number record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | no | True when more serial numbers remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of serial transactions. `GET /inventory/serial-transactions` Group: Inventory › Serial Transactions · Operation ID: `serialTransactions` This collection returns the full item schema, the same fields get and search return. Page size defaults to 25, up to a maximum of 100. [Open in the interactive reference](https://docs.api.lensing.ai/?op=serialTransactions) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `serialNumberId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `serialNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `manufacturerSerialNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `expirationDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entryDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isSold` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `itemId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `itemCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `transactionDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `sourceDocumentLineType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, transactionDate, entryDate, expirationDate, serialNumber, itemCode, locationCode, lastModifiedDate - Selectable: id, serialNumberId, serialNumber, manufacturerSerialNumber, expirationDate, entryDate, isSold, transactionId, itemId, itemCode, itemName, locationId, locationCode, locationName, transactionDate, inQuantity, outQuantity, cost, currency, sourceDocumentLineType, lastModifiedDate ## Response `200` Success Under `data`, an array of serialTransactionsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the serial transaction line. This is a different record from the ledger row (transactionId) and the unit (serialNumberId). | | `serialNumberId` | string | no | The unique identifier for the serial number that moved. See id on GET inventory/serial-numbers/:id. | | `serialNumber` | string or null | no | The serial number stamped on the unit, for example 'SN-000412'. | | `manufacturerSerialNumber` | string or null | no | The manufacturer's own serial number for the unit, where one is recorded. | | `expirationDate` | string (date) or null | no | The date the unit expires, from the serial record. Format YYYY-MM-DD. | | `entryDate` | string (date) or null | no | The date the unit entered inventory, from the serial record. Format YYYY-MM-DD. | | `isSold` | boolean or null | no | Whether the unit has since been sold. This is a current property of the unit, not of this movement, so a receipt line shows true once the unit is later sold. | | `transactionId` | string or null | no | The unique identifier for the ledger row this serial line belongs to. See id on GET inventory/inventory-transactions/:id. | | `itemId` | string or null | no | The unique identifier for the item that moved, from the ledger row, not the unit's current item. See id on GET inventory/items/:id. | | `itemCode` | string or null | no | The code of that item. | | `itemName` | string or null | no | The display name of that item. | | `locationId` | string or null | no | The unique identifier for the location the movement was posted at, from the ledger row, not where the unit is now. See id on GET inventory/warehouses/:id. | | `locationCode` | string or null | no | The code of that location. | | `locationName` | string or null | no | The display name of that location. | | `transactionDate` | string (date) or null | no | The date the movement was posted. Format YYYY-MM-DD. | | `inQuantity` | string (decimal) or null | no | The quantity into stock for this movement. It is 1 when the movement received the unit and 0 otherwise. | | `outQuantity` | string (decimal) or null | no | The quantity out of stock for this movement. It is 1 when the movement issued the unit and 0 otherwise. | | `cost` | string (decimal) or null | no | The cost of the unit at this movement, in the currency named by currency. | | `currency` | string or null | no | The currency the cost is stated in. Format ISO 4217, for example 'USD'. | | `sourceDocumentLineType` | string or null | no | The kind of document line that produced the movement, from the ledger row. Possible values: 'IMInventorySalesOrderLine', 'IMInventorySalesDeliveryLine', 'IMSalesOrderLine', 'IMInventoryPurchaseReturnLine', 'IMInventorySalesReturnLine', 'IMWarehouseTransferLine', 'IMInventoryDecreaseLine', 'IMInventoryIncreaseLine', 'IMInventoryRevaluationLine', 'IMInventoryTransferLine', 'IMInventoryAdjustmentLine', 'IMInventoryCountLine', 'IMInventoryPutAwayTask', 'IMInventoryPurchaseReturnReversal', 'PurchaseOrderLine', 'IMLandedCostAllocationLine', 'SalesOrderProductItem', 'IMWarehouseTransferOrderLine', 'VendorBillItemBase', 'InvPurchaseDeliveryLine'. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the serial line last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the serial transaction line with the given identifier. `GET /inventory/serial-transactions/{id}` Group: Inventory › Serial Transactions · Operation ID: `getSerialTransaction` Returns the same item schema as GET inventory/serial-transactions and POST inventory/serial-transactions/search. The id you pass is the serial line's identifier, not the ledger row's (transactionId) or the unit's (serialNumberId). [Open in the interactive reference](https://docs.api.lensing.ai/?op=getSerialTransaction) ## 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`. | | `id` | path | string | yes | The unique identifier for the serial transaction line to return. It is the serial line's identifier, not the ledger row's (transactionId) or the unit's (serialNumberId). | ## Response `200` Success Under `data` (getSerialTransactionOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the serial transaction line. This is a different record from the ledger row (transactionId) and the unit (serialNumberId). | | `serialNumberId` | string | yes | The unique identifier for the serial number that moved. See id on GET inventory/serial-numbers/:id. | | `serialNumber` | string | no | The serial number stamped on the unit, for example 'SN-000412'. | | `manufacturerSerialNumber` | string | no | The manufacturer's own serial number for the unit, where one is recorded. | | `expirationDate` | string (date) | no | The date the unit expires, from the serial record. Format YYYY-MM-DD. | | `entryDate` | string (date) | no | The date the unit entered inventory, from the serial record. Format YYYY-MM-DD. | | `isSold` | boolean | no | Whether the unit has since been sold. This is a current property of the unit, not of this movement, so a receipt line shows true once the unit is later sold. | | `transactionId` | string | no | The unique identifier for the ledger row this serial line belongs to. See id on GET inventory/inventory-transactions/:id. | | `itemId` | string | no | The unique identifier for the item that moved, from the ledger row, not the unit's current item. See id on GET inventory/items/:id. | | `itemCode` | string | no | The code of that item. | | `itemName` | string | no | The display name of that item. | | `locationId` | string | no | The unique identifier for the location the movement was posted at, from the ledger row, not where the unit is now. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The code of that location. | | `locationName` | string | no | The display name of that location. | | `transactionDate` | string (date) | no | The date the movement was posted. Format YYYY-MM-DD. | | `inQuantity` | string (decimal) | no | The quantity into stock for this movement. It is 1 when the movement received the unit and 0 otherwise. | | `outQuantity` | string (decimal) | no | The quantity out of stock for this movement. It is 1 when the movement issued the unit and 0 otherwise. | | `cost` | string (decimal) | no | The cost of the unit at this movement, in the currency named by currency. | | `currency` | string | no | The currency the cost is stated in. Format ISO 4217, for example 'USD'. | | `sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | The kind of document line that produced the movement, from the ledger row. | | `lastModifiedDate` | string (date-time) | no | The date and time the serial line last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the serial transactions that match the given filters. `POST /inventory/serial-transactions/search` Group: Inventory › Serial Transactions · Operation ID: `searchSerialTransactions` Every criterion is exact or a date range, and criteria combine with AND. Send an empty filters object to return every serial transaction. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchSerialTransactions) ## 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`. | ## Request body Required, `application/json`: searchSerialTransactionsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. A serial transaction must match every filter you send. Send an empty object to return all serial transactions. | | `filters.serialNumberId` | string | no | Limits results to movements of the serial number with this identifier. | | `filters.serialNumber` | string | no | Limits results to movements of the unit with this serial number. The match is exact. | | `filters.manufacturerSerialNumber` | string | no | Limits results to movements of the unit with this manufacturer serial number. The match is exact. | | `filters.isSold` | boolean | no | Limits results to movements of units that have been sold when true, and to those not yet sold when false. This is a property of the unit, not of the movement. | | `filters.itemId` | string | no | Limits results to movements of this item. This matches the ledger row's item, what moved. | | `filters.locationId` | string | no | Limits results to movements posted at this location. This matches the ledger row's location, not where the unit is now. | | `filters.fromDate` | string (date) | no | Limits results to movements on or after this date. Format YYYY-MM-DD. | | `filters.toDate` | string (date) | no | Limits results to movements on or before this date. Format YYYY-MM-DD. | | `filters.modifiedSince` | string (date-time) | no | Limits results to serial lines changed on or after this time. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of serial transactions per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "serialNumber": "SN-000412" }, "page": { "cursor": "<cursor>", "size": 0 } } ``` ## Response `200` Success Under `data` (searchSerialTransactionsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The serial transactions on this page, oldest first. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the serial transaction line. This is a different record from the ledger row (transactionId) and the unit (serialNumberId). | | `items[].serialNumberId` | string | yes | The unique identifier for the serial number that moved. See id on GET inventory/serial-numbers/:id. | | `items[].serialNumber` | string | no | The serial number stamped on the unit, for example 'SN-000412'. | | `items[].manufacturerSerialNumber` | string | no | The manufacturer's own serial number for the unit, where one is recorded. | | `items[].expirationDate` | string (date) | no | The date the unit expires, from the serial record. Format YYYY-MM-DD. | | `items[].entryDate` | string (date) | no | The date the unit entered inventory, from the serial record. Format YYYY-MM-DD. | | `items[].isSold` | boolean | no | Whether the unit has since been sold. This is a current property of the unit, not of this movement, so a receipt line shows true once the unit is later sold. | | `items[].transactionId` | string | no | The unique identifier for the ledger row this serial line belongs to. See id on GET inventory/inventory-transactions/:id. | | `items[].itemId` | string | no | The unique identifier for the item that moved, from the ledger row, not the unit's current item. See id on GET inventory/items/:id. | | `items[].itemCode` | string | no | The code of that item. | | `items[].itemName` | string | no | The display name of that item. | | `items[].locationId` | string | no | The unique identifier for the location the movement was posted at, from the ledger row, not where the unit is now. See id on GET inventory/warehouses/:id. | | `items[].locationCode` | string | no | The code of that location. | | `items[].locationName` | string | no | The display name of that location. | | `items[].transactionDate` | string (date) | no | The date the movement was posted. Format YYYY-MM-DD. | | `items[].inQuantity` | string (decimal) | no | The quantity into stock for this movement. It is 1 when the movement received the unit and 0 otherwise. | | `items[].outQuantity` | string (decimal) | no | The quantity out of stock for this movement. It is 1 when the movement issued the unit and 0 otherwise. | | `items[].cost` | string (decimal) | no | The cost of the unit at this movement, in the currency named by currency. | | `items[].currency` | string | no | The currency the cost is stated in. Format ISO 4217, for example 'USD'. | | `items[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | The kind of document line that produced the movement, from the ledger row. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the serial line last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more serial transactions remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of shipment carriers. `GET /inventory/shipment-carriers` Group: Inventory › Shipment Carriers · Operation ID: `shipmentCarriers` The collection returns the same fields as the get and search endpoints. Names are not unique, so an exact name filter can match several carriers. vendorName is returned but cannot be filtered or ordered on this endpoint. To find the carriers of one vendor, filter vendorId instead. [Open in the interactive reference](https://docs.api.lensing.ai/?op=shipmentCarriers) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `name` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `active` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `vendorId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, name, active, vendorId, lastModifiedDate - Selectable: id, name, active, vendorId, vendorName, lastModifiedDate ## Response `200` Success Under `data`, an array of shipmentCarriersRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the shipment carrier. | | `name` | string | no | The name of the carrier, for example 'DHL Express'. Names are not unique, so use id to address one carrier. | | `active` | boolean or null | no | Whether the carrier can be selected on new shipments. Absent on carriers created before the active flag existed. | | `vendorId` | string or null | no | The unique identifier for the vendor this carrier bills through. Absent when the carrier has no vendor. See id on GET accounting/vendors/:id. | | `vendorName` | string or null | no | The name of the vendor this carrier bills through. Absent when the carrier has no vendor. It is resolved from the vendor at read time and is not stored on the carrier. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the carrier record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a shipment carrier. `POST /inventory/shipment-carriers` Group: Inventory › Shipment Carriers · Operation ID: `createShipmentCarrier` Creates one shipment carrier. name is the only required field and is not checked for uniqueness, so a second carrier with an existing name is allowed. active defaults to true, so the carrier can be selected on new shipments as soon as it exists. The response contains only the new carrier's id. Read the record with GET inventory/shipment-carriers/:id. To create many carriers at once, use POST inventory/shipment-carriers/bulk. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createShipmentCarrier) ## 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`: createShipmentCarrierInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The name of the carrier, for example 'DHL Express'. It is not checked for uniqueness. | | `active` | boolean | no | Whether the carrier can be selected on new shipments. Defaults to true. | | `vendorId` | string | no | The unique identifier for the vendor this carrier bills through. Omit for a carrier with no vendor. An identifier matching no vendor fails the call with VENDOR_NOT_FOUND. See id on GET accounting/vendors/:id. | Example: ```json { "name": "DHL Express", "active": true, "vendorId": "d4a71e08-6b95-4f23-b7c1-52e08a936d47" } ``` ## Response `200` Success Under `data` (createShipmentCarrierOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created shipment carrier. | ## 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 | | --- | --- | | `VENDOR_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: VENDOR_NOT_FOUND - `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 --- # Returns the shipment carrier with the given identifier. `GET /inventory/shipment-carriers/{id}` Group: Inventory › Shipment Carriers · Operation ID: `getShipmentCarrier` Returns the same fields as GET inventory/shipment-carriers and POST inventory/shipment-carriers/search. vendorId and vendorName are present only when the carrier is linked to a vendor. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getShipmentCarrier) ## 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`. | | `id` | path | string | yes | The unique identifier for the shipment carrier to return. | ## Response `200` Success Under `data` (getShipmentCarrierOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the shipment carrier. | | `name` | string | yes | The name of the carrier, for example 'DHL Express'. Names are not unique, so use id to address one carrier. | | `active` | boolean | no | Whether the carrier can be selected on new shipments. Absent on carriers created before the active flag existed. | | `vendorId` | string | no | The unique identifier for the vendor this carrier bills through. Absent when the carrier has no vendor. See id on GET accounting/vendors/:id. | | `vendorName` | string | no | The name of the vendor this carrier bills through. Absent when the carrier has no vendor. It is resolved from the vendor at read time and is not stored on the carrier. | | `lastModifiedDate` | string (date-time) | no | The date and time the carrier record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Shipment Carrier `PATCH /inventory/shipment-carriers/{id}` Group: Inventory › Shipment Carriers · Operation ID: `updateShipmentCarrier` Updates the shipment carrier with the given identifier. Only the fields in the request body change. Only the fields in the request body change. name cannot be cleared and is not checked for uniqueness, so renaming a carrier onto a name another carrier already has is allowed. Setting active to false retires a carrier without deleting it, which is the alternative when DELETE refuses because shipments still reference it. The response contains only the id. Read the record with GET inventory/shipment-carriers/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateShipmentCarrier) ## 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. | | `id` | path | string | yes | The unique identifier for the shipment carrier to update. | ## Request body Required, `application/json`: updateShipmentCarrierInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The name of the carrier, for example 'DHL Express'. It cannot be cleared and is not checked for uniqueness. | | `active` | boolean | no | Whether the carrier can be selected on new shipments. | | `vendorId` | string or null | no | The unique identifier for the vendor this carrier bills through. Send a vendor's identifier to set or repoint the link, or send null to clear it. An identifier matching no vendor fails the call with VENDOR_NOT_FOUND. See id on GET accounting/vendors/:id. | Example: ```json { "name": "<name>", "active": false, "vendorId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` ## Response `200` Success Under `data` (updateShipmentCarrierOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the shipment carrier. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `VENDOR_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, VENDOR_NOT_FOUND - `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 --- # Deletes the shipment carrier with the given identifier. `DELETE /inventory/shipment-carriers/{id}` Group: Inventory › Shipment Carriers · Operation ID: `deleteShipmentCarrier` Deletes one shipment carrier by the id in the path. This is a hard delete. A carrier any shipment still references cannot be deleted: the call fails with SHIPMENT_CARRIER_IN_USE, and the count says how many shipments still point at it. To take a carrier out of circulation while keeping those records, PATCH inventory/shipment-carriers/:id with active set to false instead. The response returns the deleted carrier's id and the name it held. To delete many at once, use POST inventory/shipment-carriers/bulk-delete. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteShipmentCarrier) ## 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. | | `id` | path | string | yes | The unique identifier for the shipment carrier to delete. | ## Response `200` Success Under `data` (deleteShipmentCarrierOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted shipment carrier. | | `name` | string | yes | The name the deleted carrier held. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `SHIPMENT_CARRIER_IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, SHIPMENT_CARRIER_IN_USE - `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 --- # Creates many shipment carriers in one call. `POST /inventory/shipment-carriers/bulk` Group: Inventory › Shipment Carriers · Operation ID: `bulkCreateShipmentCarriers` Creates many shipment carriers in one call, all-or-nothing: if any row is rejected, nothing is created and the response names the offending row's position. Each row has the same shape as the single create body. The response returns the created carriers in input order, each with only its id. A row whose vendorId matches no vendor fails the call with VENDOR_NOT_FOUND naming that row's index. Names are not checked for uniqueness, within the batch or against existing carriers. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateShipmentCarriers) ## 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`: bulkCreateShipmentCarriersInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `shipmentCarriers` | array of object | yes | The shipment carriers to create. If any row is invalid, the whole call is rejected and nothing is created. | | `shipmentCarriers[].name` | string | yes | The name of the carrier, for example 'DHL Express'. It is not checked for uniqueness, within the batch or against existing carriers. | | `shipmentCarriers[].active` | boolean | no | Whether the carrier can be selected on new shipments. Defaults to true. | | `shipmentCarriers[].vendorId` | string | no | The unique identifier for the vendor this carrier bills through. Omit for a carrier with no vendor. An identifier matching no vendor fails the whole call with VENDOR_NOT_FOUND naming this row's index. See id on GET accounting/vendors/:id. | Example: ```json { "shipmentCarriers": [ { "name": "DHL Express", "vendorId": "d4a71e08-6b95-4f23-b7c1-52e08a936d47" }, { "name": "In-house fleet", "active": false } ] } ``` ## Response `200` Success Under `data` (bulkCreateShipmentCarriersOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `created` | array of object | yes | The created shipment carriers, in the same order as the input. | | `created[].id` | string | yes | The unique identifier for the created shipment carrier. | ## 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 | | --- | --- | | `VENDOR_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: VENDOR_NOT_FOUND - `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 --- # Deletes many shipment carriers in one call. `POST /inventory/shipment-carriers/bulk-delete` Group: Inventory › Shipment Carriers · Operation ID: `bulkDeleteShipmentCarriers` Deletes several shipment carriers in one call, all-or-nothing. Every id is checked before the first delete: an id matching nothing fails the call with NOT_FOUND, and a carrier still named on shipments fails it with SHIPMENT_CARRIER_IN_USE. Both name the offending row's index, and nothing is deleted. The response returns the deleted carriers in input order, each with the name it held. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteShipmentCarriers) ## 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`: bulkDeleteShipmentCarriersInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers for the shipment carriers to delete. If any id is unknown or its carrier is still named on a shipment, the whole call is rejected and nothing is deleted. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteShipmentCarriersOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `deleted` | array of object | yes | The deleted shipment carriers, in the same order as the input. | | `deleted[].id` | string | yes | The unique identifier for the deleted shipment carrier. | | `deleted[].name` | string | yes | The name the deleted carrier held. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `SHIPMENT_CARRIER_IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, SHIPMENT_CARRIER_IN_USE - `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 --- # Returns the shipment carriers that match the given filters. `POST /inventory/shipment-carriers/search` Group: Inventory › Shipment Carriers · Operation ID: `searchShipmentCarriers` Every criterion is exact and criteria combine with AND. Send an empty filters object to return every shipment carrier. There is no filter on vendorName. To find the carriers of one vendor, filter vendorId. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchShipmentCarriers) ## 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`. | ## Request body Required, `application/json`: searchShipmentCarriersInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. A shipment carrier must match every filter you send. Send an empty object to return all shipment carriers. | | `filters.name` | string | no | Limits results to carriers with this name. The match is exact. Names are not unique, so this can match several carriers. | | `filters.active` | boolean | no | Limits results to carriers that can be selected on new shipments when true, and to retired carriers when false. | | `filters.vendorId` | string | no | Limits results to carriers linked to this vendor. An identifier matching no vendor returns an empty page. See id on GET accounting/vendors/:id. | | `filters.modifiedSince` | string (date-time) | no | Limits results to carriers changed on or after this time. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of shipment carriers per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "active": true, "modifiedSince": "2026-08-01T00:00:00.000Z" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchShipmentCarriersOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The shipment carriers on this page, ordered by id. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the shipment carrier. | | `items[].name` | string | yes | The name of the carrier, for example 'DHL Express'. Names are not unique, so use id to address one carrier. | | `items[].active` | boolean | no | Whether the carrier can be selected on new shipments. Absent on carriers created before the active flag existed. | | `items[].vendorId` | string | no | The unique identifier for the vendor this carrier bills through. Absent when the carrier has no vendor. See id on GET accounting/vendors/:id. | | `items[].vendorName` | string | no | The name of the vendor this carrier bills through. Absent when the carrier has no vendor. It is resolved from the vendor at read time and is not stored on the carrier. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the carrier record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more shipment carriers remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of shipments. `GET /inventory/shipments` Group: Inventory › Shipments · Operation ID: `shipments` The collection returns shipment headers only, not their lines, and lineCount stands in for them. To get the lines, or to filter by customer, item, warehouse, reference number or tracking number, use POST inventory/shipments/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=shipments) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `deliveryDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `shipmentCarrierId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `returnStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, documentNumber, documentDate, deliveryDate, documentStatus, totalAmount, createdDate, lastModifiedDate - Selectable: id, documentNumber, documentStatus, documentDate, deliveryDate, entityId, entityName, businessPartnerId, businessPartnerName, shipmentCarrierId, shipmentCarrierName, returnStatus, referenceNumber, trackingNumber, currency, totalAmount, lineCount, notes, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of shipmentsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the shipment. | | `documentNumber` | string or null | no | The shipment's document number, for example 'SD-8841'. Absent while none has been assigned. | | `documentStatus` | string | no | The status of the shipment. Possible values: 'DRAFT', 'PACKED', 'PARTIALLY_SHIPPED', 'SHIPPED', 'COMMITTED', 'DELIVERED', 'COMPLETED', 'CANCELLED', 'INVOICED', 'PARTIALLY_INVOICED', 'RETURNED', 'PARTIALLY_RETURNED'. | | `documentDate` | string (date) | no | The date the shipment is dated. Format YYYY-MM-DD. | | `deliveryDate` | string (date) or null | no | The planned or actual delivery date. Format YYYY-MM-DD. | | `entityId` | string | no | The unique identifier for the legal entity the shipment belongs to. See id on GET base/entities. | | `entityName` | string or null | no | The display name of that legal entity. | | `businessPartnerId` | string | no | The unique identifier for the customer the goods go to. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `businessPartnerName` | string or null | no | The display name of that customer. | | `shipmentCarrierId` | string or null | no | The unique identifier for the carrier taking the goods. See id on GET inventory/shipment-carriers/:id. | | `shipmentCarrierName` | string or null | no | The name of that carrier. | | `returnStatus` | string or null | no | How much of the shipment has come back on sales returns. Possible values: 'OPEN', 'PARTIALLY_RETURNED', 'FULLY_RETURNED'. The return process maintains it. | | `referenceNumber` | string or null | no | The customer's own reference for the shipment. | | `trackingNumber` | string or null | no | The carrier's tracking number for the goods. | | `currency` | string or null | no | The currency of every amount on the shipment. Format ISO 4217, for example 'EUR'. | | `totalAmount` | string (decimal) or null | no | The total value of the shipment, in the shipment's currency. | | `lineCount` | number or null | no | The number of lines the shipment has. The collection does not return the lines themselves. | | `notes` | string or null | no | Free text on the shipment. | | `createdDate` | string (date-time) or null | no | The date and time the shipment was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the shipment record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Shipment `POST /inventory/shipments` Group: Inventory › Shipments · Operation ID: `createShipment` Creates a shipment as a draft. Send it with POST inventory/shipments/:id/commit. The shipment is created as a draft and nothing moves: no stock leaves and no cost of sale is posted. POST inventory/shipments/:id/commit sends the goods and posts the cost of sale. Every line must leave from the same warehouse, checked before anything is written. The whole call is one transaction, so a rejected request writes nothing. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createShipment) ## 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`: createShipmentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `businessPartnerId` | string | yes | The unique identifier for the customer the goods go to. Required. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `entityId` | string | no | The unique identifier for the legal entity. Omit it and the first line's warehouse decides it. See id on GET base/entities. | | `documentDate` | string (date) | no | The date the shipment is dated. Defaults to today. Use format YYYY-MM-DD. | | `deliveryDate` | string (date) | no | The planned delivery date. Use format YYYY-MM-DD. | | `currency` | string | no | The currency for every amount on the shipment. Format ISO 4217, for example 'EUR'. Omit it to use the currency of the legal entity's default accounting book, and creation fails when the entity has no default book. | | `shipmentCarrierId` | string | no | The unique identifier for the carrier taking the goods. See id on GET inventory/shipment-carriers/:id. | | `trackingNumber` | string | no | The carrier's tracking number. | | `referenceNumber` | string | no | The customer's own reference for the shipment. | | `notes` | string | no | Free text on the shipment. | | `lines` | array of object | yes | The lines of the shipment. At least one is required. | | `lines[].itemId` | string | yes | The unique identifier for the item going out. See id on GET inventory/items/:id. | | `lines[].locationId` | string | yes | The unique identifier for the warehouse the goods leave from. Every line must use the same warehouse. See id on GET inventory/warehouses/:id. | | `lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `lines[].quantity` | string (decimal) | yes | The quantity to ship, in that unit. Greater than zero. | | `lines[].unitPrice` | string (decimal) | no | The value of one unit, in the shipment's currency. Omit it to take the price from the sales order line. | | `lines[].sourceDocumentLineId` | string | no | The sales order line this line ships against. Each order publishes its lines as items[].id on GET accounting/sales-orders/:id. Naming it links the shipment to the order it fulfills. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches going out on this line. Required for a batch-tracked item, and the quantities must add up to the line's quantity. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch going out. | | `lines[].batches[].batchNumber` | string | no | The batch number as printed. Required unless batchNumberId is given. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch to ship, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `lines[].serials` | array of object | no | The serialized units going out, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the unit going out. | | `lines[].serials[].serialNumber` | string | no | The serial number stamped on the unit. Required unless serialNumberId is given. | | `lines[].binLocations` | array of object | no | The bins the goods come out of, in a bin-tracked warehouse. An entry may name a batchNumber or serialNumber of its own line. | | `lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin to take the goods from. See id on GET inventory/bins/:id. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `lines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. | | `lines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | Example: ```json { "businessPartnerId": "c73f2b81-5d94-4e60-a812-0f9b6d3e45c7", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "documentDate": "2026-08-14", "deliveryDate": "2026-08-19", "currency": "EUR", "shipmentCarrierId": "9d0a5e26-b183-4c47-92f5-6a4e17b0c853", "trackingNumber": "3SABCD1234567", "referenceNumber": "PO-BAKKER-4417", "notes": "Palletised", "lines": [ { "itemId": "5f21c8b0-9d34-4a76-8e12-04b93c7de5a8", "locationId": "e1d84c07-5b39-4a26-9f70-2c8b613ad495", "unitId": "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "quantity": "40.00", "unitPrice": "24.00", "sourceDocumentLineId": "f9a4cdef-6095-425e-84f1-7f8192801ea2", "notes": "Ship complete" } ] } ``` ## Response `200` Success Under `data` (createShipmentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the shipment. | | `documentNumber` | string | no | The shipment's document number, for example 'SD-8841'. Absent while none has been assigned. | | `documentStatus` | string | yes | The status of the shipment. Possible values: 'DRAFT', 'PACKED', 'PARTIALLY_SHIPPED', 'SHIPPED', 'COMMITTED', 'DELIVERED', 'COMPLETED', 'CANCELLED', 'INVOICED', 'PARTIALLY_INVOICED', 'RETURNED', 'PARTIALLY_RETURNED'. | | `documentDate` | string (date) | yes | The date the shipment is dated. Format YYYY-MM-DD. | | `deliveryDate` | string (date) | no | The planned or actual delivery date. Format YYYY-MM-DD. | | `entityId` | string | yes | The unique identifier for the legal entity the shipment belongs to. See id on GET base/entities. | | `entityName` | string | no | The display name of that legal entity. | | `businessPartnerId` | string | yes | The unique identifier for the customer the goods go to. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `businessPartnerName` | string | no | The display name of that customer. | | `shipmentCarrierId` | string | no | The unique identifier for the carrier taking the goods. See id on GET inventory/shipment-carriers/:id. | | `shipmentCarrierName` | string | no | The name of that carrier. | | `trackingNumber` | string | no | The carrier's tracking number for the goods. | | `referenceNumber` | string | no | The customer's own reference for the shipment. | | `returnStatus` | string | no | How much of the shipment has come back on sales returns. Possible values: 'OPEN', 'PARTIALLY_RETURNED', 'FULLY_RETURNED'. The return process maintains it. | | `currency` | string | no | The currency of every amount on the shipment. Format ISO 4217, for example 'EUR'. | | `totalAmount` | string (decimal) | no | The total value of the shipment, in the shipment's currency. | | `notes` | string | no | Free text on the shipment. | | `createdDate` | string (date-time) | no | The date and time the shipment was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the shipment record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the shipment. A shipment always has at least one. | | `lines[].id` | string | yes | The unique identifier for the shipment line. | | `lines[].itemId` | string | yes | The unique identifier for the item going out. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The short code of that item. | | `lines[].itemName` | string | no | The display name of that item. | | `lines[].locationId` | string | no | The unique identifier for the warehouse the goods leave from. Every line of a shipment must use the same warehouse. See id on GET inventory/warehouses/:id. | | `lines[].locationCode` | string | no | The short code of that warehouse. | | `lines[].locationName` | string | no | The display name of that warehouse. | | `lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `lines[].unitCode` | string | no | The short code of that unit. | | `lines[].quantity` | string (decimal) | yes | The quantity going out, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].pickedQuantity` | string (decimal) | no | How much of the line has been picked. This API does not set it. | | `lines[].packedQuantity` | string (decimal) | no | How much of the line has been packed. This API does not set it. | | `lines[].returnedQuantity` | string (decimal) | no | How much of the line has come back on a sales return. | | `lines[].openReturnQuantity` | string (decimal) | no | How much of the line may still be returned. | | `lines[].unitPrice` | string (decimal) | no | The value of one unit, in the shipment's currency. | | `lines[].status` | string | no | Where the line sits in the warehouse process. This is free text with no fixed set of values. | | `lines[].sourceDocumentLineId` | string | no | The sales order line this line ships against. Each order publishes its lines as items[].id on GET accounting/sales-orders/:id. | | `lines[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | The kind of document line the source line is. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches going out on this line, for a batch-tracked item. Absent when the item is not batch-tracked. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record. | | `lines[].batches[].batchNumber` | string | yes | The batch number as printed. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch going out, in the line's unit. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].batches[].returnedQuantity` | string (decimal) | no | How much of this batch has come back on a sales return. | | `lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch at shipment, in the shipment's currency. | | `lines[].serials` | array of object | no | The serialized units going out, one entry per unit. Absent when the item is not serialized. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record. | | `lines[].serials[].serialNumber` | string | yes | The serial number stamped on the unit. | | `lines[].serials[].returnedQuantity` | string (decimal) | no | How much of this unit has come back on a sales return. | | `lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit at shipment, in the shipment's currency. | | `lines[].binLocations` | array of object | no | The bins the goods come out of, in a bin-tracked warehouse. Each entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the goods come out of. See id on GET inventory/bins/:id. | | `lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `SOURCE_LINE_NOT_PROJECTED` | 400 | | `INVALID_QUANTITY` | 400 | | `MIXED_LOCATIONS` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, NO_LINES, SOURCE_LINE_NOT_PROJECTED, INVALID_QUANTITY, MIXED_LOCATIONS, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Returns the shipment with the given identifier, with all of its lines. `GET /inventory/shipments/{id}` Group: Inventory › Shipments · Operation ID: `getShipment` Returns the same fields as POST inventory/shipments/search. The collection GET inventory/shipments returns headers only, without the lines. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getShipment) ## 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`. | | `id` | path | string | yes | The unique identifier for the shipment to return. | ## Response `200` Success Under `data` (getShipmentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the shipment. | | `documentNumber` | string | no | The shipment's document number, for example 'SD-8841'. Absent while none has been assigned. | | `documentStatus` | string | yes | The status of the shipment. Possible values: 'DRAFT', 'PACKED', 'PARTIALLY_SHIPPED', 'SHIPPED', 'COMMITTED', 'DELIVERED', 'COMPLETED', 'CANCELLED', 'INVOICED', 'PARTIALLY_INVOICED', 'RETURNED', 'PARTIALLY_RETURNED'. | | `documentDate` | string (date) | yes | The date the shipment is dated. Format YYYY-MM-DD. | | `deliveryDate` | string (date) | no | The planned or actual delivery date. Format YYYY-MM-DD. | | `entityId` | string | yes | The unique identifier for the legal entity the shipment belongs to. See id on GET base/entities. | | `entityName` | string | no | The display name of that legal entity. | | `businessPartnerId` | string | yes | The unique identifier for the customer the goods go to. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `businessPartnerName` | string | no | The display name of that customer. | | `shipmentCarrierId` | string | no | The unique identifier for the carrier taking the goods. See id on GET inventory/shipment-carriers/:id. | | `shipmentCarrierName` | string | no | The name of that carrier. | | `trackingNumber` | string | no | The carrier's tracking number for the goods. | | `referenceNumber` | string | no | The customer's own reference for the shipment. | | `returnStatus` | string | no | How much of the shipment has come back on sales returns. Possible values: 'OPEN', 'PARTIALLY_RETURNED', 'FULLY_RETURNED'. The return process maintains it. | | `currency` | string | no | The currency of every amount on the shipment. Format ISO 4217, for example 'EUR'. | | `totalAmount` | string (decimal) | no | The total value of the shipment, in the shipment's currency. | | `notes` | string | no | Free text on the shipment. | | `createdDate` | string (date-time) | no | The date and time the shipment was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the shipment record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the shipment. A shipment always has at least one. | | `lines[].id` | string | yes | The unique identifier for the shipment line. | | `lines[].itemId` | string | yes | The unique identifier for the item going out. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The short code of that item. | | `lines[].itemName` | string | no | The display name of that item. | | `lines[].locationId` | string | no | The unique identifier for the warehouse the goods leave from. Every line of a shipment must use the same warehouse. See id on GET inventory/warehouses/:id. | | `lines[].locationCode` | string | no | The short code of that warehouse. | | `lines[].locationName` | string | no | The display name of that warehouse. | | `lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `lines[].unitCode` | string | no | The short code of that unit. | | `lines[].quantity` | string (decimal) | yes | The quantity going out, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].pickedQuantity` | string (decimal) | no | How much of the line has been picked. This API does not set it. | | `lines[].packedQuantity` | string (decimal) | no | How much of the line has been packed. This API does not set it. | | `lines[].returnedQuantity` | string (decimal) | no | How much of the line has come back on a sales return. | | `lines[].openReturnQuantity` | string (decimal) | no | How much of the line may still be returned. | | `lines[].unitPrice` | string (decimal) | no | The value of one unit, in the shipment's currency. | | `lines[].status` | string | no | Where the line sits in the warehouse process. This is free text with no fixed set of values. | | `lines[].sourceDocumentLineId` | string | no | The sales order line this line ships against. Each order publishes its lines as items[].id on GET accounting/sales-orders/:id. | | `lines[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | The kind of document line the source line is. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches going out on this line, for a batch-tracked item. Absent when the item is not batch-tracked. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record. | | `lines[].batches[].batchNumber` | string | yes | The batch number as printed. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch going out, in the line's unit. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].batches[].returnedQuantity` | string (decimal) | no | How much of this batch has come back on a sales return. | | `lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch at shipment, in the shipment's currency. | | `lines[].serials` | array of object | no | The serialized units going out, one entry per unit. Absent when the item is not serialized. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record. | | `lines[].serials[].serialNumber` | string | yes | The serial number stamped on the unit. | | `lines[].serials[].returnedQuantity` | string (decimal) | no | How much of this unit has come back on a sales return. | | `lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit at shipment, in the shipment's currency. | | `lines[].binLocations` | array of object | no | The bins the goods come out of, in a bin-tracked warehouse. Each entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the goods come out of. See id on GET inventory/bins/:id. | | `lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Shipment `PATCH /inventory/shipments/{id}` Group: Inventory › Shipments · Operation ID: `updateShipment` Updates the draft shipment with the given identifier. Only the fields in the request body change. Only a draft can be changed. Lines change through three arrays, deleteLines then updateLines then createLines, applied in that order, so one call can retire a line and add its replacement. A line's batches, serials or binLocations, when sent, replace what the line had. The whole call is one transaction. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateShipment) ## 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. | | `id` | path | string | yes | The unique identifier for the shipment to update. | ## Request body Required, `application/json`: updateShipmentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `businessPartnerId` | string | no | The unique identifier for the customer the goods go to. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `documentDate` | string (date) | no | The date the shipment is dated. Use format YYYY-MM-DD. | | `deliveryDate` | string (date) | no | The planned delivery date. Use format YYYY-MM-DD. | | `currency` | string | no | The currency for every amount on the shipment. Format ISO 4217, for example 'EUR'. | | `shipmentCarrierId` | string | no | The unique identifier for the carrier taking the goods. See id on GET inventory/shipment-carriers/:id. | | `trackingNumber` | string | no | The carrier's tracking number. | | `referenceNumber` | string | no | The customer's own reference for the shipment. | | `notes` | string | no | Free text on the shipment. Pass an empty string to clear it. | | `deleteLines` | array of string | no | The unique identifiers of lines to remove. Applied before updateLines and createLines. | | `updateLines` | array of object | no | The lines to change, each addressed by id. Applied after deleteLines and before createLines. | | `updateLines[].id` | string | yes | The unique identifier for the line to change. A line belonging to another shipment is rejected. | | `updateLines[].itemId` | string | no | The unique identifier for the item going out. See id on GET inventory/items/:id. | | `updateLines[].locationId` | string | no | The unique identifier for the warehouse the goods leave from. It must agree with every other line. See id on GET inventory/warehouses/:id. | | `updateLines[].unitId` | string | no | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `updateLines[].quantity` | string (decimal) | no | The quantity to ship, in the line's unit. Greater than zero. | | `updateLines[].unitPrice` | string (decimal) | no | The value of one unit, in the shipment's currency. | | `updateLines[].notes` | string | no | Free text on the line. Pass an empty string to clear it. | | `updateLines[].batches` | array of object | no | The batches going out on this line. Required for a batch-tracked item, and the quantities must add up to the line's quantity. This replaces the line's batches. | | `updateLines[].batches[].batchNumberId` | string | no | The unique identifier for the batch going out. | | `updateLines[].batches[].batchNumber` | string | no | The batch number as printed. Required unless batchNumberId is given. | | `updateLines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch to ship, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `updateLines[].serials` | array of object | no | The serialized units going out, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. This replaces the line's serials. | | `updateLines[].serials[].serialNumberId` | string | no | The unique identifier for the unit going out. | | `updateLines[].serials[].serialNumber` | string | no | The serial number stamped on the unit. Required unless serialNumberId is given. | | `updateLines[].binLocations` | array of object | no | The bins the goods come out of, in a bin-tracked warehouse. An entry may name a batchNumber or serialNumber of its own line. This replaces the line's bin entries. | | `updateLines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin to take the goods from. See id on GET inventory/bins/:id. | | `updateLines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `updateLines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. | | `updateLines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | | `createLines` | array of object | no | The lines to add. | | `createLines[].itemId` | string | yes | The unique identifier for the item going out. See id on GET inventory/items/:id. | | `createLines[].locationId` | string | yes | The unique identifier for the warehouse the goods leave from. Every line must use the same warehouse. See id on GET inventory/warehouses/:id. | | `createLines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `createLines[].quantity` | string (decimal) | yes | The quantity to ship, in that unit. Greater than zero. | | `createLines[].unitPrice` | string (decimal) | no | The value of one unit, in the shipment's currency. Omit it to take the price from the sales order line. | | `createLines[].sourceDocumentLineId` | string | no | The sales order line this line ships against. Each order publishes its lines as items[].id on GET accounting/sales-orders/:id. Naming it links the shipment to the order it fulfills. | | `createLines[].notes` | string | no | Free text on the line. | | `createLines[].batches` | array of object | no | The batches going out on this line. Required for a batch-tracked item, and the quantities must add up to the line's quantity. | | `createLines[].batches[].batchNumberId` | string | no | The unique identifier for the batch going out. | | `createLines[].batches[].batchNumber` | string | no | The batch number as printed. Required unless batchNumberId is given. | | `createLines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch to ship, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `createLines[].serials` | array of object | no | The serialized units going out, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. | | `createLines[].serials[].serialNumberId` | string | no | The unique identifier for the unit going out. | | `createLines[].serials[].serialNumber` | string | no | The serial number stamped on the unit. Required unless serialNumberId is given. | | `createLines[].binLocations` | array of object | no | The bins the goods come out of, in a bin-tracked warehouse. An entry may name a batchNumber or serialNumber of its own line. | | `createLines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin to take the goods from. See id on GET inventory/bins/:id. | | `createLines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `createLines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. | | `createLines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | Example: ```json { "businessPartnerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "documentDate": "2026-01-15", "deliveryDate": "2026-01-15", "currency": "<currency>", "shipmentCarrierId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "trackingNumber": "3SABCD7654321", "referenceNumber": "<referenceNumber>", "notes": "<notes>", "deleteLines": [ "<deleteLines>" ], "updateLines": [ { "id": "8c14a6f2-3b70-4d59-9e02-6f5a1d38b47c", "quantity": "36.00" } ], "createLines": [ { "itemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "locationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "unitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "unitPrice": "19.99", "sourceDocumentLineId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "notes": "<notes>", "batches": [ { "batchNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "batchNumber": "<batchNumber>", "quantity": "19.99" } ], "serials": [ { "serialNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "serialNumber": "<serialNumber>" } ], "binLocations": [ { "binLocationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "batchNumber": "<batchNumber>", "serialNumber": "<serialNumber>" } ] } ] } ``` ## Response `200` Success Under `data` (updateShipmentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the shipment. | | `documentNumber` | string | no | The shipment's document number, for example 'SD-8841'. Absent while none has been assigned. | | `documentStatus` | string | yes | The status of the shipment. Possible values: 'DRAFT', 'PACKED', 'PARTIALLY_SHIPPED', 'SHIPPED', 'COMMITTED', 'DELIVERED', 'COMPLETED', 'CANCELLED', 'INVOICED', 'PARTIALLY_INVOICED', 'RETURNED', 'PARTIALLY_RETURNED'. | | `documentDate` | string (date) | yes | The date the shipment is dated. Format YYYY-MM-DD. | | `deliveryDate` | string (date) | no | The planned or actual delivery date. Format YYYY-MM-DD. | | `entityId` | string | yes | The unique identifier for the legal entity the shipment belongs to. See id on GET base/entities. | | `entityName` | string | no | The display name of that legal entity. | | `businessPartnerId` | string | yes | The unique identifier for the customer the goods go to. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `businessPartnerName` | string | no | The display name of that customer. | | `shipmentCarrierId` | string | no | The unique identifier for the carrier taking the goods. See id on GET inventory/shipment-carriers/:id. | | `shipmentCarrierName` | string | no | The name of that carrier. | | `trackingNumber` | string | no | The carrier's tracking number for the goods. | | `referenceNumber` | string | no | The customer's own reference for the shipment. | | `returnStatus` | string | no | How much of the shipment has come back on sales returns. Possible values: 'OPEN', 'PARTIALLY_RETURNED', 'FULLY_RETURNED'. The return process maintains it. | | `currency` | string | no | The currency of every amount on the shipment. Format ISO 4217, for example 'EUR'. | | `totalAmount` | string (decimal) | no | The total value of the shipment, in the shipment's currency. | | `notes` | string | no | Free text on the shipment. | | `createdDate` | string (date-time) | no | The date and time the shipment was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the shipment record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the shipment. A shipment always has at least one. | | `lines[].id` | string | yes | The unique identifier for the shipment line. | | `lines[].itemId` | string | yes | The unique identifier for the item going out. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The short code of that item. | | `lines[].itemName` | string | no | The display name of that item. | | `lines[].locationId` | string | no | The unique identifier for the warehouse the goods leave from. Every line of a shipment must use the same warehouse. See id on GET inventory/warehouses/:id. | | `lines[].locationCode` | string | no | The short code of that warehouse. | | `lines[].locationName` | string | no | The display name of that warehouse. | | `lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `lines[].unitCode` | string | no | The short code of that unit. | | `lines[].quantity` | string (decimal) | yes | The quantity going out, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].pickedQuantity` | string (decimal) | no | How much of the line has been picked. This API does not set it. | | `lines[].packedQuantity` | string (decimal) | no | How much of the line has been packed. This API does not set it. | | `lines[].returnedQuantity` | string (decimal) | no | How much of the line has come back on a sales return. | | `lines[].openReturnQuantity` | string (decimal) | no | How much of the line may still be returned. | | `lines[].unitPrice` | string (decimal) | no | The value of one unit, in the shipment's currency. | | `lines[].status` | string | no | Where the line sits in the warehouse process. This is free text with no fixed set of values. | | `lines[].sourceDocumentLineId` | string | no | The sales order line this line ships against. Each order publishes its lines as items[].id on GET accounting/sales-orders/:id. | | `lines[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | The kind of document line the source line is. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches going out on this line, for a batch-tracked item. Absent when the item is not batch-tracked. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record. | | `lines[].batches[].batchNumber` | string | yes | The batch number as printed. | | `lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch going out, in the line's unit. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].batches[].returnedQuantity` | string (decimal) | no | How much of this batch has come back on a sales return. | | `lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch at shipment, in the shipment's currency. | | `lines[].serials` | array of object | no | The serialized units going out, one entry per unit. Absent when the item is not serialized. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record. | | `lines[].serials[].serialNumber` | string | yes | The serial number stamped on the unit. | | `lines[].serials[].returnedQuantity` | string (decimal) | no | How much of this unit has come back on a sales return. | | `lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit at shipment, in the shipment's currency. | | `lines[].binLocations` | array of object | no | The bins the goods come out of, in a bin-tracked warehouse. Each entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the goods come out of. See id on GET inventory/bins/:id. | | `lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `SOURCE_LINE_NOT_PROJECTED` | 400 | | `INVALID_QUANTITY` | 400 | | `MIXED_LOCATIONS` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, REFERENCE_NOT_FOUND, NO_LINES, SOURCE_LINE_NOT_PROJECTED, INVALID_QUANTITY, MIXED_LOCATIONS, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Deletes the shipment with the given identifier. `DELETE /inventory/shipments/{id}` Group: Inventory › Shipments · Operation ID: `deleteShipment` Only a draft or a canceled shipment can be deleted. A shipment that has moved stock must be canceled first, which posts the reversals. Deleting an identifier that does not exist fails with NOT_FOUND. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteShipment) ## 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. | | `id` | path | string | yes | The unique identifier for the shipment to delete. | ## Response `200` Success Under `data` (deleteShipmentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted shipment. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, POSTING_REJECTED - `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 --- # Ships and delivers the shipment in one call. `POST /inventory/shipments/{id}/commit` Group: Inventory › Shipments · Operation ID: `commitShipment` Sends the goods and closes the shipment in one call: a draft becomes delivered and the cost of sale is posted. Every line must leave from the same warehouse, checked before anything is written. [Open in the interactive reference](https://docs.api.lensing.ai/?op=commitShipment) ## 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. | | `id` | path | string | yes | The unique identifier for the shipment to commit. | ## Request body Required, `application/json`: commitShipmentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `cogsChoice` | string, one of `"NON_AMORTIZED"`, `"AMORTIZED"` | no | Whether the cost of sale is amortized. AMORTIZED requests COGS amortization schedules, which are created only for service lines, so on a shipment of goods it changes nothing. Defaults to NON_AMORTIZED. | Example: ```json { "cogsChoice": "NON_AMORTIZED" } ``` ## Response `200` Success Under `data` (commitShipmentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the shipment. | | `documentStatus` | string | yes | The status the shipment is now in, read back from the document. Possible values: 'DRAFT', 'PACKED', 'PARTIALLY_SHIPPED', 'SHIPPED', 'COMMITTED', 'DELIVERED', 'COMPLETED', 'CANCELLED', 'INVOICED', 'PARTIALLY_INVOICED', 'RETURNED', 'PARTIALLY_RETURNED'. | | `journalEntryId` | string | no | The unique identifier for the journal entry this step posted, when it posted one. See id on GET accounting/journal-entries/:id. | | `warnings` | array of string | yes | Non-fatal advisories from the step. An empty array on a clean run. The step succeeded regardless of what is listed here. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `NO_LINES` | 400 | | `SOURCE_LINE_NOT_PROJECTED` | 400 | | `MIXED_LOCATIONS` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, NO_LINES, SOURCE_LINE_NOT_PROJECTED, MIXED_LOCATIONS, POSTING_REJECTED - `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 --- # Creates several shipments in one call, each as a draft. `POST /inventory/shipments/bulk` Group: Inventory › Shipments · Operation ID: `bulkCreateShipments` All or nothing: if any shipment is rejected, nothing is written, not even the entries before it. index on the error says which, counting from zero. Each entry is exactly what POST inventory/shipments takes, and the results come back in input order. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateShipments) ## 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`: bulkCreateShipmentsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `shipments` | array of object | yes | The shipments to create. At least one, and each is exactly what POST inventory/shipments takes. | | `shipments[].businessPartnerId` | string | yes | The unique identifier for the customer the goods go to. Required. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `shipments[].entityId` | string | no | The unique identifier for the legal entity. Omit it and the first line's warehouse decides it. See id on GET base/entities. | | `shipments[].documentDate` | string (date) | no | The date the shipment is dated. Defaults to today. Use format YYYY-MM-DD. | | `shipments[].deliveryDate` | string (date) | no | The planned delivery date. Use format YYYY-MM-DD. | | `shipments[].currency` | string | no | The currency for every amount on the shipment. Format ISO 4217, for example 'EUR'. Omit it to use the currency of the legal entity's default accounting book, and creation fails when the entity has no default book. | | `shipments[].shipmentCarrierId` | string | no | The unique identifier for the carrier taking the goods. See id on GET inventory/shipment-carriers/:id. | | `shipments[].trackingNumber` | string | no | The carrier's tracking number. | | `shipments[].referenceNumber` | string | no | The customer's own reference for the shipment. | | `shipments[].notes` | string | no | Free text on the shipment. | | `shipments[].lines` | array of object | yes | The lines of the shipment. At least one is required. | | `shipments[].lines[].itemId` | string | yes | The unique identifier for the item going out. See id on GET inventory/items/:id. | | `shipments[].lines[].locationId` | string | yes | The unique identifier for the warehouse the goods leave from. Every line must use the same warehouse. See id on GET inventory/warehouses/:id. | | `shipments[].lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `shipments[].lines[].quantity` | string (decimal) | yes | The quantity to ship, in that unit. Greater than zero. | | `shipments[].lines[].unitPrice` | string (decimal) | no | The value of one unit, in the shipment's currency. Omit it to take the price from the sales order line. | | `shipments[].lines[].sourceDocumentLineId` | string | no | The sales order line this line ships against. Each order publishes its lines as items[].id on GET accounting/sales-orders/:id. Naming it links the shipment to the order it fulfills. | | `shipments[].lines[].notes` | string | no | Free text on the line. | | `shipments[].lines[].batches` | array of object | no | The batches going out on this line. Required for a batch-tracked item, and the quantities must add up to the line's quantity. | | `shipments[].lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch going out. | | `shipments[].lines[].batches[].batchNumber` | string | no | The batch number as printed. Required unless batchNumberId is given. | | `shipments[].lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch to ship, in the line's unit. The batch quantities of a line must add up to the line's quantity. | | `shipments[].lines[].serials` | array of object | no | The serialized units going out, one entry per unit. Required for a serialized item, and the count must equal the line's quantity. | | `shipments[].lines[].serials[].serialNumberId` | string | no | The unique identifier for the unit going out. | | `shipments[].lines[].serials[].serialNumber` | string | no | The serial number stamped on the unit. Required unless serialNumberId is given. | | `shipments[].lines[].binLocations` | array of object | no | The bins the goods come out of, in a bin-tracked warehouse. An entry may name a batchNumber or serialNumber of its own line. | | `shipments[].lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin to take the goods from. See id on GET inventory/bins/:id. | | `shipments[].lines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `shipments[].lines[].binLocations[].batchNumber` | string | no | Names one of this line's batches, when the bin entry belongs to it. | | `shipments[].lines[].binLocations[].serialNumber` | string | no | Names one of this line's serialized units, when the bin entry belongs to it. | Example: ```json { "shipments": [ { "businessPartnerId": "c73f2b81-5d94-4e60-a812-0f9b6d3e45c7", "documentDate": "2026-08-14", "deliveryDate": "2026-08-19", "shipmentCarrierId": "9d0a5e26-b183-4c47-92f5-6a4e17b0c853", "trackingNumber": "3SABCD1234567", "referenceNumber": "PO-BAKKER-4417", "currency": "EUR", "notes": "Palletised", "lines": [ { "itemId": "5f21c8b0-9d34-4a76-8e12-04b93c7de5a8", "locationId": "e1d84c07-5b39-4a26-9f70-2c8b613ad495", "unitId": "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "quantity": "40.00", "unitPrice": "24.00", "sourceDocumentLineId": "f9a4cdef-6095-425e-84f1-7f8192801ea2", "notes": "Ship complete" } ] } ] } ``` ## Response `200` Success Under `data` (bulkCreateShipmentsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The created shipments, in input order. Match them to inputs by position. | | `items[].id` | string | yes | The unique identifier for the shipment. | | `items[].documentNumber` | string | no | The shipment's document number, for example 'SD-8841'. Absent while none has been assigned. | | `items[].documentStatus` | string | yes | The status of the shipment. Possible values: 'DRAFT', 'PACKED', 'PARTIALLY_SHIPPED', 'SHIPPED', 'COMMITTED', 'DELIVERED', 'COMPLETED', 'CANCELLED', 'INVOICED', 'PARTIALLY_INVOICED', 'RETURNED', 'PARTIALLY_RETURNED'. | | `items[].documentDate` | string (date) | yes | The date the shipment is dated. Format YYYY-MM-DD. | | `items[].deliveryDate` | string (date) | no | The planned or actual delivery date. Format YYYY-MM-DD. | | `items[].entityId` | string | yes | The unique identifier for the legal entity the shipment belongs to. See id on GET base/entities. | | `items[].entityName` | string | no | The display name of that legal entity. | | `items[].businessPartnerId` | string | yes | The unique identifier for the customer the goods go to. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `items[].businessPartnerName` | string | no | The display name of that customer. | | `items[].shipmentCarrierId` | string | no | The unique identifier for the carrier taking the goods. See id on GET inventory/shipment-carriers/:id. | | `items[].shipmentCarrierName` | string | no | The name of that carrier. | | `items[].trackingNumber` | string | no | The carrier's tracking number for the goods. | | `items[].referenceNumber` | string | no | The customer's own reference for the shipment. | | `items[].returnStatus` | string | no | How much of the shipment has come back on sales returns. Possible values: 'OPEN', 'PARTIALLY_RETURNED', 'FULLY_RETURNED'. The return process maintains it. | | `items[].currency` | string | no | The currency of every amount on the shipment. Format ISO 4217, for example 'EUR'. | | `items[].totalAmount` | string (decimal) | no | The total value of the shipment, in the shipment's currency. | | `items[].notes` | string | no | Free text on the shipment. | | `items[].createdDate` | string (date-time) | no | The date and time the shipment was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the shipment record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | yes | The lines of the shipment. A shipment always has at least one. | | `items[].lines[].id` | string | yes | The unique identifier for the shipment line. | | `items[].lines[].itemId` | string | yes | The unique identifier for the item going out. See id on GET inventory/items/:id. | | `items[].lines[].itemCode` | string | no | The short code of that item. | | `items[].lines[].itemName` | string | no | The display name of that item. | | `items[].lines[].locationId` | string | no | The unique identifier for the warehouse the goods leave from. Every line of a shipment must use the same warehouse. See id on GET inventory/warehouses/:id. | | `items[].lines[].locationCode` | string | no | The short code of that warehouse. | | `items[].lines[].locationName` | string | no | The display name of that warehouse. | | `items[].lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `items[].lines[].unitCode` | string | no | The short code of that unit. | | `items[].lines[].quantity` | string (decimal) | yes | The quantity going out, in the line's unit. | | `items[].lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].pickedQuantity` | string (decimal) | no | How much of the line has been picked. This API does not set it. | | `items[].lines[].packedQuantity` | string (decimal) | no | How much of the line has been packed. This API does not set it. | | `items[].lines[].returnedQuantity` | string (decimal) | no | How much of the line has come back on a sales return. | | `items[].lines[].openReturnQuantity` | string (decimal) | no | How much of the line may still be returned. | | `items[].lines[].unitPrice` | string (decimal) | no | The value of one unit, in the shipment's currency. | | `items[].lines[].status` | string | no | Where the line sits in the warehouse process. This is free text with no fixed set of values. | | `items[].lines[].sourceDocumentLineId` | string | no | The sales order line this line ships against. Each order publishes its lines as items[].id on GET accounting/sales-orders/:id. | | `items[].lines[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | The kind of document line the source line is. | | `items[].lines[].notes` | string | no | Free text on the line. | | `items[].lines[].batches` | array of object | no | The batches going out on this line, for a batch-tracked item. Absent when the item is not batch-tracked. | | `items[].lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record. | | `items[].lines[].batches[].batchNumber` | string | yes | The batch number as printed. | | `items[].lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `items[].lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch going out, in the line's unit. | | `items[].lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].batches[].returnedQuantity` | string (decimal) | no | How much of this batch has come back on a sales return. | | `items[].lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch at shipment, in the shipment's currency. | | `items[].lines[].serials` | array of object | no | The serialized units going out, one entry per unit. Absent when the item is not serialized. | | `items[].lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record. | | `items[].lines[].serials[].serialNumber` | string | yes | The serial number stamped on the unit. | | `items[].lines[].serials[].returnedQuantity` | string (decimal) | no | How much of this unit has come back on a sales return. | | `items[].lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit at shipment, in the shipment's currency. | | `items[].lines[].binLocations` | array of object | no | The bins the goods come out of, in a bin-tracked warehouse. Each entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `items[].lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the goods come out of. See id on GET inventory/bins/:id. | | `items[].lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `items[].lines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `items[].lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `items[].lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | ## 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 | | --- | --- | | `NOTHING_TO_CREATE` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `SOURCE_LINE_NOT_PROJECTED` | 400 | | `INVALID_QUANTITY` | 400 | | `MIXED_LOCATIONS` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOTHING_TO_CREATE, REFERENCE_NOT_FOUND, NO_LINES, SOURCE_LINE_NOT_PROJECTED, INVALID_QUANTITY, MIXED_LOCATIONS, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Deletes several shipments in one call. `POST /inventory/shipments/bulk-delete` Group: Inventory › Shipments · Operation ID: `bulkDeleteShipments` All or nothing: if any identifier matches no shipment, or names one past draft, nothing is deleted. index on the error says which, counting from zero. Delete one at a time with DELETE inventory/shipments/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteShipments) ## 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`: bulkDeleteShipmentsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the shipments to delete. All or nothing: if any is missing or past draft, none is deleted. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteShipmentsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the deleted shipments, in input order. | ## 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 | | --- | --- | | `NOTHING_TO_DELETE` | 400 | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOTHING_TO_DELETE, NOT_FOUND, WRONG_STATUS, POSTING_REJECTED - `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 --- # Returns the shipments that match the given filters, each with its lines. `POST /inventory/shipments/search` Group: Inventory › Shipments · Operation ID: `searchShipments` This is the only endpoint that returns the lines, and the only one that filters by customer, reference number or tracking number. Every criterion is exact or a range, and criteria combine with AND. Send an empty filters object to return every shipment. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchShipments) ## 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`. | ## Request body Required, `application/json`: searchShipmentsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. A shipment must match every filter you send. Send an empty object to return all shipments. | | `filters.documentNumber` | string | no | Limits results to the shipment with this document number. The match is exact. | | `filters.documentStatus` | string | no | Limits results to shipments with this status. The match is exact. Possible values: 'DRAFT', 'PACKED', 'PARTIALLY_SHIPPED', 'SHIPPED', 'COMMITTED', 'DELIVERED', 'COMPLETED', 'CANCELLED', 'INVOICED', 'PARTIALLY_INVOICED', 'RETURNED', 'PARTIALLY_RETURNED'. | | `filters.entityId` | string | no | Limits results to shipments for the legal entity with this identifier. | | `filters.businessPartnerId` | string | no | Limits results to shipments for the customer with this identifier. Despite the name, this is a customer id. | | `filters.locationId` | string | no | Limits results to shipments with a line for the warehouse with this identifier. | | `filters.itemId` | string | no | Limits results to shipments with a line for the item with this identifier. | | `filters.referenceNumber` | string | no | Limits results to the shipment with this customer reference. The match is exact. | | `filters.trackingNumber` | string | no | Limits results to the shipment with this carrier tracking number. The match is exact. | | `filters.fromDate` | string (date) | no | Limits results to shipments dated on or after this date. Format YYYY-MM-DD. | | `filters.toDate` | string (date) | no | Limits results to shipments dated on or before this date. Format YYYY-MM-DD. | | `filters.modifiedSince` | string (date-time) | no | Limits results to shipments changed on or after this time. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of shipments per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "documentStatus": "SHIPPED" }, "page": { "cursor": "<cursor>", "size": 0 } } ``` ## Response `200` Success Under `data` (searchShipmentsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The shipments on this page, each with its lines. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the shipment. | | `items[].documentNumber` | string | no | The shipment's document number, for example 'SD-8841'. Absent while none has been assigned. | | `items[].documentStatus` | string | yes | The status of the shipment. Possible values: 'DRAFT', 'PACKED', 'PARTIALLY_SHIPPED', 'SHIPPED', 'COMMITTED', 'DELIVERED', 'COMPLETED', 'CANCELLED', 'INVOICED', 'PARTIALLY_INVOICED', 'RETURNED', 'PARTIALLY_RETURNED'. | | `items[].documentDate` | string (date) | yes | The date the shipment is dated. Format YYYY-MM-DD. | | `items[].deliveryDate` | string (date) | no | The planned or actual delivery date. Format YYYY-MM-DD. | | `items[].entityId` | string | yes | The unique identifier for the legal entity the shipment belongs to. See id on GET base/entities. | | `items[].entityName` | string | no | The display name of that legal entity. | | `items[].businessPartnerId` | string | yes | The unique identifier for the customer the goods go to. Despite the name, this is a customer id. See id on GET accounting/customers/:id. | | `items[].businessPartnerName` | string | no | The display name of that customer. | | `items[].shipmentCarrierId` | string | no | The unique identifier for the carrier taking the goods. See id on GET inventory/shipment-carriers/:id. | | `items[].shipmentCarrierName` | string | no | The name of that carrier. | | `items[].trackingNumber` | string | no | The carrier's tracking number for the goods. | | `items[].referenceNumber` | string | no | The customer's own reference for the shipment. | | `items[].returnStatus` | string | no | How much of the shipment has come back on sales returns. Possible values: 'OPEN', 'PARTIALLY_RETURNED', 'FULLY_RETURNED'. The return process maintains it. | | `items[].currency` | string | no | The currency of every amount on the shipment. Format ISO 4217, for example 'EUR'. | | `items[].totalAmount` | string (decimal) | no | The total value of the shipment, in the shipment's currency. | | `items[].notes` | string | no | Free text on the shipment. | | `items[].createdDate` | string (date-time) | no | The date and time the shipment was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the shipment record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | yes | The lines of the shipment. A shipment always has at least one. | | `items[].lines[].id` | string | yes | The unique identifier for the shipment line. | | `items[].lines[].itemId` | string | yes | The unique identifier for the item going out. See id on GET inventory/items/:id. | | `items[].lines[].itemCode` | string | no | The short code of that item. | | `items[].lines[].itemName` | string | no | The display name of that item. | | `items[].lines[].locationId` | string | no | The unique identifier for the warehouse the goods leave from. Every line of a shipment must use the same warehouse. See id on GET inventory/warehouses/:id. | | `items[].lines[].locationCode` | string | no | The short code of that warehouse. | | `items[].lines[].locationName` | string | no | The display name of that warehouse. | | `items[].lines[].unitId` | string | yes | The unique identifier for the unit the quantity is counted in. See id on GET inventory/unit-of-measures/:id. | | `items[].lines[].unitCode` | string | no | The short code of that unit. | | `items[].lines[].quantity` | string (decimal) | yes | The quantity going out, in the line's unit. | | `items[].lines[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].pickedQuantity` | string (decimal) | no | How much of the line has been picked. This API does not set it. | | `items[].lines[].packedQuantity` | string (decimal) | no | How much of the line has been packed. This API does not set it. | | `items[].lines[].returnedQuantity` | string (decimal) | no | How much of the line has come back on a sales return. | | `items[].lines[].openReturnQuantity` | string (decimal) | no | How much of the line may still be returned. | | `items[].lines[].unitPrice` | string (decimal) | no | The value of one unit, in the shipment's currency. | | `items[].lines[].status` | string | no | Where the line sits in the warehouse process. This is free text with no fixed set of values. | | `items[].lines[].sourceDocumentLineId` | string | no | The sales order line this line ships against. Each order publishes its lines as items[].id on GET accounting/sales-orders/:id. | | `items[].lines[].sourceDocumentLineType` | string, one of `"IMInventorySalesOrderLine"`, `"IMInventorySalesDeliveryLine"`, `"IMSalesOrderLine"`, `"IMInventoryPurchaseReturnLine"`, `"IMInventorySalesReturnLine"`, `"IMWarehouseTransferLine"`, `"IMInventoryDecreaseLine"`, `"IMInventoryIncreaseLine"`, `"IMInventoryRevaluationLine"`, `"IMInventoryTransferLine"`, `"IMInventoryAdjustmentLine"`, `"IMInventoryCountLine"`, `"IMInventoryPutAwayTask"`, `"IMInventoryPurchaseReturnReversal"`, `"PurchaseOrderLine"`, `"IMLandedCostAllocationLine"`, `"SalesOrderProductItem"`, `"IMWarehouseTransferOrderLine"`, `"VendorBillItemBase"`, `"InvPurchaseDeliveryLine"` | no | The kind of document line the source line is. | | `items[].lines[].notes` | string | no | Free text on the line. | | `items[].lines[].batches` | array of object | no | The batches going out on this line, for a batch-tracked item. Absent when the item is not batch-tracked. | | `items[].lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch record. | | `items[].lines[].batches[].batchNumber` | string | yes | The batch number as printed. | | `items[].lines[].batches[].expirationDate` | string (date) | no | The expiration date of the batch. Format YYYY-MM-DD. | | `items[].lines[].batches[].quantity` | string (decimal) | yes | The quantity of this batch going out, in the line's unit. | | `items[].lines[].batches[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].batches[].returnedQuantity` | string (decimal) | no | How much of this batch has come back on a sales return. | | `items[].lines[].batches[].unitCost` | string (decimal) | no | The cost of one unit of this batch at shipment, in the shipment's currency. | | `items[].lines[].serials` | array of object | no | The serialized units going out, one entry per unit. Absent when the item is not serialized. | | `items[].lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial record. | | `items[].lines[].serials[].serialNumber` | string | yes | The serial number stamped on the unit. | | `items[].lines[].serials[].returnedQuantity` | string (decimal) | no | How much of this unit has come back on a sales return. | | `items[].lines[].serials[].unitCost` | string (decimal) | no | The cost of this unit at shipment, in the shipment's currency. | | `items[].lines[].binLocations` | array of object | no | The bins the goods come out of, in a bin-tracked warehouse. Each entry that names a batchNumber or serialNumber belongs to that batch or unit, and one that names neither belongs to the line. | | `items[].lines[].binLocations[].binLocationId` | string | yes | The unique identifier for the bin the goods come out of. See id on GET inventory/bins/:id. | | `items[].lines[].binLocations[].binLocationCode` | string | no | The short code of that bin. | | `items[].lines[].binLocations[].quantity` | string (decimal) | yes | The quantity taken from this bin, in the line's unit. | | `items[].lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same quantity in the item's stocking unit. | | `items[].lines[].binLocations[].batchNumber` | string | no | The batch this bin entry belongs to, when it belongs to one of the line's batches. | | `items[].lines[].binLocations[].serialNumber` | string | no | The serialized unit this bin entry belongs to, when it belongs to one of the line's units. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more shipments remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of unit of measure groups. `GET /inventory/unit-of-measure-groups` Group: Inventory › Unit of Measure Groups · Operation ID: `unitOfMeasureGroups` This collection returns the group header only, not its conversion lines or base unit. Use GET inventory/unit-of-measure-groups/:id or POST inventory/unit-of-measure-groups/search when you need the conversion table, and POST inventory/unit-of-measure-groups/search to find the groups that convert a given unit of measure. [Open in the interactive reference](https://docs.api.lensing.ai/?op=unitOfMeasureGroups) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `name` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, name, description, lastModifiedDate - Selectable: id, name, description, lastModifiedDate ## Response `200` Success Under `data`, an array of unitOfMeasureGroupsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the unit of measure group. | | `name` | string | no | The display name of the unit of measure group. Names are not unique. | | `description` | string or null | no | A free-form description of what the group covers. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the unit of measure group's own fields last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Adding or changing a conversion line does not change it. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a unit of measure group. `POST /inventory/unit-of-measure-groups` Group: Inventory › Unit of Measure Groups · Operation ID: `createUnitOfMeasureGroup` The response is only the created unit of measure group's id. Read the group with GET inventory/unit-of-measure-groups/:id when you need its stored state, including the ids assigned to its lines. The base unit gets its own line automatically with a factor of 1, so send baseUnitOfMeasureId and leave it out of lines. Each unit of measure may appear at most once in the group. Use POST inventory/unit-of-measure-groups/bulk to create several at once. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createUnitOfMeasureGroup) ## 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`: createUnitOfMeasureGroupInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The display name of the unit of measure group. Required. It is not checked for uniqueness. | | `description` | string | no | A free-form description of what the group covers. Omit for none. | | `baseUnitOfMeasureId` | string | yes | The unique identifier for the base unit of measure that every conversion factor in the group is relative to. Required. It is fixed for the life of the group and gets its own line automatically with a factor of 1, so do not list it in lines. See id on GET inventory/unit-of-measures/:id. | | `lines` | array of object | no | The conversion lines to add, beyond the base unit's automatic one. Omit for a group with only its base unit. Each unit of measure may appear at most once, and none may be the base unit. | | `lines[].unitOfMeasureId` | string | yes | The unique identifier for the unit of measure this line converts. It must not be the base unit and must not repeat another line in this request. See id on GET inventory/unit-of-measures/:id. | | `lines[].conversionFactor` | string (decimal) | yes | How many base units of measure make up one of this unit. It must be greater than zero. | Example: ```json { "name": "Packaging", "description": "Eaches, boxes and pallets", "baseUnitOfMeasureId": "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "lines": [ { "unitOfMeasureId": "8c1f4a72-6b09-4d15-a3e7-40d2b6915cf8", "conversionFactor": "12.00" } ] } ``` ## Response `200` Success Under `data` (createUnitOfMeasureGroupOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created unit of measure group. | ## 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 | | --- | --- | | `UNIT_NOT_FOUND` | 400 | | `DUPLICATE_UNIT_IN_GROUP` | 400 | | `INVALID_CONVERSION_FACTOR` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: UNIT_NOT_FOUND, DUPLICATE_UNIT_IN_GROUP, INVALID_CONVERSION_FACTOR - `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 --- # Returns the unit of measure group with the given identifier. `GET /inventory/unit-of-measure-groups/{id}` Group: Inventory › Unit of Measure Groups · Operation ID: `getUnitOfMeasureGroup` The response is the whole group, with its base unit and every conversion line. GET inventory/unit-of-measure-groups returns the header only, so use this endpoint or POST inventory/unit-of-measure-groups/search when you need the conversion lines. Each line has its own id, which is what updateLines and deleteLines address on PATCH inventory/unit-of-measure-groups/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getUnitOfMeasureGroup) ## 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`. | | `id` | path | string | yes | The unique identifier for the unit of measure group to return. | ## Response `200` Success Under `data` (getUnitOfMeasureGroupOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the unit of measure group. | | `name` | string | yes | The display name of the unit of measure group. Names are not unique. | | `description` | string | no | A free-form description of what the group covers. Absent when none was given. | | `baseUnitOfMeasureId` | string | no | The unique identifier for the base unit of measure that every conversion factor in the group is relative to. Absent when the group has no lines. See id on GET inventory/unit-of-measures/:id. | | `baseUnitCode` | string | no | The code of the base unit of measure. Absent when the group has no lines. | | `lines` | array of object | yes | The group's conversion lines, ordered by line id. An empty array when the group has none. | | `lines[].id` | string | yes | The unique identifier for the conversion line. This is what updateLines and deleteLines address on PATCH inventory/unit-of-measure-groups/:id, not the unit. | | `lines[].unitOfMeasureId` | string | yes | The unique identifier for the unit of measure this line converts. See id on GET inventory/unit-of-measures/:id. | | `lines[].unitCode` | string | yes | The code of that unit of measure. | | `lines[].unitName` | string | yes | The display name of that unit of measure. | | `lines[].conversionFactor` | string (decimal) | yes | How many base units of measure make up one of this unit. It is always greater than zero, and 1 on the base unit's own line. | | `lines[].isBaseUnit` | boolean | yes | Whether this line is for the group's own base unit of measure. Exactly one line has it, and its conversionFactor is 1. | | `lastModifiedDate` | string (date-time) | no | The date and time the unit of measure group's own fields last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Adding or changing a conversion line does not change it. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Unit Of Measure Group `PATCH /inventory/unit-of-measure-groups/{id}` Group: Inventory › Unit of Measure Groups · Operation ID: `updateUnitOfMeasureGroup` Updates the unit of measure group with the given identifier. Only the fields in the request body change. Everything omitted from the request body is left as it was. The conversion lines are edited through three arrays. createLines adds new lines, each with no id. updateLines changes existing lines, each keyed by its id. deleteLines removes lines by id. Omit all three to leave the conversion table unchanged. The arrays apply in the order delete, update, create, so one call can free a unit of measure and re-add it with a different factor. The base unit cannot be changed. The response is only the group's id. Read the updated group with GET inventory/unit-of-measure-groups/:id when you need its state after the update, including the ids of any lines you created. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateUnitOfMeasureGroup) ## 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. | | `id` | path | string | yes | The unique identifier for the unit of measure group to update. | ## Request body Required, `application/json`: updateUnitOfMeasureGroupInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The display name of the unit of measure group. Omit to leave unchanged. It is not checked for uniqueness. | | `description` | string | no | A free-form description of what the group covers. Omit to leave unchanged. | | `createLines` | array of object | no | The conversion lines to add, each a full line with no id. A unit of measure the group already has a line for fails with DUPLICATE_UNIT_IN_GROUP, unless the same call also deletes that line, since deletes are applied first. | | `createLines[].unitOfMeasureId` | string | yes | The unique identifier for the unit of measure this line converts. See id on GET inventory/unit-of-measures/:id. | | `createLines[].conversionFactor` | string (decimal) | yes | How many base units of measure make up one of this unit. It must be greater than zero. | | `updateLines` | array of object | no | The conversion lines to change, each keyed by its own id. The remaining fields are optional, and omitting one leaves it unchanged. A line id belonging to another group fails with LINE_NOT_FOUND. | | `updateLines[].id` | string | yes | The unique identifier for the conversion line to change. Read it from the line's id on GET inventory/unit-of-measure-groups/:id, not the unit's id. | | `updateLines[].conversionFactor` | string (decimal) | no | How many base units of measure make up one of this line's unit. Omit to leave unchanged. It must be greater than zero. | | `deleteLines` | array of string | no | The unique identifiers for the conversion lines to remove. They are removed before createLines is applied, so one call can free a unit of measure and re-add it with a different factor. Removing every line, the base unit's included, leaves a group that has forgotten its base unit and can take no new lines. | Example: ```json { "name": "<name>", "description": "<description>", "createLines": [ { "unitOfMeasureId": "d1f7b204-53ea-4c98-8b16-7f420e93ac57", "conversionFactor": "480.00" } ], "updateLines": [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "conversionFactor": "19.99" } ], "deleteLines": [ "<deleteLines>" ] } ``` ## Response `200` Success Under `data` (updateUnitOfMeasureGroupOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the unit of measure group. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `UNIT_NOT_FOUND` | 400 | | `DUPLICATE_UNIT_IN_GROUP` | 400 | | `LINE_NOT_FOUND` | 400 | | `INVALID_CONVERSION_FACTOR` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, UNIT_NOT_FOUND, DUPLICATE_UNIT_IN_GROUP, LINE_NOT_FOUND, INVALID_CONVERSION_FACTOR - `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 --- # Deletes the unit of measure group with the given identifier. `DELETE /inventory/unit-of-measure-groups/{id}` Group: Inventory › Unit of Measure Groups · Operation ID: `deleteUnitOfMeasureGroup` A unit of measure group must be empty before it can be deleted: no items may point at it, and it must have no conversion lines, the base unit's line included. Reassign any items and remove the lines with deleteLines on PATCH inventory/unit-of-measure-groups/:id first. This is a hard delete. Use POST inventory/unit-of-measure-groups/bulk-delete to delete several at once. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteUnitOfMeasureGroup) ## 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. | | `id` | path | string | yes | The unique identifier for the unit of measure group to delete. | ## Response `200` Success Under `data` (deleteUnitOfMeasureGroupOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted unit of measure group. | | `name` | string | yes | The display name the deleted unit of measure group held. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `UNIT_OF_MEASURE_GROUP_IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, UNIT_OF_MEASURE_GROUP_IN_USE - `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 --- # Creates several unit of measure groups in one call. `POST /inventory/unit-of-measure-groups/bulk` Group: Inventory › Unit of Measure Groups · Operation ID: `bulkCreateUnitOfMeasureGroups` All or nothing. If any entry is rejected, nothing is created and the whole call fails, and the response names the offending entry's position in the input array. Each entry has the same shape as the single create's body, including its own lines array. The one-line-per-unit rule applies within an entry, not across entries, so two entries may convert the same units. Use POST inventory/unit-of-measure-groups to create a single group. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateUnitOfMeasureGroups) ## 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`: bulkCreateUnitOfMeasureGroupsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `unitOfMeasureGroups` | array of object | yes | The unit of measure groups to create, each with its own conversion lines. If any entry is invalid, nothing is created and the whole call fails. | | `unitOfMeasureGroups[].name` | string | yes | The display name of the unit of measure group. Required. It is not checked for uniqueness. | | `unitOfMeasureGroups[].description` | string | no | A free-form description of what the group covers. Omit for none. | | `unitOfMeasureGroups[].baseUnitOfMeasureId` | string | yes | The unique identifier for the base unit of measure that every conversion factor in the group is relative to. Required. It is fixed for the life of the group and gets its own line automatically with a factor of 1, so do not list it in lines. See id on GET inventory/unit-of-measures/:id. | | `unitOfMeasureGroups[].lines` | array of object | no | The conversion lines for this group, beyond the base unit's automatic one. Each unit of measure may appear at most once within this entry. | | `unitOfMeasureGroups[].lines[].unitOfMeasureId` | string | yes | The unique identifier for the unit of measure this line converts. See id on GET inventory/unit-of-measures/:id. | | `unitOfMeasureGroups[].lines[].conversionFactor` | string (decimal) | yes | How many base units of measure make up one of this unit. It must be greater than zero. | Example: ```json { "unitOfMeasureGroups": [ { "name": "Packaging", "baseUnitOfMeasureId": "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "lines": [ { "unitOfMeasureId": "8c1f4a72-6b09-4d15-a3e7-40d2b6915cf8", "conversionFactor": "12.00" } ] }, { "name": "Weight", "baseUnitOfMeasureId": "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" } ] } ``` ## Response `200` Success Under `data` (bulkCreateUnitOfMeasureGroupsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `created` | array of object | yes | The created unit of measure groups, in the same order as the input. Match entries to inputs by index. Read a group with GET inventory/unit-of-measure-groups/:id to learn the ids assigned to its lines. | | `created[].id` | string | yes | The unique identifier for the created unit of measure group. | ## 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 | | --- | --- | | `UNIT_NOT_FOUND` | 400 | | `DUPLICATE_UNIT_IN_GROUP` | 400 | | `INVALID_CONVERSION_FACTOR` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: UNIT_NOT_FOUND, DUPLICATE_UNIT_IN_GROUP, INVALID_CONVERSION_FACTOR - `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 --- # Deletes several unit of measure groups in one call. `POST /inventory/unit-of-measure-groups/bulk-delete` Group: Inventory › Unit of Measure Groups · Operation ID: `bulkDeleteUnitOfMeasureGroups` All or nothing. Every identifier is resolved and checked for references before anything is deleted, so one unknown identifier or one group still in use fails the whole call and nothing is deleted. A group must be empty first: reassign any items and remove its conversion lines, the base unit's line included, with deleteLines on PATCH inventory/unit-of-measure-groups/:id. Use DELETE inventory/unit-of-measure-groups/:id to delete a single group. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteUnitOfMeasureGroups) ## 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`: bulkDeleteUnitOfMeasureGroupsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers for the unit of measure groups to delete. Each must already be empty, with no items pointing at it and no conversion lines left. If any identifier matches nothing or any group is still in use, nothing is deleted and the whole call fails. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteUnitOfMeasureGroupsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `deleted` | array of object | yes | The deleted unit of measure groups, in the same order as the input. Match entries to inputs by index. | | `deleted[].id` | string | yes | The unique identifier for the deleted unit of measure group. | | `deleted[].name` | string | yes | The display name the deleted unit of measure group held. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `UNIT_OF_MEASURE_GROUP_IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, UNIT_OF_MEASURE_GROUP_IN_USE - `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 --- # Returns the unit of measure groups that match the given filters. `POST /inventory/unit-of-measure-groups/search` Group: Inventory › Unit of Measure Groups · Operation ID: `searchUnitOfMeasureGroups` Every criterion is exact and criteria combine with AND. Send an empty filters object to return every unit of measure group. There is no free-text search. The response is the whole group, with its base unit and conversion lines, which the collection cannot carry. This is the only endpoint that can find the groups that convert a given unit of measure. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchUnitOfMeasureGroups) ## 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`. | ## Request body Required, `application/json`: searchUnitOfMeasureGroupsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. A unit of measure group must match every filter you send. Send an empty object to return all unit of measure groups. | | `filters.name` | string | no | Limits results to unit of measure groups with this name. The match is exact. Names are not unique, so this may match several groups. | | `filters.unitOfMeasureId` | string | no | Limits results to unit of measure groups with a conversion line for this unit of measure, including groups where it is the base unit. See id on GET inventory/unit-of-measures/:id. | | `filters.modifiedSince` | string (date-time) | no | Limits results to unit of measure groups changed on or after this time. Format YYYY-MM-DDTHH:mm:ss.sssZ. This tracks the group header only, so a changed conversion line does not affect it. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of unit of measure groups per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "unitOfMeasureId": "8c1f4a72-6b09-4d15-a3e7-40d2b6915cf8" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchUnitOfMeasureGroupsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The unit of measure groups on this page, ordered by id. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the unit of measure group. | | `items[].name` | string | yes | The display name of the unit of measure group. Names are not unique. | | `items[].description` | string | no | A free-form description of what the group covers. Absent when none was given. | | `items[].baseUnitOfMeasureId` | string | no | The unique identifier for the base unit of measure that every conversion factor in the group is relative to. Absent when the group has no lines. See id on GET inventory/unit-of-measures/:id. | | `items[].baseUnitCode` | string | no | The code of the base unit of measure. Absent when the group has no lines. | | `items[].lines` | array of object | yes | The group's conversion lines, ordered by line id. An empty array when the group has none. | | `items[].lines[].id` | string | yes | The unique identifier for the conversion line. This is what updateLines and deleteLines address on PATCH inventory/unit-of-measure-groups/:id, not the unit. | | `items[].lines[].unitOfMeasureId` | string | yes | The unique identifier for the unit of measure this line converts. See id on GET inventory/unit-of-measures/:id. | | `items[].lines[].unitCode` | string | yes | The code of that unit of measure. | | `items[].lines[].unitName` | string | yes | The display name of that unit of measure. | | `items[].lines[].conversionFactor` | string (decimal) | yes | How many base units of measure make up one of this unit. It is always greater than zero, and 1 on the base unit's own line. | | `items[].lines[].isBaseUnit` | boolean | yes | Whether this line is for the group's own base unit of measure. Exactly one line has it, and its conversionFactor is 1. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the unit of measure group's own fields last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Adding or changing a conversion line does not change it. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more unit of measure groups remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of units of measure. `GET /inventory/unit-of-measures` Group: Inventory › Units of Measure · Operation ID: `unitOfMeasures` This collection returns the same fields as GET inventory/unit-of-measures/:id and POST inventory/unit-of-measures/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=unitOfMeasures) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `unitCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, unitCode, unitName, lastModifiedDate - Selectable: id, unitCode, unitName, lastModifiedDate ## Response `200` Success Under `data`, an array of unitOfMeasuresRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the unit of measure. | | `unitCode` | string | no | The short code of the unit of measure, for example 'KG'. It is unique within the tenant. | | `unitName` | string | no | The display name of the unit of measure, for example 'Kilogram'. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the unit of measure record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a unit of measure. `POST /inventory/unit-of-measures` Group: Inventory › Units of Measure · Operation ID: `createUnitOfMeasure` The response is only the created unit of measure's id. Read the record with GET inventory/unit-of-measures/:id when you need its stored state. Use POST inventory/unit-of-measures/bulk to create several at once. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createUnitOfMeasure) ## 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`: createUnitOfMeasureInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `unitCode` | string | yes | The short code of the unit of measure, for example 'KG'. Required. It must be unique within the tenant. | | `unitName` | string | yes | The display name of the unit of measure, for example 'Kilogram'. | Example: ```json { "unitCode": "KG", "unitName": "Kilogram" } ``` ## Response `200` Success Under `data` (createUnitOfMeasureOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created unit of measure. | ## 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 | | --- | --- | | `DUPLICATE_UNIT_CODE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: DUPLICATE_UNIT_CODE - `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 --- # Returns the unit of measure with the given identifier. `GET /inventory/unit-of-measures/{id}` Group: Inventory › Units of Measure · Operation ID: `getUnitOfMeasure` The response has the same fields as GET inventory/unit-of-measures and POST inventory/unit-of-measures/search. To look a unit of measure up by its code instead of its id, filter the collection GET inventory/unit-of-measures on unitCode. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getUnitOfMeasure) ## 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`. | | `id` | path | string | yes | The unique identifier for the unit of measure to return. | ## Response `200` Success Under `data` (getUnitOfMeasureOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the unit of measure. | | `unitCode` | string | yes | The short code of the unit of measure, for example 'KG'. It is unique within the tenant. | | `unitName` | string | yes | The display name of the unit of measure, for example 'Kilogram'. | | `lastModifiedDate` | string (date-time) | no | The date and time the unit of measure record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Unit Of Measure `PATCH /inventory/unit-of-measures/{id}` Group: Inventory › Units of Measure · Operation ID: `updateUnitOfMeasure` Updates the unit of measure with the given identifier. Only the fields in the request body change. Everything omitted from the request body is left as it was. The response is only the unit of measure's id. Read the updated record with GET inventory/unit-of-measures/:id when you need its state after the update. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateUnitOfMeasure) ## 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. | | `id` | path | string | yes | The unique identifier for the unit of measure to update. | ## Request body Required, `application/json`: updateUnitOfMeasureInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `unitCode` | string | no | The short code of the unit of measure, for example 'KG'. It must stay unique within the tenant. Reusing the unit of measure's own current code is not a collision. | | `unitName` | string | no | The display name of the unit of measure, for example 'Kilogram'. | Example: ```json { "unitCode": "<unitCode>", "unitName": "Kilograms" } ``` ## Response `200` Success Under `data` (updateUnitOfMeasureOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the unit of measure. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `DUPLICATE_UNIT_CODE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, DUPLICATE_UNIT_CODE - `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 --- # Deletes the unit of measure with the given identifier. `DELETE /inventory/unit-of-measures/{id}` Group: Inventory › Units of Measure · Operation ID: `deleteUnitOfMeasure` A unit of measure that items or unit-of-measure group lines still reference cannot be deleted. Clear those references first. Use POST inventory/unit-of-measures/bulk-delete to delete several at once. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteUnitOfMeasure) ## 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. | | `id` | path | string | yes | The unique identifier for the unit of measure to delete. | ## Response `200` Success Under `data` (deleteUnitOfMeasureOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted unit of measure. | | `unitCode` | string | yes | The short code the deleted unit of measure held. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `UNIT_OF_MEASURE_IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, UNIT_OF_MEASURE_IN_USE - `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 --- # Creates several units of measure in one call. `POST /inventory/unit-of-measures/bulk` Group: Inventory › Units of Measure · Operation ID: `bulkCreateUnitOfMeasures` All or nothing. If any entry is rejected, nothing is written and the whole call fails. Use POST inventory/unit-of-measures to create a single unit of measure. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateUnitOfMeasures) ## 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`: bulkCreateUnitOfMeasuresInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `unitOfMeasures` | array of object | yes | The units of measure to create. If any entry is invalid, nothing is written and the whole call fails. | | `unitOfMeasures[].unitCode` | string | yes | The short code of the unit of measure, for example 'KG'. Required. It must be unique within the tenant and within this request. | | `unitOfMeasures[].unitName` | string | yes | The display name of the unit of measure, for example 'Kilogram'. | Example: ```json { "unitOfMeasures": [ { "unitCode": "KG", "unitName": "Kilogram" }, { "unitCode": "G", "unitName": "Gram" } ] } ``` ## Response `200` Success Under `data` (bulkCreateUnitOfMeasuresOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `created` | array of object | yes | The created units of measure, in the same order as the input. Match entries to inputs by index. Read a record with GET inventory/unit-of-measures/:id when you need its stored state. | | `created[].id` | string | yes | The unique identifier for the created unit of measure. | ## 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 | | --- | --- | | `DUPLICATE_UNIT_CODE` | 400 | | `DUPLICATE_UNIT_CODE_IN_REQUEST` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: DUPLICATE_UNIT_CODE, DUPLICATE_UNIT_CODE_IN_REQUEST - `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 --- # Deletes several units of measure in one call. `POST /inventory/unit-of-measures/bulk-delete` Group: Inventory › Units of Measure · Operation ID: `bulkDeleteUnitOfMeasures` All or nothing. Every identifier is resolved and checked for references before anything is deleted, so one unknown identifier or one unit still in use fails the whole call and nothing is deleted. Use DELETE inventory/unit-of-measures/:id to delete a single unit of measure. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteUnitOfMeasures) ## 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`: bulkDeleteUnitOfMeasuresInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers for the units of measure to delete. If any identifier matches nothing or its unit is still in use, nothing is deleted and the whole call fails. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteUnitOfMeasuresOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `deleted` | array of object | yes | The deleted units of measure, in the same order as the input. Match entries to inputs by index. | | `deleted[].id` | string | yes | The unique identifier for the deleted unit of measure. | | `deleted[].unitCode` | string | yes | The short code the deleted unit of measure held. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `UNIT_OF_MEASURE_IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, UNIT_OF_MEASURE_IN_USE - `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 --- # Returns the units of measure that match the given filters. `POST /inventory/unit-of-measures/search` Group: Inventory › Units of Measure · Operation ID: `searchUnitOfMeasures` Every criterion is exact and criteria combine with AND. Send an empty filters object to return every unit of measure. There is no free-text search. These filters are a subset of what GET inventory/unit-of-measures accepts, using the same names. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchUnitOfMeasures) ## 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`. | ## Request body Required, `application/json`: searchUnitOfMeasuresInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. A unit of measure must match every filter you send. Send an empty object to return all units of measure. | | `filters.unitCode` | string | no | Limits results to the unit of measure with this code. The match is exact. Codes are unique, so this matches at most one unit of measure. | | `filters.modifiedSince` | string (date-time) | no | Limits results to units of measure changed on or after this time. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of units of measure per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "unitCode": "KG" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchUnitOfMeasuresOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The units of measure on this page, ordered by id. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the unit of measure. | | `items[].unitCode` | string | yes | The short code of the unit of measure, for example 'KG'. It is unique within the tenant. | | `items[].unitName` | string | yes | The display name of the unit of measure, for example 'Kilogram'. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the unit of measure record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more units of measure remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Warehouse Transfer Orders `GET /inventory/warehouse-transfer-orders` Group: Inventory › Warehouse Transfer Orders · Operation ID: `warehouseTransferOrders` Returns a list of warehouse transfer orders. To filter by item, use POST inventory/warehouse-transfer-orders/search. The warehouse transfer order collection returns headers only. lines is a collection field and cannot be expressed here, so the collection returns lineCount and openLineCount instead. Every other field matches GET inventory/warehouse-transfer-orders/:id. To filter by item, use POST inventory/warehouse-transfer-orders/search, which returns the lines. openLineCount is how much of the order is still open. Zero means every line has been taken by a transfer, and on a draft order it equals lineCount. The warehouse names and referenceNumber are returned but cannot be filtered or ordered on. entityId can be filtered, so a collection can be scoped to one legal entity. [Open in the interactive reference](https://docs.api.lensing.ai/?op=warehouseTransferOrders) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `targetLocationId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, documentNumber, documentDate, documentStatus, createdDate, lastModifiedDate - Selectable: id, documentNumber, documentStatus, documentDate, referenceNumber, entityId, entityName, locationId, locationCode, locationName, targetLocationId, targetLocationCode, targetLocationName, notes, lineCount, openLineCount, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of warehouseTransferOrdersRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the transfer order. | | `documentNumber` | string or null | no | The transfer order's number, for example 'WTO-1043'. | | `documentStatus` | string | no | The status of the transfer order, computed from the lines and set by no endpoint directly. Possible values: 'DRAFT', 'IN_TRANSFER', 'PARTIALLY_PROCESSED', 'PROCESSED'. 'DRAFT' while the order is being written, 'IN_TRANSFER' while a transfer created from it has yet to commit, 'PARTIALLY_PROCESSED' once a transfer has moved part of what was asked for and a remainder is still open, and 'PROCESSED' once nothing is open. | | `documentDate` | string (date) or null | no | The date the transfer order is dated. Format YYYY-MM-DD. | | `referenceNumber` | string or null | no | Your own reference on the transfer order. | | `entityId` | string or null | no | The legal entity the transfer order belongs to. See id on GET base/entities. | | `entityName` | string or null | no | The name of that legal entity. | | `locationId` | string or null | no | The warehouse the goods leave, where the order names one for the whole document. A line may name its own. See id on GET inventory/warehouses/:id. | | `locationCode` | string or null | no | The code of that warehouse. | | `locationName` | string or null | no | The name of that warehouse. | | `targetLocationId` | string or null | no | The warehouse the goods go to. See id on GET inventory/warehouses/:id. | | `targetLocationCode` | string or null | no | The code of that warehouse. | | `targetLocationName` | string or null | no | The name of that warehouse. | | `notes` | string or null | no | Free text on the document. | | `lineCount` | number or null | no | The number of lines on the transfer order. | | `openLineCount` | number or null | no | How many lines still have quantity left to transfer. Zero means everything has been taken. | | `createdDate` | string (date-time) or null | no | The date and time the transfer order was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the transfer order record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Transfer Order `POST /inventory/warehouse-transfer-orders` Group: Inventory › Warehouse Transfer Orders · Operation ID: `createTransferOrder` Creates a warehouse transfer order, as a draft that moves nothing until you convert it. Nothing moves here. An order is a request: the movement happens when POST inventory/warehouse-transfer-orders/:id/commit turns it into a warehouse transfer. Every line names both warehouses, and its source is required. A line without a source is something the replenishment flow produces internally, and this API does not create it. A line may never name the same warehouse twice, which is refused here rather than at conversion, because such an order could never become a transfer. The header's warehouses are the order's overall from and to. A line may name its own, which is how one order draws the same delivery from several warehouses. Quantities have no currency. An order asks for an amount of an item, and what it is worth is settled when the transfer values it. The whole call is one transaction: a rejected request writes nothing. The response is the created order in the same shape GET inventory/warehouse-transfer-orders/:id returns. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createTransferOrder) ## 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`: createTransferOrderInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `entityId` | string | no | The legal entity. Defaults to the entity that owns the first line’s warehouse. See id on GET base/entities. | | `documentDate` | string (date) | no | The date to give the transfer order. Defaults to today. Use format YYYY-MM-DD. | | `locationId` | string | no | The warehouse the goods leave, for the transfer order as a whole. See id on GET inventory/warehouses/:id. | | `targetLocationId` | string | no | The warehouse the goods go to, for the transfer order as a whole. See id on GET inventory/warehouses/:id. | | `referenceNumber` | string | no | Your own reference on the transfer order, for example a replenishment run or a ticket number. | | `notes` | string | no | Free text on the document. | | `lines` | array of object | yes | The lines of the transfer order. At least one is required. | | `lines[].itemId` | string | yes | The item to move. See id on GET inventory/items/:id. | | `lines[].locationId` | string | yes | The warehouse to draw from, required on a line. See id on GET inventory/warehouses/:id. | | `lines[].targetLocationId` | string | yes | The warehouse to deliver to. See id on GET inventory/warehouses/:id. | | `lines[].unitId` | string | no | The unit the quantity is in. Defaults to the item’s stocking unit. There is no endpoint for it. | | `lines[].quantity` | string (decimal) | yes | How much to move, in the line’s unit. Must be greater than zero. | | `lines[].notes` | string | no | Free text on the line. | Example: ```json { "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "documentDate": "2026-08-18", "locationId": "b6f0d2a4-77c1-4e39-8a52-0c4e1f9b73d5", "targetLocationId": "e94a17c3-5b28-4d60-91f7-3a8c2e05b6d1", "referenceNumber": "REPLEN-2026-33", "notes": "<notes>", "lines": [ { "itemId": "517a93e4-cbb4-4571-9e3d-2343e56cfd84", "locationId": "b6f0d2a4-77c1-4e39-8a52-0c4e1f9b73d5", "targetLocationId": "e94a17c3-5b28-4d60-91f7-3a8c2e05b6d1", "quantity": "40" } ] } ``` ## Response `200` Success Under `data` (createTransferOrderOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the transfer order. | | `documentNumber` | string | no | The transfer order's number, for example 'WTO-1043'. | | `documentStatus` | string | yes | The status of the transfer order, computed from the lines and set by no endpoint directly. Possible values: 'DRAFT', 'IN_TRANSFER', 'PARTIALLY_PROCESSED', 'PROCESSED'. 'DRAFT' while the order is being written, 'IN_TRANSFER' while a transfer created from it has yet to commit, 'PARTIALLY_PROCESSED' once a transfer has moved part of what was asked for and a remainder is still open, and 'PROCESSED' once nothing is open. | | `documentDate` | string (date) | no | The date the transfer order is dated. Format YYYY-MM-DD. | | `referenceNumber` | string | no | Your own reference on the transfer order. | | `entityId` | string | no | The legal entity the transfer order belongs to. See id on GET base/entities. | | `entityName` | string | no | The name of that legal entity. | | `locationId` | string | no | The warehouse the goods leave, where the order names one for the whole document. A line may name its own. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The code of that warehouse. | | `locationName` | string | no | The name of that warehouse. | | `targetLocationId` | string | no | The warehouse the goods go to. See id on GET inventory/warehouses/:id. | | `targetLocationCode` | string | no | The code of that warehouse. | | `targetLocationName` | string | no | The name of that warehouse. | | `notes` | string | no | Free text on the document. | | `lineCount` | number | no | The number of lines on the transfer order. | | `openLineCount` | number | no | How many lines still have quantity left to transfer. Zero means everything has been taken. | | `createdDate` | string (date-time) | no | The date and time the transfer order was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the transfer order record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the created transfer order. | | `lines[].id` | string | yes | The unique identifier for the line. updateLines and deleteLines address a line by it. | | `lines[].itemId` | string | no | The item to move. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The code of that item. | | `lines[].itemName` | string | no | The name of that item. | | `lines[].locationId` | string | no | The warehouse this line draws from. See id on GET inventory/warehouses/:id. Absent while no source warehouse has been resolved, and such a line blocks the conversion. | | `lines[].locationCode` | string | no | The code of that warehouse. | | `lines[].locationName` | string | no | The name of that warehouse. | | `lines[].targetLocationId` | string | no | The warehouse this line delivers to. See id on GET inventory/warehouses/:id. | | `lines[].targetLocationCode` | string | no | The code of that warehouse. | | `lines[].targetLocationName` | string | no | The name of that warehouse. | | `lines[].unitId` | string | no | The unit the quantity is in. There is no endpoint for it. | | `lines[].unitCode` | string | no | The code of that unit. | | `lines[].quantity` | string (decimal) | no | How much to move, in the line's unit. Always greater than zero. | | `lines[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit, which is what the warehouse counts. | | `lines[].openQuantity` | string (decimal) | no | How much of the line is still to be transferred. | | `lines[].processedQuantity` | string (decimal) | no | How much of the line a transfer has already taken. | | `lines[].status` | string | no | The state of the line. 'NEEDS_SOURCE' means no source warehouse has been resolved, and the transfer order cannot be converted until it is. | | `lines[].notes` | string | no | Free text on the line. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `SAME_LOCATION` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, SAME_LOCATION - `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 --- # Get Transfer Order `GET /inventory/warehouse-transfer-orders/{id}` Group: Inventory › Warehouse Transfer Orders · Operation ID: `getTransferOrder` Returns the transfer order with the given identifier, with its lines. To look one up by documentNumber, use POST inventory/warehouse-transfer-orders/search. The id is the order's own, the value POST inventory/warehouse-transfer-orders returned and the collection publishes. A document number is not accepted here: look one up by documentNumber on the collection or on POST inventory/warehouse-transfer-orders/search. Read this before converting. A line whose status is 'NEEDS_SOURCE', or whose locationId is absent, has no warehouse to draw from, and POST inventory/warehouse-transfer-orders/:id/commit refuses the whole order while any such line remains. openLineCount says how much of the order is still outstanding. The response is the same item schema as the collection, plus the lines a collection field cannot carry. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getTransferOrder) ## 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`. | | `id` | path | string | yes | The unique identifier for the transfer order to return. | ## Response `200` Success Under `data` (getTransferOrderOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the transfer order. | | `documentNumber` | string | no | The transfer order's number, for example 'WTO-1043'. | | `documentStatus` | string | yes | The status of the transfer order, computed from the lines and set by no endpoint directly. Possible values: 'DRAFT', 'IN_TRANSFER', 'PARTIALLY_PROCESSED', 'PROCESSED'. 'DRAFT' while the order is being written, 'IN_TRANSFER' while a transfer created from it has yet to commit, 'PARTIALLY_PROCESSED' once a transfer has moved part of what was asked for and a remainder is still open, and 'PROCESSED' once nothing is open. | | `documentDate` | string (date) | no | The date the transfer order is dated. Format YYYY-MM-DD. | | `referenceNumber` | string | no | Your own reference on the transfer order. | | `entityId` | string | no | The legal entity the transfer order belongs to. See id on GET base/entities. | | `entityName` | string | no | The name of that legal entity. | | `locationId` | string | no | The warehouse the goods leave, where the order names one for the whole document. A line may name its own. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The code of that warehouse. | | `locationName` | string | no | The name of that warehouse. | | `targetLocationId` | string | no | The warehouse the goods go to. See id on GET inventory/warehouses/:id. | | `targetLocationCode` | string | no | The code of that warehouse. | | `targetLocationName` | string | no | The name of that warehouse. | | `notes` | string | no | Free text on the document. | | `lineCount` | number | no | The number of lines on the transfer order. | | `openLineCount` | number | no | How many lines still have quantity left to transfer. Zero means everything has been taken. | | `createdDate` | string (date-time) | no | The date and time the transfer order was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the transfer order record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the transfer order. Absent from the collection GET inventory/warehouse-transfer-orders, which returns lineCount instead. | | `lines[].id` | string | yes | The unique identifier for the line. updateLines and deleteLines address a line by it. | | `lines[].itemId` | string | no | The item to move. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The code of that item. | | `lines[].itemName` | string | no | The name of that item. | | `lines[].locationId` | string | no | The warehouse this line draws from. See id on GET inventory/warehouses/:id. Absent while no source warehouse has been resolved, and such a line blocks the conversion. | | `lines[].locationCode` | string | no | The code of that warehouse. | | `lines[].locationName` | string | no | The name of that warehouse. | | `lines[].targetLocationId` | string | no | The warehouse this line delivers to. See id on GET inventory/warehouses/:id. | | `lines[].targetLocationCode` | string | no | The code of that warehouse. | | `lines[].targetLocationName` | string | no | The name of that warehouse. | | `lines[].unitId` | string | no | The unit the quantity is in. There is no endpoint for it. | | `lines[].unitCode` | string | no | The code of that unit. | | `lines[].quantity` | string (decimal) | no | How much to move, in the line's unit. Always greater than zero. | | `lines[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit, which is what the warehouse counts. | | `lines[].openQuantity` | string (decimal) | no | How much of the line is still to be transferred. | | `lines[].processedQuantity` | string (decimal) | no | How much of the line a transfer has already taken. | | `lines[].status` | string | no | The state of the line. 'NEEDS_SOURCE' means no source warehouse has been resolved, and the transfer order cannot be converted until it is. | | `lines[].notes` | string | no | Free text on the line. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Transfer Order `PATCH /inventory/warehouse-transfer-orders/{id}` Group: Inventory › Warehouse Transfer Orders · Operation ID: `updateTransferOrder` Updates the warehouse transfer order with the given identifier. Only the fields in the request body change. It must be a draft. Edits a draft warehouse transfer order. Once converted, the order has a transfer behind it and is closed to changes, whatever that transfer has moved so far. Every header field is optional, and an omitted field is left unchanged. Lines change through three arrays, applied in the order deleteLines, updateLines, createLines, so one call can retire a line and add its replacement. updateLines and deleteLines address lines by the id the order publishes, and a line id belonging to another order is refused. Setting a line's locationId is how a 'NEEDS_SOURCE' line is resolved, which is the edit that unblocks the conversion. The whole call is one transaction: a rejected request writes nothing. The response is the updated order in the same shape GET inventory/warehouse-transfer-orders/:id returns. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateTransferOrder) ## 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. | | `id` | path | string | yes | The unique identifier for the transfer order to update. | ## Request body Required, `application/json`: updateTransferOrderInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `documentDate` | string (date) | no | The date the transfer order is dated. Use format YYYY-MM-DD. | | `locationId` | string | no | The warehouse the goods leave, for the transfer order as a whole. See id on GET inventory/warehouses/:id. | | `targetLocationId` | string | no | The warehouse the goods go to, for the transfer order as a whole. See id on GET inventory/warehouses/:id. | | `referenceNumber` | string | no | Your own reference on the transfer order. | | `notes` | string | no | Free text on the document. | | `createLines` | array of object | no | The lines to add. The same shape as on create. | | `createLines[].itemId` | string | yes | The item to move. See id on GET inventory/items/:id. | | `createLines[].locationId` | string | no | The warehouse to draw from. See id on GET inventory/warehouses/:id. | | `createLines[].targetLocationId` | string | yes | The warehouse to deliver to. See id on GET inventory/warehouses/:id. | | `createLines[].unitId` | string | no | The unit the quantity is in. Defaults to the item’s stocking unit. There is no endpoint for it. | | `createLines[].quantity` | string (decimal) | yes | How much to move, in the line’s unit. Must be greater than zero. | | `createLines[].notes` | string | no | Free text on the line. | | `updateLines` | array of object | no | The lines to change, addressed by the id the transfer order publishes. Everything but the id is optional. Setting locationId is how a 'NEEDS_SOURCE' line is resolved. | | `updateLines[].id` | string | yes | The line to change, by the id the transfer order publishes. | | `updateLines[].itemId` | string | no | A different item to move. See id on GET inventory/items/:id. | | `updateLines[].locationId` | string | no | The warehouse to draw from. See id on GET inventory/warehouses/:id. | | `updateLines[].targetLocationId` | string | no | The warehouse to deliver to. See id on GET inventory/warehouses/:id. | | `updateLines[].unitId` | string | no | The unit the quantity is in. There is no endpoint for it. | | `updateLines[].quantity` | string (decimal) | no | How much to move. Must be greater than zero. | | `updateLines[].notes` | string | no | Free text on the line. | | `deleteLines` | array of string | no | The lines to remove, by the id the transfer order publishes. Applied before the updates and creates. | Example: ```json { "documentDate": "2026-01-15", "locationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "targetLocationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "referenceNumber": "<referenceNumber>", "notes": "<notes>", "createLines": [ { "itemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "locationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "targetLocationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "unitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "notes": "<notes>" } ], "updateLines": [ { "id": "9a4e2c76-1b38-4f05-8d92-6c7a3e15b204", "locationId": "b6f0d2a4-77c1-4e39-8a52-0c4e1f9b73d5" } ], "deleteLines": [ "<deleteLines>" ] } ``` ## Response `200` Success Under `data` (updateTransferOrderOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the transfer order. | | `documentNumber` | string | no | The transfer order's number, for example 'WTO-1043'. | | `documentStatus` | string | yes | The status of the transfer order, computed from the lines and set by no endpoint directly. Possible values: 'DRAFT', 'IN_TRANSFER', 'PARTIALLY_PROCESSED', 'PROCESSED'. 'DRAFT' while the order is being written, 'IN_TRANSFER' while a transfer created from it has yet to commit, 'PARTIALLY_PROCESSED' once a transfer has moved part of what was asked for and a remainder is still open, and 'PROCESSED' once nothing is open. | | `documentDate` | string (date) | no | The date the transfer order is dated. Format YYYY-MM-DD. | | `referenceNumber` | string | no | Your own reference on the transfer order. | | `entityId` | string | no | The legal entity the transfer order belongs to. See id on GET base/entities. | | `entityName` | string | no | The name of that legal entity. | | `locationId` | string | no | The warehouse the goods leave, where the order names one for the whole document. A line may name its own. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The code of that warehouse. | | `locationName` | string | no | The name of that warehouse. | | `targetLocationId` | string | no | The warehouse the goods go to. See id on GET inventory/warehouses/:id. | | `targetLocationCode` | string | no | The code of that warehouse. | | `targetLocationName` | string | no | The name of that warehouse. | | `notes` | string | no | Free text on the document. | | `lineCount` | number | no | The number of lines on the transfer order. | | `openLineCount` | number | no | How many lines still have quantity left to transfer. Zero means everything has been taken. | | `createdDate` | string (date-time) | no | The date and time the transfer order was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the transfer order record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | yes | The lines of the transfer order as it now stands. | | `lines[].id` | string | yes | The unique identifier for the line. updateLines and deleteLines address a line by it. | | `lines[].itemId` | string | no | The item to move. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The code of that item. | | `lines[].itemName` | string | no | The name of that item. | | `lines[].locationId` | string | no | The warehouse this line draws from. See id on GET inventory/warehouses/:id. Absent while no source warehouse has been resolved, and such a line blocks the conversion. | | `lines[].locationCode` | string | no | The code of that warehouse. | | `lines[].locationName` | string | no | The name of that warehouse. | | `lines[].targetLocationId` | string | no | The warehouse this line delivers to. See id on GET inventory/warehouses/:id. | | `lines[].targetLocationCode` | string | no | The code of that warehouse. | | `lines[].targetLocationName` | string | no | The name of that warehouse. | | `lines[].unitId` | string | no | The unit the quantity is in. There is no endpoint for it. | | `lines[].unitCode` | string | no | The code of that unit. | | `lines[].quantity` | string (decimal) | no | How much to move, in the line's unit. Always greater than zero. | | `lines[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit, which is what the warehouse counts. | | `lines[].openQuantity` | string (decimal) | no | How much of the line is still to be transferred. | | `lines[].processedQuantity` | string (decimal) | no | How much of the line a transfer has already taken. | | `lines[].status` | string | no | The state of the line. 'NEEDS_SOURCE' means no source warehouse has been resolved, and the transfer order cannot be converted until it is. | | `lines[].notes` | string | no | Free text on the line. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `INVALID_QUANTITY` | 400 | | `SAME_LOCATION` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, REFERENCE_NOT_FOUND, INVALID_QUANTITY, SAME_LOCATION - `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 --- # Delete Transfer Order `DELETE /inventory/warehouse-transfer-orders/{id}` Group: Inventory › Warehouse Transfer Orders · Operation ID: `deleteTransferOrder` Deletes the warehouse transfer order with the given identifier. It must be a draft. Deletes a draft warehouse transfer order and its lines. Only a draft can be deleted. Once converted, a warehouse transfer refers back to the order's lines, and deleting the order would leave that transfer pointing at nothing, so it is refused. Delete that transfer instead to return the order to 'DRAFT'. Deleting is permanent. The response is the id it removed. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteTransferOrder) ## 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. | | `id` | path | string | yes | The unique identifier for the transfer order to delete. | ## Response `200` Success Under `data` (deleteTransferOrderOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted transfer order. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS - `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 --- # Commit Transfer Order `POST /inventory/warehouse-transfer-orders/{id}/commit` Group: Inventory › Warehouse Transfer Orders · Operation ID: `commitTransferOrder` Converts a warehouse transfer order into a warehouse transfer, which moves the goods. Converts what the order still has open into the warehouse transfer that moves the goods. This is what committing means for an order. Every document on this API commits, and an order has no stock of its own to post, so its commit creates the transfer instead. The response names it: transferId is the transfer's own id and transferDocumentNumber its number, and the order comes back 'IN_TRANSFER'. The transfer it creates is a draft. It moves nothing until it is committed in its own right, and the order's lines are linked to the transfer's, so committing the transfer is what fills in this order's processed and open quantities. Every line must have a source warehouse first. A line at 'NEEDS_SOURCE', without a locationId, or naming its destination as its source, is refused with UNRESOLVED_SOURCE_LINES, and the whole order with it, because a transfer built from a partial order would be invalid rather than incomplete. Resolve them with PATCH inventory/warehouse-transfer-orders/:id and call this again. An order converts once per remainder. While its transfer is uncommitted the order is 'IN_TRANSFER', and a second call fails with WRONG_STATUS, which stops a duplicate. If that transfer commits less than was asked for, the order becomes 'PARTIALLY_PROCESSED', and calling this again creates a transfer for exactly what is left. Only 'PROCESSED', with nothing open, is the end. [Open in the interactive reference](https://docs.api.lensing.ai/?op=commitTransferOrder) ## 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. | | `id` | path | string | yes | The unique identifier for the transfer order to convert. | ## Response `200` Success Under `data` (commitTransferOrderOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the transfer order. | | `documentStatus` | string | yes | The status of the transfer order, read back from the document. 'IN_TRANSFER' on success: the transfer this created has the open quantity and has yet to commit. | | `transferId` | string | no | The warehouse transfer this created. See id on GET inventory/warehouse-transfers/:id. It is a draft and moves nothing until it is committed in its own right. | | `transferDocumentNumber` | string | no | The number of that transfer. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `NO_LINES` | 400 | | `UNRESOLVED_SOURCE_LINES` | 400 | | `CONVERSION_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, NO_LINES, UNRESOLVED_SOURCE_LINES, CONVERSION_REJECTED - `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 --- # Creates several warehouse transfer orders in one call. `POST /inventory/warehouse-transfer-orders/bulk` Group: Inventory › Warehouse Transfer Orders · Operation ID: `bulkCreateTransferOrders` Creates several warehouse transfer orders in one call. It is all or nothing: one rejected order rejects the whole request and nothing is written, so a failed import is safe to retry unchanged. 'index' on an error names the transfer order at fault, counting from zero. The response has the created orders in input order, headers only, and each can be read in full on GET inventory/warehouse-transfer-orders/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateTransferOrders) ## 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`: bulkCreateTransferOrdersInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `transferOrders` | array of object | yes | The transfer orders to create. All or nothing: if any is rejected, none is created. | | `transferOrders[].entityId` | string | no | The legal entity. Defaults to the entity that owns the first line’s warehouse. See id on GET base/entities. | | `transferOrders[].documentDate` | string (date) | no | The date to give the transfer order. Defaults to today. Use format YYYY-MM-DD. | | `transferOrders[].locationId` | string | no | The warehouse the goods leave, for the transfer order as a whole. See id on GET inventory/warehouses/:id. | | `transferOrders[].targetLocationId` | string | no | The warehouse the goods go to, for the transfer order as a whole. See id on GET inventory/warehouses/:id. | | `transferOrders[].referenceNumber` | string | no | Your own reference on the transfer order. | | `transferOrders[].notes` | string | no | Free text on the document. | | `transferOrders[].lines` | array of object | yes | The lines of the transfer order. At least one is required. | | `transferOrders[].lines[].itemId` | string | yes | The item to move. See id on GET inventory/items/:id. | | `transferOrders[].lines[].locationId` | string | yes | The warehouse to draw from, required on a line. See id on GET inventory/warehouses/:id. | | `transferOrders[].lines[].targetLocationId` | string | yes | The warehouse to deliver to. See id on GET inventory/warehouses/:id. | | `transferOrders[].lines[].unitId` | string | no | The unit the quantity is in. There is no endpoint for it. | | `transferOrders[].lines[].quantity` | string (decimal) | yes | How much to move, in the line’s unit. Must be greater than zero. | | `transferOrders[].lines[].notes` | string | no | Free text on the line. | Example: ```json { "transferOrders": [ { "targetLocationId": "e94a17c3-5b28-4d60-91f7-3a8c2e05b6d1", "lines": [ { "itemId": "517a93e4-cbb4-4571-9e3d-2343e56cfd84", "targetLocationId": "e94a17c3-5b28-4d60-91f7-3a8c2e05b6d1", "quantity": "12", "locationId": "b6f0d2a4-77c1-4e39-8a52-0c4e1f9b73d5" } ], "locationId": "b6f0d2a4-77c1-4e39-8a52-0c4e1f9b73d5" } ] } ``` ## Response `200` Success Under `data` (bulkCreateTransferOrdersOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `transferOrders` | array of object | yes | The created transfer orders, in input order. Headers only. Read one in full on GET inventory/warehouse-transfer-orders/:id. | | `transferOrders[].id` | string | yes | The unique identifier for the transfer order. | | `transferOrders[].documentNumber` | string | no | The transfer order's number, for example 'WTO-1043'. | | `transferOrders[].documentStatus` | string | yes | The status of the transfer order, computed from the lines and set by no endpoint directly. Possible values: 'DRAFT', 'IN_TRANSFER', 'PARTIALLY_PROCESSED', 'PROCESSED'. 'DRAFT' while the order is being written, 'IN_TRANSFER' while a transfer created from it has yet to commit, 'PARTIALLY_PROCESSED' once a transfer has moved part of what was asked for and a remainder is still open, and 'PROCESSED' once nothing is open. | | `transferOrders[].documentDate` | string (date) | no | The date the transfer order is dated. Format YYYY-MM-DD. | | `transferOrders[].referenceNumber` | string | no | Your own reference on the transfer order. | | `transferOrders[].entityId` | string | no | The legal entity the transfer order belongs to. See id on GET base/entities. | | `transferOrders[].entityName` | string | no | The name of that legal entity. | | `transferOrders[].locationId` | string | no | The warehouse the goods leave, where the order names one for the whole document. A line may name its own. See id on GET inventory/warehouses/:id. | | `transferOrders[].locationCode` | string | no | The code of that warehouse. | | `transferOrders[].locationName` | string | no | The name of that warehouse. | | `transferOrders[].targetLocationId` | string | no | The warehouse the goods go to. See id on GET inventory/warehouses/:id. | | `transferOrders[].targetLocationCode` | string | no | The code of that warehouse. | | `transferOrders[].targetLocationName` | string | no | The name of that warehouse. | | `transferOrders[].notes` | string | no | Free text on the document. | | `transferOrders[].lineCount` | number | no | The number of lines on the transfer order. | | `transferOrders[].openLineCount` | number | no | How many lines still have quantity left to transfer. Zero means everything has been taken. | | `transferOrders[].createdDate` | string (date-time) | no | The date and time the transfer order was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `transferOrders[].lastModifiedDate` | string (date-time) | no | The date and time the transfer order record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## 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 | | --- | --- | | `NOTHING_TO_CREATE` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `SAME_LOCATION` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOTHING_TO_CREATE, REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, SAME_LOCATION - `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 --- # Deletes several draft warehouse transfer orders in one call. `POST /inventory/warehouse-transfer-orders/bulk-delete` Group: Inventory › Warehouse Transfer Orders · Operation ID: `bulkDeleteTransferOrders` Deletes several warehouse transfer orders in one call. It is all or nothing: every order is checked before any is removed, so a request naming one order that is no longer 'DRAFT' deletes none of them. 'index' on an error names the entry at fault, counting from zero. To delete one at a time, use DELETE inventory/warehouse-transfer-orders/:id. The ids come back in input order. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteTransferOrders) ## 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`: bulkDeleteTransferOrdersInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers for the transfer orders to delete. All or nothing: if any is missing or is not 'DRAFT', none is deleted. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteTransferOrdersOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers for the deleted transfer orders, in input order. | ## 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 | | --- | --- | | `NOTHING_TO_DELETE` | 400 | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOTHING_TO_DELETE, NOT_FOUND, WRONG_STATUS - `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 --- # Returns the warehouse transfer orders that match the given filters. `POST /inventory/warehouse-transfer-orders/search` Group: Inventory › Warehouse Transfer Orders · Operation ID: `searchTransferOrders` Searches warehouse transfer orders by exact criteria in the request body, and returns the same item schema as GET inventory/warehouse-transfer-orders/:id, lines included. It is a query: it answers 200 and writes nothing. itemId is accepted here and nowhere else. It matches orders that have a line for that item, the question the collection cannot answer, because the lines are not on it. referenceNumber is accepted here rather than on the collection, because a query string is logged and it identifies someone else's transaction. An identifier that resolves to nothing returns an empty page, not an error. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchTransferOrders) ## 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`. | ## Request body Required, `application/json`: searchTransferOrdersInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. A transfer order must match every filter you send. Omit to return all transfer orders. | | `filters.documentNumber` | string | no | Limits results to the transfer order with this document number, for example 'WTO-1043'. The match is exact. | | `filters.documentStatus` | string | no | Limits results to transfer orders in this status. Accepted values: 'DRAFT', 'IN_TRANSFER', 'PARTIALLY_PROCESSED', 'PROCESSED'. Any other value matches no transfer order. | | `filters.entityId` | string | no | Limits results to transfer orders for this legal entity. See id on GET base/entities. | | `filters.locationId` | string | no | Limits results to transfer orders with this source warehouse on the header. See id on GET inventory/warehouses/:id. | | `filters.targetLocationId` | string | no | Limits results to transfer orders with this target warehouse on the header. See id on GET inventory/warehouses/:id. | | `filters.itemId` | string | no | Limits results to transfer orders that have a line for this item. See id on GET inventory/items/:id. The collection GET inventory/warehouse-transfer-orders cannot filter by item, because it does not return the lines. | | `filters.referenceNumber` | string | no | Limits results to the transfer order with this reference. The match is exact. | | `filters.fromDate` | string (date) | no | Limits results to transfer orders dated on or after this date. Omit for an open-ended start. Format YYYY-MM-DD. | | `filters.toDate` | string (date) | no | Limits results to transfer orders dated on or before this date. Omit for an open-ended end. Format YYYY-MM-DD. | | `filters.modifiedSince` | string (date-time) | no | Limits results to transfer orders whose record changed on or after this time, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of transfer orders per page. Defaults to 50, capped at 200. | Example: ```json { "filters": { "documentStatus": "DRAFT", "targetLocationId": "e94a17c3-5b28-4d60-91f7-3a8c2e05b6d1" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchTransferOrdersOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The transfer orders that match, oldest first, each with its lines. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the transfer order. | | `items[].documentNumber` | string | no | The transfer order's number, for example 'WTO-1043'. | | `items[].documentStatus` | string | yes | The status of the transfer order, computed from the lines and set by no endpoint directly. Possible values: 'DRAFT', 'IN_TRANSFER', 'PARTIALLY_PROCESSED', 'PROCESSED'. 'DRAFT' while the order is being written, 'IN_TRANSFER' while a transfer created from it has yet to commit, 'PARTIALLY_PROCESSED' once a transfer has moved part of what was asked for and a remainder is still open, and 'PROCESSED' once nothing is open. | | `items[].documentDate` | string (date) | no | The date the transfer order is dated. Format YYYY-MM-DD. | | `items[].referenceNumber` | string | no | Your own reference on the transfer order. | | `items[].entityId` | string | no | The legal entity the transfer order belongs to. See id on GET base/entities. | | `items[].entityName` | string | no | The name of that legal entity. | | `items[].locationId` | string | no | The warehouse the goods leave, where the order names one for the whole document. A line may name its own. See id on GET inventory/warehouses/:id. | | `items[].locationCode` | string | no | The code of that warehouse. | | `items[].locationName` | string | no | The name of that warehouse. | | `items[].targetLocationId` | string | no | The warehouse the goods go to. See id on GET inventory/warehouses/:id. | | `items[].targetLocationCode` | string | no | The code of that warehouse. | | `items[].targetLocationName` | string | no | The name of that warehouse. | | `items[].notes` | string | no | Free text on the document. | | `items[].lineCount` | number | no | The number of lines on the transfer order. | | `items[].openLineCount` | number | no | How many lines still have quantity left to transfer. Zero means everything has been taken. | | `items[].createdDate` | string (date-time) | no | The date and time the transfer order was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the transfer order record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | no | The lines of the transfer order. | | `items[].lines[].id` | string | yes | The unique identifier for the line. updateLines and deleteLines address a line by it. | | `items[].lines[].itemId` | string | no | The item to move. See id on GET inventory/items/:id. | | `items[].lines[].itemCode` | string | no | The code of that item. | | `items[].lines[].itemName` | string | no | The name of that item. | | `items[].lines[].locationId` | string | no | The warehouse this line draws from. See id on GET inventory/warehouses/:id. Absent while no source warehouse has been resolved, and such a line blocks the conversion. | | `items[].lines[].locationCode` | string | no | The code of that warehouse. | | `items[].lines[].locationName` | string | no | The name of that warehouse. | | `items[].lines[].targetLocationId` | string | no | The warehouse this line delivers to. See id on GET inventory/warehouses/:id. | | `items[].lines[].targetLocationCode` | string | no | The code of that warehouse. | | `items[].lines[].targetLocationName` | string | no | The name of that warehouse. | | `items[].lines[].unitId` | string | no | The unit the quantity is in. There is no endpoint for it. | | `items[].lines[].unitCode` | string | no | The code of that unit. | | `items[].lines[].quantity` | string (decimal) | no | How much to move, in the line's unit. Always greater than zero. | | `items[].lines[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit, which is what the warehouse counts. | | `items[].lines[].openQuantity` | string (decimal) | no | How much of the line is still to be transferred. | | `items[].lines[].processedQuantity` | string (decimal) | no | How much of the line a transfer has already taken. | | `items[].lines[].status` | string | no | The state of the line. 'NEEDS_SOURCE' means no source warehouse has been resolved, and the transfer order cannot be converted until it is. | | `items[].lines[].notes` | string | no | Free text on the line. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | no | Whether more results remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Warehouse Transfers `GET /inventory/warehouse-transfers` Group: Inventory › Warehouse Transfers · Operation ID: `warehouseTransfers` Returns a list of warehouse transfers. To filter by item, use POST inventory/warehouse-transfers/search. The warehouse transfer collection, headers only. It returns lineCount in place of the lines, which a collection field cannot contain. Every other field matches GET inventory/warehouse-transfers/:id. To filter by item, use POST inventory/warehouse-transfers/search, where itemId lives. transferType and billingStatus are both filterable, so committed intercompany transfers that are not yet billed are one query. [Open in the interactive reference](https://docs.api.lensing.ai/?op=warehouseTransfers) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `documentDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `transferType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `toEntityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `targetLocationId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `billingStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, documentNumber, documentDate, documentStatus, transferType, createdDate, lastModifiedDate - Selectable: id, documentNumber, documentStatus, documentDate, referenceNumber, transferType, entityId, entityName, toEntityId, toEntityName, locationId, locationCode, locationName, targetLocationId, targetLocationCode, targetLocationName, billingStatus, intercompanyBillId, contractualCurrency, currency, sourceAmount, destinationAmount, notes, lineCount, createdDate, lastModifiedDate ## Response `200` Success Under `data`, an array of warehouseTransfersRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the transfer. | | `documentNumber` | string or null | no | The transfer's number, for example 'WTR-2291'. | | `documentStatus` | string | no | The status of the transfer. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `documentDate` | string (date) or null | no | The date the transfer is dated. Format YYYY-MM-DD. | | `referenceNumber` | string or null | no | Your own reference on the transfer. | | `transferType` | string or null | no | The type of transfer. Possible values: 'NORMAL', 'INTERCOMPANY', 'INTERCOMPANY_RETURN'. An intercompany transfer sells the goods to another legal entity and can be billed. | | `entityId` | string or null | no | The legal entity the goods leave. See id on GET base/entities. | | `entityName` | string or null | no | The name of that legal entity. | | `toEntityId` | string or null | no | The legal entity receiving the goods, on an intercompany transfer. See id on GET base/entities. | | `toEntityName` | string or null | no | The name of that legal entity. | | `locationId` | string or null | no | The warehouse the goods leave. See id on GET inventory/warehouses/:id. | | `locationCode` | string or null | no | The code of that warehouse. | | `locationName` | string or null | no | The name of that warehouse. | | `targetLocationId` | string or null | no | The warehouse the goods go to. See id on GET inventory/warehouses/:id. | | `targetLocationCode` | string or null | no | The code of that warehouse. | | `targetLocationName` | string or null | no | The name of that warehouse. | | `billingStatus` | string or null | no | The billing status of the transfer. Possible values: 'NOT_BILLED', 'BILLED'. Only an intercompany transfer is billed, through POST inventory/warehouse-transfers/:id/bill. | | `intercompanyBillId` | string or null | no | The identifier of the intercompany bill this transfer created, as the platform's own number. GET accounting/vendor-bills/:id takes a UUID, so this number cannot fetch that bill. | | `contractualCurrency` | string or null | no | The currency an intercompany transfer is contracted in. Format ISO 4217, for example 'EUR'. Each line's unitTransferValue is stated in it. | | `currency` | string or null | no | The currency the amounts on the transfer are stated in, the source entity's functional currency. Format ISO 4217, for example 'EUR'. | | `sourceAmount` | string (decimal) or null | no | What the goods were worth leaving the source entity, in the transfer's currency. Written by the commit. | | `destinationAmount` | string (decimal) or null | no | What the goods were worth arriving at the destination entity, in the transfer's currency. On an intercompany transfer it differs from sourceAmount, and the difference is the margin. | | `notes` | string or null | no | Free text on the document. | | `lineCount` | number or null | no | The number of lines on the transfer. | | `createdDate` | string (date-time) or null | no | The date and time the transfer was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the transfer record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Warehouse Transfer `POST /inventory/warehouse-transfers` Group: Inventory › Warehouse Transfers · Operation ID: `createWarehouseTransfer` Creates a warehouse transfer as a draft. The source and target warehouses must differ. Creates one warehouse transfer as a draft. Nothing moves until POST inventory/warehouse-transfers/:id/commit posts it. The source and target warehouses are set on the document and apply to every line. An intercompany transfer needs more: transferType 'INTERCOMPANY', a toEntityId for the receiving entity, a contractualCurrency, and a unitTransferValue on every line, all checked here rather than at commit. A batch-tracked item needs batches that add up to the line's quantity, a serialized item one serials entry per unit, and a batch or serial number new to the item is created. The whole call is one transaction, so a rejected request writes nothing. The response has the same shape as GET inventory/warehouse-transfers/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createWarehouseTransfer) ## 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`: createWarehouseTransferInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `locationId` | string | yes | The warehouse the goods leave, applied to every line. See id on GET inventory/warehouses/:id. | | `targetLocationId` | string | yes | The warehouse the goods go to. It must differ from locationId. See id on GET inventory/warehouses/:id. | | `entityId` | string | no | The legal entity. Defaults to the entity that owns the source warehouse. See id on GET base/entities. | | `documentDate` | string (date) | no | The date to give the transfer. Defaults to today. Use format YYYY-MM-DD. | | `referenceNumber` | string | no | Your own reference on the transfer. | | `notes` | string | no | Free text on the document. | | `transferType` | string | no | The type of transfer to create. Accepted values: 'NORMAL', 'INTERCOMPANY'. Defaults to 'NORMAL'. 'INTERCOMPANY_RETURN' is created by the intercompany return flow, not here. | | `toEntityId` | string | no | The legal entity receiving the goods. Required when transferType is 'INTERCOMPANY'. See id on GET base/entities. | | `contractualCurrency` | string | no | The currency an intercompany transfer is contracted in. Required when transferType is 'INTERCOMPANY'. Format ISO 4217, for example 'EUR'. Each line's unitTransferValue is stated in it. | | `lines` | array of object | yes | The lines of the transfer. At least one is required. | | `lines[].itemId` | string | yes | The item to move. See id on GET inventory/items/:id. | | `lines[].unitId` | string | no | The unit the quantity is in. Defaults to the item's stocking unit. There is no endpoint for it. | | `lines[].quantity` | string (decimal) | yes | How much to move, in the line's unit. Must be greater than zero. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].unitTransferValue` | string (decimal) | no | The contractual price of one unit, in the transfer's contractual currency. Required on an intercompany transfer. The two entities agree the price, and the API cannot derive it from a cost. | | `lines[].batches` | array of object | no | The batches that make up the line's quantity. Required for a batch-tracked item, and they must add up to the line's quantity. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch number. See id on GET inventory/batch-numbers/:id. | | `lines[].batches[].batchNumber` | string | no | The batch number. Naming one the item does not have yet creates it. | | `lines[].batches[].quantity` | string (decimal) | yes | How much of the line this batch accounts for. | | `lines[].serials` | array of object | no | The serialized units that move. Required for a serialized item, one entry per unit. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial number. See id on GET inventory/serial-numbers/:id. | | `lines[].serials[].serialNumber` | string | no | The serial number. Naming one the item does not have yet creates it. | | `lines[].binLocations` | array of object | no | The bins the goods leave and arrive in. Each entry names both ends of the move, and the batch or serial it belongs to where it belongs to one. Leave both names off for a quantity that moves at line level. | | `lines[].binLocations[].fromBinLocationId` | string | no | The bin the goods leave, in the source warehouse. See id on GET inventory/bins/:id. | | `lines[].binLocations[].toBinLocationId` | string | no | The bin the goods arrive in, in the target warehouse. See id on GET inventory/bins/:id. | | `lines[].binLocations[].quantity` | string (decimal) | yes | How much moves between these two bins. | | `lines[].binLocations[].batchNumber` | string | no | The batch this entry belongs to, when it belongs to one of the line's batches. | | `lines[].binLocations[].serialNumber` | string | no | The serialized unit this entry belongs to, when it belongs to one of the line's serials. | Example: ```json { "locationId": "b6f0d2a4-77c1-4e39-8a52-0c4e1f9b73d5", "targetLocationId": "e94a17c3-5b28-4d60-91f7-3a8c2e05b6d1", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "documentDate": "2026-08-18", "referenceNumber": "<referenceNumber>", "notes": "<notes>", "transferType": "<transferType>", "toEntityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "contractualCurrency": "<contractualCurrency>", "lines": [ { "itemId": "517a93e4-cbb4-4571-9e3d-2343e56cfd84", "quantity": "24" } ] } ``` ## Response `200` Success Under `data` (createWarehouseTransferOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the transfer. | | `documentNumber` | string | no | The transfer's number, for example 'WTR-2291'. | | `documentStatus` | string | yes | The status of the transfer. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `documentDate` | string (date) | no | The date the transfer is dated. Format YYYY-MM-DD. | | `referenceNumber` | string | no | Your own reference on the transfer. | | `transferType` | string | no | The type of transfer. Possible values: 'NORMAL', 'INTERCOMPANY', 'INTERCOMPANY_RETURN'. An intercompany transfer sells the goods to another legal entity and can be billed. | | `entityId` | string | no | The legal entity the goods leave. See id on GET base/entities. | | `entityName` | string | no | The name of that legal entity. | | `toEntityId` | string | no | The legal entity receiving the goods, on an intercompany transfer. See id on GET base/entities. | | `toEntityName` | string | no | The name of that legal entity. | | `locationId` | string | no | The warehouse the goods leave. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The code of that warehouse. | | `locationName` | string | no | The name of that warehouse. | | `targetLocationId` | string | no | The warehouse the goods go to. See id on GET inventory/warehouses/:id. | | `targetLocationCode` | string | no | The code of that warehouse. | | `targetLocationName` | string | no | The name of that warehouse. | | `billingStatus` | string | no | The billing status of the transfer. Possible values: 'NOT_BILLED', 'BILLED'. Only an intercompany transfer is billed, through POST inventory/warehouse-transfers/:id/bill. | | `intercompanyBillId` | string | no | The identifier of the intercompany bill this transfer created, as the platform's own number. GET accounting/vendor-bills/:id takes a UUID, so this number cannot fetch that bill. | | `contractualCurrency` | string | no | The currency an intercompany transfer is contracted in. Format ISO 4217, for example 'EUR'. Each line's unitTransferValue is stated in it. | | `currency` | string | no | The currency the amounts on the transfer are stated in, the source entity's functional currency. Format ISO 4217, for example 'EUR'. | | `sourceAmount` | string (decimal) | no | What the goods were worth leaving the source entity, in the transfer's currency. Written by the commit. | | `destinationAmount` | string (decimal) | no | What the goods were worth arriving at the destination entity, in the transfer's currency. On an intercompany transfer it differs from sourceAmount, and the difference is the margin. | | `notes` | string | no | Free text on the document. | | `lineCount` | number | no | The number of lines on the transfer. | | `createdDate` | string (date-time) | no | The date and time the transfer was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the transfer record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | no | The lines of the transfer. The collection GET inventory/warehouse-transfers returns lineCount in place of these. | | `lines[].id` | string | yes | The unique identifier for the transfer line. updateLines and deleteLines address lines by it. | | `lines[].itemId` | string | no | The item being moved. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The code of that item. | | `lines[].itemName` | string | no | The name of that item. | | `lines[].locationId` | string | no | The warehouse this line draws from, the document's warehouse applied to every line. See id on GET inventory/warehouses/:id. | | `lines[].locationCode` | string | no | The code of that warehouse. | | `lines[].locationName` | string | no | The name of that warehouse. | | `lines[].targetLocationId` | string | no | The warehouse this line delivers to. See id on GET inventory/warehouses/:id. | | `lines[].targetLocationCode` | string | no | The code of that warehouse. | | `lines[].targetLocationName` | string | no | The name of that warehouse. | | `lines[].unitId` | string | no | The unit the quantity is in. There is no endpoint for it. | | `lines[].unitCode` | string | no | The code of that unit. | | `lines[].quantity` | string (decimal) | no | How much moves, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit, which is what the warehouse counts. | | `lines[].transferCost` | string (decimal) | no | What one unit left the source warehouse at, in the transfer's currency. The valuation method sets it at commit. Absent on a draft. | | `lines[].unitTransferValue` | string (decimal) | no | The contractual price of one unit on an intercompany transfer, in the contractual currency. Supplied on create. | | `lines[].totalTransferValue` | string (decimal) | no | The line's total on an intercompany transfer, in the contractual currency: totalTransferValue = unitTransferValue x quantity. Written by the platform. | | `lines[].returnedQuantity` | string (decimal) | no | How much of this line has come back on an intercompany return. | | `lines[].status` | string | no | The status of the line. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches that make up the line's quantity. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch number. See id on GET inventory/batch-numbers/:id. | | `lines[].batches[].batchNumber` | string | no | The batch number. | | `lines[].batches[].quantity` | string (decimal) | no | How much of the line this batch accounts for. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit. | | `lines[].batches[].unitCost` | string (decimal) | no | What this batch moved at, in the transfer's currency. Set at commit. | | `lines[].serials` | array of object | no | The serialized units that move. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial number. See id on GET inventory/serial-numbers/:id. | | `lines[].serials[].serialNumber` | string | no | The serial number. | | `lines[].serials[].quantity` | string (decimal) | no | Always one, for a serialized unit. | | `lines[].serials[].unitCost` | string (decimal) | no | What this unit moved at, in the transfer's currency. Set at commit. | | `lines[].binLocations` | array of object | no | The bins the goods leave and arrive in. Each entry names both ends of the move, and the batch or serial it belongs to where it belongs to one. | | `lines[].binLocations[].fromBinLocationId` | string | no | The bin the goods leave, in the source warehouse. See id on GET inventory/bins/:id. | | `lines[].binLocations[].fromBinLocationCode` | string | no | The code of that bin. | | `lines[].binLocations[].toBinLocationId` | string | no | The bin the goods arrive in, in the target warehouse. See id on GET inventory/bins/:id. | | `lines[].binLocations[].toBinLocationCode` | string | no | The code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | no | How much moves between these two bins. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | The batch this entry belongs to, when it belongs to one of the line's batches. | | `lines[].binLocations[].serialNumber` | string | no | The serialized unit this entry belongs to, when it belongs to one of the line's serials. | ## 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 | | --- | --- | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `SAME_LOCATION` | 400 | | `INTERCOMPANY_CONFIG_MISSING` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, SAME_LOCATION, INTERCOMPANY_CONFIG_MISSING, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Get Warehouse Transfer `GET /inventory/warehouse-transfers/{id}` Group: Inventory › Warehouse Transfers · Operation ID: `getWarehouseTransfer` Returns the warehouse transfer with the given identifier, with its lines. Returns the transfer with its lines and their batch, serial and bin tracking. The collection GET inventory/warehouse-transfers returns the same fields without the lines. A draft has no costs yet: transferCost, the batch and serial unit costs, sourceAmount and destinationAmount are set when the transfer commits. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getWarehouseTransfer) ## 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`. | | `id` | path | string | yes | The unique identifier for the transfer to return. | ## Response `200` Success Under `data` (getWarehouseTransferOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the transfer. | | `documentNumber` | string | no | The transfer's number, for example 'WTR-2291'. | | `documentStatus` | string | yes | The status of the transfer. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `documentDate` | string (date) | no | The date the transfer is dated. Format YYYY-MM-DD. | | `referenceNumber` | string | no | Your own reference on the transfer. | | `transferType` | string | no | The type of transfer. Possible values: 'NORMAL', 'INTERCOMPANY', 'INTERCOMPANY_RETURN'. An intercompany transfer sells the goods to another legal entity and can be billed. | | `entityId` | string | no | The legal entity the goods leave. See id on GET base/entities. | | `entityName` | string | no | The name of that legal entity. | | `toEntityId` | string | no | The legal entity receiving the goods, on an intercompany transfer. See id on GET base/entities. | | `toEntityName` | string | no | The name of that legal entity. | | `locationId` | string | no | The warehouse the goods leave. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The code of that warehouse. | | `locationName` | string | no | The name of that warehouse. | | `targetLocationId` | string | no | The warehouse the goods go to. See id on GET inventory/warehouses/:id. | | `targetLocationCode` | string | no | The code of that warehouse. | | `targetLocationName` | string | no | The name of that warehouse. | | `billingStatus` | string | no | The billing status of the transfer. Possible values: 'NOT_BILLED', 'BILLED'. Only an intercompany transfer is billed, through POST inventory/warehouse-transfers/:id/bill. | | `intercompanyBillId` | string | no | The identifier of the intercompany bill this transfer created, as the platform's own number. GET accounting/vendor-bills/:id takes a UUID, so this number cannot fetch that bill. | | `contractualCurrency` | string | no | The currency an intercompany transfer is contracted in. Format ISO 4217, for example 'EUR'. Each line's unitTransferValue is stated in it. | | `currency` | string | no | The currency the amounts on the transfer are stated in, the source entity's functional currency. Format ISO 4217, for example 'EUR'. | | `sourceAmount` | string (decimal) | no | What the goods were worth leaving the source entity, in the transfer's currency. Written by the commit. | | `destinationAmount` | string (decimal) | no | What the goods were worth arriving at the destination entity, in the transfer's currency. On an intercompany transfer it differs from sourceAmount, and the difference is the margin. | | `notes` | string | no | Free text on the document. | | `lineCount` | number | no | The number of lines on the transfer. | | `createdDate` | string (date-time) | no | The date and time the transfer was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the transfer record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | no | The lines of the transfer. The collection GET inventory/warehouse-transfers returns lineCount in place of these. | | `lines[].id` | string | yes | The unique identifier for the transfer line. updateLines and deleteLines address lines by it. | | `lines[].itemId` | string | no | The item being moved. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The code of that item. | | `lines[].itemName` | string | no | The name of that item. | | `lines[].locationId` | string | no | The warehouse this line draws from, the document's warehouse applied to every line. See id on GET inventory/warehouses/:id. | | `lines[].locationCode` | string | no | The code of that warehouse. | | `lines[].locationName` | string | no | The name of that warehouse. | | `lines[].targetLocationId` | string | no | The warehouse this line delivers to. See id on GET inventory/warehouses/:id. | | `lines[].targetLocationCode` | string | no | The code of that warehouse. | | `lines[].targetLocationName` | string | no | The name of that warehouse. | | `lines[].unitId` | string | no | The unit the quantity is in. There is no endpoint for it. | | `lines[].unitCode` | string | no | The code of that unit. | | `lines[].quantity` | string (decimal) | no | How much moves, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit, which is what the warehouse counts. | | `lines[].transferCost` | string (decimal) | no | What one unit left the source warehouse at, in the transfer's currency. The valuation method sets it at commit. Absent on a draft. | | `lines[].unitTransferValue` | string (decimal) | no | The contractual price of one unit on an intercompany transfer, in the contractual currency. Supplied on create. | | `lines[].totalTransferValue` | string (decimal) | no | The line's total on an intercompany transfer, in the contractual currency: totalTransferValue = unitTransferValue x quantity. Written by the platform. | | `lines[].returnedQuantity` | string (decimal) | no | How much of this line has come back on an intercompany return. | | `lines[].status` | string | no | The status of the line. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches that make up the line's quantity. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch number. See id on GET inventory/batch-numbers/:id. | | `lines[].batches[].batchNumber` | string | no | The batch number. | | `lines[].batches[].quantity` | string (decimal) | no | How much of the line this batch accounts for. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit. | | `lines[].batches[].unitCost` | string (decimal) | no | What this batch moved at, in the transfer's currency. Set at commit. | | `lines[].serials` | array of object | no | The serialized units that move. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial number. See id on GET inventory/serial-numbers/:id. | | `lines[].serials[].serialNumber` | string | no | The serial number. | | `lines[].serials[].quantity` | string (decimal) | no | Always one, for a serialized unit. | | `lines[].serials[].unitCost` | string (decimal) | no | What this unit moved at, in the transfer's currency. Set at commit. | | `lines[].binLocations` | array of object | no | The bins the goods leave and arrive in. Each entry names both ends of the move, and the batch or serial it belongs to where it belongs to one. | | `lines[].binLocations[].fromBinLocationId` | string | no | The bin the goods leave, in the source warehouse. See id on GET inventory/bins/:id. | | `lines[].binLocations[].fromBinLocationCode` | string | no | The code of that bin. | | `lines[].binLocations[].toBinLocationId` | string | no | The bin the goods arrive in, in the target warehouse. See id on GET inventory/bins/:id. | | `lines[].binLocations[].toBinLocationCode` | string | no | The code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | no | How much moves between these two bins. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | The batch this entry belongs to, when it belongs to one of the line's batches. | | `lines[].binLocations[].serialNumber` | string | no | The serialized unit this entry belongs to, when it belongs to one of the line's serials. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Warehouse Transfer `PATCH /inventory/warehouse-transfers/{id}` Group: Inventory › Warehouse Transfers · Operation ID: `updateWarehouseTransfer` Updates the draft warehouse transfer with the given identifier. Only the fields in the request body change. Edits a draft warehouse transfer. A committed transfer has moved the stock, so correct it by canceling rather than editing. Every field is optional, and only the fields you send change. Lines change through three arrays, applied in the order deleteLines, updateLines, createLines, so one call can retire a line and add its replacement. updateLines and deleteLines address lines by the id the transfer publishes, and a line id from another transfer is refused. A tracking array you send on a line replaces what that line had. transferType cannot be changed. The response has the same shape as GET inventory/warehouse-transfers/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateWarehouseTransfer) ## 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. | | `id` | path | string | yes | The unique identifier for the transfer to update. | ## Request body Required, `application/json`: updateWarehouseTransferInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `locationId` | string | no | The warehouse the goods leave, applied to every line. See id on GET inventory/warehouses/:id. | | `targetLocationId` | string | no | The warehouse the goods go to. It must differ from locationId. See id on GET inventory/warehouses/:id. | | `documentDate` | string (date) | no | The date to give the transfer. Use format YYYY-MM-DD. | | `referenceNumber` | string | no | Your own reference on the transfer. | | `notes` | string | no | Free text on the document. | | `contractualCurrency` | string | no | The currency an intercompany transfer is contracted in. Format ISO 4217, for example 'EUR'. Each line's unitTransferValue is stated in it. | | `createLines` | array of object | no | Lines to add. Same shape as on create. | | `createLines[].itemId` | string | yes | The item to move. See id on GET inventory/items/:id. | | `createLines[].unitId` | string | no | The unit the quantity is in. Defaults to the item's stocking unit. There is no endpoint for it. | | `createLines[].quantity` | string (decimal) | yes | How much to move, in the line's unit. Must be greater than zero. | | `createLines[].notes` | string | no | Free text on the line. | | `createLines[].unitTransferValue` | string (decimal) | no | The contractual price of one unit, in the transfer's contractual currency. Required on an intercompany transfer. The two entities agree the price, and the API cannot derive it from a cost. | | `createLines[].batches` | array of object | no | The batches that make up the line's quantity. Required for a batch-tracked item, and they must add up to the line's quantity. | | `createLines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch number. See id on GET inventory/batch-numbers/:id. | | `createLines[].batches[].batchNumber` | string | no | The batch number. Naming one the item does not have yet creates it. | | `createLines[].batches[].quantity` | string (decimal) | yes | How much of the line this batch accounts for. | | `createLines[].serials` | array of object | no | The serialized units that move. Required for a serialized item, one entry per unit. | | `createLines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial number. See id on GET inventory/serial-numbers/:id. | | `createLines[].serials[].serialNumber` | string | no | The serial number. Naming one the item does not have yet creates it. | | `createLines[].binLocations` | array of object | no | The bins the goods leave and arrive in. Each entry names both ends of the move, and the batch or serial it belongs to where it belongs to one. Leave both names off for a quantity that moves at line level. | | `createLines[].binLocations[].fromBinLocationId` | string | no | The bin the goods leave, in the source warehouse. See id on GET inventory/bins/:id. | | `createLines[].binLocations[].toBinLocationId` | string | no | The bin the goods arrive in, in the target warehouse. See id on GET inventory/bins/:id. | | `createLines[].binLocations[].quantity` | string (decimal) | yes | How much moves between these two bins. | | `createLines[].binLocations[].batchNumber` | string | no | The batch this entry belongs to, when it belongs to one of the line's batches. | | `createLines[].binLocations[].serialNumber` | string | no | The serialized unit this entry belongs to, when it belongs to one of the line's serials. | | `updateLines` | array of object | no | Lines to change, each addressed by its id. Every field but the id is optional, and a tracking array you send replaces what the line had. | | `updateLines[].id` | string | yes | The unique identifier for the line to change. | | `updateLines[].itemId` | string | no | The item to move. See id on GET inventory/items/:id. | | `updateLines[].unitId` | string | no | The unit the quantity is in. Defaults to the item's stocking unit. There is no endpoint for it. | | `updateLines[].quantity` | string (decimal) | no | How much to move, in the line's unit. Must be greater than zero. | | `updateLines[].notes` | string | no | Free text on the line. | | `updateLines[].unitTransferValue` | string (decimal) | no | The contractual price of one unit, in the transfer's contractual currency. Required on an intercompany transfer. The two entities agree the price, and the API cannot derive it from a cost. | | `updateLines[].batches` | array of object | no | The batches that make up the line's quantity. Required for a batch-tracked item, and they must add up to the line's quantity. | | `updateLines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch number. See id on GET inventory/batch-numbers/:id. | | `updateLines[].batches[].batchNumber` | string | no | The batch number. Naming one the item does not have yet creates it. | | `updateLines[].batches[].quantity` | string (decimal) | yes | How much of the line this batch accounts for. | | `updateLines[].serials` | array of object | no | The serialized units that move. Required for a serialized item, one entry per unit. | | `updateLines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial number. See id on GET inventory/serial-numbers/:id. | | `updateLines[].serials[].serialNumber` | string | no | The serial number. Naming one the item does not have yet creates it. | | `updateLines[].binLocations` | array of object | no | The bins the goods leave and arrive in. Each entry names both ends of the move, and the batch or serial it belongs to where it belongs to one. Leave both names off for a quantity that moves at line level. | | `updateLines[].binLocations[].fromBinLocationId` | string | no | The bin the goods leave, in the source warehouse. See id on GET inventory/bins/:id. | | `updateLines[].binLocations[].toBinLocationId` | string | no | The bin the goods arrive in, in the target warehouse. See id on GET inventory/bins/:id. | | `updateLines[].binLocations[].quantity` | string (decimal) | yes | How much moves between these two bins. | | `updateLines[].binLocations[].batchNumber` | string | no | The batch this entry belongs to, when it belongs to one of the line's batches. | | `updateLines[].binLocations[].serialNumber` | string | no | The serialized unit this entry belongs to, when it belongs to one of the line's serials. | | `deleteLines` | array of string | no | The ids of lines to remove. They are applied before the updates and the creates. | Example: ```json { "locationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "targetLocationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "documentDate": "2026-01-15", "referenceNumber": "<referenceNumber>", "notes": "Consolidated with WTR-2290", "contractualCurrency": "<contractualCurrency>", "createLines": [ { "itemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "unitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "notes": "<notes>", "unitTransferValue": "19.99", "batches": [ { "batchNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "batchNumber": "<batchNumber>", "quantity": "19.99" } ], "serials": [ { "serialNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "serialNumber": "<serialNumber>" } ], "binLocations": [ { "fromBinLocationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "toBinLocationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "batchNumber": "<batchNumber>", "serialNumber": "<serialNumber>" } ] } ], "updateLines": [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "itemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "unitId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "notes": "<notes>", "unitTransferValue": "19.99", "batches": [ { "batchNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "batchNumber": "<batchNumber>", "quantity": "19.99" } ], "serials": [ { "serialNumberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "serialNumber": "<serialNumber>" } ], "binLocations": [ { "fromBinLocationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "toBinLocationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": "19.99", "batchNumber": "<batchNumber>", "serialNumber": "<serialNumber>" } ] } ], "deleteLines": [ "<deleteLines>" ] } ``` ## Response `200` Success Under `data` (updateWarehouseTransferOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the transfer. | | `documentNumber` | string | no | The transfer's number, for example 'WTR-2291'. | | `documentStatus` | string | yes | The status of the transfer. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `documentDate` | string (date) | no | The date the transfer is dated. Format YYYY-MM-DD. | | `referenceNumber` | string | no | Your own reference on the transfer. | | `transferType` | string | no | The type of transfer. Possible values: 'NORMAL', 'INTERCOMPANY', 'INTERCOMPANY_RETURN'. An intercompany transfer sells the goods to another legal entity and can be billed. | | `entityId` | string | no | The legal entity the goods leave. See id on GET base/entities. | | `entityName` | string | no | The name of that legal entity. | | `toEntityId` | string | no | The legal entity receiving the goods, on an intercompany transfer. See id on GET base/entities. | | `toEntityName` | string | no | The name of that legal entity. | | `locationId` | string | no | The warehouse the goods leave. See id on GET inventory/warehouses/:id. | | `locationCode` | string | no | The code of that warehouse. | | `locationName` | string | no | The name of that warehouse. | | `targetLocationId` | string | no | The warehouse the goods go to. See id on GET inventory/warehouses/:id. | | `targetLocationCode` | string | no | The code of that warehouse. | | `targetLocationName` | string | no | The name of that warehouse. | | `billingStatus` | string | no | The billing status of the transfer. Possible values: 'NOT_BILLED', 'BILLED'. Only an intercompany transfer is billed, through POST inventory/warehouse-transfers/:id/bill. | | `intercompanyBillId` | string | no | The identifier of the intercompany bill this transfer created, as the platform's own number. GET accounting/vendor-bills/:id takes a UUID, so this number cannot fetch that bill. | | `contractualCurrency` | string | no | The currency an intercompany transfer is contracted in. Format ISO 4217, for example 'EUR'. Each line's unitTransferValue is stated in it. | | `currency` | string | no | The currency the amounts on the transfer are stated in, the source entity's functional currency. Format ISO 4217, for example 'EUR'. | | `sourceAmount` | string (decimal) | no | What the goods were worth leaving the source entity, in the transfer's currency. Written by the commit. | | `destinationAmount` | string (decimal) | no | What the goods were worth arriving at the destination entity, in the transfer's currency. On an intercompany transfer it differs from sourceAmount, and the difference is the margin. | | `notes` | string | no | Free text on the document. | | `lineCount` | number | no | The number of lines on the transfer. | | `createdDate` | string (date-time) | no | The date and time the transfer was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lastModifiedDate` | string (date-time) | no | The date and time the transfer record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `lines` | array of object | no | The lines of the transfer. The collection GET inventory/warehouse-transfers returns lineCount in place of these. | | `lines[].id` | string | yes | The unique identifier for the transfer line. updateLines and deleteLines address lines by it. | | `lines[].itemId` | string | no | The item being moved. See id on GET inventory/items/:id. | | `lines[].itemCode` | string | no | The code of that item. | | `lines[].itemName` | string | no | The name of that item. | | `lines[].locationId` | string | no | The warehouse this line draws from, the document's warehouse applied to every line. See id on GET inventory/warehouses/:id. | | `lines[].locationCode` | string | no | The code of that warehouse. | | `lines[].locationName` | string | no | The name of that warehouse. | | `lines[].targetLocationId` | string | no | The warehouse this line delivers to. See id on GET inventory/warehouses/:id. | | `lines[].targetLocationCode` | string | no | The code of that warehouse. | | `lines[].targetLocationName` | string | no | The name of that warehouse. | | `lines[].unitId` | string | no | The unit the quantity is in. There is no endpoint for it. | | `lines[].unitCode` | string | no | The code of that unit. | | `lines[].quantity` | string (decimal) | no | How much moves, in the line's unit. | | `lines[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit, which is what the warehouse counts. | | `lines[].transferCost` | string (decimal) | no | What one unit left the source warehouse at, in the transfer's currency. The valuation method sets it at commit. Absent on a draft. | | `lines[].unitTransferValue` | string (decimal) | no | The contractual price of one unit on an intercompany transfer, in the contractual currency. Supplied on create. | | `lines[].totalTransferValue` | string (decimal) | no | The line's total on an intercompany transfer, in the contractual currency: totalTransferValue = unitTransferValue x quantity. Written by the platform. | | `lines[].returnedQuantity` | string (decimal) | no | How much of this line has come back on an intercompany return. | | `lines[].status` | string | no | The status of the line. | | `lines[].notes` | string | no | Free text on the line. | | `lines[].batches` | array of object | no | The batches that make up the line's quantity. | | `lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch number. See id on GET inventory/batch-numbers/:id. | | `lines[].batches[].batchNumber` | string | no | The batch number. | | `lines[].batches[].quantity` | string (decimal) | no | How much of the line this batch accounts for. | | `lines[].batches[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit. | | `lines[].batches[].unitCost` | string (decimal) | no | What this batch moved at, in the transfer's currency. Set at commit. | | `lines[].serials` | array of object | no | The serialized units that move. | | `lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial number. See id on GET inventory/serial-numbers/:id. | | `lines[].serials[].serialNumber` | string | no | The serial number. | | `lines[].serials[].quantity` | string (decimal) | no | Always one, for a serialized unit. | | `lines[].serials[].unitCost` | string (decimal) | no | What this unit moved at, in the transfer's currency. Set at commit. | | `lines[].binLocations` | array of object | no | The bins the goods leave and arrive in. Each entry names both ends of the move, and the batch or serial it belongs to where it belongs to one. | | `lines[].binLocations[].fromBinLocationId` | string | no | The bin the goods leave, in the source warehouse. See id on GET inventory/bins/:id. | | `lines[].binLocations[].fromBinLocationCode` | string | no | The code of that bin. | | `lines[].binLocations[].toBinLocationId` | string | no | The bin the goods arrive in, in the target warehouse. See id on GET inventory/bins/:id. | | `lines[].binLocations[].toBinLocationCode` | string | no | The code of that bin. | | `lines[].binLocations[].quantity` | string (decimal) | no | How much moves between these two bins. | | `lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit. | | `lines[].binLocations[].batchNumber` | string | no | The batch this entry belongs to, when it belongs to one of the line's batches. | | `lines[].binLocations[].serialNumber` | string | no | The serialized unit this entry belongs to, when it belongs to one of the line's serials. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `SAME_LOCATION` | 400 | | `INTERCOMPANY_CONFIG_MISSING` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, SAME_LOCATION, INTERCOMPANY_CONFIG_MISSING, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Deletes a draft warehouse transfer. `DELETE /inventory/warehouse-transfers/{id}` Group: Inventory › Warehouse Transfers · Operation ID: `deleteWarehouseTransfer` Deletes a draft warehouse transfer and its lines. Only a draft can be deleted. A committed transfer has moved stock and written a journal entry, so it is reversed by canceling, which stays auditable, rather than deleted. The response is the id that was removed. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteWarehouseTransfer) ## 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. | | `id` | path | string | yes | The unique identifier for the transfer to delete. | ## Response `200` Success Under `data` (deleteWarehouseTransferOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted transfer. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS - `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 --- # Creates the intercompany bill for a committed intercompany transfer. `POST /inventory/warehouse-transfers/{id}/bill` Group: Inventory › Warehouse Transfers · Operation ID: `billWarehouseTransfer` Creates the intercompany bill for a committed intercompany transfer and marks it billed. Billing is a separate call from the commit because it cannot be undone: the cancel path refuses a billed transfer, so folding billing into the commit would make every intercompany transfer uncancelable the moment the stock moved. The bill is priced from each line's unitTransferValue, in the contractual currency the transfer supplied when it was created. A transfer that is not intercompany, one not yet committed, and one already billed each fail with their own error. The bill belongs to the vendor-bill records, so intercompanyBillId is the platform's own number, not a UUID. [Open in the interactive reference](https://docs.api.lensing.ai/?op=billWarehouseTransfer) ## 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. | | `id` | path | string | yes | The unique identifier for the transfer to bill. | ## Response `200` Success Under `data` (billWarehouseTransferOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the transfer. | | `billingStatus` | string | yes | The billing status of the transfer, read back from the document. 'BILLED' on success. | | `intercompanyBillId` | string | no | The identifier of the bill this created, as the platform's own number. GET accounting/vendor-bills/:id takes a UUID, so this number cannot fetch that bill. | | `intercompanyBillNumber` | string | no | The number of that bill. | | `journalEntryId` | string | no | The journal entry the billing wrote. See id on GET accounting/journal-entries/:id. | | `warnings` | array of string | yes | Non-fatal advisories from the billing. An empty array on a clean run. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `NOT_BILLABLE` | 400 | | `ALREADY_BILLED` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, NOT_BILLABLE, ALREADY_BILLED, POSTING_REJECTED - `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 --- # Commit Warehouse Transfer `POST /inventory/warehouse-transfers/{id}/commit` Group: Inventory › Warehouse Transfers · Operation ID: `commitWarehouseTransfer` Commits a warehouse transfer, moving the stock and recording it in a journal entry. Posts the transfer: the goods leave the source warehouse, arrive at the target, and a journal entry records the movement. The transfer becomes committed. Stock availability is checked here, and a transfer of stock that is not there fails with POSTING_REJECTED. This does not create the intercompany bill. On an intercompany transfer the invoice is a separate call, POST inventory/warehouse-transfers/:id/bill. journalEntryId is absent when the posting produced no entry: an intercompany return writes two and names neither, and a movement with no financial posting has none. The stock has moved either way. [Open in the interactive reference](https://docs.api.lensing.ai/?op=commitWarehouseTransfer) ## 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. | | `id` | path | string | yes | The unique identifier for the transfer to commit. | ## Response `200` Success Under `data` (commitWarehouseTransferOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the transfer. | | `documentStatus` | string | yes | The status of the transfer, read back from the document. 'COMMITTED' on success. | | `journalEntryId` | string | no | The journal entry the commit wrote. Absent when the posting produced none, as on an intercompany return that writes two entries and names neither, or a movement with no financial posting. See id on GET accounting/journal-entries/:id. | | `warnings` | array of string | yes | Non-fatal advisories from the posting. An empty array on a clean run. The commit succeeded whatever it lists. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WRONG_STATUS, POSTING_REJECTED - `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 --- # Creates several warehouse transfers in one call. `POST /inventory/warehouse-transfers/bulk` Group: Inventory › Warehouse Transfers · Operation ID: `bulkCreateWarehouseTransfers` Creates several warehouse transfers in one call. It is all or nothing: one rejected transfer rejects the whole request and nothing is written, so a failed import is safe to retry unchanged. 'index' on an error names the transfer at fault, counting from zero. The response has the created transfers in input order, headers only, and each can be read in full on GET inventory/warehouse-transfers/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateWarehouseTransfers) ## 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`: bulkCreateWarehouseTransfersInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `warehouseTransfers` | array of object | yes | The transfers to create. All or nothing: if any is rejected, none is created. | | `warehouseTransfers[].locationId` | string | yes | The warehouse the goods leave, applied to every line. See id on GET inventory/warehouses/:id. | | `warehouseTransfers[].targetLocationId` | string | yes | The warehouse the goods go to. It must differ from locationId. See id on GET inventory/warehouses/:id. | | `warehouseTransfers[].entityId` | string | no | The legal entity. Defaults to the entity that owns the source warehouse. See id on GET base/entities. | | `warehouseTransfers[].documentDate` | string (date) | no | The date to give the transfer. Defaults to today. Use format YYYY-MM-DD. | | `warehouseTransfers[].referenceNumber` | string | no | Your own reference on the transfer. | | `warehouseTransfers[].notes` | string | no | Free text on the document. | | `warehouseTransfers[].transferType` | string | no | The type of transfer to create. Accepted values: 'NORMAL', 'INTERCOMPANY'. Defaults to 'NORMAL'. 'INTERCOMPANY_RETURN' is created by the intercompany return flow, not here. | | `warehouseTransfers[].toEntityId` | string | no | The legal entity receiving the goods. Required when transferType is 'INTERCOMPANY'. See id on GET base/entities. | | `warehouseTransfers[].contractualCurrency` | string | no | The currency an intercompany transfer is contracted in. Required when transferType is 'INTERCOMPANY'. Format ISO 4217, for example 'EUR'. Each line's unitTransferValue is stated in it. | | `warehouseTransfers[].lines` | array of object | yes | The lines of the transfer. At least one is required. | | `warehouseTransfers[].lines[].itemId` | string | yes | The item to move. See id on GET inventory/items/:id. | | `warehouseTransfers[].lines[].unitId` | string | no | The unit the quantity is in. Defaults to the item's stocking unit. There is no endpoint for it. | | `warehouseTransfers[].lines[].quantity` | string (decimal) | yes | How much to move, in the line's unit. Must be greater than zero. | | `warehouseTransfers[].lines[].notes` | string | no | Free text on the line. | | `warehouseTransfers[].lines[].unitTransferValue` | string (decimal) | no | The contractual price of one unit, in the transfer's contractual currency. Required on an intercompany transfer. The two entities agree the price, and the API cannot derive it from a cost. | | `warehouseTransfers[].lines[].batches` | array of object | no | The batches that make up the line's quantity. Required for a batch-tracked item, and they must add up to the line's quantity. | | `warehouseTransfers[].lines[].batches[].batchNumberId` | string | no | The unique identifier for an existing batch number. See id on GET inventory/batch-numbers/:id. | | `warehouseTransfers[].lines[].batches[].batchNumber` | string | no | The batch number. Naming one the item does not have yet creates it. | | `warehouseTransfers[].lines[].batches[].quantity` | string (decimal) | yes | How much of the line this batch accounts for. | | `warehouseTransfers[].lines[].serials` | array of object | no | The serialized units that move. Required for a serialized item, one entry per unit. | | `warehouseTransfers[].lines[].serials[].serialNumberId` | string | no | The unique identifier for an existing serial number. See id on GET inventory/serial-numbers/:id. | | `warehouseTransfers[].lines[].serials[].serialNumber` | string | no | The serial number. Naming one the item does not have yet creates it. | | `warehouseTransfers[].lines[].binLocations` | array of object | no | The bins the goods leave and arrive in. Each entry names both ends of the move, and the batch or serial it belongs to where it belongs to one. Leave both names off for a quantity that moves at line level. | | `warehouseTransfers[].lines[].binLocations[].fromBinLocationId` | string | no | The bin the goods leave, in the source warehouse. See id on GET inventory/bins/:id. | | `warehouseTransfers[].lines[].binLocations[].toBinLocationId` | string | no | The bin the goods arrive in, in the target warehouse. See id on GET inventory/bins/:id. | | `warehouseTransfers[].lines[].binLocations[].quantity` | string (decimal) | yes | How much moves between these two bins. | | `warehouseTransfers[].lines[].binLocations[].batchNumber` | string | no | The batch this entry belongs to, when it belongs to one of the line's batches. | | `warehouseTransfers[].lines[].binLocations[].serialNumber` | string | no | The serialized unit this entry belongs to, when it belongs to one of the line's serials. | Example: ```json { "warehouseTransfers": [ { "locationId": "b6f0d2a4-77c1-4e39-8a52-0c4e1f9b73d5", "targetLocationId": "e94a17c3-5b28-4d60-91f7-3a8c2e05b6d1", "lines": [ { "itemId": "517a93e4-cbb4-4571-9e3d-2343e56cfd84", "quantity": "24" } ] } ] } ``` ## Response `200` Success Under `data` (bulkCreateWarehouseTransfersOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `warehouseTransfers` | array of object | yes | The created transfers, in input order. Headers only. Read one in full on GET inventory/warehouse-transfers/:id. | | `warehouseTransfers[].id` | string | yes | The unique identifier for the transfer. | | `warehouseTransfers[].documentNumber` | string | no | The transfer's number, for example 'WTR-2291'. | | `warehouseTransfers[].documentStatus` | string | yes | The status of the transfer. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `warehouseTransfers[].documentDate` | string (date) | no | The date the transfer is dated. Format YYYY-MM-DD. | | `warehouseTransfers[].referenceNumber` | string | no | Your own reference on the transfer. | | `warehouseTransfers[].transferType` | string | no | The type of transfer. Possible values: 'NORMAL', 'INTERCOMPANY', 'INTERCOMPANY_RETURN'. An intercompany transfer sells the goods to another legal entity and can be billed. | | `warehouseTransfers[].entityId` | string | no | The legal entity the goods leave. See id on GET base/entities. | | `warehouseTransfers[].entityName` | string | no | The name of that legal entity. | | `warehouseTransfers[].toEntityId` | string | no | The legal entity receiving the goods, on an intercompany transfer. See id on GET base/entities. | | `warehouseTransfers[].toEntityName` | string | no | The name of that legal entity. | | `warehouseTransfers[].locationId` | string | no | The warehouse the goods leave. See id on GET inventory/warehouses/:id. | | `warehouseTransfers[].locationCode` | string | no | The code of that warehouse. | | `warehouseTransfers[].locationName` | string | no | The name of that warehouse. | | `warehouseTransfers[].targetLocationId` | string | no | The warehouse the goods go to. See id on GET inventory/warehouses/:id. | | `warehouseTransfers[].targetLocationCode` | string | no | The code of that warehouse. | | `warehouseTransfers[].targetLocationName` | string | no | The name of that warehouse. | | `warehouseTransfers[].billingStatus` | string | no | The billing status of the transfer. Possible values: 'NOT_BILLED', 'BILLED'. Only an intercompany transfer is billed, through POST inventory/warehouse-transfers/:id/bill. | | `warehouseTransfers[].intercompanyBillId` | string | no | The identifier of the intercompany bill this transfer created, as the platform's own number. GET accounting/vendor-bills/:id takes a UUID, so this number cannot fetch that bill. | | `warehouseTransfers[].contractualCurrency` | string | no | The currency an intercompany transfer is contracted in. Format ISO 4217, for example 'EUR'. Each line's unitTransferValue is stated in it. | | `warehouseTransfers[].currency` | string | no | The currency the amounts on the transfer are stated in, the source entity's functional currency. Format ISO 4217, for example 'EUR'. | | `warehouseTransfers[].sourceAmount` | string (decimal) | no | What the goods were worth leaving the source entity, in the transfer's currency. Written by the commit. | | `warehouseTransfers[].destinationAmount` | string (decimal) | no | What the goods were worth arriving at the destination entity, in the transfer's currency. On an intercompany transfer it differs from sourceAmount, and the difference is the margin. | | `warehouseTransfers[].notes` | string | no | Free text on the document. | | `warehouseTransfers[].lineCount` | number | no | The number of lines on the transfer. | | `warehouseTransfers[].createdDate` | string (date-time) | no | The date and time the transfer was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `warehouseTransfers[].lastModifiedDate` | string (date-time) | no | The date and time the transfer record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## 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 | | --- | --- | | `NOTHING_TO_CREATE` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `NO_LINES` | 400 | | `INVALID_QUANTITY` | 400 | | `SAME_LOCATION` | 400 | | `INTERCOMPANY_CONFIG_MISSING` | 400 | | `DUPLICATE_TRACKING_NUMBER` | 400 | | `UNKNOWN_TRACKING_REFERENCE` | 400 | | `POSTING_REJECTED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOTHING_TO_CREATE, REFERENCE_NOT_FOUND, NO_LINES, INVALID_QUANTITY, SAME_LOCATION, INTERCOMPANY_CONFIG_MISSING, DUPLICATE_TRACKING_NUMBER, UNKNOWN_TRACKING_REFERENCE, POSTING_REJECTED - `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 --- # Deletes several draft warehouse transfers in one call. `POST /inventory/warehouse-transfers/bulk-delete` Group: Inventory › Warehouse Transfers · Operation ID: `bulkDeleteWarehouseTransfers` Deletes several draft warehouse transfers in one call. It is all or nothing: every transfer is checked before any is removed, so a request naming one that cannot be deleted removes none. 'index' on an error names the transfer at fault, counting from zero. To delete one transfer, use DELETE inventory/warehouse-transfers/:id. The ids come back in input order. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteWarehouseTransfers) ## 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`: bulkDeleteWarehouseTransfersInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The ids of the transfers to delete. All or nothing: if any is missing or not a draft, none is deleted. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteWarehouseTransfersOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The ids of the deleted transfers, in input order. | ## 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 | | --- | --- | | `NOTHING_TO_DELETE` | 400 | | `NOT_FOUND` | 400 | | `WRONG_STATUS` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOTHING_TO_DELETE, NOT_FOUND, WRONG_STATUS - `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 --- # Returns the warehouse transfers that match the given filters. `POST /inventory/warehouse-transfers/search` Group: Inventory › Warehouse Transfers · Operation ID: `searchWarehouseTransfers` Searches warehouse transfers by exact criteria in a request body, returning the same fields as GET inventory/warehouse-transfers/:id, lines included. Criteria combine with AND, and an empty filters object returns every transfer. itemId is here and not on the collection: it matches transfers that have a line for the item, which the collection cannot do because it does not return lines. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchWarehouseTransfers) ## 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`. | ## Request body Required, `application/json`: searchWarehouseTransfersInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. A transfer must match every filter you send. Send an empty object to return all transfers. | | `filters.documentNumber` | string | no | Limits results to the transfer with this number, for example 'WTR-2291'. The match is exact. | | `filters.documentStatus` | string | no | Limits results to transfers with this status. Accepted values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `filters.transferType` | string | no | Limits results to transfers of this type. Accepted values: 'NORMAL', 'INTERCOMPANY', 'INTERCOMPANY_RETURN'. | | `filters.billingStatus` | string | no | Limits results to transfers with this billing status. Accepted values: 'NOT_BILLED', 'BILLED'. | | `filters.entityId` | string | no | Limits results to transfers whose goods leave this legal entity. See id on GET base/entities. | | `filters.toEntityId` | string | no | Limits results to transfers whose goods go to this legal entity, on an intercompany transfer. See id on GET base/entities. | | `filters.locationId` | string | no | Limits results to transfers whose goods leave this warehouse. See id on GET inventory/warehouses/:id. | | `filters.targetLocationId` | string | no | Limits results to transfers whose goods go to this warehouse. See id on GET inventory/warehouses/:id. | | `filters.itemId` | string | no | Limits results to transfers with a line for this item. See id on GET inventory/items/:id. | | `filters.referenceNumber` | string | no | Limits results to transfers with this reference. The match is exact. | | `filters.fromDate` | string (date) | no | Limits results to transfers dated on or after this date. Use format YYYY-MM-DD. | | `filters.toDate` | string (date) | no | Limits results to transfers dated on or before this date. Use format YYYY-MM-DD. | | `filters.modifiedSince` | string (date-time) | no | Limits results to transfers changed on or after this time. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of transfers per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "transferType": "INTERCOMPANY", "billingStatus": "NOT_BILLED" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchWarehouseTransfersOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The transfers on this page, oldest first, each with its lines. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the transfer. | | `items[].documentNumber` | string | no | The transfer's number, for example 'WTR-2291'. | | `items[].documentStatus` | string | yes | The status of the transfer. Possible values: 'DRAFT', 'COMMITTED', 'CANCELLED'. | | `items[].documentDate` | string (date) | no | The date the transfer is dated. Format YYYY-MM-DD. | | `items[].referenceNumber` | string | no | Your own reference on the transfer. | | `items[].transferType` | string | no | The type of transfer. Possible values: 'NORMAL', 'INTERCOMPANY', 'INTERCOMPANY_RETURN'. An intercompany transfer sells the goods to another legal entity and can be billed. | | `items[].entityId` | string | no | The legal entity the goods leave. See id on GET base/entities. | | `items[].entityName` | string | no | The name of that legal entity. | | `items[].toEntityId` | string | no | The legal entity receiving the goods, on an intercompany transfer. See id on GET base/entities. | | `items[].toEntityName` | string | no | The name of that legal entity. | | `items[].locationId` | string | no | The warehouse the goods leave. See id on GET inventory/warehouses/:id. | | `items[].locationCode` | string | no | The code of that warehouse. | | `items[].locationName` | string | no | The name of that warehouse. | | `items[].targetLocationId` | string | no | The warehouse the goods go to. See id on GET inventory/warehouses/:id. | | `items[].targetLocationCode` | string | no | The code of that warehouse. | | `items[].targetLocationName` | string | no | The name of that warehouse. | | `items[].billingStatus` | string | no | The billing status of the transfer. Possible values: 'NOT_BILLED', 'BILLED'. Only an intercompany transfer is billed, through POST inventory/warehouse-transfers/:id/bill. | | `items[].intercompanyBillId` | string | no | The identifier of the intercompany bill this transfer created, as the platform's own number. GET accounting/vendor-bills/:id takes a UUID, so this number cannot fetch that bill. | | `items[].contractualCurrency` | string | no | The currency an intercompany transfer is contracted in. Format ISO 4217, for example 'EUR'. Each line's unitTransferValue is stated in it. | | `items[].currency` | string | no | The currency the amounts on the transfer are stated in, the source entity's functional currency. Format ISO 4217, for example 'EUR'. | | `items[].sourceAmount` | string (decimal) | no | What the goods were worth leaving the source entity, in the transfer's currency. Written by the commit. | | `items[].destinationAmount` | string (decimal) | no | What the goods were worth arriving at the destination entity, in the transfer's currency. On an intercompany transfer it differs from sourceAmount, and the difference is the margin. | | `items[].notes` | string | no | Free text on the document. | | `items[].lineCount` | number | no | The number of lines on the transfer. | | `items[].createdDate` | string (date-time) | no | The date and time the transfer was created, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the transfer record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].lines` | array of object | no | The lines of the transfer. The collection GET inventory/warehouse-transfers returns lineCount in place of these. | | `items[].lines[].id` | string | yes | The unique identifier for the transfer line. updateLines and deleteLines address lines by it. | | `items[].lines[].itemId` | string | no | The item being moved. See id on GET inventory/items/:id. | | `items[].lines[].itemCode` | string | no | The code of that item. | | `items[].lines[].itemName` | string | no | The name of that item. | | `items[].lines[].locationId` | string | no | The warehouse this line draws from, the document's warehouse applied to every line. See id on GET inventory/warehouses/:id. | | `items[].lines[].locationCode` | string | no | The code of that warehouse. | | `items[].lines[].locationName` | string | no | The name of that warehouse. | | `items[].lines[].targetLocationId` | string | no | The warehouse this line delivers to. See id on GET inventory/warehouses/:id. | | `items[].lines[].targetLocationCode` | string | no | The code of that warehouse. | | `items[].lines[].targetLocationName` | string | no | The name of that warehouse. | | `items[].lines[].unitId` | string | no | The unit the quantity is in. There is no endpoint for it. | | `items[].lines[].unitCode` | string | no | The code of that unit. | | `items[].lines[].quantity` | string (decimal) | no | How much moves, in the line's unit. | | `items[].lines[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit, which is what the warehouse counts. | | `items[].lines[].transferCost` | string (decimal) | no | What one unit left the source warehouse at, in the transfer's currency. The valuation method sets it at commit. Absent on a draft. | | `items[].lines[].unitTransferValue` | string (decimal) | no | The contractual price of one unit on an intercompany transfer, in the contractual currency. Supplied on create. | | `items[].lines[].totalTransferValue` | string (decimal) | no | The line's total on an intercompany transfer, in the contractual currency: totalTransferValue = unitTransferValue x quantity. Written by the platform. | | `items[].lines[].returnedQuantity` | string (decimal) | no | How much of this line has come back on an intercompany return. | | `items[].lines[].status` | string | no | The status of the line. | | `items[].lines[].notes` | string | no | Free text on the line. | | `items[].lines[].batches` | array of object | no | The batches that make up the line's quantity. | | `items[].lines[].batches[].batchNumberId` | string | no | The unique identifier for the batch number. See id on GET inventory/batch-numbers/:id. | | `items[].lines[].batches[].batchNumber` | string | no | The batch number. | | `items[].lines[].batches[].quantity` | string (decimal) | no | How much of the line this batch accounts for. | | `items[].lines[].batches[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit. | | `items[].lines[].batches[].unitCost` | string (decimal) | no | What this batch moved at, in the transfer's currency. Set at commit. | | `items[].lines[].serials` | array of object | no | The serialized units that move. | | `items[].lines[].serials[].serialNumberId` | string | no | The unique identifier for the serial number. See id on GET inventory/serial-numbers/:id. | | `items[].lines[].serials[].serialNumber` | string | no | The serial number. | | `items[].lines[].serials[].quantity` | string (decimal) | no | Always one, for a serialized unit. | | `items[].lines[].serials[].unitCost` | string (decimal) | no | What this unit moved at, in the transfer's currency. Set at commit. | | `items[].lines[].binLocations` | array of object | no | The bins the goods leave and arrive in. Each entry names both ends of the move, and the batch or serial it belongs to where it belongs to one. | | `items[].lines[].binLocations[].fromBinLocationId` | string | no | The bin the goods leave, in the source warehouse. See id on GET inventory/bins/:id. | | `items[].lines[].binLocations[].fromBinLocationCode` | string | no | The code of that bin. | | `items[].lines[].binLocations[].toBinLocationId` | string | no | The bin the goods arrive in, in the target warehouse. See id on GET inventory/bins/:id. | | `items[].lines[].binLocations[].toBinLocationCode` | string | no | The code of that bin. | | `items[].lines[].binLocations[].quantity` | string (decimal) | no | How much moves between these two bins. | | `items[].lines[].binLocations[].inventoryQuantity` | string (decimal) | no | The same amount in the item's stocking unit. | | `items[].lines[].binLocations[].batchNumber` | string | no | The batch this entry belongs to, when it belongs to one of the line's batches. | | `items[].lines[].binLocations[].serialNumber` | string | no | The serialized unit this entry belongs to, when it belongs to one of the line's serials. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more transfers remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Warehouses `GET /inventory/warehouses` Group: Inventory › Warehouses · Operation ID: `warehouses` Returns a list of warehouses. To filter by name, city, country or region, use POST inventory/warehouses/search. This collection returns the same fields as GET inventory/warehouses/:id and POST inventory/warehouses/search. The site name and the address fields are returned but cannot be filtered or ordered here. Filter name, city, countryCode and region through POST inventory/warehouses/search instead. locationCode, entityId, warehouseTypeId, inventoryLocationType, isActive, isPrimaryReceiving, id and lastModifiedDate can be filtered here. [Open in the interactive reference](https://docs.api.lensing.ai/?op=warehouses) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `inventoryLocationType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isActive` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isPrimaryReceiving` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `warehouseTypeId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, locationCode, entityId, warehouseTypeId, inventoryLocationType, isActive, isPrimaryReceiving, lastModifiedDate - Selectable: id, locationCode, name, inventoryLocationType, isActive, isPrimaryReceiving, addressLine1, addressLine2, city, stateProvince, postalCode, countryCode, region, entityId, entityName, warehouseTypeId, warehouseTypeName, lastModifiedDate ## Response `200` Success Under `data`, an array of warehousesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the warehouse. | | `locationCode` | string | no | The location code of the warehouse, for example 'WH-01'. It is unique within the tenant. | | `name` | string | no | The site name of the warehouse, for example 'Rotterdam Distribution Center'. | | `inventoryLocationType` | string | no | The kind of location. | | `isActive` | boolean | no | Whether the warehouse can be selected on new inventory documents. When false, existing stock and history keep pointing at it and new documents cannot select it. | | `isPrimaryReceiving` | boolean or null | no | Whether this is the entity's primary receiving warehouse. Open purchase-order on-order quantity for the entity is attributed here for replenishment. At most one warehouse per entity is the primary receiving warehouse. | | `addressLine1` | string or null | no | The first address line: street number and name, or PO box. Absent when unset. | | `addressLine2` | string or null | no | The second address line: apartment, suite, unit, building or floor. Absent when unset. | | `city` | string or null | no | The city or locality. Absent when unset. | | `stateProvince` | string or null | no | The state, province or region. Absent when unset. | | `postalCode` | string or null | no | The postal or ZIP code. Absent when unset. | | `countryCode` | string or null | no | The country of the address. Format ISO 3166-1 alpha-2, for example 'NL'. Absent when unset. | | `region` | string or null | no | A free-text business region grouping, independent of country and state. Absent when unset. | | `entityId` | string or null | no | The unique identifier for the legal entity that owns this warehouse. See id on GET base/entities. | | `entityName` | string or null | no | The name of the legal entity that owns this warehouse. | | `warehouseTypeId` | string or null | no | The unique identifier for the warehouse type, which limits the document types the warehouse allows. Absent when the warehouse allows every type. There is no endpoint for it. | | `warehouseTypeName` | string or null | no | The name of the warehouse type. Absent when the warehouse allows every document type. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the warehouse record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a warehouse. `POST /inventory/warehouses` Group: Inventory › Warehouses · Operation ID: `createWarehouse` The response is only the created warehouse's id. Read the record with GET inventory/warehouses/:id when you need its stored state. Use POST inventory/warehouses/bulk to create several at once. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createWarehouse) ## 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`: createWarehouseInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `locationCode` | string | yes | The location code of the warehouse, for example 'WH-01'. Required. It must be unique within the tenant. | | `name` | string | yes | The site name of the warehouse, for example 'Rotterdam Distribution Center'. Required. | | `entityId` | string | yes | The unique identifier for the legal entity that owns this warehouse. Required. An unknown identifier fails the call with ENTITY_NOT_FOUND. See id on GET base/entities. | | `inventoryLocationType` | string, one of `"WAREHOUSE"`, `"STORE"`, `"VIRTUAL_QC"`, `"VIRTUAL_RECEIVING"`, `"VIRTUAL_SHIPPING"`, `"EXTERNAL_3PL"`, `"EXTERNAL_CONSIGNMENT"` | yes | The kind of location. Required. | | `isActive` | boolean | no | Whether the warehouse can be selected on new inventory documents. Defaults to true. Set it to false to stage a warehouse before it goes into use. | | `isPrimaryReceiving` | boolean | no | Whether this is the entity's primary receiving warehouse, where open purchase-order on-order quantity is attributed for replenishment. Defaults to false. At most one warehouse per entity is meant to be the primary receiving warehouse. It is not enforced, and setting it on a second warehouse is accepted. | | `warehouseTypeId` | string | no | The unique identifier for the warehouse type, which limits the document types the warehouse allows. Omit to allow every document type. An unknown identifier fails the call with WAREHOUSE_TYPE_NOT_FOUND. There is no endpoint for it. | | `addressLine1` | string | no | The first address line: street number and name, or PO box. | | `addressLine2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `city` | string | no | The city or locality. | | `stateProvince` | string | no | The state, province or region. | | `postalCode` | string | no | The postal or ZIP code. | | `countryCode` | string | no | The country of the address. Format ISO 3166-1 alpha-2, for example 'NL'. It is not validated against the ISO list. | | `region` | string | no | A free-text business region grouping, independent of country and state. | Example: ```json { "locationCode": "WH-01", "name": "Rotterdam Distribution Centre", "entityId": "3f9c1e28-7b45-4d60-a913-5e07c286bd41", "inventoryLocationType": "WAREHOUSE", "isActive": true, "isPrimaryReceiving": true, "warehouseTypeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "addressLine1": "Waalhaven 12", "addressLine2": "<addressLine2>", "city": "Rotterdam", "stateProvince": "<stateProvince>", "postalCode": "3087 BM", "countryCode": "NL", "region": "Benelux" } ``` ## Response `200` Success Under `data` (createWarehouseOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created warehouse. | ## 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 | | --- | --- | | `DUPLICATE_LOCATION_CODE` | 400 | | `ENTITY_NOT_FOUND` | 400 | | `WAREHOUSE_TYPE_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: DUPLICATE_LOCATION_CODE, ENTITY_NOT_FOUND, WAREHOUSE_TYPE_NOT_FOUND - `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 --- # Returns the warehouse with the given identifier. `GET /inventory/warehouses/{id}` Group: Inventory › Warehouses · Operation ID: `getWarehouse` The response has the same fields as GET inventory/warehouses and POST inventory/warehouses/search. The owning entity and the warehouse type are each returned as both an identifier and a name. To look a warehouse up by its code instead of its id, filter the collection GET inventory/warehouses on locationCode. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getWarehouse) ## 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`. | | `id` | path | string | yes | The unique identifier for the warehouse to return. | ## Response `200` Success Under `data` (getWarehouseOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the warehouse. | | `locationCode` | string | yes | The location code of the warehouse, for example 'WH-01'. It is unique within the tenant. | | `entityId` | string | yes | The unique identifier for the legal entity that owns this warehouse. See id on GET base/entities. | | `entityName` | string | no | The name of the legal entity that owns this warehouse. | | `warehouseTypeId` | string | no | The unique identifier for the warehouse type, which limits the document types the warehouse allows. Absent when the warehouse allows every type. There is no endpoint for it. | | `warehouseTypeName` | string | no | The name of the warehouse type. Absent when the warehouse allows every document type. | | `name` | string | yes | The site name of the warehouse, for example 'Rotterdam Distribution Center'. | | `inventoryLocationType` | string, one of `"WAREHOUSE"`, `"STORE"`, `"VIRTUAL_QC"`, `"VIRTUAL_RECEIVING"`, `"VIRTUAL_SHIPPING"`, `"EXTERNAL_3PL"`, `"EXTERNAL_CONSIGNMENT"` | yes | The kind of location. | | `isActive` | boolean | yes | Whether the warehouse can be selected on new inventory documents. When false, existing stock and history keep pointing at it and new documents cannot select it. | | `isPrimaryReceiving` | boolean | no | Whether this is the entity's primary receiving warehouse. Open purchase-order on-order quantity for the entity is attributed here for replenishment. At most one warehouse per entity is the primary receiving warehouse. | | `addressLine1` | string | no | The first address line: street number and name, or PO box. Absent when unset. | | `addressLine2` | string | no | The second address line: apartment, suite, unit, building or floor. Absent when unset. | | `city` | string | no | The city or locality. Absent when unset. | | `stateProvince` | string | no | The state, province or region. Absent when unset. | | `postalCode` | string | no | The postal or ZIP code. Absent when unset. | | `countryCode` | string | no | The country of the address. Format ISO 3166-1 alpha-2, for example 'NL'. Absent when unset. | | `region` | string | no | A free-text business region grouping, independent of country and state. Absent when unset. | | `lastModifiedDate` | string (date-time) | no | The date and time the warehouse record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Warehouse `PATCH /inventory/warehouses/{id}` Group: Inventory › Warehouses · Operation ID: `updateWarehouse` Updates the warehouse with the given identifier. Only the fields in the request body change. Everything omitted from the request body is left as it was. warehouseTypeId is the one field where null means something: pass null to clear the warehouse type, which lets the warehouse allow every document type again. A warehouse that already has inventory transactions cannot be moved to another entity. The response is only the warehouse's id. Read the updated record with GET inventory/warehouses/:id when you need its state after the update. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateWarehouse) ## 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. | | `id` | path | string | yes | The unique identifier for the warehouse to update. | ## Request body Required, `application/json`: updateWarehouseInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `locationCode` | string | no | The location code of the warehouse, for example 'WH-01'. It must stay unique within the tenant. Reusing the warehouse's own current code is not a collision. | | `name` | string | no | The site name of the warehouse. | | `entityId` | string | no | The unique identifier for the legal entity that owns this warehouse. See id on GET base/entities. A warehouse that already has inventory transactions cannot be moved to another entity: the platform refuses it and the call fails with a validation error. | | `inventoryLocationType` | string, one of `"WAREHOUSE"`, `"STORE"`, `"VIRTUAL_QC"`, `"VIRTUAL_RECEIVING"`, `"VIRTUAL_SHIPPING"`, `"EXTERNAL_3PL"`, `"EXTERNAL_CONSIGNMENT"` | no | The kind of location. | | `isActive` | boolean | no | Whether the warehouse can be selected on new inventory documents. Set it to false to retire the warehouse without deleting it: its stock, transactions and bins keep pointing at it and new documents cannot select it. | | `isPrimaryReceiving` | boolean | no | Whether this is the entity's primary receiving warehouse. At most one warehouse per entity is meant to be the primary receiving warehouse. It is not enforced. | | `warehouseTypeId` | string or null | no | The unique identifier for the warehouse type, which limits the document types the warehouse allows. Pass null to clear it, which lets the warehouse allow every document type again. An unknown identifier fails the call with WAREHOUSE_TYPE_NOT_FOUND. There is no endpoint for it. | | `addressLine1` | string | no | The first address line: street number and name, or PO box. | | `addressLine2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `city` | string | no | The city or locality. | | `stateProvince` | string | no | The state, province or region. | | `postalCode` | string | no | The postal or ZIP code. | | `countryCode` | string | no | The country of the address. Format ISO 3166-1 alpha-2, for example 'NL'. It is not validated against the ISO list. | | `region` | string | no | A free-text business region grouping, independent of country and state. | Example: ```json { "locationCode": "<locationCode>", "name": "<name>", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "inventoryLocationType": "WAREHOUSE", "isActive": false, "isPrimaryReceiving": true, "warehouseTypeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "addressLine1": "<addressLine1>", "addressLine2": "<addressLine2>", "city": "<city>", "stateProvince": "<stateProvince>", "postalCode": "<postalCode>", "countryCode": "<countryCode>", "region": "<region>" } ``` ## Response `200` Success Under `data` (updateWarehouseOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the warehouse. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `DUPLICATE_LOCATION_CODE` | 400 | | `ENTITY_NOT_FOUND` | 400 | | `WAREHOUSE_TYPE_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, DUPLICATE_LOCATION_CODE, ENTITY_NOT_FOUND, WAREHOUSE_TYPE_NOT_FOUND - `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 --- # Deletes the warehouse with the given identifier. `DELETE /inventory/warehouses/{id}` Group: Inventory › Warehouses · Operation ID: `deleteWarehouse` A warehouse that has stock, movement history, bins, counts or allocations cannot be deleted, because deleting would strip the location from those historical records. Set isActive to false with PATCH inventory/warehouses/:id to retire the warehouse instead. A delete succeeds only for a warehouse nothing has stocked yet. Use POST inventory/warehouses/bulk-delete to delete several at once. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteWarehouse) ## 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. | | `id` | path | string | yes | The unique identifier for the warehouse to delete. | ## Response `200` Success Under `data` (deleteWarehouseOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted warehouse. | | `locationCode` | string | yes | The location code the deleted warehouse held. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WAREHOUSE_IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WAREHOUSE_IN_USE - `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 --- # Creates several warehouses in one call. `POST /inventory/warehouses/bulk` Group: Inventory › Warehouses · Operation ID: `bulkCreateWarehouses` All or nothing. Every entry is checked before any is written, so one rejected entry leaves the store untouched and the whole call fails. The response is the created warehouses in the same order as the input. Use POST inventory/warehouses to create a single warehouse. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateWarehouses) ## 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`: bulkCreateWarehousesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `warehouses` | array of object | yes | The warehouses to create. If any entry is invalid, nothing is created and the whole call fails. | | `warehouses[].locationCode` | string | yes | The location code of the warehouse, for example 'WH-01'. Required. It must be unique within the tenant and within this request. | | `warehouses[].name` | string | yes | The site name of the warehouse, for example 'Rotterdam Distribution Center'. Required. | | `warehouses[].entityId` | string | yes | The unique identifier for the legal entity that owns this warehouse. Required. An unknown identifier fails the whole call with ENTITY_NOT_FOUND, which names this entry's index. See id on GET base/entities. | | `warehouses[].inventoryLocationType` | string, one of `"WAREHOUSE"`, `"STORE"`, `"VIRTUAL_QC"`, `"VIRTUAL_RECEIVING"`, `"VIRTUAL_SHIPPING"`, `"EXTERNAL_3PL"`, `"EXTERNAL_CONSIGNMENT"` | yes | The kind of location. Required. | | `warehouses[].isActive` | boolean | no | Whether the warehouse can be selected on new inventory documents. Defaults to true. | | `warehouses[].isPrimaryReceiving` | boolean | no | Whether this is the entity's primary receiving warehouse. Defaults to false. At most one warehouse per entity is meant to be the primary receiving warehouse. It is not enforced, and a request setting it on several is accepted. | | `warehouses[].warehouseTypeId` | string | no | The unique identifier for the warehouse type, which limits the document types the warehouse allows. Omit to allow every document type. There is no endpoint for it. | | `warehouses[].addressLine1` | string | no | The first address line: street number and name, or PO box. | | `warehouses[].addressLine2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `warehouses[].city` | string | no | The city or locality. | | `warehouses[].stateProvince` | string | no | The state, province or region. | | `warehouses[].postalCode` | string | no | The postal or ZIP code. | | `warehouses[].countryCode` | string | no | The country of the address. Format ISO 3166-1 alpha-2, for example 'NL'. It is not validated against the ISO list. | | `warehouses[].region` | string | no | A free-text business region grouping, independent of country and state. | Example: ```json { "warehouses": [ { "locationCode": "WH-01", "name": "Rotterdam Distribution Centre", "entityId": "3f9c1e28-7b45-4d60-a913-5e07c286bd41", "inventoryLocationType": "WAREHOUSE", "countryCode": "NL" }, { "locationCode": "WH-02", "name": "Antwerp Overflow", "entityId": "3f9c1e28-7b45-4d60-a913-5e07c286bd41", "inventoryLocationType": "WAREHOUSE", "countryCode": "BE" } ] } ``` ## Response `200` Success Under `data` (bulkCreateWarehousesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `created` | array of object | yes | The created warehouses, in the same order as the input. Match entries to inputs by index. | | `created[].id` | string | yes | The unique identifier for the created warehouse. | ## 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 | | --- | --- | | `DUPLICATE_LOCATION_CODE` | 400 | | `DUPLICATE_LOCATION_CODE_IN_REQUEST` | 400 | | `ENTITY_NOT_FOUND` | 400 | | `WAREHOUSE_TYPE_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: DUPLICATE_LOCATION_CODE, DUPLICATE_LOCATION_CODE_IN_REQUEST, ENTITY_NOT_FOUND, WAREHOUSE_TYPE_NOT_FOUND - `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 --- # Deletes several warehouses in one call. `POST /inventory/warehouses/bulk-delete` Group: Inventory › Warehouses · Operation ID: `bulkDeleteWarehouses` All or nothing. Every identifier is resolved and checked for references before anything is deleted, so one unknown identifier or one warehouse still in use fails the whole call and nothing is deleted. Use DELETE inventory/warehouses/:id to delete a single warehouse. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkDeleteWarehouses) ## 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`: bulkDeleteWarehousesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers for the warehouses to delete. If any identifier matches nothing or its warehouse is still in use, nothing is deleted and the whole call fails. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ] } ``` ## Response `200` Success Under `data` (bulkDeleteWarehousesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `deleted` | array of object | yes | The deleted warehouses, in the same order as the input. Match entries to inputs by index. | | `deleted[].id` | string | yes | The unique identifier for the deleted warehouse. | | `deleted[].locationCode` | string | yes | The location code the deleted warehouse held. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `WAREHOUSE_IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, WAREHOUSE_IN_USE - `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 --- # Returns the warehouses that match the given filters. `POST /inventory/warehouses/search` Group: Inventory › Warehouses · Operation ID: `searchWarehouses` Every criterion is exact and criteria combine with AND. There is no free-text or substring search. This endpoint accepts the filters that must not appear in a URL: name, city, countryCode and region. entityId, warehouseTypeId, inventoryLocationType, isActive, isPrimaryReceiving and locationCode are also accepted here and share the names they have on GET inventory/warehouses. An identifier matching nothing returns an empty page, not an error. [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchWarehouses) ## 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`. | ## Request body Required, `application/json`: searchWarehousesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. A warehouse must match every filter you send. Send an empty object to return all warehouses. | | `filters.locationCode` | string | no | Limits results to the warehouse with this location code. The match is exact. Codes are unique, so this matches at most one warehouse. | | `filters.name` | string | no | Limits results to the warehouse with this site name. The match is exact. This filter is available only here, not on the collection. | | `filters.entityId` | string | no | Limits results to the warehouses owned by the legal entity with this identifier. An identifier matching no entity returns an empty page, not an error. See id on GET base/entities. | | `filters.warehouseTypeId` | string | no | Limits results to the warehouses assigned to the warehouse type with this identifier. An identifier matching no warehouse type returns an empty page. There is no endpoint for it. | | `filters.inventoryLocationType` | string, one of `"WAREHOUSE"`, `"STORE"`, `"VIRTUAL_QC"`, `"VIRTUAL_RECEIVING"`, `"VIRTUAL_SHIPPING"`, `"EXTERNAL_3PL"`, `"EXTERNAL_CONSIGNMENT"` | no | Limits results to warehouses of this kind of location. It is also available as filter[inventoryLocationType] on the collection. | | `filters.isActive` | boolean | no | Limits results to warehouses that can be selected on new documents when true, or to retired ones when false. Omit to match both. | | `filters.isPrimaryReceiving` | boolean | no | Limits results to each entity's primary receiving warehouse when true, or excludes it when false. Omit to match both. | | `filters.city` | string | no | Limits results to warehouses in this city. The match is exact. This filter is available only here, not on the collection. | | `filters.countryCode` | string | no | Limits results to warehouses in this country. Format ISO 3166-1 alpha-2, for example 'NL'. The match is exact. This filter is available only here, not on the collection. | | `filters.region` | string | no | Limits results to warehouses in this business region grouping. The match is exact. This filter is available only here, not on the collection. | | `filters.modifiedSince` | string (date-time) | no | Limits results to warehouses changed on or after this time. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit for the first page. | | `page.size` | number | no | The number of warehouses per page. Defaults to 50. A value above 200 is treated as 200, not rejected. | Example: ```json { "filters": { "entityId": "3f9c1e28-7b45-4d60-a913-5e07c286bd41", "isActive": true, "inventoryLocationType": "WAREHOUSE" }, "page": { "size": 50 } } ``` ## Response `200` Success Under `data` (searchWarehousesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The warehouses on this page, ordered by id. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the warehouse. | | `items[].locationCode` | string | yes | The location code of the warehouse, for example 'WH-01'. It is unique within the tenant. | | `items[].entityId` | string | yes | The unique identifier for the legal entity that owns this warehouse. See id on GET base/entities. | | `items[].entityName` | string | no | The name of the legal entity that owns this warehouse. | | `items[].warehouseTypeId` | string | no | The unique identifier for the warehouse type, which limits the document types the warehouse allows. Absent when the warehouse allows every type. There is no endpoint for it. | | `items[].warehouseTypeName` | string | no | The name of the warehouse type. Absent when the warehouse allows every document type. | | `items[].name` | string | yes | The site name of the warehouse, for example 'Rotterdam Distribution Center'. | | `items[].inventoryLocationType` | string, one of `"WAREHOUSE"`, `"STORE"`, `"VIRTUAL_QC"`, `"VIRTUAL_RECEIVING"`, `"VIRTUAL_SHIPPING"`, `"EXTERNAL_3PL"`, `"EXTERNAL_CONSIGNMENT"` | yes | The kind of location. | | `items[].isActive` | boolean | yes | Whether the warehouse can be selected on new inventory documents. When false, existing stock and history keep pointing at it and new documents cannot select it. | | `items[].isPrimaryReceiving` | boolean | no | Whether this is the entity's primary receiving warehouse. Open purchase-order on-order quantity for the entity is attributed here for replenishment. At most one warehouse per entity is the primary receiving warehouse. | | `items[].addressLine1` | string | no | The first address line: street number and name, or PO box. Absent when unset. | | `items[].addressLine2` | string | no | The second address line: apartment, suite, unit, building or floor. Absent when unset. | | `items[].city` | string | no | The city or locality. Absent when unset. | | `items[].stateProvince` | string | no | The state, province or region. Absent when unset. | | `items[].postalCode` | string | no | The postal or ZIP code. Absent when unset. | | `items[].countryCode` | string | no | The country of the address. Format ISO 3166-1 alpha-2, for example 'NL'. Absent when unset. | | `items[].region` | string | no | A free-text business region grouping, independent of country and state. Absent when unset. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the warehouse record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more warehouses remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Absence Types `GET /people/absence-types` Group: People Management › Absence Types · Operation ID: `absenceTypes` List absence types with structured filters. GET data-set exposing filter[field][op]=, sort=, sparse fields= and page[size]/page[cursor] cursor pagination. [Open in the interactive reference](https://docs.api.lensing.ai/?op=absenceTypes) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `name` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `category` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, name, category - Selectable: id, name, category, description ## Response `200` Success Under `data`, an array of absenceTypesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | Stable, platform-assigned id (a UUID) of the absence type — the public identifier used by GET people/absence-types/:id and by createAbsence's absenceTypeId. | | `name` | string | no | Display name of the absence type. | | `category` | string | no | Category the absence type belongs to: sickLeave, vacation, other or overtime. Try filter[category][eq]=vacation. | | `description` | string or null | no | Longer description of the absence type, when one is configured. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the absence type with the given identifier. `GET /people/absence-types/{id}` Group: People Management › Absence Types · Operation ID: `getAbsenceType` [Open in the interactive reference](https://docs.api.lensing.ai/?op=getAbsenceType) ## 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`. | | `id` | path | string | yes | The unique identifier for the absence type to return. | ## Response `200` Success Under `data` (getAbsenceTypeOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the absence type. | | `name` | string | yes | The display name of the absence type. | | `category` | string, one of `"sickLeave"`, `"vacation"`, `"other"`, `"overtime"` | yes | The category the absence type belongs to. | | `description` | string | no | A longer description of the absence type. Absent when none is set. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Absences `GET /people/absences` Group: People Management › Absences · Operation ID: `absences` List absences with structured filters. GET data-set exposing filter[field][op]= (eq/ne/gt/gte/lt/lte/in/notIn/between/like), sort=-startDate, sparse fields=, and page[size]/page[cursor] cursor pagination. Free-text description queries belong in POST people/absences/search. [Open in the interactive reference](https://docs.api.lensing.ai/?op=absences) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `absenceTypeId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `startDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `endDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `days` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, leaveNumber, startDate, endDate, days, status - Selectable: id, leaveNumber, employeeId, absenceTypeId, startDate, endDate, days, status, leaveDescription ## Response `200` Success Under `data`, an array of absencesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | Stable, platform-assigned id (a UUID) of the absence — the public identifier used by the item routes (GET/PATCH/DELETE people/absences/:id). | | `leaveNumber` | string or null | no | System-generated absence number. Sortable but not filterable: sorting puts only the field name in the URL, whereas filtering would put the number itself there. Look an absence number up through POST people/absences/search. | | `employeeId` | string | no | Platform id (a UUID) of the employee the absence belongs to. Not filterable here — naming an employee in a GET query string would record who was asked about in every access log; filter by employee through POST people/absences/search instead. | | `absenceTypeId` | string | no | Platform id (a UUID) of the absence type. Filter e.g. filter[absenceTypeId][in]=<id1>,<id2>. | | `startDate` | string (date-time) | no | Start of the absence (ISO 8601 UTC). Filter with filter[startDate][gte]=2026-01-01T00:00:00Z; a literal '+' in a UTC offset must be percent-encoded as %2B. | | `endDate` | string (date-time) | no | End of the absence (ISO 8601 UTC). | | `days` | string (decimal) or null | no | Number of absence days; may be fractional for partial days. Decimal filter target: filter[days][gte]=1. | | `status` | string or null | no | Current status of the absence: requested, approved, rejected or canceled. | | `leaveDescription` | string or null | no | Free-text reason or description supplied with the absence. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Absence `POST /people/absences` Group: People Management › Absences · Operation ID: `createAbsence` Creates an absence. The response contains only the new absence's id. Read the record with GET people/absences/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createAbsence) ## 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`: createAbsenceInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `employeeId` | string | yes | The unique identifier for the employee the absence belongs to. See id on GET people/:id. | | `absenceTypeId` | string | yes | The unique identifier for the absence type. See id on GET people/absence-types/:id. | | `startDate` | string (date-time) | yes | The date and time the absence starts, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `endDate` | string (date-time) | yes | The date and time the absence ends, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Must not precede startDate. | | `days` | string (decimal) | yes | The number of absence days. May be fractional for partial days. | | `status` | string, one of `"requested"`, `"approved"`, `"rejected"`, `"canceled"` | no | The status to create the absence in. Defaults to 'requested', the only status that enters the approval workflow. Approve a requested absence with POST people/absences/approve. Supplying 'approved' or 'rejected' records the decision directly with no approver behind it, and 'canceled' passes through 'approved' first, firing that transition's side effects. Use the default unless you are importing absences already decided elsewhere. | | `leaveDescription` | string | no | A free-text reason for the absence. | | `leaveCertificate` | string | no | The certificate file content, for example a medical certificate for sick leave. | | `certificateName` | string | no | The file name of the certificate. | | `googleEvent` | boolean | no | Whether to mirror this absence to the linked Google Calendar. | | `timezoneDiff` | string | no | The time zone offset used for calendar synchronization. | | `timezone` | string | no | The time zone the absence dates are expressed in. Format IANA time-zone name, for example 'Europe/Berlin'. | Example: ```json { "employeeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "absenceTypeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "startDate": "2026-01-15T09:30:00.000Z", "endDate": "2026-01-15T09:30:00.000Z", "days": "19.99", "status": "requested", "leaveDescription": "<leaveDescription>", "leaveCertificate": "<leaveCertificate>", "certificateName": "<certificateName>", "googleEvent": true, "timezoneDiff": "<timezoneDiff>", "timezone": "<timezone>" } ``` ## Response `200` Success Under `data` (createAbsenceOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created absence. | ## 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 | | --- | --- | | `EMPLOYEE_NOT_FOUND` | 400 | | `ABSENCE_TYPE_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: EMPLOYEE_NOT_FOUND, ABSENCE_TYPE_NOT_FOUND - `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 --- # Returns the absence with the given identifier. `GET /people/absences/{id}` Group: People Management › Absences · Operation ID: `getAbsence` [Open in the interactive reference](https://docs.api.lensing.ai/?op=getAbsence) ## 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`. | | `id` | path | string | yes | The unique identifier for the absence to return. | ## Response `200` Success Under `data` (getAbsenceOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the absence. | | `leaveNumber` | string | no | The absence number. | | `employeeId` | string | yes | The unique identifier for the employee the absence belongs to. See id on GET people/:id. | | `absenceTypeId` | string | yes | The unique identifier for the absence type. See id on GET people/absence-types/:id. | | `startDate` | string (date-time) | yes | The date and time the absence starts, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `endDate` | string (date-time) | yes | The date and time the absence ends, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `days` | string (decimal) | no | The number of absence days. May be fractional for partial days. | | `status` | string, one of `"requested"`, `"approved"`, `"rejected"`, `"canceled"` | no | The status of the absence. | | `leaveDescription` | string | no | The free-text reason for the absence. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Absence `PATCH /people/absences/{id}` Group: People Management › Absences · Operation ID: `updateAbsence` Updates the absence with the given identifier. Only the fields in the request body change. This is not a plain partial update in two ways. When editReason contains the text 'sick certificate', only leaveCertificate and certificateName are written and every other field in the request is ignored. On any other update, the status is recomputed and rewritten even when you did not send it, so updating an approved absence can return it to 'requested' and restart the approval workflow. Read the absence back with GET people/absences/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateAbsence) ## 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. | | `id` | path | string | yes | The unique identifier for the absence to update. | ## Request body Required, `application/json`: updateAbsenceInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `editReason` | string | yes | The reason for the edit. It is recorded on the absence's history. If it contains the text 'sick certificate', only leaveCertificate and certificateName are written and every other field in the request is ignored. | | `startDate` | string (date-time) | no | The date and time the absence starts, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `endDate` | string (date-time) | no | The date and time the absence ends, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `days` | string (decimal) | no | The number of absence days. May be fractional for partial days. | | `absenceTypeId` | string | no | The unique identifier for the absence type. See id on GET people/absence-types/:id. | | `leaveDescription` | string | no | A free-text reason for the absence. | | `googleEvent` | boolean | no | Whether to mirror this absence to the linked Google Calendar. | | `leaveCertificate` | string | no | The certificate file content. It is written only when editReason contains the text 'sick certificate'. Sent without that text, it is ignored. | | `certificateName` | string | no | The file name of the certificate. Written only when editReason contains the text 'sick certificate', like leaveCertificate. | Example: ```json { "editReason": "<editReason>", "startDate": "2026-01-15T09:30:00.000Z", "endDate": "2026-01-15T09:30:00.000Z", "days": "19.99", "absenceTypeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "leaveDescription": "<leaveDescription>", "googleEvent": true, "leaveCertificate": "<leaveCertificate>", "certificateName": "<certificateName>" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `ABSENCE_TYPE_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, ABSENCE_TYPE_NOT_FOUND - `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 --- # Delete Absence `DELETE /people/absences/{id}` Group: People Management › Absences · Operation ID: `deleteAbsence` Deletes the absence with the given identifier. The row is not always removed: an approved or rejected absence is canceled in place and kept, and an already-canceled absence is left unchanged, yet the call reports success in every case. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteAbsence) ## 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. | | `id` | path | string | yes | The unique identifier for the absence to delete. | ## Response `200` Success Under `data` (deleteAbsenceOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted absence. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Approve Absences `POST /people/absences/approve` Group: People Management › Absences · Operation ID: `approveAbsences` Approves one or more absence requests. If any id does not resolve, the whole call is rejected and nothing is approved. Returns the approved ids in input order. [Open in the interactive reference](https://docs.api.lensing.ai/?op=approveAbsences) ## 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`: approveAbsencesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `absences` | array of object | yes | The absences to approve. | | `absences[].id` | string | yes | The unique identifier for the absence to approve. | | `approverEmployeeId` | string | no | The unique identifier for the employee performing the approval. Omit to let the platform infer it from the session. See id on GET people/:id. | | `autoApprove` | boolean | no | Whether to bypass the remaining approval steps. Defaults to false. | Example: ```json { "absences": [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ], "approverEmployeeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "autoApprove": true } ``` ## Response `200` Success Under `data` (approveAbsencesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers for the approved absences, in the same order as the input. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `EMPLOYEE_NOT_FOUND` | 400 | | `ALREADY_DECIDED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, EMPLOYEE_NOT_FOUND, ALREADY_DECIDED - `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 --- # Reject Absences `POST /people/absences/reject` Group: People Management › Absences · Operation ID: `rejectAbsences` Rejects one or more absence requests. If any id does not resolve, the whole call is rejected and nothing is changed. Returns the rejected ids in input order. [Open in the interactive reference](https://docs.api.lensing.ai/?op=rejectAbsences) ## 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`: rejectAbsencesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `absences` | array of object | yes | The absences to reject. | | `absences[].id` | string | yes | The unique identifier for the absence to reject. | | `approverEmployeeId` | string | no | The unique identifier for the employee performing the rejection. Omit to let the platform infer it from the session. See id on GET people/:id. | Example: ```json { "absences": [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ], "approverEmployeeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` ## Response `200` Success Under `data` (rejectAbsencesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers for the rejected absences, in the same order as the input. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `EMPLOYEE_NOT_FOUND` | 400 | | `ALREADY_DECIDED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, EMPLOYEE_NOT_FOUND, ALREADY_DECIDED - `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 --- # Returns the absences that match the given filters. `POST /people/absences/search` Group: People Management › Absences · Operation ID: `searchAbsences` [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchAbsences) ## 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`. | ## Request body Required, `application/json`: searchAbsencesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. An absence must match every filter you send. Omit to return all absences. | | `filters.employeeId` | string | no | Limits results to absences for this employee. | | `filters.absenceTypeId` | string | no | Limits results to absences of this absence type. | | `filters.status` | string, one of `"requested"`, `"approved"`, `"rejected"`, `"canceled"` | no | Limits results to absences with this status. | | `filters.startsAfter` | string (date-time) | no | Limits results to absences whose startDate is on or after this value, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `filters.endsBefore` | string (date-time) | no | Limits results to absences whose endDate is on or before this value, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `filters.descriptionContains` | string | no | Limits results to absences whose description contains this text. The match is case-insensitive. | | `page` | object | no | Pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The number of absences per page, at most 100. Defaults to 25. A value above 100 is treated as 100, not rejected. | Example: ```json { "filters": { "employeeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "absenceTypeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "status": "requested", "startsAfter": "2026-01-15T09:30:00.000Z", "endsBefore": "2026-01-15T09:30:00.000Z", "descriptionContains": "<descriptionContains>" }, "page": { "cursor": "<cursor>", "size": 0 } } ``` ## Response `200` Success Under `data` (searchAbsencesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The absences on this page. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the absence. | | `items[].leaveNumber` | string | no | The absence number. | | `items[].employeeId` | string | yes | The unique identifier for the employee the absence belongs to. See id on GET people/:id. | | `items[].absenceTypeId` | string | yes | The unique identifier for the absence type. See id on GET people/absence-types/:id. | | `items[].startDate` | string (date-time) | yes | The date and time the absence starts, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].endDate` | string (date-time) | yes | The date and time the absence ends, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].days` | string (decimal) | no | The number of absence days. May be fractional for partial days. | | `items[].status` | string, one of `"requested"`, `"approved"`, `"rejected"`, `"canceled"` | no | The status of the absence. | | `items[].leaveDescription` | string | no | The free-text reason for the absence. | | `pagination` | object | yes | The pagination details for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether more absences remain after this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Applicant `POST /applicants` Group: People Management › Applicants · Operation ID: `createApplicant` Creates an applicant: a person and their first application to a recruitment, in one request. If the email already belongs to a person, that person is reused rather than created again. The application starts in status 'new', and the response is only the new application's id. An existing person applying to another recruitment uses POST applications instead. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createApplicant) ## 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`: createApplicantInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `person` | object | yes | The personal details of the applicant. | | `person.firstName` | string | yes | The applicant's first name. | | `person.lastName` | string | yes | The applicant's last name. | | `person.email` | string | yes | The applicant's email address. If it already belongs to a person, that person is reused rather than created again. | | `person.middleName` | string | no | The applicant's middle name. | | `person.phoneNumber` | string | no | The applicant's phone number. | | `application` | object | yes | The application details. | | `application.recruitmentId` | string | yes | The identifier of the recruitment being applied to. See id on GET people/recruitments/:id. | | `application.source` | string, one of `"appliedDirectly"`, `"sourcedInternally"`, `"sourcedExternally"`, `"employeeReferral"`, `"externalRecruiter"` | no | How the application arrived. Omit to leave it unset. | | `application.sourceDetails` | string | no | Free-text detail about the source, for example the referring employee or the job board. | | `application.expectedSalary` | string | no | The applicant's salary expectation, as free text. | | `application.linkedIn` | string | no | The URL of the applicant's LinkedIn profile. | | `application.shortIntroduction` | string | no | A short free-text introduction or motivation supplied with the application. | | `address` | object | no | The applicant's home address. When you send it, line1, city, country and zipCode are all required. | | `address.line1` | string | yes | The first address line: street number and name, or PO box. | | `address.line2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `address.city` | string | yes | The city or locality. | | `address.country` | string | yes | The country. Format ISO 3166-1 alpha-2, for example 'US'. | | `address.stateProvince` | string | no | The state or province, as the code the platform's state and province list uses, for example 'CA'. Not a spelled-out name. | | `address.zipCode` | string | yes | The postal or ZIP code. Required whenever you send an address. | Example: ```json { "person": { "firstName": "<firstName>", "lastName": "<lastName>", "email": "<email>", "middleName": "<middleName>", "phoneNumber": "<phoneNumber>" }, "application": { "recruitmentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "source": "appliedDirectly", "sourceDetails": "<sourceDetails>", "expectedSalary": "<expectedSalary>", "linkedIn": "<linkedIn>", "shortIntroduction": "<shortIntroduction>" }, "address": { "line1": "<line1>", "line2": "<line2>", "city": "<city>", "country": "<country>", "stateProvince": "<stateProvince>", "zipCode": "<zipCode>" } } ``` ## Response `200` Success Under `data` (createApplicantOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created application. | ## 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 | | --- | --- | | `RECRUITMENT_NOT_FOUND` | 400 | | `DUPLICATE_APPLICATION` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: RECRUITMENT_NOT_FOUND, DUPLICATE_APPLICATION - `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 --- # Applications `GET /applications` Group: People Management › Applications · Operation ID: `applications` Returns a list of applications. An application is one person applied to one recruitment. Create one with POST applications, or create a new applicant and their first application with POST applicants. [Open in the interactive reference](https://docs.api.lensing.ai/?op=applications) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `recruitmentId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `recruitmentName` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `source` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, applicantNumber, recruitmentName, status, source, lastModifiedDate - Selectable: id, applicantNumber, name, email, personId, recruitmentId, recruitmentName, positionName, status, source, lastModifiedDate ## Response `200` Success Under `data`, an array of applicationsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the application. | | `applicantNumber` | string or null | no | The system-generated applicant number. | | `name` | string or null | no | The applicant's display name. | | `email` | string or null | no | The applicant's email address. | | `personId` | string or null | no | The identifier of the person behind this application. | | `recruitmentId` | string or null | no | The identifier of the recruitment applied to. See id on GET people/recruitments/:id. | | `recruitmentName` | string or null | no | The name of the recruitment applied to. | | `positionName` | string or null | no | The position or job title the recruitment hires for. | | `status` | string or null | no | The status of the application. Possible values: 'draft', 'inProcess', 'waiting', 'accpeted', 'rejected', 'withdrawn', 'deleted', 'new', 'purged', 'anonymized'. | | `source` | string or null | no | How the application arrived. Possible values: 'appliedDirectly', 'sourcedInternally', 'sourcedExternally', 'employeeReferral', 'externalRecruiter'. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the application record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Application `POST /applications` Group: People Management › Applications · Operation ID: `createApplication` Creates an application: one person applied to one recruitment. The application starts in status 'new', and its recruiter is taken from the recruitment's owner. A person cannot have a second application in status 'new' or 'inProcess' for the same recruitment. To apply a person who does not exist yet, use POST applicants instead. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createApplication) ## 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`: createApplicationInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `personId` | string | yes | The identifier of the person who applies. | | `recruitmentId` | string | yes | The identifier of the recruitment being applied to. See id on GET people/recruitments/:id. | | `source` | string, one of `"appliedDirectly"`, `"sourcedInternally"`, `"sourcedExternally"`, `"employeeReferral"`, `"externalRecruiter"` | no | How the application arrived. When omitted, no source is recorded. | | `sourceDetails` | string | no | Free-text detail about the source, for example the referring employee or the job board. | | `expectedSalary` | string | no | The applicant's salary expectation, as free text. | | `linkedIn` | string | no | The URL of the applicant's LinkedIn profile. | | `shortIntroduction` | string | no | A short free-text introduction or motivation supplied with the application. | Example: ```json { "personId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "recruitmentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "source": "appliedDirectly", "sourceDetails": "<sourceDetails>", "expectedSalary": "<expectedSalary>", "linkedIn": "<linkedIn>", "shortIntroduction": "<shortIntroduction>" } ``` ## Response `200` Success Under `data` (createApplicationOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created application. | | `applicantNumber` | string | no | The system-generated applicant number. Absent while none has been assigned. | | `personId` | string | yes | The identifier of the person the application belongs to. | | `recruitmentId` | string | yes | The identifier of the recruitment applied to. See id on GET people/recruitments/:id. | | `status` | string | yes | The status the application is created in, always 'new'. | ## 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 | | --- | --- | | `PERSON_NOT_FOUND` | 400 | | `RECRUITMENT_NOT_FOUND` | 400 | | `DUPLICATE_APPLICATION` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: PERSON_NOT_FOUND, RECRUITMENT_NOT_FOUND, DUPLICATE_APPLICATION - `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 --- # Departments `GET /departments` Group: People Management › Departments · Operation ID: `departments` Returns a list of departments. GET departments/:id returns a richer record, including the parent and manager display names and the external identifiers. [Open in the interactive reference](https://docs.api.lensing.ai/?op=departments) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `departmentNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `departmentCode` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `parentId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `name` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, departmentNumber, departmentCode, name, managerName, status, employeeCount, lastModifiedDate - Selectable: id, departmentNumber, departmentCode, parentId, name, description, managerName, status, employeeCount, lastModifiedDate ## Response `200` Success Under `data`, an array of departmentsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the department. | | `departmentNumber` | string or null | no | The system-generated department number, for example '100010'. Absent while none has been assigned. | | `departmentCode` | string or null | no | The short code for the department, for example 'IT', 'HR' or 'FIN'. | | `parentId` | string or null | no | The identifier of the parent department. Absent for a top-level department. See id on GET departments/:id. | | `name` | string | no | The display name of the department. | | `description` | string or null | no | A free-form description of the department's purpose and responsibilities. | | `managerName` | string or null | no | The full name of the department's managing employee. Absent when no manager is assigned. | | `status` | string or null | no | The timeline status of the department. Possible values: 'planned', 'inactive', 'active', 'canceled'. | | `employeeCount` | number or null | no | The number of employees in the department. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the department record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Department `POST /departments` Group: People Management › Departments · Operation ID: `createDepartment` Creates a department. The response is only the new department's id, so read the full record with GET departments/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createDepartment) ## 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`: createDepartmentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The display name of the department. | | `departmentCode` | string | no | A short code for the department, for example 'IT', 'HR' or 'FIN'. Must be unique across departments. When omitted, the system-generated department number is used as the code. | | `description` | string | no | A free-form description of the department's purpose and responsibilities. | | `managerId` | string | no | The identifier of the employee who manages the department. See id on GET people/:id. | | `parentId` | string | no | The identifier of the parent department. Omit for a top-level department. See id on GET departments/:id. | | `startDate` | string | yes | The date the department becomes active. Use format YYYY-MM-DD. | | `endDate` | string | no | The date the department becomes inactive. Use format YYYY-MM-DD. Omit for an open-ended department. | | `externalIdentifiers` | array of object | no | The department's identifiers in other systems, one per configured field name. A field name not configured for your tenant fails with UNKNOWN_EXTERNAL_IDENTIFIER. | | `externalIdentifiers[].fieldName` | string | yes | The configured field name of the identifier, for example 'netsuiteDepartmentId'. | | `externalIdentifiers[].value` | string | yes | The identifier in the other system. | | `externalIdentifiers[].externalUrl` | string | no | A link to the record in the other system. | Example: ```json { "name": "<name>", "departmentCode": "<departmentCode>", "description": "<description>", "managerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "parentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "startDate": "<startDate>", "endDate": "<endDate>", "externalIdentifiers": [ { "fieldName": "<fieldName>", "value": "<value>", "externalUrl": "<externalUrl>" } ] } ``` ## Response `200` Success Under `data` (createDepartmentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created department. | ## 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 | | --- | --- | | `MANAGER_NOT_FOUND` | 400 | | `PARENT_DEPARTMENT_NOT_FOUND` | 400 | | `DUPLICATE_DEPARTMENT_CODE` | 400 | | `UNKNOWN_EXTERNAL_IDENTIFIER` | 400 | | `DUPLICATE_EXTERNAL_IDENTIFIER` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: MANAGER_NOT_FOUND, PARENT_DEPARTMENT_NOT_FOUND, DUPLICATE_DEPARTMENT_CODE, UNKNOWN_EXTERNAL_IDENTIFIER, DUPLICATE_EXTERNAL_IDENTIFIER - `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 --- # Get Department `GET /departments/{id}` Group: People Management › Departments · Operation ID: `getDepartment` Returns the department with the given identifier. Use the department's id, not its departmentNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getDepartment) ## 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`. | | `id` | path | string | yes | The unique identifier for the department to return. | ## Response `200` Success Under `data` (getDepartmentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the department. | | `departmentNumber` | string | no | The system-generated department number, for example '100010'. Absent while none has been assigned. | | `name` | string | yes | The display name of the department. | | `departmentCode` | string | no | The short code for the department, for example 'IT', 'HR' or 'FIN'. | | `description` | string | no | A free-form description of the department's purpose and responsibilities. | | `managerId` | string | no | The identifier of the managing employee. Absent when no manager is assigned. See id on GET people/:id. | | `managerName` | string | no | The full name of the managing employee. Absent when no manager is assigned. | | `parentId` | string | no | The identifier of the parent department. Absent for a top-level department. See id on GET departments/:id. | | `parentName` | string | no | The display name of the parent department. Absent for a top-level department. | | `status` | string | no | The timeline status of the department. Possible values: 'planned', 'inactive', 'active', 'canceled'. | | `startDate` | string | no | The date the department becomes active. Format YYYY-MM-DD. | | `endDate` | string | no | The date the department becomes inactive. Format YYYY-MM-DD. Absent for an open-ended department. | | `employeeCount` | number | no | The number of employees in the department. | | `lastModifiedDate` | string (date-time) | yes | The date and time the department record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `externalIdentifiers` | array of object | no | The department's identifiers in other systems, one per configured field name. An empty array or absent when the department has none. | | `externalIdentifiers[].fieldName` | string | yes | The configured field name of the identifier, for example 'netsuiteDepartmentId'. | | `externalIdentifiers[].value` | string | yes | The identifier in the other system. | | `externalIdentifiers[].externalUrl` | string | no | A link to the record in the other system. Absent when none is stored. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Department `PATCH /departments/{id}` Group: People Management › Departments · Operation ID: `updateDepartment` Updates the department with the given identifier. Only the fields in the request body change. Use the department's id, not its departmentNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateDepartment) ## 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. | | `id` | path | string | yes | The unique identifier for the department to update. | ## Request body Required, `application/json`: updateDepartmentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The display name of the department. | | `departmentCode` | string | no | The short code for the department. Must be unique across departments. Send an empty string to reset it to the system-generated department number. | | `description` | string | no | A free-form description of the department's purpose and responsibilities. | | `managerId` | string | no | The identifier of the employee who manages the department. See id on GET people/:id. | | `parentId` | string | no | The identifier of the new parent department. It cannot be cleared through this endpoint, and it must not be a descendant of this department. See id on GET departments/:id. | | `startDate` | string | no | The date the department becomes active. Use format YYYY-MM-DD. | | `endDate` | string | no | The date the department becomes inactive. Use format YYYY-MM-DD. It cannot be cleared through this endpoint. | | `createExternalIdentifiers` | array of object | no | External identifiers to add. A fieldName already present on the record fails with DUPLICATE_EXTERNAL_IDENTIFIER. | | `createExternalIdentifiers[].fieldName` | string | yes | The configured field name of the identifier, for example 'netsuiteDepartmentId'. | | `createExternalIdentifiers[].value` | string | yes | The identifier in the other system. | | `createExternalIdentifiers[].externalUrl` | string | no | A link to the record in the other system. | | `updateExternalIdentifiers` | array of object | no | External identifiers to rewrite, matched by fieldName. A fieldName not present on the record fails with EXTERNAL_IDENTIFIER_NOT_FOUND. | | `updateExternalIdentifiers[].fieldName` | string | yes | The configured field name of an identifier already present on the record. | | `updateExternalIdentifiers[].value` | string | yes | The new identifier value in the other system. | | `updateExternalIdentifiers[].externalUrl` | string | no | A link to the record in the other system. | | `deleteExternalIdentifiers` | array of string | no | The field names of external identifiers to remove. A fieldName not present on the record fails with EXTERNAL_IDENTIFIER_NOT_FOUND. Removal is always explicit, never by omission. | Example: ```json { "name": "<name>", "departmentCode": "<departmentCode>", "description": "<description>", "managerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "parentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "startDate": "<startDate>", "endDate": "<endDate>", "createExternalIdentifiers": [ { "fieldName": "<fieldName>", "value": "<value>", "externalUrl": "<externalUrl>" } ], "updateExternalIdentifiers": [ { "fieldName": "<fieldName>", "value": "<value>", "externalUrl": "<externalUrl>" } ], "deleteExternalIdentifiers": [ "<deleteExternalIdentifiers>" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `MANAGER_NOT_FOUND` | 400 | | `PARENT_DEPARTMENT_NOT_FOUND` | 400 | | `DUPLICATE_DEPARTMENT_CODE` | 400 | | `UNKNOWN_EXTERNAL_IDENTIFIER` | 400 | | `DUPLICATE_EXTERNAL_IDENTIFIER` | 400 | | `EXTERNAL_IDENTIFIER_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, MANAGER_NOT_FOUND, PARENT_DEPARTMENT_NOT_FOUND, DUPLICATE_DEPARTMENT_CODE, UNKNOWN_EXTERNAL_IDENTIFIER, DUPLICATE_EXTERNAL_IDENTIFIER, EXTERNAL_IDENTIFIER_NOT_FOUND - `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 --- # Returns a list of document categories. `GET /people/document-categories` Group: People Management › Document Categories · Operation ID: `documentCategories` [Open in the interactive reference](https://docs.api.lensing.ai/?op=documentCategories) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `name` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `label` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `requestable` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, name, label, requestable, lastModifiedDate - Selectable: id, name, label, description, requestable, lastModifiedDate ## Response `200` Success Under `data`, an array of documentCategoriesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the document category. | | `name` | string or null | no | The internal name of the category. Absent when none is set. | | `label` | string or null | no | The display label of the category. Documents filed under this category are named with it as a prefix. Absent when none is set. | | `description` | string or null | no | A longer description of what belongs in this category. Absent when none is set. | | `requestable` | boolean or null | no | Whether an employee may request a document of this category from HR, rather than the category only receiving uploads. | | `lastModifiedDate` | string (date-time) | no | The date and time the category record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Documents `GET /people/documents` Group: People Management › Documents · Operation ID: `documents` Returns a list of employee documents. Each entry is document details only. Fetch the file itself through GET people/documents/:id/download. [Open in the interactive reference](https://docs.api.lensing.ai/?op=documents) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `employeeId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `categoryId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `expiryDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, documentNumber, categoryId, status, expiryDate, lastModifiedDate - Selectable: id, documentNumber, employeeId, categoryId, categoryLabel, fileName, status, expiryDate, lastModifiedDate ## Response `200` Success Under `data`, an array of documentsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the document. | | `documentNumber` | string or null | no | The system-generated document number. | | `employeeId` | string or null | no | The identifier of the employee the document belongs to. Absent when the linked employee no longer resolves. See id on GET people/:id. | | `categoryId` | string or null | no | The identifier of the document category the document is filed under. Absent when the linked category no longer resolves. See id on GET people/document-categories. | | `categoryLabel` | string or null | no | The display label of the document category the document is filed under. See label on GET people/document-categories. | | `fileName` | string or null | no | The stored file name, generated on upload from the category label and the employee's name. | | `status` | string or null | no | The current status of the document. Possible values: 'pending', 'approved', 'rejected', 'deprecated', 'readyToDownload', 'downloaded'. | | `expiryDate` | string (date-time) or null | no | The day the document expires, projected as UTC midnight of that day. Format YYYY-MM-DDTHH:mm:ss.sssZ. Absent when the document does not expire. | | `lastModifiedDate` | string (date-time) | no | The date and time the document record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Upload Document `POST /people/documents` Group: People Management › Documents · Operation ID: `uploadDocument` Creates an employee document from a base64-encoded file. The stored file name and the initial status are set by the system, not by you: the status is 'approved' when an HR administrator files a document for another employee, and 'pending' otherwise. The response is only the new document's id, so read the stored name and status from GET people/documents. [Open in the interactive reference](https://docs.api.lensing.ai/?op=uploadDocument) ## 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`: uploadDocumentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `employeeId` | string | yes | The identifier of the employee the document belongs to. See id on GET people/:id. | | `categoryId` | string | yes | The identifier of the document category to file the document under. See id on GET people/document-categories. | | `fileName` | string | yes | The name of the file being uploaded, with its extension. Only the extension is kept, and the stored file name is generated from the category label and the employee's name. | | `mimeType` | string | yes | The MIME type of the file, for example 'application/pdf'. | | `contentBase64` | string | yes | The file's bytes, base64-encoded with the standard alphabet and padding. Must be non-empty and well-formed. | | `expiryDate` | string | no | The day the document expires. Use format YYYY-MM-DD. Omit for a document that does not expire. | Example: ```json { "employeeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "categoryId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "fileName": "<fileName>", "mimeType": "<mimeType>", "contentBase64": "<contentBase64>", "expiryDate": "<expiryDate>" } ``` ## Response `200` Success Under `data` (uploadDocumentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created document. | ## 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 | | --- | --- | | `EMPLOYEE_NOT_FOUND` | 400 | | `CATEGORY_NOT_FOUND` | 400 | | `INVALID_FILE_CONTENT` | 400 | | `INVALID_EXPIRY_DATE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: EMPLOYEE_NOT_FOUND, CATEGORY_NOT_FOUND, INVALID_FILE_CONTENT, INVALID_EXPIRY_DATE - `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 --- # Delete Document `DELETE /people/documents/{id}` Group: People Management › Documents · Operation ID: `deleteDocument` Deletes the document with the given identifier. The document record is removed and the stored file is kept. A repeated delete of the same identifier fails with NOT_FOUND. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteDocument) ## 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. | | `id` | path | string | yes | The unique identifier for the document to delete. | ## Response `200` Success Under `data` (deleteDocumentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the deleted document. | | `fileName` | string | yes | The stored file name the deleted document had. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Download Document `GET /people/documents/{id}/download` Group: People Management › Documents · Operation ID: `downloadDocument` Returns a short-lived download link for the document with the given identifier, along with the file's name, MIME type and size. The file content itself is not returned. Request the link when a download starts rather than storing it, because it expires. [Open in the interactive reference](https://docs.api.lensing.ai/?op=downloadDocument) ## 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`. | | `id` | path | string | yes | The unique identifier for the document to download. | ## Response `200` Success Under `data` (downloadDocumentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the document the link points at. | | `fileName` | string | yes | The stored file name of the document. Save the downloaded bytes under it. | | `mimeType` | string | no | The MIME type of the stored file. Absent when storage does not report one. | | `fileSizeBytes` | number | no | The size of the stored file in bytes. Absent when storage does not report one. | | `downloadUrl` | string | yes | A short-lived pre-signed URL the file's bytes are fetched from. It expires and is not a stable link. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | | `FILE_UNAVAILABLE` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, FILE_UNAVAILABLE - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # People `GET /people` Group: People Management › People · Operation ID: `people` Returns a list of people. To filter by name, email or business number, use POST people/search. GET people/:id returns a richer record, including compensation and external identifiers. [Open in the interactive reference](https://docs.api.lensing.ai/?op=people) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `employmentCategory` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, employeeNumber, status, employmentCategory, lastModifiedDate - Selectable: id, employeeNumber, firstName, lastName, name, email, status, employmentCategory, entityName, locationName, lastModifiedDate ## Response `200` Success Under `data`, an array of peopleRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the person. | | `employeeNumber` | string or null | no | The person's business number. | | `firstName` | string or null | no | The person's first name. | | `lastName` | string or null | no | The person's last name. | | `name` | string or null | no | The person's full name, in first-name-last-name order. | | `email` | string or null | no | The person's work email address. | | `status` | string or null | no | The current employment status of the person. Possible values: 'Active', 'Inactive', 'Planned', 'Cancelled'. | | `employmentCategory` | string or null | no | The employment category assigned to this person. | | `entityName` | string or null | no | The name of the legal entity this person belongs to. | | `locationName` | string or null | no | The name of this person's work location. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the person record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Person `POST /people` Group: People Management › People · Operation ID: `createPerson` Creates a person. The response is only the new person's id, so read the full record with GET people/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createPerson) ## 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`: createPersonInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `person` | object | yes | The person's personal details. | | `person.firstName` | string | yes | The person's first name. | | `person.lastName` | string | yes | The person's last name. | | `person.email` | string | yes | The person's work email address. | | `person.startDate` | string | yes | The employment start date. Use format YYYY-MM-DD. | | `person.endDate` | string | no | The employment end date. Use format YYYY-MM-DD. | | `person.middleName` | string | no | The person's middle name. | | `person.gender` | string | no | The person's self-identified gender. Any string value is accepted. | | `person.dateOfBirth` | string | no | The person's date of birth. Use format YYYY-MM-DD. | | `person.nationality` | string | no | The person's nationality. Format ISO 3166-1 alpha-2, for example 'US'. | | `employee` | object | yes | The person's employment details. | | `employee.entityId` | string | yes | The identifier of the legal entity this person belongs to. See id on GET base/entities. | | `employee.personalEmail` | string | no | The person's personal email address. | | `employee.employmentCategory` | string | no | The person's employment category. | | `employee.holidayCalendarId` | string | no | The identifier of the holiday calendar. Required when absencePolicies is provided. There is no endpoint for it. | | `job` | object | yes | The person's job assignment details. | | `job.departmentId` | string | yes | The identifier of the department. See id on GET departments/:id. | | `job.positionId` | string | no | The identifier of the position. See id on GET positions/:id. | | `job.jobManagerId` | string | no | The identifier of the manager's job assignment, which is the manager's job id and not their person id. There is no endpoint for it. | | `address` | object | no | The person's home address. | | `address.line1` | string | yes | The first address line: street number and name, or PO box. | | `address.line2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `address.city` | string | yes | The city or locality. | | `address.country` | string | yes | The country. Format ISO 3166-1 alpha-2, for example 'US'. | | `address.stateProvince` | string | no | The state, province or region. | | `address.zipCode` | string | yes | The postal or ZIP code. | | `absencePolicies` | array of object | no | The absence policies assigned to the person. | | `absencePolicies[].absencePolicyId` | string | yes | The identifier of the absence policy. There is no endpoint for it. | | `externalIdentifiers` | array of object | no | The external identifiers owned by other systems, as the whole initial set. At most one entry per fieldName. Two entries with the same fieldName fail with DUPLICATE_EXTERNAL_IDENTIFIER, and a fieldName with no configuration fails with UNKNOWN_EXTERNAL_IDENTIFIER, which lists every unrecognized key. | | `externalIdentifiers[].fieldName` | string | yes | The configured key of the external identifier, a camelCase slug such as 'netsuiteEmployeeId'. It must be the fieldName of an existing employee external-identifier configuration, or the call fails with UNKNOWN_EXTERNAL_IDENTIFIER. | | `externalIdentifiers[].value` | string | yes | The identifier as the source system holds it. | | `externalIdentifiers[].externalUrl` | string | no | An absolute http(s) link to the object in the source system. | Example: ```json { "person": { "firstName": "<firstName>", "lastName": "<lastName>", "email": "<email>", "startDate": "<startDate>", "endDate": "<endDate>", "middleName": "<middleName>", "gender": "<gender>", "dateOfBirth": "<dateOfBirth>", "nationality": "<nationality>" }, "employee": { "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "personalEmail": "<personalEmail>", "employmentCategory": "<employmentCategory>", "holidayCalendarId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }, "job": { "departmentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "positionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "jobManagerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }, "address": { "line1": "<line1>", "line2": "<line2>", "city": "<city>", "country": "<country>", "stateProvince": "<stateProvince>", "zipCode": "<zipCode>" }, "absencePolicies": [ { "absencePolicyId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ], "externalIdentifiers": [ { "fieldName": "<fieldName>", "value": "<value>", "externalUrl": "<externalUrl>" } ] } ``` ## Response `200` Success Under `data` (createPersonOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created person. | ## 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 | | --- | --- | | `ENTITY_NOT_FOUND` | 400 | | `DEPARTMENT_NOT_FOUND` | 400 | | `POSITION_NOT_FOUND` | 400 | | `MANAGER_NOT_FOUND` | 400 | | `HOLIDAY_CALENDAR_NOT_FOUND` | 400 | | `HOLIDAY_CALENDAR_REQUIRED` | 400 | | `ABSENCE_POLICY_NOT_FOUND` | 400 | | `UNKNOWN_EXTERNAL_IDENTIFIER` | 400 | | `DUPLICATE_EXTERNAL_IDENTIFIER` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: ENTITY_NOT_FOUND, DEPARTMENT_NOT_FOUND, POSITION_NOT_FOUND, MANAGER_NOT_FOUND, HOLIDAY_CALENDAR_NOT_FOUND, HOLIDAY_CALENDAR_REQUIRED, ABSENCE_POLICY_NOT_FOUND, UNKNOWN_EXTERNAL_IDENTIFIER, DUPLICATE_EXTERNAL_IDENTIFIER - `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 --- # Get Person `GET /people/{id}` Group: People Management › People · Operation ID: `getPerson` Returns the person with the given identifier. Use the person's id, not their employeeNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getPerson) ## 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`. | | `id` | path | string | yes | The unique identifier for the person to return. | ## Response `200` Success Under `data` (getPersonOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the person. | | `employeeNumber` | string | no | The person's business number. | | `firstName` | string | no | The person's first name. | | `lastName` | string | no | The person's last name. | | `middleName` | string | no | The person's middle name. | | `preferredFirstName` | string | no | The person's preferred first name. | | `preferredLastName` | string | no | The person's preferred last name. | | `name` | string | no | The person's full name, in first-name-last-name order. | | `email` | string | no | The person's work email address. | | `status` | string | no | The current employment status of the person. Possible values: 'Active', 'Inactive', 'Planned', 'Cancelled'. | | `employmentCategory` | string | no | The employment category assigned to this person. | | `startDate` | string | no | The employment start date. Format YYYY-MM-DD. | | `endDate` | string | no | The employment end date. Format YYYY-MM-DD. Absent while the person is still employed. | | `probationEndDate` | string | no | The probation end date. Format YYYY-MM-DD. Absent when no probation period applies. | | `jobId` | string | no | The identifier of this person's active primary job. Absent when the person has no active primary job. There is no endpoint for it. | | `departmentName` | string | no | The name of the department this person belongs to. Absent when no department is assigned. | | `positionName` | string | no | The position or job title assigned to this person. Absent when no position is assigned. | | `managerName` | string | no | The full name of this person's primary manager. Absent when there is no active primary job or no manager assigned to it. | | `compensation` | object | no | The person's current active compensation record. Absent when no compensation record exists. | | `compensation.amount` | object | no | The base salary as a currency amount. | | `compensation.amount.currency` | string | no | The currency of the base salary. Format ISO 4217, for example 'USD'. | | `compensation.amount.amount` | string (decimal) | no | The numeric value of the base salary, in the given currency. | | `compensation.paymentFrequency` | string | no | The frequency at which this person is paid. Possible values: 'monthly', 'biweekly', 'weekly', 'yearly'. | | `compensation.unit` | string | no | The unit the base salary is measured in. Possible values: 'annually', 'quarterly', 'monthly', 'semiMonthly', 'weekly', 'biWeekly', 'daily', 'hourly', 'fixed'. | | `compensation.monthlyAmount` | object | no | The base salary converted to a monthly amount. Only the 'annually' and 'hourly' units are converted. For every other unit this repeats the base salary unchanged and is not a monthly figure. | | `compensation.monthlyAmount.currency` | string | no | The currency of the monthly-equivalent salary. Format ISO 4217, for example 'USD'. | | `compensation.monthlyAmount.amount` | string (decimal) | no | The numeric value of the monthly-equivalent salary, in the given currency. | | `compensation.salaryType` | string | no | The name of the compensation type. Absent when none is assigned. | | `compensation.salaryCode` | string | no | The code of the compensation type. Absent when the type has no code or none is assigned. | | `compensation.startDate` | string | no | The date from which this compensation record is effective. Format YYYY-MM-DD. | | `compensation.endDate` | string | no | The date on which this compensation record expires. Format YYYY-MM-DD. Absent when it is open-ended. | | `entityName` | string | no | The name of the legal entity this person belongs to. | | `locationName` | string | no | The name of this person's work location. Absent when none is assigned. | | `customFields` | UnknownObject | no | The person's custom field values, as key-value pairs of field name and value. | | `lastModifiedDate` | string (date-time) | yes | The date and time the person record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `externalIdentifiers` | array of object | no | The external identifiers owned by other systems, at most one per fieldName. Absent when the person has none. | | `externalIdentifiers[].fieldName` | string | yes | The configured key of the external identifier, a camelCase slug such as 'netsuiteEmployeeId'. It is the fieldName of an existing employee external-identifier configuration. | | `externalIdentifiers[].value` | string | yes | The identifier as the source system holds it. | | `externalIdentifiers[].externalUrl` | string | no | An absolute http(s) link to the object in the source system. Absent when none is stored. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Person `PATCH /people/{id}` Group: People Management › People · Operation ID: `updatePerson` Updates the person with the given identifier. Only the fields in the request body change. Use the person's id, not their employeeNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updatePerson) ## 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. | | `id` | path | string | yes | The unique identifier for the person to update. | ## Request body Required, `application/json`: updatePersonInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `firstName` | string | no | The person's first name. Changing first or last name recomputes the display name. | | `lastName` | string | no | The person's last name. Changing first or last name recomputes the display name. | | `middleName` | string | no | The person's middle name. | | `email` | string | no | The person's work email address. It is stored in lowercase. | | `personalEmail` | string | no | The person's personal email address. It is stored in lowercase. | | `gender` | string | no | The person's self-identified gender. Any string value is accepted. | | `dateOfBirth` | string | no | The person's date of birth. Use format YYYY-MM-DD. | | `nationality` | string | no | The person's nationality. Format ISO 3166-1 alpha-2, for example 'US'. | | `startDate` | string | no | The employment start date. Use format YYYY-MM-DD. It must not overlap the person's other employee profiles. Changing it also re-anchors incomplete boarding deadlines and, for future-dated employments, absence policy assignments and the first job's start. | | `endDate` | string | no | The employment end date. Use format YYYY-MM-DD. It cannot be cleared through this endpoint. Setting it on a previously open-ended employment also end-dates the person's active jobs and open absence policy assignments to the same date. | | `employmentCategory` | string | no | The person's employment category. | | `address` | object | no | The person's home address. When present it replaces the whole address, not a per-field patch. | | `address.line1` | string | yes | The first address line: street number and name, or PO box. | | `address.line2` | string | no | The second address line: apartment, suite, unit, building or floor. | | `address.city` | string | yes | The city or locality. | | `address.country` | string | yes | The country. Format ISO 3166-1 alpha-2, for example 'US'. | | `address.stateProvince` | string | no | The state, province or region. | | `address.zipCode` | string | yes | The postal or ZIP code. | | `createExternalIdentifiers` | array of object | no | The external identifiers to add. A fieldName already present on the record fails with DUPLICATE_EXTERNAL_IDENTIFIER, because adding a duplicate key is not an implicit update. | | `createExternalIdentifiers[].fieldName` | string | yes | The configured key of the external identifier, a camelCase slug such as 'netsuiteEmployeeId'. It must be the fieldName of an existing employee external-identifier configuration, or the call fails with UNKNOWN_EXTERNAL_IDENTIFIER. | | `createExternalIdentifiers[].value` | string | yes | The identifier as the source system holds it. | | `createExternalIdentifiers[].externalUrl` | string | no | An absolute http(s) link to the object in the source system. | | `updateExternalIdentifiers` | array of object | no | The external identifiers to rewrite, matched by fieldName. A fieldName not already present fails with EXTERNAL_IDENTIFIER_NOT_FOUND, because rewriting is not an implicit create. | | `updateExternalIdentifiers[].fieldName` | string | yes | The configured key of the external identifier already present on the record. | | `updateExternalIdentifiers[].value` | string | yes | The new identifier value. | | `updateExternalIdentifiers[].externalUrl` | string | no | An absolute http(s) link to the object in the source system. | | `deleteExternalIdentifiers` | array of string | no | The fieldNames of external identifiers to remove. A fieldName not present on the record fails with EXTERNAL_IDENTIFIER_NOT_FOUND. Omit all three external-identifier arrays to leave the set unchanged, since deletion is always explicit. | Example: ```json { "firstName": "<firstName>", "lastName": "<lastName>", "middleName": "<middleName>", "email": "<email>", "personalEmail": "<personalEmail>", "gender": "<gender>", "dateOfBirth": "<dateOfBirth>", "nationality": "<nationality>", "startDate": "<startDate>", "endDate": "<endDate>", "employmentCategory": "<employmentCategory>", "address": { "line1": "<line1>", "line2": "<line2>", "city": "<city>", "country": "<country>", "stateProvince": "<stateProvince>", "zipCode": "<zipCode>" }, "createExternalIdentifiers": [ { "fieldName": "<fieldName>", "value": "<value>", "externalUrl": "<externalUrl>" } ], "updateExternalIdentifiers": [ { "fieldName": "<fieldName>", "value": "<value>", "externalUrl": "<externalUrl>" } ], "deleteExternalIdentifiers": [ "<deleteExternalIdentifiers>" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `UNKNOWN_EXTERNAL_IDENTIFIER` | 400 | | `DUPLICATE_EXTERNAL_IDENTIFIER` | 400 | | `EXTERNAL_IDENTIFIER_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, UNKNOWN_EXTERNAL_IDENTIFIER, DUPLICATE_EXTERNAL_IDENTIFIER, EXTERNAL_IDENTIFIER_NOT_FOUND - `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 --- # Returns the people that match the given filters. `POST /people/search` Group: People Management › People · Operation ID: `searchPeople` [Open in the interactive reference](https://docs.api.lensing.ai/?op=searchPeople) ## 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`. | ## Request body Required, `application/json`: searchPeopleInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | object | yes | The filters to apply. A person must match every filter you send. | | `filters.firstName` | string | no | Limits results to people with this first name. | | `filters.lastName` | string | no | Limits results to people with this last name. | | `filters.name` | string | no | Limits results to people with this full name. | | `filters.email` | string | no | Limits results to people with this work email address. | | `filters.employeeNumber` | string | no | Limits results to the person with this business number. The match is exact. | | `filters.entityName` | string | no | Limits results to people in the legal entity with this name. The match is exact. | | `filters.status` | string | no | Limits results to people with this employment status. Accepted values: 'Active', 'Inactive', 'Planned', 'Cancelled'. Any other value matches no person. | | `filters.employmentCategory` | string | no | Limits results to people with this employment category. | | `filters.externalIdentifier` | object | no | Limits results to people with this external identifier. Both fieldName and value are required. Several people may share a value, so this returns the items array, not a single record. | | `filters.externalIdentifier.fieldName` | string | yes | The configured key of the external identifier, a camelCase slug such as 'netsuiteEmployeeId'. It must be the fieldName of an existing employee external-identifier configuration, or the call fails with UNKNOWN_EXTERNAL_IDENTIFIER. | | `filters.externalIdentifier.value` | string | yes | The identifier as the source system holds it. The match is exact. | | `page` | object | no | The pagination controls. Omit for the first page. | | `page.cursor` | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `page.size` | number | no | The maximum number of people per page, between 1 and 200. Defaults to 50. A value above 200 is treated as 200. | Example: ```json { "filters": { "firstName": "<firstName>", "lastName": "<lastName>", "name": "<name>", "email": "<email>", "employeeNumber": "<employeeNumber>", "entityName": "<entityName>", "status": "<status>", "employmentCategory": "<employmentCategory>", "externalIdentifier": { "fieldName": "<fieldName>", "value": "<value>" } }, "page": { "cursor": "<cursor>", "size": 0 } } ``` ## Response `200` Success Under `data` (searchPeopleOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The people that match this page of the search. An empty array when nothing matches. | | `items[].id` | string | yes | The unique identifier for the person. | | `items[].employeeNumber` | string | no | The person's business number. | | `items[].firstName` | string | no | The person's first name. | | `items[].lastName` | string | no | The person's last name. | | `items[].middleName` | string | no | The person's middle name. | | `items[].preferredFirstName` | string | no | The person's preferred first name. | | `items[].preferredLastName` | string | no | The person's preferred last name. | | `items[].name` | string | no | The person's full name, in first-name-last-name order. | | `items[].email` | string | no | The person's work email address. | | `items[].status` | string | no | The current employment status of the person. Possible values: 'Active', 'Inactive', 'Planned', 'Cancelled'. | | `items[].employmentCategory` | string | no | The employment category assigned to this person. | | `items[].startDate` | string | no | The employment start date. Format YYYY-MM-DD. | | `items[].endDate` | string | no | The employment end date. Format YYYY-MM-DD. Absent while the person is still employed. | | `items[].probationEndDate` | string | no | The probation end date. Format YYYY-MM-DD. Absent when no probation period applies. | | `items[].jobId` | string | no | The identifier of this person's active primary job. Absent when the person has no active primary job. There is no endpoint for it. | | `items[].departmentName` | string | no | The name of the department this person belongs to. Absent when no department is assigned. | | `items[].positionName` | string | no | The position or job title assigned to this person. Absent when no position is assigned. | | `items[].managerName` | string | no | The full name of this person's primary manager. Absent when there is no active primary job or no manager assigned to it. | | `items[].compensation` | object | no | The person's current active compensation record. Absent when no compensation record exists. | | `items[].compensation.amount` | object | no | The base salary as a currency amount. | | `items[].compensation.amount.currency` | string | no | The currency of the base salary. Format ISO 4217, for example 'USD'. | | `items[].compensation.amount.amount` | string (decimal) | no | The numeric value of the base salary, in the given currency. | | `items[].compensation.paymentFrequency` | string | no | The frequency at which this person is paid. Possible values: 'monthly', 'biweekly', 'weekly', 'yearly'. | | `items[].compensation.unit` | string | no | The unit the base salary is measured in. Possible values: 'annually', 'quarterly', 'monthly', 'semiMonthly', 'weekly', 'biWeekly', 'daily', 'hourly', 'fixed'. | | `items[].compensation.monthlyAmount` | object | no | The base salary converted to a monthly amount. Only the 'annually' and 'hourly' units are converted. For every other unit this repeats the base salary unchanged and is not a monthly figure. | | `items[].compensation.monthlyAmount.currency` | string | no | The currency of the monthly-equivalent salary. Format ISO 4217, for example 'USD'. | | `items[].compensation.monthlyAmount.amount` | string (decimal) | no | The numeric value of the monthly-equivalent salary, in the given currency. | | `items[].compensation.salaryType` | string | no | The name of the compensation type. Absent when none is assigned. | | `items[].compensation.salaryCode` | string | no | The code of the compensation type. Absent when the type has no code or none is assigned. | | `items[].compensation.startDate` | string | no | The date from which this compensation record is effective. Format YYYY-MM-DD. | | `items[].compensation.endDate` | string | no | The date on which this compensation record expires. Format YYYY-MM-DD. Absent when it is open-ended. | | `items[].entityName` | string | no | The name of the legal entity this person belongs to. | | `items[].locationName` | string | no | The name of this person's work location. Absent when none is assigned. | | `items[].customFields` | UnknownObject | no | The person's custom field values, as key-value pairs of field name and value. | | `items[].lastModifiedDate` | string (date-time) | yes | The date and time the person record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].externalIdentifiers` | array of object | no | The external identifiers owned by other systems, at most one per fieldName. Absent when the person has none. | | `items[].externalIdentifiers[].fieldName` | string | yes | The configured key of the external identifier, a camelCase slug such as 'netsuiteEmployeeId'. It is the fieldName of an existing employee external-identifier configuration. | | `items[].externalIdentifiers[].value` | string | yes | The identifier as the source system holds it. | | `items[].externalIdentifiers[].externalUrl` | string | no | An absolute http(s) link to the object in the source system. Absent when none is stored. | | `pagination` | object | yes | The pagination details for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as page.cursor. Absent on the last page. | | `pagination.hasMore` | boolean | yes | Whether more results remain after this page. | ## Errors | Code | Status | | --- | --- | | `UNKNOWN_EXTERNAL_IDENTIFIER` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: UNKNOWN_EXTERNAL_IDENTIFIER - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Hr/people/created event `hr/people/created` Group: People Management › People · Operation ID: `hr_people_created` A person was added. Carries the person id only; read the person for everything else. Delivered when a person record is created, whether through POST people, the create-employee flow in the app, or an integration syncing people from a payroll or HR system. A person is created together with their first job, so this event is normally followed by one or more `hr/people/updated` for the same business action. Both describe the same new person; read the person back and take the current state from there. It fires for people whose employment has not started yet as well as for those already working: read `status` and `startDate` rather than assuming a new person is employed today. The payload carries the identifier only. Read the person for names, employment dates, job, compensation and external identifiers. There is no deleted event, and none is missing: people are never removed. Employment ends by setting `endDate` and `status`, which arrives as an update with the person still readable. A delivery may be retried, so deduplicate on `event.uuid`, the id of the emission itself. Do not confuse it with `event.data.id`, which is the person. [Open in the interactive reference](https://docs.api.lensing.ai/?op=hr_people_created) Delivered as `POST` to the webhook endpoint of a subscription to `hr/people/created`. ## Payload Required, `application/json`: object. | Field | Type | Required | Description | | --- | --- | --- | --- | | `subscription` | object | yes | The subscription this delivery was made for. | | `subscription.uuid` | string | yes | Stable identifier of the subscription. | | `subscription.type` | string | yes | Transport the subscription delivers through; `webhook` for deliveries carried by this request. | | `subscription.name` | string | yes | Authored name of the subscription. | | `event` | object | yes | The emitted event being delivered. | | `event.uuid` | string | yes | Stable identifier of this emission — the idempotency key for a receiver, since a delivery may be retried. | | `event.identifier` | `"hr/people/created"` | yes | Public identifier of the event type. | | `event.emittedAt` | string (date-time) | yes | When the event was emitted (ISO 8601). | | `event.data` | hr_people_createdPayload | yes | | | `event.data.id` | string | yes | | Example: ```json { "subscription": { "uuid": "<uuid>", "type": "<type>", "name": "<name>" }, "event": { "uuid": "<uuid>", "identifier": "hr/people/created", "emittedAt": "2026-01-15T09:30:00.000Z", "data": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } } } ``` --- # A person changed. Carries the id only; re-read for their new state. event `hr/people/updated` Group: People Management › People · Operation ID: `hr_people_updated` Delivered when the person's own employment record changes — status, start, end and probation dates, employment category, entity, location and custom fields; when their primary job changes, which is what moves `jobId`, `departmentName`, `positionName` and `managerName`; when their compensation changes; and when an external identifier is added, changed or removed. `firstName`, `lastName`, `middleName`, the preferred names, `name` and `email` are held on a separate person record and projected onto the employment record rather than stored on it. A write to either record publishes this event, so a rename arrives however it was made. This is a change notification rather than a description of the change. The payload names the person but not what moved, and there is no before-and-after, so re-read and take the current state from there. Changes to what the person merely references are not reported: the department, position and manager behind `departmentName`, `positionName` and `managerName` are their own records, as are the entity, the location, the employment category and the compensation type. Renaming any of them changes what this API returns but publishes nothing, because the person did not change. Expect more than one delivery for a single business action — assigning a job and setting a salary are separate writes to the same person — and a re-read after the first may already show the last. Deduplicate on `event.uuid` and treat the read as the source of truth. [Open in the interactive reference](https://docs.api.lensing.ai/?op=hr_people_updated) Delivered as `POST` to the webhook endpoint of a subscription to `hr/people/updated`. ## Payload Required, `application/json`: object. | Field | Type | Required | Description | | --- | --- | --- | --- | | `subscription` | object | yes | The subscription this delivery was made for. | | `subscription.uuid` | string | yes | Stable identifier of the subscription. | | `subscription.type` | string | yes | Transport the subscription delivers through; `webhook` for deliveries carried by this request. | | `subscription.name` | string | yes | Authored name of the subscription. | | `event` | object | yes | The emitted event being delivered. | | `event.uuid` | string | yes | Stable identifier of this emission — the idempotency key for a receiver, since a delivery may be retried. | | `event.identifier` | `"hr/people/updated"` | yes | Public identifier of the event type. | | `event.emittedAt` | string (date-time) | yes | When the event was emitted (ISO 8601). | | `event.data` | hr_people_updatedPayload | yes | | | `event.data.id` | string | yes | | Example: ```json { "subscription": { "uuid": "<uuid>", "type": "<type>", "name": "<name>" }, "event": { "uuid": "<uuid>", "identifier": "hr/people/updated", "emittedAt": "2026-01-15T09:30:00.000Z", "data": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } } } ``` --- # Positions `GET /positions` Group: People Management › Positions · Operation ID: `positions` Returns a list of positions. GET positions/:id returns a richer record, including the job profile and region references. [Open in the interactive reference](https://docs.api.lensing.ai/?op=positions) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `positionNumber` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `name` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `overtimeStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `timelineStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, positionNumber, name, hiringManagerName, desiredHeadcount, employeeCount, status, timelineStatus, lastModifiedDate - Selectable: id, positionNumber, name, description, desiredHeadcount, employeeCount, expectedSalary, hiringManagerName, overtimeStatus, status, timelineStatus, lastModifiedDate ## Response `200` Success Under `data`, an array of positionsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the position. | | `positionNumber` | string or null | no | The system-generated position number, for example 'POS-100010'. Absent while none has been assigned. | | `name` | string | no | The display name of the position. | | `description` | string or null | no | A free-form description of the position's responsibilities. Absent when none is set. | | `desiredHeadcount` | number or null | no | The target number of people to fill this position. | | `employeeCount` | number or null | no | The number of employees currently in this position. | | `expectedSalary` | number or null | no | The expected annual salary for this position, in the entity's currency. | | `hiringManagerName` | string or null | no | The full name of the hiring manager. Absent when none is assigned. | | `overtimeStatus` | string or null | no | The overtime eligibility of the position. Possible values: 'EXEMPT', 'NON_EXEMPT'. | | `status` | string or null | no | The fill status of the position. Possible values: 'unfilled', 'partiallyFilled', 'filled', 'overfilled', 'inactive'. | | `timelineStatus` | string or null | no | The timeline status of the position. Possible values: 'planned', 'inactive', 'active', 'canceled'. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the position record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Position `POST /positions` Group: People Management › Positions · Operation ID: `createPosition` Creates a position. The combination of name and job profile must be unique. The response is only the new position's id, so read the full record with GET positions/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createPosition) ## 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`: createPositionInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The display name of the position. | | `description` | string | no | A free-form description of the position's responsibilities. | | `desiredHeadcount` | number | no | The target number of people to fill this position. | | `expectedSalary` | number | no | The expected annual salary for this position, in the entity's currency. | | `startDate` | string | no | The date the position becomes active. Use format YYYY-MM-DD. | | `endDate` | string | no | The date the position becomes inactive. Use format YYYY-MM-DD. Omit for an open-ended position. | | `overtimeStatus` | string, one of `"EXEMPT"`, `"NON_EXEMPT"` | no | The overtime eligibility of the position. | | `hiringManagerId` | string | no | The identifier of the employee who is the hiring manager. See id on GET people/:id. | | `jobProfileId` | string | no | The identifier of the job profile the position is associated with. There is no endpoint for it. | | `regionId` | string | no | The identifier of the region the position is based in. There is no endpoint for it. | Example: ```json { "name": "<name>", "description": "<description>", "desiredHeadcount": 0, "expectedSalary": 0, "startDate": "<startDate>", "endDate": "<endDate>", "overtimeStatus": "EXEMPT", "hiringManagerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "jobProfileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "regionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` ## Response `200` Success Under `data` (createPositionOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created position. | ## 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 | | --- | --- | | `MANAGER_NOT_FOUND` | 400 | | `JOB_PROFILE_NOT_FOUND` | 400 | | `REGION_NOT_FOUND` | 400 | | `DUPLICATE_POSITION` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: MANAGER_NOT_FOUND, JOB_PROFILE_NOT_FOUND, REGION_NOT_FOUND, DUPLICATE_POSITION - `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 --- # Get Position `GET /positions/{id}` Group: People Management › Positions · Operation ID: `getPosition` Returns the position with the given identifier. Use the position's id, not its positionNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getPosition) ## 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`. | | `id` | path | string | yes | The unique identifier for the position to return. | ## Response `200` Success Under `data` (getPositionOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the position. | | `positionNumber` | string | no | The system-generated position number, for example 'POS-100010'. Absent while none has been assigned. | | `name` | string | yes | The display name of the position. | | `description` | string | no | A free-form description of the position's responsibilities. Absent when none is set. | | `desiredHeadcount` | number | no | The target number of people to fill this position. | | `employeeCount` | number | no | The number of employees currently in this position. | | `expectedSalary` | number | no | The expected annual salary for this position, in the entity's currency. | | `overtimeStatus` | string | no | The overtime eligibility of the position. Possible values: 'EXEMPT', 'NON_EXEMPT'. | | `status` | string | no | The fill status of the position. Possible values: 'unfilled', 'partiallyFilled', 'filled', 'overfilled', 'inactive'. | | `timelineStatus` | string | no | The timeline status of the position. Possible values: 'planned', 'inactive', 'active', 'canceled'. | | `startDate` | string | no | The date the position becomes active. Format YYYY-MM-DD. | | `endDate` | string | no | The date the position becomes inactive. Format YYYY-MM-DD. Absent for an open-ended position. | | `hiringManagerId` | string | no | The identifier of the hiring manager. Absent when none is assigned. See id on GET people/:id. | | `hiringManagerName` | string | no | The full name of the hiring manager. Absent when none is assigned. | | `jobProfileId` | string | no | The identifier of the associated job profile. Absent when none is assigned. There is no endpoint for it. | | `jobProfileName` | string | no | The name of the associated job profile. Absent when none is assigned. | | `regionId` | string | no | The identifier of the region the position is based in. Absent when none is assigned. There is no endpoint for it. | | `regionName` | string | no | The name of the region the position is based in. Absent when none is assigned. | | `lastModifiedDate` | string (date-time) | yes | The date and time the position record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Position `PATCH /positions/{id}` Group: People Management › Positions · Operation ID: `updatePosition` Updates the position with the given identifier. Only the fields in the request body change. Use the position's id, not its positionNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updatePosition) ## 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. | | `id` | path | string | yes | The unique identifier for the position to update. | ## Request body Required, `application/json`: updatePositionInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The display name of the position. | | `description` | string | no | A free-form description of the position's responsibilities. | | `desiredHeadcount` | number | no | The target number of people to fill this position. | | `expectedSalary` | number | no | The expected annual salary for this position, in the entity's currency. | | `startDate` | string | no | The date the position becomes active. Use format YYYY-MM-DD. | | `endDate` | string | no | The date the position becomes inactive. Use format YYYY-MM-DD. It cannot be cleared through this endpoint. | | `overtimeStatus` | string, one of `"EXEMPT"`, `"NON_EXEMPT"` | no | The overtime eligibility of the position. | | `hiringManagerId` | string | no | The identifier of the employee who is the hiring manager. See id on GET people/:id. | | `jobProfileId` | string | no | The identifier of the job profile the position is associated with. There is no endpoint for it. | | `regionId` | string | no | The identifier of the region the position is based in. There is no endpoint for it. | Example: ```json { "name": "<name>", "description": "<description>", "desiredHeadcount": 0, "expectedSalary": 0, "startDate": "<startDate>", "endDate": "<endDate>", "overtimeStatus": "EXEMPT", "hiringManagerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "jobProfileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "regionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `MANAGER_NOT_FOUND` | 400 | | `JOB_PROFILE_NOT_FOUND` | 400 | | `REGION_NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, MANAGER_NOT_FOUND, JOB_PROFILE_NOT_FOUND, REGION_NOT_FOUND - `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 --- # Returns a list of recruitments. `GET /people/recruitments` Group: People Management › Recruitments · Operation ID: `recruitments` [Open in the interactive reference](https://docs.api.lensing.ai/?op=recruitments) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `name` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `category` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `published` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `unsolicited` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `deadline` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `vacancies` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `departmentId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `positionId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `teamId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `locationId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `entityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `employmentCategoryId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `hiringManagerId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `ownerId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, recruitmentNumber, name, category, status, published, unsolicited, deadline, vacancies, lastModifiedDate - Selectable: id, recruitmentNumber, name, category, status, published, unsolicited, deadline, vacancies, departmentId, departmentName, positionId, positionName, teamId, teamName, locationId, locationName, entityId, entityName, employmentCategoryId, hiringManagerId, hiringManagerName, ownerId, ownerName, jobDescription, viabilityCriteria, website, relocationRequired, lastModifiedDate ## Response `200` Success Under `data`, an array of recruitmentsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the recruitment. | | `recruitmentNumber` | string or null | no | The system-generated recruitment number. Absent while none has been assigned. | | `name` | string or null | no | The name of the opening. | | `category` | string or null | no | A free-text category of the opening, for example internal, external or student. | | `status` | string or null | no | The status of the opening, derived from the deadline. 'inactive' once the deadline has passed, 'active' otherwise, including when no deadline is set. | | `published` | boolean or null | no | Whether the opening is published externally. It is set by the external-publishing flow, not through this API. | | `unsolicited` | boolean or null | no | Whether this is the catch-all opening that unsolicited applications are filed against. | | `deadline` | string (date-time) or null | no | The application deadline, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. Absent when no deadline is set. | | `vacancies` | number or null | no | The number of vacant positions the opening covers. | | `departmentId` | string or null | no | The identifier of the department the opening belongs to. Absent when not set. See id on GET departments/:id. | | `departmentName` | string or null | no | The name of the department. Absent when not set. | | `positionId` | string or null | no | The identifier of the position being recruited for. Absent when not set. See id on GET positions/:id. | | `positionName` | string or null | no | The name of the position. Absent when not set. | | `teamId` | string or null | no | The identifier of the team. Absent when none is assigned. See id on GET people/teams/:id. | | `teamName` | string or null | no | The name of the team. Absent when none is assigned. | | `locationId` | string or null | no | The identifier of the work location. Absent when none is assigned. There is no endpoint for it. | | `locationName` | string or null | no | The name of the work location. Absent when none is assigned. | | `entityId` | string or null | no | The legal entity that will employ the hire. Absent when none is assigned. | | `entityName` | string or null | no | The name of the legal entity that will employ the hire. Absent when none is assigned. | | `employmentCategoryId` | string or null | no | The identifier of the employment category. Absent when none is assigned. There is no endpoint for it. | | `hiringManagerId` | string or null | no | The identifier of the hiring manager. Absent when none is assigned. See id on GET people/:id. | | `hiringManagerName` | string or null | no | The full name of the hiring manager. Absent when none is assigned. | | `ownerId` | string or null | no | The identifier of the recruitment owner, the recruiter. Absent when none is assigned. See id on GET people/:id. | | `ownerName` | string or null | no | The full name of the recruitment owner. Absent when none is assigned. | | `jobDescription` | string or null | no | The job description, as plain text. | | `viabilityCriteria` | string or null | no | The internal viability criteria used to screen applicants, as plain text. | | `website` | string or null | no | The URL of the posting or the hiring team's page. | | `relocationRequired` | string or null | no | Whether and how relocation is required. Free text, not a boolean. | | `lastModifiedDate` | string (date-time) | no | The date and time the recruitment record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Recruitment `POST /people/recruitments` Group: People Management › Recruitments · Operation ID: `createRecruitment` Creates a recruitment, one job opening. A department, a position, or both must be named. When name is omitted, it is derived from whichever was given. The hiring manager and the owner are granted the permissions they need to work the opening. The response is only the new recruitment's id, so read the full record with GET people/recruitments/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createRecruitment) ## 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`: createRecruitmentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The name of the opening. When omitted, it is derived from the department or position name. | | `category` | string | no | A free-text category of the opening, for example internal, external or student. | | `deadline` | string | no | The application deadline. Use format YYYY-MM-DD. The status is derived from it. | | `vacancies` | number | no | The number of vacant positions the opening covers, from 0 to 1000. | | `departmentId` | string | no | The identifier of the department the opening belongs to. Required unless positionId is given. See id on GET departments/:id. | | `positionId` | string | no | The identifier of the position being recruited for. Required unless departmentId is given. See id on GET positions/:id. | | `teamId` | string | no | The identifier of the team. See id on GET people/teams/:id. | | `locationId` | string | no | The identifier of the work location. There is no endpoint for it. | | `entityId` | string | no | The legal entity that will employ the hire. | | `employmentCategoryId` | string | no | The identifier of the employment category. There is no endpoint for it. | | `hiringManagerId` | string | no | The identifier of the employee who is the hiring manager. They are granted the permissions to work the opening. See id on GET people/:id. | | `ownerId` | string | no | The identifier of the employee who owns the recruitment, the recruiter. They are granted the permissions to work the opening. See id on GET people/:id. | | `jobDescription` | string | no | The job description, as plain text. | | `viabilityCriteria` | string | no | The internal viability criteria used to screen applicants, as plain text. Stored as an empty string when omitted. | | `website` | string | no | The URL of the posting or the hiring team's page. | | `relocationRequired` | string | no | Whether and how relocation is required. Free text, not a boolean. | Example: ```json { "name": "<name>", "category": "<category>", "deadline": "<deadline>", "vacancies": 0, "departmentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "positionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "teamId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "locationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "employmentCategoryId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "hiringManagerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "ownerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "jobDescription": "<jobDescription>", "viabilityCriteria": "<viabilityCriteria>", "website": "<website>", "relocationRequired": "<relocationRequired>" } ``` ## Response `200` Success Under `data` (createRecruitmentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created recruitment. | ## 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 | | --- | --- | | `DEPARTMENT_OR_POSITION_REQUIRED` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `INVALID_DEADLINE` | 400 | | `INVALID_VACANCIES` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: DEPARTMENT_OR_POSITION_REQUIRED, REFERENCE_NOT_FOUND, INVALID_DEADLINE, INVALID_VACANCIES - `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 --- # Get Recruitment `GET /people/recruitments/{id}` Group: People Management › Recruitments · Operation ID: `getRecruitment` Returns the recruitment with the given identifier. Use the recruitment's id, not its recruitmentNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getRecruitment) ## 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`. | | `id` | path | string | yes | The unique identifier for the recruitment to return. | ## Response `200` Success Under `data` (getRecruitmentOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the recruitment. | | `recruitmentNumber` | string | no | The system-generated recruitment number. Absent while none has been assigned. | | `name` | string | no | The name of the opening. | | `category` | string | no | A free-text category of the opening, for example internal, external or student. | | `status` | string, one of `"active"`, `"inactive"` | no | The status of the opening, derived from the deadline. 'inactive' once the deadline has passed, 'active' otherwise, including when no deadline is set. | | `published` | boolean | no | Whether the opening is published externally. It is set by the external-publishing flow, not through this API. | | `unsolicited` | boolean | no | Whether this is the catch-all opening that unsolicited applications are filed against. | | `deadline` | string | no | The application deadline. Format YYYY-MM-DD. Absent when no deadline is set. | | `vacancies` | number | no | The number of vacant positions the opening covers. | | `departmentId` | string | no | The identifier of the department the opening belongs to. Absent when not set. See id on GET departments/:id. | | `departmentName` | string | no | The name of the department. Absent when not set. | | `positionId` | string | no | The identifier of the position being recruited for. Absent when not set. See id on GET positions/:id. | | `positionName` | string | no | The name of the position. Absent when not set. | | `teamId` | string | no | The identifier of the team. Absent when none is assigned. See id on GET people/teams/:id. | | `teamName` | string | no | The name of the team. Absent when none is assigned. | | `locationId` | string | no | The identifier of the work location. Absent when none is assigned. There is no endpoint for it. | | `locationName` | string | no | The name of the work location. Absent when none is assigned. | | `entityId` | string | no | The legal entity that will employ the hire. Absent when none is assigned. | | `entityName` | string | no | The name of the legal entity that will employ the hire. Absent when none is assigned. | | `employmentCategoryId` | string | no | The identifier of the employment category. Absent when none is assigned. There is no endpoint for it. | | `hiringManagerId` | string | no | The identifier of the hiring manager. Absent when none is assigned. See id on GET people/:id. | | `hiringManagerName` | string | no | The full name of the hiring manager. Absent when none is assigned. | | `ownerId` | string | no | The identifier of the recruitment owner, the recruiter. Absent when none is assigned. See id on GET people/:id. | | `ownerName` | string | no | The full name of the recruitment owner. Absent when none is assigned. | | `jobDescription` | string | no | The job description, as plain text. | | `viabilityCriteria` | string | no | The internal viability criteria used to screen applicants, as plain text. | | `website` | string | no | The URL of the posting or the hiring team's page. | | `relocationRequired` | string | no | Whether and how relocation is required. Free text, not a boolean. | | `lastModifiedDate` | string (date-time) | yes | The date and time the recruitment record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Recruitment `PATCH /people/recruitments/{id}` Group: People Management › Recruitments · Operation ID: `updateRecruitment` Updates the recruitment with the given identifier. Only the fields in the request body change. Naming a new hiring manager or owner also grants them the permissions to work the opening. Use the recruitment's id, not its recruitmentNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateRecruitment) ## 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. | | `id` | path | string | yes | The unique identifier for the recruitment to update. | ## Request body Required, `application/json`: updateRecruitmentInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The name of the opening. | | `category` | string | no | A free-text category of the opening, for example internal, external or student. | | `deadline` | string | no | The application deadline. Use format YYYY-MM-DD. The status is derived from it. | | `vacancies` | number | no | The number of vacant positions the opening covers, from 0 to 1000. | | `departmentId` | string | no | The identifier of the department to move the opening to. See id on GET departments/:id. | | `positionId` | string | no | The identifier of the position to recruit for. See id on GET positions/:id. | | `teamId` | string | no | The identifier of the team. See id on GET people/teams/:id. | | `locationId` | string | no | The identifier of the work location. There is no endpoint for it. | | `entityId` | string | no | The legal entity that will employ the hire. | | `employmentCategoryId` | string | no | The identifier of the employment category. There is no endpoint for it. | | `hiringManagerId` | string | no | The identifier of the employee to make hiring manager. They are granted the permissions to work the opening. See id on GET people/:id. | | `ownerId` | string | no | The identifier of the employee to make recruitment owner, the recruiter. They are granted the permissions to work the opening. See id on GET people/:id. | | `jobDescription` | string | no | The job description, as plain text. Changing it does not re-score applicants already in the pipeline. | | `viabilityCriteria` | string | no | The internal viability criteria used to screen applicants, as plain text. Changing it does not re-score applicants already in the pipeline. | | `website` | string | no | The URL of the posting or the hiring team's page. | | `relocationRequired` | string | no | Whether and how relocation is required. Free text, not a boolean. | Example: ```json { "name": "<name>", "category": "<category>", "deadline": "<deadline>", "vacancies": 0, "departmentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "positionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "teamId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "locationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "employmentCategoryId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "hiringManagerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "ownerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "jobDescription": "<jobDescription>", "viabilityCriteria": "<viabilityCriteria>", "website": "<website>", "relocationRequired": "<relocationRequired>" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `REFERENCE_NOT_FOUND` | 400 | | `INVALID_DEADLINE` | 400 | | `INVALID_VACANCIES` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, REFERENCE_NOT_FOUND, INVALID_DEADLINE, INVALID_VACANCIES - `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 --- # Teams `GET /people/teams` Group: People Management › Teams · Operation ID: `teams` Returns a list of teams. To list one team's direct children, filter by parentId. [Open in the interactive reference](https://docs.api.lensing.ai/?op=teams) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `name` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `teamType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `startDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `endDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `employeeCount` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `parentId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, teamNumber, name, teamType, status, startDate, endDate, employeeCount, lastModifiedDate - Selectable: id, teamNumber, name, teamType, status, startDate, endDate, employeeCount, parentId, parentName, lastModifiedDate ## Response `200` Success Under `data`, an array of teamsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the team. | | `teamNumber` | string or null | no | The system-generated team number. Absent while none has been assigned. | | `name` | string | no | The name of the team. | | `teamType` | string or null | no | The type of the team. Possible values: 'hrTeam', 'psaTeam'. | | `status` | string or null | no | The status of the team, derived from its dates. 'planned' before the start date, 'active' between the dates, 'inactive' after the end date. | | `startDate` | string (date) or null | no | The first day the team exists. Format YYYY-MM-DD. | | `endDate` | string (date) or null | no | The last day the team exists. Format YYYY-MM-DD. Absent for a team with no planned end. | | `employeeCount` | number or null | no | The number of active members of the team. | | `parentId` | string or null | no | The identifier of the parent team. Absent for a top-level team. See id on GET people/teams/:id. | | `parentName` | string or null | no | The name of the parent team. Absent for a top-level team. | | `lastModifiedDate` | string (date-time) | no | The date and time the team record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Team `POST /people/teams` Group: People Management › Teams · Operation ID: `createTeam` Creates a team. The response is only the new team's id, so read the full record with GET people/teams/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createTeam) ## 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`: createTeamInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The name of the team. Must be unique across teams. | | `teamType` | string, one of `"hrTeam"`, `"psaTeam"` | no | The type of the team. Defaults to 'hrTeam'. | | `startDate` | string | yes | The first day the team exists. Use format YYYY-MM-DD. | | `endDate` | string | no | The last day the team exists. Use format YYYY-MM-DD. Omit for a team with no planned end. It may not fall before startDate. | | `parentId` | string | no | The identifier of the parent team. Omit to create a top-level team. See id on GET people/teams/:id. | Example: ```json { "name": "<name>", "teamType": "hrTeam", "startDate": "<startDate>", "endDate": "<endDate>", "parentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` ## Response `200` Success Under `data` (createTeamOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created team. | ## 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 | | --- | --- | | `DUPLICATE_TEAM_NAME` | 400 | | `PARENT_TEAM_NOT_FOUND` | 400 | | `INVALID_DATE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: DUPLICATE_TEAM_NAME, PARENT_TEAM_NOT_FOUND, INVALID_DATE - `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 --- # Get Team `GET /people/teams/{id}` Group: People Management › Teams · Operation ID: `getTeam` Returns the team with the given identifier. Use the team's id, not its teamNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getTeam) ## 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`. | | `id` | path | string | yes | The unique identifier for the team to return. | ## Response `200` Success Under `data` (getTeamOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the team. | | `teamNumber` | string | no | The system-generated team number. Absent while none has been assigned. | | `name` | string | yes | The name of the team. | | `teamType` | string, one of `"hrTeam"`, `"psaTeam"` | no | The type of the team. | | `status` | string, one of `"planned"`, `"active"`, `"inactive"` | no | The status of the team, derived from its dates. 'planned' before the start date, 'active' between the dates, 'inactive' after the end date. | | `startDate` | string | no | The first day the team exists. Format YYYY-MM-DD. | | `endDate` | string | no | The last day the team exists. Format YYYY-MM-DD. Absent for a team with no planned end. | | `employeeCount` | number | no | The number of active members of the team. | | `parentId` | string | no | The identifier of the parent team. Absent for a top-level team. See id on GET people/teams/:id. | | `parentName` | string | no | The name of the parent team. Absent for a top-level team. | | `lastModifiedDate` | string (date-time) | yes | The date and time the team record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Team `PATCH /people/teams/{id}` Group: People Management › Teams · Operation ID: `updateTeam` Updates the team with the given identifier. Only the fields in the request body change, and no field can be cleared. Use the team's id, not its teamNumber. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateTeam) ## 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. | | `id` | path | string | yes | The unique identifier for the team to update. | ## Request body Required, `application/json`: updateTeamInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | The name of the team. | | `teamType` | string, one of `"hrTeam"`, `"psaTeam"` | no | The type of the team. | | `startDate` | string | no | The first day the team exists. Use format YYYY-MM-DD. | | `endDate` | string | no | The last day the team exists. Use format YYYY-MM-DD. It may not fall before the team's start date, whether you change that start date in the same request or it is already stored. | | `parentId` | string | no | The identifier of the team to move this one under. It may not be the team itself or one of its descendants. See id on GET people/teams/:id. | Example: ```json { "name": "<name>", "teamType": "hrTeam", "startDate": "<startDate>", "endDate": "<endDate>", "parentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `PARENT_TEAM_NOT_FOUND` | 400 | | `PARENT_TEAM_CYCLE` | 400 | | `INVALID_DATE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, PARENT_TEAM_NOT_FOUND, PARENT_TEAM_CYCLE, INVALID_DATE - `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 --- # Delete Team `DELETE /people/teams/{id}` Group: People Management › Teams · Operation ID: `deleteTeam` Deletes the team with the given identifier. Read the outcome field: 'deleted' means the team was removed and its direct child teams were re-parented to its own parent. 'deactivated' means it could not be removed and was end-dated today instead. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteTeam) ## 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. | | `id` | path | string | yes | The unique identifier for the team to delete. | ## Response `200` Success Under `data` (deleteTeamOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the team the call acted on. | | `name` | string | yes | The name of the team. | | `outcome` | string, one of `"deleted"`, `"deactivated"` | yes | What happened to the team. 'deleted' when it was removed. 'deactivated' when it was end-dated today instead because something still depends on it. | | `reason` | string | no | Why the team was deactivated rather than deleted. Absent when outcome is 'deleted'. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `TEAM_IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, TEAM_IN_USE - `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 --- # Returns a list of activities. `GET /psa/activities` Group: PSA › Activities · Operation ID: `activities` Each row is one activity, so counting rows counts activities. Who is assigned to an activity is not published here. There is no endpoint to match activities by name or description. [Open in the interactive reference](https://docs.api.lensing.ai/?op=activities) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `billable` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `invoiced` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `startDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `endDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `billingType` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `category` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `psaProjectId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, name, startDate, endDate, hoursBudget, billingType, category, psaProjectId, lastModifiedDate - Selectable: id, name, description, hoursBudget, billable, invoiced, startDate, endDate, billingType, category, milestoneId, fixedFeeId, activityStatusId, statusName, parentActivityId, psaProjectId, lastModifiedDate ## Response `200` Success Under `data`, an array of activitiesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the activity. | | `name` | string | no | The display name of the activity. | | `description` | string or null | no | A free-form description of the activity. Absent when none was set. | | `hoursBudget` | string (decimal) or null | no | The budgeted hours for this activity, as a count of hours. It is not a monetary amount. | | `billable` | boolean or null | no | Whether the activity is billable to a customer. Derived from category being 'ClientBillable'. | | `invoiced` | boolean or null | no | Whether the activity has been invoiced. | | `startDate` | string (date) or null | no | The activity's start date. Format YYYY-MM-DD. It is within the owning project's date range when the project's enforceActivityDateRange setting is on. | | `endDate` | string (date) or null | no | The activity's end date. Format YYYY-MM-DD. | | `billingType` | string | no | The activity's billing type. Possible values: 'Hours', 'Milestone', 'FixedFee'. | | `category` | string or null | no | The activity's category. Possible values: 'ClientBillable', 'NonBillable', 'NonValuable', 'Valuable'. | | `milestoneId` | number or null | no | The id of the milestone this activity is billed against. Absent unless billingType is 'Milestone'. It is a numeric package-layer key, not the UUID that GET psa/milestones/:id takes. | | `fixedFeeId` | number or null | no | The id of the fixed fee this activity is billed against. Absent unless billingType is 'FixedFee'. It is a numeric package-layer key, not the UUID that GET psa/fixed-fees/:id takes. | | `activityStatusId` | number or null | no | The id of the activity's progress status. Absent when no status is set. Status values are tenant-configured, and there is no endpoint that lists them. Use statusName for the label. | | `statusName` | string or null | no | The name of the activity's progress status, for example 'In progress'. Absent when no status is set. | | `parentActivityId` | number or null | no | The id of the parent activity when this is a sub-task. Absent when the activity is top-level. It is a numeric package-layer key, not the UUID that GET psa/activities/:id takes. | | `psaProjectId` | number or null | no | The id of the PSA project that owns this activity. Absent for a timetracking project with no PSA layer. It is a numeric key you can filter on here, but not the UUID that GET psa/projects/:id takes. | | `lastModifiedDate` | string (date-time) or null | no | The date and time this activity last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the activity with the given identifier. `GET /psa/activities/{id}` Group: PSA › Activities · Operation ID: `getActivity` An activity is a project task. This route returns the activity itself, not who is assigned to it: assignment is a separate concern with its own cardinality, and no assignment fields are published here. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getActivity) ## 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`. | | `id` | path | string | yes | The unique identifier for the activity to return. | ## Response `200` Success Under `data` (getActivityOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the activity. | | `name` | string | yes | The display name of the activity. | | `description` | string | no | A free-form description of the activity. Absent when none was set. | | `hoursBudget` | string (decimal) | no | The budgeted hours for this activity, as a count of hours. It is not a monetary amount. | | `billable` | boolean | no | Whether the activity is billable to a customer. Derived from category being 'ClientBillable'. | | `invoiced` | boolean | no | Whether the activity has been invoiced. | | `startDate` | string (date) | no | The activity's start date. Format YYYY-MM-DD. It is within the owning project's date range when the project's enforceActivityDateRange setting is on. | | `endDate` | string (date) | no | The activity's end date. Format YYYY-MM-DD. | | `billingType` | string | yes | The activity's billing type. Possible values: 'Hours', 'Milestone', 'FixedFee'. | | `category` | string | no | The activity's category. Possible values: 'ClientBillable', 'NonBillable', 'NonValuable', 'Valuable'. | | `milestoneId` | number | no | The id of the milestone this activity is billed against. Absent unless billingType is 'Milestone'. It is a numeric package-layer key, not the UUID that GET psa/milestones/:id takes. | | `fixedFeeId` | number | no | The id of the fixed fee this activity is billed against. Absent unless billingType is 'FixedFee'. It is a numeric package-layer key, not the UUID that GET psa/fixed-fees/:id takes. | | `activityStatusId` | number | no | The id of the activity's progress status. Absent when no status is set. Status values are tenant-configured, and there is no endpoint that lists them. Use statusName for the label. | | `statusName` | string | no | The name of the activity's progress status, for example 'In progress'. Absent when no status is set. | | `parentActivityId` | number | no | The id of the parent activity when this is a sub-task. Absent when the activity is top-level. It is a numeric package-layer key, not the UUID that GET psa/activities/:id takes. | | `psaProjectId` | number | no | The id of the PSA project that owns this activity. Absent for a timetracking project with no PSA layer. It is a numeric key you can filter on here, but not the UUID that GET psa/projects/:id takes. | | `lastModifiedDate` | string (date-time) | yes | The date and time this activity last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of allocations. `GET /psa/allocations` Group: PSA › Allocations · Operation ID: `allocations` The memberId, employeeId, projectActivityId, projectPositionId and psaProjectId on an allocation are numeric package-layer keys, not the UUIDs the public GET endpoints take, so none of them can be followed over this API. Allocations cannot be matched by member, project or activity: those fields are returned but are not filters, and there is no search endpoint, so what a person is allocated to cannot be answered over this API. [Open in the interactive reference](https://docs.api.lensing.ai/?op=allocations) ## 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`. | ## Query surface - `startDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `endDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `method` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: startDate, endDate, totalHours, method, lastModifiedDate - Selectable: id, startDate, endDate, totalHours, notes, method, memberId, employeeId, projectActivityId, projectPositionId, psaProjectId, memberName, memberPlaceholderName, projectActivityName, projectPositionName, lastModifiedDate ## Response `200` Success Under `data`, an array of allocationsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the allocation. | | `startDate` | string (date) | no | The first day of the allocation period, inclusive. Format YYYY-MM-DD. | | `endDate` | string (date) | no | The last day of the allocation period, inclusive. Format YYYY-MM-DD. | | `totalHours` | string (decimal) | no | The total planned hours for the allocation period. | | `notes` | string or null | no | Free-form notes about the allocation. | | `method` | string | no | The distribution method for the planned hours. Possible values: 'totalHours', 'hoursPerDay', 'hoursPerWeek', 'hoursPerMonth'. | | `memberId` | number or null | no | The id of the PSA team-member assignment the allocation is booked against. It is a numeric package-layer key, not the UUID that GET psa/team-members/:id takes. | | `employeeId` | number or null | no | The id of the employee for the allocated team member. It is a numeric package-layer key, and there is no public endpoint for an employee. | | `projectActivityId` | number or null | no | The id of the project activity the allocation is against. It is a numeric package-layer key, not the UUID that GET psa/activities/:id takes. | | `projectPositionId` | number or null | no | The id of the project position the allocation is against. It is a numeric package-layer key, not the UUID that GET psa/positions/:id takes. | | `psaProjectId` | number | no | The id of the PSA project the allocation belongs to. It is a numeric package-layer key, not the UUID that GET psa/projects/:id takes. | | `memberName` | string or null | no | The name of the allocated team member. | | `memberPlaceholderName` | string or null | no | The name of the placeholder, when the allocation is to a placeholder rather than an employee. | | `projectActivityName` | string or null | no | The name of the project activity. | | `projectPositionName` | string or null | no | The name of the project position. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the allocation record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates allocations. `POST /psa/allocations` Group: PSA › Allocations · Operation ID: `createAllocations` Every allocation in the request is created in one transaction, and a failure writes nothing. The response returns the created ids in the order the allocations were sent. memberId identifies the timetracking member assignment the hours are booked against and resolves the project, the person and the permission check. Read a record with GET psa/allocations/:id for its stored state. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createAllocations) ## 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`: createAllocationsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `allocations` | array of object | yes | The allocations to create. | | `allocations[].memberId` | number | yes | The id of the timetracking member assignment the daily hours are booked against. It resolves the project, the person and the permission check. It is a numeric package-layer key, not the UUID that GET psa/team-members/:id takes. | | `allocations[].projectActivityId` | number | yes | The id of the project activity the allocation is booked against. It is a numeric package-layer key, not the UUID that GET psa/activities/:id takes. | | `allocations[].startDate` | string (date) | yes | The first day of the allocation period, inclusive. Use format YYYY-MM-DD. | | `allocations[].endDate` | string (date) | yes | The last day of the allocation period, inclusive. Use format YYYY-MM-DD. | | `allocations[].totalHours` | string (decimal) | yes | The planned hours, interpreted according to method. For 'totalHours' and 'hoursPerDay', this is the total for the whole period, spread evenly over the working days in range. For 'hoursPerWeek', it is the period total, divided across the weeks and then the working days. For 'hoursPerMonth', it is a per-month rate, with a working month approximated as 21 days. Days that compute to zero hours produce no daily entry. | | `allocations[].method` | string, one of `"totalHours"`, `"hoursPerDay"`, `"hoursPerWeek"`, `"hoursPerMonth"` | yes | The distribution method for the planned hours. | | `allocations[].notes` | string | no | Free-form notes stored on the allocation. | | `allocations[].workingDays` | array of string, one of `"Sunday"`, `"Monday"`, `"Tuesday"`, `"Wednesday"`, `"Thursday"`, `"Friday"`, `"Saturday"` | no | The days of the week the allocation applies to. | | `allocations[].psaProjectId` | number | yes | The id of the PSA project the allocation belongs to. It must be the project the member belongs to. It is a numeric package-layer key, not the UUID that GET psa/projects/:id takes. | Example: ```json { "allocations": [ { "memberId": 4021, "projectActivityId": 91, "startDate": "2026-09-01", "endDate": "2026-09-12", "totalHours": "40.00", "method": "totalHours", "psaProjectId": 32341 }, { "memberId": 4022, "projectActivityId": 91, "startDate": "2026-10-01", "endDate": "2026-12-31", "totalHours": "160.00", "method": "hoursPerWeek", "psaProjectId": 32341, "notes": "Phase 2 delivery capacity.", "workingDays": [ "Monday", "Tuesday", "Wednesday", "Thursday" ] } ] } ``` ## Response `200` Success Under `data` (createAllocationsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `created` | array of object | yes | The created allocations, in the order the request sent them. Read a record with GET psa/allocations/:id for its stored state. | | `created[].id` | string | yes | The unique identifier for the created allocation. | ## 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 | | --- | --- | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure - `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 --- # Updates allocations. Only the fields in the request body change. `PATCH /psa/allocations` Group: PSA › Allocations · Operation ID: `updateAllocations` Only the fields sent are changed. An absent field is left unchanged, and notes and workingDays accept null to clear. The response is empty. Read the updated record with GET psa/allocations/:id. To reassign the member, send exactly one of memberEmployeeId or memberPlaceholderId together with psaProjectId, which is the project the reassignment is authorized against. An allocation cannot change project, so psaProjectId has no effect on any other update. Changing totalHours, the dates, method or workingDays regenerates the allocation's daily entries. The call is all-or-nothing: if any id matches no allocation, nothing is written and the call fails with NOT_FOUND. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateAllocations) ## 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`: updateAllocationsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `allocations` | array of object | yes | The allocations to update. | | `allocations[].id` | string | yes | The unique identifier of the allocation to update. The call is all-or-nothing: if any identifier matches no allocation, nothing is updated. | | `allocations[].memberId` | number | no | A new timetracking member assignment to book the allocation against. It is a numeric package-layer key, not the UUID that GET psa/team-members/:id takes. | | `allocations[].projectActivityId` | number | no | A new project activity for the allocation. It is a numeric package-layer key, not the UUID that GET psa/activities/:id takes. | | `allocations[].startDate` | string (date) | no | A new first day of the allocation period, inclusive. Use format YYYY-MM-DD. | | `allocations[].endDate` | string (date) | no | A new last day of the allocation period, inclusive. Use format YYYY-MM-DD. | | `allocations[].totalHours` | string (decimal) | no | New planned hours, interpreted according to method as on create. Changing this regenerates the allocation's daily entries. | | `allocations[].method` | string, one of `"totalHours"`, `"hoursPerDay"`, `"hoursPerWeek"`, `"hoursPerMonth"` | no | The distribution method for the planned hours. | | `allocations[].notes` | string or null | no | Free-form notes stored on the allocation. | | `allocations[].workingDays` | array of string, one of `"Sunday"`, `"Monday"`, `"Tuesday"`, `"Wednesday"`, `"Thursday"`, `"Friday"`, `"Saturday"` or null | no | The days of the week the allocation applies to. | | `allocations[].psaProjectId` | number | no | The PSA project the reassignment is authorized against, when reassigning the member. It has no effect on any other update, and an allocation cannot change project. It is a numeric package-layer key, not the UUID that GET psa/projects/:id takes. | | `allocations[].memberEmployeeId` | number or null | no | The id of the employee to reassign the allocation to. Send it with psaProjectId, and pass null when switching to a placeholder. It is a numeric package-layer key, and there is no public endpoint for an employee. | | `allocations[].memberPlaceholderId` | number or null | no | The id of the placeholder to reassign the allocation to. Send it with psaProjectId, and pass null when switching to an employee. It is a numeric package-layer key, not the UUID that GET psa/placeholders/:id takes. | Example: ```json { "allocations": [ { "id": "3f2a91c7-5d84-4b1e-9c60-8a7e2d4f0b13", "totalHours": "32.00" }, { "id": "b7c40e29-1af6-42d8-8e55-6d3b9c018a74", "memberId": 51204, "projectActivityId": 8317, "startDate": "2026-09-01", "endDate": "2026-11-30", "totalHours": "160.00", "method": "hoursPerWeek", "notes": "Rescoped after the change order.", "workingDays": [ "Monday", "Tuesday", "Wednesday", "Thursday" ], "memberEmployeeId": 3889611, "psaProjectId": 32341 } ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Returns the allocation with the given identifier. `GET /psa/allocations/{id}` Group: PSA › Allocations · Operation ID: `getAllocation` The memberId, employeeId, projectActivityId, projectPositionId and psaProjectId on an allocation are numeric package-layer keys, not the UUIDs the public GET endpoints take, so none of them can be followed over this API. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getAllocation) ## 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`. | | `id` | path | string | yes | The unique identifier for the allocation to return. | ## Response `200` Success Under `data` (getAllocationOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the allocation. | | `startDate` | string (date) | yes | The first day of the allocation period, inclusive. Format YYYY-MM-DD. | | `endDate` | string (date) | yes | The last day of the allocation period, inclusive. Format YYYY-MM-DD. | | `totalHours` | string (decimal) | yes | The total planned hours for the allocation period. | | `notes` | string | no | Free-form notes about the allocation. | | `method` | string | yes | The distribution method for the planned hours. Possible values: 'totalHours', 'hoursPerDay', 'hoursPerWeek', 'hoursPerMonth'. | | `memberId` | number | no | The id of the PSA team-member assignment the allocation is booked against. It is a numeric package-layer key, not the UUID that GET psa/team-members/:id takes. | | `employeeId` | number | no | The id of the employee for the allocated team member. It is a numeric package-layer key, and there is no public endpoint for an employee. | | `projectActivityId` | number | no | The id of the project activity the allocation is against. It is a numeric package-layer key, not the UUID that GET psa/activities/:id takes. | | `projectPositionId` | number | no | The id of the project position the allocation is against. It is a numeric package-layer key, not the UUID that GET psa/positions/:id takes. | | `psaProjectId` | number | yes | The id of the PSA project the allocation belongs to. It is a numeric package-layer key, not the UUID that GET psa/projects/:id takes. | | `memberName` | string | no | The name of the allocated team member. | | `memberPlaceholderName` | string | no | The name of the placeholder, when the allocation is to a placeholder rather than an employee. | | `projectActivityName` | string | no | The name of the project activity. | | `projectPositionName` | string | no | The name of the project position. | | `lastModifiedDate` | string (date-time) | yes | The date and time the allocation record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Deletes allocations. `POST /psa/allocations/bulk-delete` Group: PSA › Allocations · Operation ID: `deleteAllocations` Each allocation's daily planned-hours entries are removed with it. This is a permanent delete, with no restore. The call is all-or-nothing: an id that matches no allocation fails the whole call with NOT_FOUND and nothing is deleted. Only planned time is deleted. Logged time entries have no link to an allocation and are never affected. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteAllocations) ## 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`: deleteAllocationsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the allocations to delete. The call is all-or-nothing: if any identifier matches no allocation, nothing is deleted. | Example: ```json { "ids": [ "3f2a91c7-5d84-4b1e-9c60-8a7e2d4f0b13", "b7c40e29-1af6-42d8-8e55-6d3b9c018a74" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Returns the expense budgets defined for a PSA project. `GET /psa/expenses/budgets` Group: PSA › Expenses · Operation ID: `getExpenseBudgets` Returns the budgets defined for one PSA project, selected by a numeric psaProjectId in the query string. The endpoint requires view access to that project. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getExpenseBudgets) ## 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`. | | `psaProjectId` | query | number | yes | The id of the PSA project. It is the numeric package-layer key, not the UUID that GET psa/projects/:id takes. | | `isDefault` | query | boolean | no | When true, return only the project's default catch-all budget. When false or omitted, return all of the project's budgets. False does not filter to non-default budgets. | | `cursor` | query | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `pageSize` | query | number | no | The number of budgets per page, between 1 and 200. Defaults to 50. A value above 200 is treated as 200, not rejected. | ## Response `200` Success Under `data` (getExpenseBudgetsOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | The project's budgets on this page. An empty array when the project has no budgets. | | `items[].id` | number | yes | The unique identifier for the budget. | | `items[].name` | string | yes | The display name of the budget. | | `items[].budget` | string (decimal) | no | The budget amount, in the currency given by currency. Absent when the budget has no amount. | | `items[].currency` | string | no | The currency of budget. Format ISO 4217, for example 'USD'. Absent when the budget has no amount. | | `items[].isDefault` | boolean | yes | Whether this is the project's default catch-all budget. | | `items[].lastModifiedDate` | string (date-time) | no | The date and time the budget definition last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `items[].billable` | boolean | no | Whether expenses against this budget are billable to the customer. | | `items[].productId` | number | no | The id of the product tied to this budget. It is a numeric package-layer key, not the UUID that GET accounting/products/:id takes. Absent when the budget is not tied to a product. | | `items[].productLineId` | number | no | The id of the product line tied to this budget. It is a numeric package-layer key, not the UUID that GET accounting/product-lines/:id takes. Absent when the budget is not tied to a product line. | | `items[].salesOrderId` | number | no | The id of the sales order this budget bills against. It is a numeric package-layer key, not the UUID that GET accounting/sales-orders/:id takes. | | `pagination` | object | yes | The pagination cursor for this page. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as the cursor query parameter. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more budgets remain beyond this page. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a PSA project's expenses with their budget assignments. `GET /psa/expenses/summary` Group: PSA › Expenses · Operation ID: `getExpensesSummary` Returns one PSA project's expenses together with their budget assignments and every budget defined for the project, selected by a numeric psaProjectId in the query string. Only the expenses array is paginated. budgetMappings covers just the expenses on the current page, while expenseBudgets lists every budget for the project on every page, and defaultBudget is the catch-all budget when one is set. Every amount is a bare decimal with its currency in a sibling field. With only View permission you see only your own expenses, and with Edit permission you see all of the project's expenses. The expenses cursor wraps an offset, so a page is not stable if expenses are added or removed between fetches. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getExpensesSummary) ## 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`. | | `psaProjectId` | query | number | yes | The id of the PSA project. It is the numeric package-layer key, not the UUID that GET psa/projects/:id takes. | | `pageCursor` | query | string | no | The cursor from the previous response's pagination.nextCursor. Omit on the first request. | | `pageSize` | query | number | no | The number of expenses per page, between 1 and 200. Defaults to 50. The maximum is 200. | ## Response `200` Success Under `data` (getExpensesSummaryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `expenses` | array of object | yes | The expense entries on this page, each linked to the project through its assigned budget. | | `expenses[].id` | number | no | The unique identifier for the expense entry. | | `expenses[].expenseReportId` | number | no | The id of the expense report this expense belongs to. It is a numeric package-layer key, not the UUID that GET accounting/expense-reports/:id takes. | | `expenses[].employeeName` | string | no | The name of the employee who incurred the expense. | | `expenses[].categoryName` | string | no | The name of the expense category. | | `expenses[].amount` | string (decimal) | no | The gross amount of the expense, in the currency given by amountCurrency. | | `expenses[].amountCurrency` | string | no | The currency of amount. Format ISO 4217, for example 'USD'. | | `expenses[].reimbursableNetAmount` | string (decimal) | no | The reimbursable net amount of the expense, after tax and any company-paid portions, in the currency given by reimbursableNetAmountCurrency. | | `expenses[].reimbursableNetAmountCurrency` | string | no | The currency of reimbursableNetAmount. Format ISO 4217, for example 'USD'. | | `expenses[].status` | string | no | The status of the expense entry. | | `expenses[].notes` | string | no | Free-form notes on the expense. | | `expenses[].billDate` | string (date) | no | The date the expense was incurred. Format YYYY-MM-DD. | | `expenses[].billableExpense` | string | no | The billable classification of the expense. | | `expenses[].expenseReportNumber` | string | no | The human-readable number of the expense report. | | `budgetMappings` | array of object | yes | The mapping of each expense on this page to its assigned budget. It covers only the expenses in this page's expenses array. | | `budgetMappings[].expenseItemBaseId` | number | yes | The id of the expense this mapping is for. It matches an id in the expenses array of this response. | | `budgetMappings[].expenseBudgetId` | number | yes | The id of the budget this expense is mapped to. It matches an id in the expenseBudgets array of this response. | | `budgetMappings[].name` | string | yes | The display name of the budget. | | `budgetMappings[].budget` | string (decimal) | no | The budget amount, in the currency given by currency. Absent when the budget has no amount. | | `budgetMappings[].currency` | string | no | The currency of budget. Format ISO 4217, for example 'USD'. Absent when the budget has no amount. | | `budgetMappings[].isDefault` | boolean or null | no | Whether this is the project's default catch-all budget. | | `budgetMappings[].billable` | boolean or null | no | Whether expenses against this budget are billable to the customer. | | `budgetMappings[].productId` | number or null | no | The id of the product tied to this budget. It is a numeric package-layer key, not the UUID that GET accounting/products/:id takes. Absent when the budget is not tied to a product. | | `budgetMappings[].productLineId` | number or null | no | The id of the product line tied to this budget. It is a numeric package-layer key, not the UUID that GET accounting/product-lines/:id takes. Absent when the budget is not tied to a product line. | | `budgetMappings[].salesOrderId` | number | no | The id of the sales order this budget bills against. It is a numeric package-layer key, not the UUID that GET accounting/sales-orders/:id takes. | | `expenseBudgets` | array of object | yes | Every budget defined for the project. This array is never paginated. | | `expenseBudgets[].id` | number | yes | The unique identifier for the budget. | | `expenseBudgets[].name` | string | yes | The display name of the budget. | | `expenseBudgets[].budget` | string (decimal) | no | The budget amount, in the currency given by currency. Absent when the budget has no amount. | | `expenseBudgets[].currency` | string | no | The currency of budget. Format ISO 4217, for example 'USD'. Absent when the budget has no amount. | | `expenseBudgets[].isDefault` | boolean | yes | Whether this is the project's default catch-all budget. | | `expenseBudgets[].lastModifiedDate` | string (date-time) | no | The date and time the budget definition last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `expenseBudgets[].billable` | boolean or null | no | Whether expenses against this budget are billable to the customer. | | `expenseBudgets[].productId` | number or null | no | The id of the product tied to this budget. It is a numeric package-layer key, not the UUID that GET accounting/products/:id takes. Absent when the budget is not tied to a product. | | `expenseBudgets[].productLineId` | number or null | no | The id of the product line tied to this budget. It is a numeric package-layer key, not the UUID that GET accounting/product-lines/:id takes. Absent when the budget is not tied to a product line. | | `expenseBudgets[].salesOrderId` | number | no | The id of the sales order this budget bills against. It is a numeric package-layer key, not the UUID that GET accounting/sales-orders/:id takes. | | `defaultBudget` | object | no | The project's default catch-all budget for expenses with no explicit mapping. Absent when the project has no default budget. | | `defaultBudget.id` | number | yes | The unique identifier for the budget. | | `defaultBudget.name` | string | yes | The display name of the budget. | | `defaultBudget.budget` | string (decimal) | no | The budget amount, in the currency given by currency. Absent when the budget has no amount. | | `defaultBudget.currency` | string | no | The currency of budget. Format ISO 4217, for example 'USD'. Absent when the budget has no amount. | | `defaultBudget.isDefault` | boolean | yes | Always true, because this is the default budget. | | `defaultBudget.lastModifiedDate` | string (date-time) | no | The date and time the budget definition last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `defaultBudget.billable` | boolean or null | no | Whether expenses against this budget are billable to the customer. | | `defaultBudget.productId` | number or null | no | The id of the product tied to this budget. It is a numeric package-layer key, not the UUID that GET accounting/products/:id takes. Absent when the budget is not tied to a product. | | `defaultBudget.productLineId` | number or null | no | The id of the product line tied to this budget. It is a numeric package-layer key, not the UUID that GET accounting/product-lines/:id takes. Absent when the budget is not tied to a product line. | | `defaultBudget.salesOrderId` | number | no | The id of the sales order this budget bills against. It is a numeric package-layer key, not the UUID that GET accounting/sales-orders/:id takes. | | `pagination` | object | yes | The pagination cursor for the expenses array. | | `pagination.nextCursor` | string | no | The cursor for the next page. Pass it as the pageCursor query parameter. Absent on the last page. | | `pagination.hasMore` | boolean | yes | True when more expenses remain beyond this page. | ## Errors | Code | Status | | --- | --- | | `EMPLOYEE_NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: EMPLOYEE_NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of fixed fees. `GET /psa/fixed-fees` Group: PSA › Fixed Fees · Operation ID: `fixedFees` The amount and its currency are the two halves of one stored value. They are present together or absent together, and neither is meaningful alone. [Open in the interactive reference](https://docs.api.lensing.ai/?op=fixedFees) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `frequency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `startDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `endDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `psaProjectId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, frequency, currency, startDate, endDate, psaProjectId, lastModifiedDate - Selectable: id, amount, currency, frequency, startDate, endDate, psaProjectId, salesOrderProductLineId, productId, productLineId, lastModifiedDate ## Response `200` Success Under `data`, an array of fixedFeesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the fixed fee. | | `amount` | string (decimal) or null | no | The fixed fee amount, in the currency given by currency. | | `currency` | string or null | no | The currency of amount. Format ISO 4217, for example 'USD'. | | `frequency` | string or null | no | The billing frequency of the fixed fee. Possible values: 'Monthly', 'BiMonthly', 'Quarterly', 'SemiAnnually', 'Annually', 'Once'. | | `startDate` | string (date) or null | no | The first day of the fixed fee's billing period, inclusive. Format YYYY-MM-DD. | | `endDate` | string (date) or null | no | The last day of the fixed fee's billing period, inclusive. Format YYYY-MM-DD. | | `psaProjectId` | number | no | The id of the PSA project this fixed fee belongs to. It is a numeric package-layer key, not the UUID that GET psa/projects/:id takes. | | `salesOrderProductLineId` | number or null | no | The id of the sales order product line this fixed fee fulfills. It is a numeric package-layer key. There is no endpoint for it. | | `productId` | number or null | no | The id of the product this fixed fee is associated with. It is a numeric package-layer key, not the UUID that GET accounting/products/:id takes. | | `productLineId` | number or null | no | The id of the product line this fixed fee is associated with. It is a numeric package-layer key, not the UUID that GET accounting/product-lines/:id takes. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the fixed fee record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the fixed fee with the given identifier. `GET /psa/fixed-fees/{id}` Group: PSA › Fixed Fees · Operation ID: `getFixedFee` The amount and its currency are the two halves of one stored value. They are present together or absent together, and neither is meaningful alone. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getFixedFee) ## 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`. | | `id` | path | string | yes | The unique identifier for the fixed fee to return. | ## Response `200` Success Under `data` (getFixedFeeOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the fixed fee. | | `amount` | string (decimal) | no | The fixed fee amount, in the currency given by currency. | | `currency` | string | no | The currency of amount. Format ISO 4217, for example 'USD'. | | `frequency` | string | no | The billing frequency of the fixed fee. Possible values: 'Monthly', 'BiMonthly', 'Quarterly', 'SemiAnnually', 'Annually', 'Once'. | | `startDate` | string (date) | no | The first day of the fixed fee's billing period, inclusive. Format YYYY-MM-DD. | | `endDate` | string (date) | no | The last day of the fixed fee's billing period, inclusive. Format YYYY-MM-DD. | | `psaProjectId` | number | yes | The id of the PSA project this fixed fee belongs to. It is a numeric package-layer key, not the UUID that GET psa/projects/:id takes. | | `salesOrderProductLineId` | number | no | The id of the sales order product line this fixed fee fulfills. It is a numeric package-layer key. There is no endpoint for it. | | `productId` | number | no | The id of the product this fixed fee is associated with. It is a numeric package-layer key, not the UUID that GET accounting/products/:id takes. | | `productLineId` | number | no | The id of the product line this fixed fee is associated with. It is a numeric package-layer key, not the UUID that GET accounting/product-lines/:id takes. | | `lastModifiedDate` | string (date-time) | yes | The date and time the fixed fee record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of milestones. `GET /psa/milestones` Group: PSA › Milestones · Operation ID: `milestones` The psaProjectId, invoiceId, productId and productLineId on a milestone are numeric package-layer keys, not the UUIDs the public GET endpoints take, so none of them can be followed over this API. Milestones cannot be matched by name or description. [Open in the interactive reference](https://docs.api.lensing.ai/?op=milestones) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `dueDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `billable` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `psaProjectId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, name, status, currency, dueDate, psaProjectId, lastModifiedDate - Selectable: id, name, description, amount, currency, dueDate, status, billable, psaProjectId, invoiceId, productId, productLineId, lastModifiedDate ## Response `200` Success Under `data`, an array of milestonesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the milestone. | | `name` | string or null | no | The display name of the milestone. | | `description` | string or null | no | A free-form description of the milestone. | | `amount` | string (decimal) or null | no | The milestone amount, in the currency given by currency. | | `currency` | string or null | no | The currency of amount. Format ISO 4217, for example 'USD'. | | `dueDate` | string (date) or null | no | The date the milestone is due for completion. Format YYYY-MM-DD. | | `status` | string or null | no | The status of the milestone. Possible values: 'pending', 'accomplished', 'invoiced'. | | `billable` | boolean or null | no | Whether the milestone is billable to a customer. | | `psaProjectId` | number | no | The id of the PSA project this milestone belongs to. It is a numeric package-layer key, not the UUID that GET psa/projects/:id takes. | | `invoiceId` | number or null | no | The id of the invoice this milestone has been billed on. Absent when the milestone has not been invoiced. It is a numeric package-layer key, not the UUID that GET accounting/invoices/:id takes. | | `productId` | number or null | no | The id of the product this milestone is associated with. It is a numeric package-layer key, not the UUID that GET accounting/products/:id takes. | | `productLineId` | number or null | no | The id of the product line this milestone is associated with. It is a numeric package-layer key, not the UUID that GET accounting/product-lines/:id takes. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the milestone record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the milestone with the given identifier. `GET /psa/milestones/{id}` Group: PSA › Milestones · Operation ID: `getMilestone` The psaProjectId, invoiceId, productId and productLineId on a milestone are numeric package-layer keys, not the UUIDs the public GET endpoints take, so none of them can be followed over this API. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getMilestone) ## 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`. | | `id` | path | string | yes | The unique identifier for the milestone to return. | ## Response `200` Success Under `data` (getMilestoneOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the milestone. | | `name` | string | no | The display name of the milestone. | | `description` | string | no | A free-form description of the milestone. | | `amount` | string (decimal) | no | The milestone amount, in the currency given by currency. | | `currency` | string | no | The currency of amount. Format ISO 4217, for example 'USD'. | | `dueDate` | string (date) | no | The date the milestone is due for completion. Format YYYY-MM-DD. | | `status` | string | no | The status of the milestone. Possible values: 'pending', 'accomplished', 'invoiced'. | | `billable` | boolean | no | Whether the milestone is billable to a customer. | | `psaProjectId` | number | yes | The id of the PSA project this milestone belongs to. It is a numeric package-layer key, not the UUID that GET psa/projects/:id takes. | | `invoiceId` | number | no | The id of the invoice this milestone has been billed on. Absent when the milestone has not been invoiced. It is a numeric package-layer key, not the UUID that GET accounting/invoices/:id takes. | | `productId` | number | no | The id of the product this milestone is associated with. It is a numeric package-layer key, not the UUID that GET accounting/products/:id takes. | | `productLineId` | number | no | The id of the product line this milestone is associated with. It is a numeric package-layer key, not the UUID that GET accounting/product-lines/:id takes. | | `lastModifiedDate` | string (date-time) | yes | The date and time the milestone record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of placeholders. `GET /psa/placeholders` Group: PSA › Placeholders · Operation ID: `placeholders` Only active placeholders are returned. Inactive ones cannot be requested. [Open in the interactive reference](https://docs.api.lensing.ai/?op=placeholders) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `name` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: name, lastModifiedDate - Selectable: id, name, weeklyWorkingHours, lastModifiedDate ## Response `200` Success Under `data`, an array of placeholdersRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the placeholder. | | `name` | string | no | The display name of the placeholder, for example 'Unassigned Backend Engineer'. Unique within the tenant. | | `weeklyWorkingHours` | string (decimal) or null | no | The weekly working hours used for planning, as a non-negative number of hours, for example 40.00. Absent when none was set. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the placeholder last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates one or more placeholders. Either all are created, or none are. `POST /psa/placeholders` Group: PSA › Placeholders · Operation ID: `createPlaceholders` The response returns the new placeholders' ids in the order they were sent. New placeholders are not assigned to any project. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createPlaceholders) ## 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`: createPlaceholdersInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `placeholders` | array of object | yes | The placeholders to create. Send at least one. Either all are created, or none are. | | `placeholders[].name` | string | yes | The display name of the placeholder. Required, and unique within the tenant. A blank name fails the call with NAME_REQUIRED. A name another placeholder already has fails with DUPLICATE_NAME. | | `placeholders[].weeklyWorkingHours` | string (decimal) | no | The weekly working hours used for planning, as a non-negative number of hours, for example 40.00. Omitted or zero stores no hours. | Example: ```json { "placeholders": [ { "name": "Unassigned Backend Engineer", "weeklyWorkingHours": "40.00" }, { "name": "Unassigned Designer" } ] } ``` ## Response `200` Success Under `data` (createPlaceholdersOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The identifiers of the created placeholders, in the order they were sent. | ## 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 | | --- | --- | | `NAME_REQUIRED` | 400 | | `DUPLICATE_NAME` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NAME_REQUIRED, DUPLICATE_NAME - `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 --- # Returns the placeholder with the given identifier. `GET /psa/placeholders/{id}` Group: PSA › Placeholders · Operation ID: `getPlaceholder` [Open in the interactive reference](https://docs.api.lensing.ai/?op=getPlaceholder) ## 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`. | | `id` | path | string | yes | The unique identifier for the placeholder to return. | ## Response `200` Success Under `data` (getPlaceholderOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the placeholder. | | `name` | string | yes | The display name of the placeholder, for example 'Unassigned Backend Engineer'. Unique within the tenant. | | `weeklyWorkingHours` | string (decimal) | no | The weekly working hours used for planning, as a non-negative number of hours, for example 40.00. Absent when none was set. | | `lastModifiedDate` | string (date-time) | yes | The date and time the placeholder last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Placeholder `PATCH /psa/placeholders/{id}` Group: PSA › Placeholders · Operation ID: `updatePlaceholder` Updates the placeholder with the given identifier. Only the fields in the request body change. The response is empty. Read the placeholder back with GET psa/placeholders/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updatePlaceholder) ## 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. | | `id` | path | string | yes | The unique identifier for the placeholder to update. | ## Request body Required, `application/json`: updatePlaceholderInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | A new display name. Omit to leave it unchanged. Must be unique within the tenant. A blank name fails with NAME_REQUIRED, and a name another placeholder already has fails with DUPLICATE_NAME. | | `weeklyWorkingHours` | string (decimal) | no | New weekly working hours, as a non-negative number of hours, for example 32.00. Omit to leave the stored value unchanged. A zero is stored as zero. | Example: ```json { "name": "<name>", "weeklyWorkingHours": "32.00" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `NAME_REQUIRED` | 400 | | `DUPLICATE_NAME` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, NAME_REQUIRED, DUPLICATE_NAME - `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 --- # Permanently deletes the placeholder with the given identifier. `DELETE /psa/placeholders/{id}` Group: PSA › Placeholders · Operation ID: `deletePlaceholder` Deleting a placeholder is permanent. Any project assignments that reference it are deleted with it. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deletePlaceholder) ## 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. | | `id` | path | string | yes | The unique identifier for the placeholder to delete. Its project assignments are deleted with it, and this cannot be undone. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Returns a list of positions. `GET /psa/positions` Group: PSA › Positions · Operation ID: `psaPositions` The psaProjectId, product, product-line, rate-card, HR-position and skill-level references on a position are numeric package-layer keys, not the UUIDs the public GET endpoints take, so none of them can be followed over this API. Positions cannot be matched by name or description: neither is a filter, and there is no search endpoint. [Open in the interactive reference](https://docs.api.lensing.ai/?op=psaPositions) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `psaProjectId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `billable` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `unitOfMeasure` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `hourlyBillRateCurrency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `hourlyCostRateCurrency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `invoiced` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, psaProjectId, unitOfMeasure, lastModifiedDate - Selectable: id, psaProjectId, name, description, salesOrderProductLineId, salesOrderProductLineRateCardEntryId, productId, productLineId, productLineRateCardEntryId, billable, unitOfMeasure, hourlyBillRate, hourlyBillRateCurrency, hourlyCostRate, hourlyCostRateCurrency, hoursBudget, invoiced, hrPositionId, skillLevelId, lastModifiedDate ## Response `200` Success Under `data`, an array of psaPositionsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the position. | | `psaProjectId` | number | no | The id of the PSA project this position belongs to. It is a numeric package-layer key, not the UUID that GET psa/projects/:id takes. | | `name` | string or null | no | The display name of the position. | | `description` | string or null | no | A free-form description of the position's responsibilities. | | `salesOrderProductLineId` | number or null | no | The id of the sales order product line this position fulfills. It is a numeric package-layer key. There is no endpoint for it. | | `salesOrderProductLineRateCardEntryId` | number or null | no | The id of the sales order product line rate card entry used for billing. It is a numeric package-layer key. There is no endpoint for it. | | `productId` | number or null | no | The id of the product this position is billed against. It is a numeric package-layer key, not the UUID that GET accounting/products/:id takes. | | `productLineId` | number or null | no | The id of the product line this position is billed against. It is a numeric package-layer key, not the UUID that GET accounting/product-lines/:id takes. | | `productLineRateCardEntryId` | number or null | no | The id of the product line rate card entry. It is a numeric package-layer key. There is no endpoint for it. | | `billable` | boolean or null | no | Whether work in this position is billable to the customer. | | `unitOfMeasure` | string or null | no | The unit the stored bill rate, cost rate and budget are expressed in. It is a member of the UnitOfMeasureType enum, in practice 'hour' or 'day' for a position, with 'hour' the default. | | `hourlyBillRate` | string (decimal) or null | no | The bill rate for this position, as an hourly amount, in the currency given by hourlyBillRateCurrency. | | `hourlyBillRateCurrency` | string or null | no | The currency of hourlyBillRate. Format ISO 4217, for example 'USD'. | | `hourlyCostRate` | string (decimal) or null | no | The cost rate for this position, as an hourly amount, in the currency given by hourlyCostRateCurrency. | | `hourlyCostRateCurrency` | string or null | no | The currency of hourlyCostRate. Format ISO 4217, for example 'USD'. | | `hoursBudget` | string (decimal) or null | no | The budgeted hours for this position. It is a quantity in hours and has no currency. | | `invoiced` | boolean or null | no | Whether work in this position has been invoiced. | | `hrPositionId` | number or null | no | The id of the HR position this PSA position maps to. It is a numeric package-layer key, not the UUID that GET positions/:id takes. | | `skillLevelId` | number or null | no | The id of the skill level (position seniority) this PSA position maps to. It is a numeric package-layer key. There is no endpoint for it. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the position record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the position with the given identifier. `GET /psa/positions/{id}` Group: PSA › Positions · Operation ID: `getPsaPosition` The psaProjectId, product, product-line, rate-card, HR-position and skill-level references on a position are numeric package-layer keys, not the UUIDs the public GET endpoints take, so none of them can be followed over this API. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getPsaPosition) ## 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`. | | `id` | path | string | yes | The unique identifier for the position to return. | ## Response `200` Success Under `data` (getPsaPositionOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the position. | | `psaProjectId` | number | yes | The id of the PSA project this position belongs to. It is a numeric package-layer key, not the UUID that GET psa/projects/:id takes. | | `name` | string | no | The display name of the position. | | `description` | string | no | A free-form description of the position's responsibilities. | | `salesOrderProductLineId` | number | no | The id of the sales order product line this position fulfills. It is a numeric package-layer key. There is no endpoint for it. | | `salesOrderProductLineRateCardEntryId` | number | no | The id of the sales order product line rate card entry used for billing. It is a numeric package-layer key. There is no endpoint for it. | | `productId` | number | no | The id of the product this position is billed against. It is a numeric package-layer key, not the UUID that GET accounting/products/:id takes. | | `productLineId` | number | no | The id of the product line this position is billed against. It is a numeric package-layer key, not the UUID that GET accounting/product-lines/:id takes. | | `productLineRateCardEntryId` | number | no | The id of the product line rate card entry. It is a numeric package-layer key. There is no endpoint for it. | | `billable` | boolean | no | Whether work in this position is billable to the customer. | | `unitOfMeasure` | string | no | The unit the stored bill rate, cost rate and budget are expressed in. It is a member of the UnitOfMeasureType enum, in practice 'hour' or 'day' for a position, with 'hour' the default. | | `hourlyBillRate` | string (decimal) | no | The bill rate for this position, as an hourly amount, in the currency given by hourlyBillRateCurrency. | | `hourlyBillRateCurrency` | string | no | The currency of hourlyBillRate. Format ISO 4217, for example 'USD'. | | `hourlyCostRate` | string (decimal) | no | The cost rate for this position, as an hourly amount, in the currency given by hourlyCostRateCurrency. | | `hourlyCostRateCurrency` | string | no | The currency of hourlyCostRate. Format ISO 4217, for example 'USD'. | | `hoursBudget` | string (decimal) | no | The budgeted hours for this position. It is a quantity in hours and has no currency. | | `invoiced` | boolean | no | Whether work in this position has been invoiced. | | `hrPositionId` | number | no | The id of the HR position this PSA position maps to. It is a numeric package-layer key, not the UUID that GET positions/:id takes. | | `skillLevelId` | number | no | The id of the skill level (position seniority) this PSA position maps to. It is a numeric package-layer key. There is no endpoint for it. | | `lastModifiedDate` | string (date-time) | yes | The date and time the position record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of project types. `GET /psa/project-types` Group: PSA › Project Types · Operation ID: `projectTypes` Project types cannot be matched by name: name is returned but is not a filter, and there is no search endpoint. [Open in the interactive reference](https://docs.api.lensing.ai/?op=projectTypes) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, name, lastModifiedDate - Selectable: id, name, lastModifiedDate ## Response `200` Success Under `data`, an array of projectTypesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the project type. | | `name` | string | no | The display name of the project type. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the project type record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a project type. `POST /psa/project-types` Group: PSA › Project Types · Operation ID: `createProjectType` The response contains only the id of the created project type. Read the record with GET psa/project-types/:id. To create several at once, use POST psa/project-types/bulk. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createProjectType) ## 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`: createProjectTypeInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The display name of the project type. It must be unique across the tenant. A name another project type already uses fails with DUPLICATE_NAME. | Example: ```json { "name": "Retainer" } ``` ## Response `200` Success Under `data` (createProjectTypeOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created project type. | ## 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 | | --- | --- | | `DUPLICATE_NAME` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: DUPLICATE_NAME - `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 --- # Returns the project type with the given identifier. `GET /psa/project-types/{id}` Group: PSA › Project Types · Operation ID: `getProjectType` A project type is read only by its id. There is no endpoint to match project types by name. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getProjectType) ## 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`. | | `id` | path | string | yes | The unique identifier for the project type to return. | ## Response `200` Success Under `data` (getProjectTypeOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the project type. | | `name` | string | yes | The display name of the project type. | | `lastModifiedDate` | string (date-time) | no | The date and time the project type record last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Project Type `PATCH /psa/project-types/{id}` Group: PSA › Project Types · Operation ID: `updateProjectType` Updates the project type with the given identifier. Only the fields in the request body change. Only the fields in the request body change. The response is empty. Read the updated record with GET psa/project-types/:id. Renaming to a name another project type uses fails with DUPLICATE_NAME. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateProjectType) ## 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. | | `id` | path | string | yes | The unique identifier for the project type to update. | ## Request body Required, `application/json`: updateProjectTypeInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | A new display name for the project type. It must be unique across the tenant. A name another project type already uses fails with DUPLICATE_NAME. | Example: ```json { "name": "Retainer (Annual)" } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `DUPLICATE_NAME` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, DUPLICATE_NAME - `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 --- # Deletes the project type with the given identifier. `DELETE /psa/project-types/{id}` Group: PSA › Project Types · Operation ID: `deleteProjectType` The response is empty. A project type still referenced by a project cannot be deleted and fails with PROJECT_TYPE_IN_USE. Deleting an id that no longer exists fails with NOT_FOUND. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteProjectType) ## 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. | | `id` | path | string | yes | The unique identifier for the project type to delete. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `PROJECT_TYPE_IN_USE` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, PROJECT_TYPE_IN_USE - `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 --- # Creates many project types in one call. `POST /psa/project-types/bulk` Group: PSA › Project Types · Operation ID: `bulkCreateProjectTypes` Every project type in the request is created in one transaction. If any entry is rejected, nothing is written. Naming the same project type twice creates it once, so the response can return fewer ids than the request sent. Only a name the tenant already uses fails, with DUPLICATE_NAME. The response contains only the created ids. Read a record with GET psa/project-types/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=bulkCreateProjectTypes) ## 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`: bulkCreateProjectTypesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `projectTypes` | array of object | yes | The project types to create. Naming the same project type twice creates it once. If any entry is invalid, the whole call is rejected and nothing is written. | | `projectTypes[].name` | string | yes | The display name of the project type. | Example: ```json { "projectTypes": [ { "name": "Retainer" }, { "name": "Fixed Bid" } ] } ``` ## Response `200` Success Under `data` (bulkCreateProjectTypesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `created` | array of object | yes | The identifiers of the created project types, in the order their distinct names were sent. A repeated name produces one entry, so a request that repeats a name returns fewer entries than it sent. | | `created[].id` | string | yes | The unique identifier for a created project type. | ## 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 | | --- | --- | | `DUPLICATE_NAME` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: DUPLICATE_NAME - `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 --- # Returns a list of PSA projects. `GET /psa/projects` Group: PSA › Projects · Operation ID: `projects` Returns the PSA project collection. There is no lookup by name or code, and no way to list the projects of one department or one sales arrangement from the server, so finding a project by name means listing and matching in your client. [Open in the interactive reference](https://docs.api.lensing.ai/?op=projects) ## 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`. | | `showArchivedProjects` | query | boolean | no | When true, archived projects are included in the results. Set via ?showArchivedProjects=true, not a filter[] entry. | | `showPlanningProjects` | query | boolean | no | When true, projects in Planning and PlanningDraft status are included in the results. Set via ?showPlanningProjects=true, not a filter[] entry. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `category` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `status` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `invoiced` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `startDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `endDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `projectTypeId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `quoteId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `opportunityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, lastModifiedDate, category, status, currency, invoiced, startDate, endDate - Selectable: id, lastModifiedDate, name, code, category, status, currency, invoiced, startDate, endDate, notes, enforceActivityDateRange, budgetAmount, budgetCurrency, departmentId, teamId, teamName, teamStartDate, teamEndDate, salesRepresentativeId, salesRepresentativeName, projectTypeId, projectTypeName, quoteId, quoteNumber, opportunityId, opportunityName, opportunityCurrency, salesArrangementId, salesArrangementCurrency, salesArrangementCustomerId, salesArrangementCustomerName, salesArrangementEntityId, salesArrangementEntityName, revenueRecordedTimeToDate, revenueAccomplishedMilestones, revenueFixedFeeToDate, revenueToDate, budgetRemaining ## Response `200` Success Under `data`, an array of projectsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the project. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the project last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `name` | string or null | no | The display name of the project. | | `code` | string or null | no | The project code, for example 'PSA-29767'. Absent when none has been set. | | `category` | string or null | no | The project category. Possible values: 'Billable', 'Internal', 'NonBillable'. | | `status` | string or null | no | The project status. Possible values: 'Tentative', 'Planning', 'PlanningDraft', 'NotStarted', 'InProgress', 'Paused', 'Completed', 'Cancelled'. | | `currency` | string or null | no | The project's default currency. Format ISO 4217, for example 'USD'. It applies to revenueRecordedTimeToDate, revenueAccomplishedMilestones, revenueFixedFeeToDate, revenueToDate, and budgetRemaining. | | `invoiced` | boolean or null | no | Whether the project is marked as invoiced. | | `startDate` | string (date) or null | no | The project start date. Format YYYY-MM-DD. | | `endDate` | string (date) or null | no | The project end date. Format YYYY-MM-DD. | | `notes` | string or null | no | Free-form notes about the project. | | `enforceActivityDateRange` | boolean or null | no | Whether time entries must fall within the project's startDate and endDate. | | `budgetAmount` | string (decimal) or null | no | The project budget amount, in budgetCurrency. | | `budgetCurrency` | string or null | no | The currency of budgetAmount. Format ISO 4217, for example 'USD'. | | `departmentId` | number or null | no | The numeric identifier of the department the project is allocated to. GET departments/:id takes a UUID, so the number cannot be used to fetch the department. | | `teamId` | number or null | no | The numeric identifier of the team assigned to this project. GET people/teams/:id takes a UUID, so the number cannot be used to fetch the team. | | `teamName` | string or null | no | The display name of the team. | | `teamStartDate` | string (date) or null | no | The team's start date, which may differ from startDate. Format YYYY-MM-DD. | | `teamEndDate` | string (date) or null | no | The team's end date, which may differ from endDate. Format YYYY-MM-DD. | | `salesRepresentativeId` | number or null | no | The numeric identifier of the sales representative, an employee. There is no endpoint for it. | | `salesRepresentativeName` | string or null | no | The display name of the sales representative. | | `projectTypeId` | number or null | no | The numeric identifier of the project type. GET psa/project-types/:id takes a UUID, so the number cannot be used to fetch the project type. | | `projectTypeName` | string or null | no | The display name of the project type. | | `quoteId` | number or null | no | The numeric identifier of the CRM quote this project was created from. GET crm/quotes/:id takes a UUID, so the number cannot be used to fetch the quote. | | `quoteNumber` | string or null | no | The quote number. | | `opportunityId` | number or null | no | The numeric identifier of the CRM opportunity this project is linked to. GET crm/opportunities/:id takes a UUID, so the number cannot be used to fetch the opportunity. | | `opportunityName` | string or null | no | The display name of the opportunity. | | `opportunityCurrency` | string or null | no | The currency of the opportunity. Format ISO 4217, for example 'USD'. | | `salesArrangementId` | number or null | no | The numeric identifier of the sales arrangement that links the project to billing terms. There is no endpoint for it. | | `salesArrangementCurrency` | string or null | no | The functional currency of the sales arrangement. Format ISO 4217, for example 'USD'. | | `salesArrangementCustomerId` | number or null | no | The numeric identifier of the customer on the sales arrangement. GET accounting/customers/:id takes a UUID, so the number cannot be used to fetch the customer. | | `salesArrangementCustomerName` | string or null | no | The display name of the customer on the sales arrangement. | | `salesArrangementEntityId` | number or null | no | The numeric identifier of the legal entity on the sales arrangement. There is no endpoint for it. | | `salesArrangementEntityName` | string or null | no | The display name of the legal entity on the sales arrangement. | | `revenueRecordedTimeToDate` | string (decimal) or null | no | The revenue from recorded time up to today, in the project currency. Absent when the project has no time entries. | | `revenueAccomplishedMilestones` | string (decimal) or null | no | The revenue from accomplished and invoiced milestones, in the project currency. Absent when the project has no completed milestones. | | `revenueFixedFeeToDate` | string (decimal) or null | no | The fixed-fee revenue to date, in the project currency. Absent when the project has no fixed fees. | | `revenueToDate` | string (decimal) or null | no | The total revenue to date, in the project currency: revenueRecordedTimeToDate + revenueAccomplishedMilestones + revenueFixedFeeToDate. | | `budgetRemaining` | string (decimal) or null | no | The budget remaining, in the project currency: budgetAmount - revenueToDate. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Creates a PSA project. `POST /psa/projects` Group: PSA › Projects · Operation ID: `createPsaProject` Creating a PSA project also creates its underlying timetracking project and team. teamLeadIds must contain at least one employee. The timetracking layer rejects a project with no team lead, and that failure currently comes back as an untyped server error rather than a typed diagnostic. The response contains only the new project's id. Read the record with GET psa/projects/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createPsaProject) ## 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`: createPsaProjectInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | The display name for the project. | | `startDate` | string (date) | yes | The project start date. Format YYYY-MM-DD. | | `endDate` | string (date) | yes | The project end date. Format YYYY-MM-DD. | | `teamStartDate` | string (date) | no | The team assignment start date. Defaults to startDate when omitted. Format YYYY-MM-DD. | | `teamEndDate` | string (date) | no | The team assignment end date. Defaults to endDate when omitted. Format YYYY-MM-DD. | | `teamLeadIds` | array of number | yes | The numeric identifiers of the employees to assign as project and team leads. At least one is required. There is no endpoint for them. | | `notifyProjectLeads` | boolean | no | Whether to notify the assigned project leads. | | `category` | string, one of `"Billable"`, `"Internal"`, `"NonBillable"` | yes | The project category. Accepted values: 'Billable', 'Internal', 'NonBillable'. | | `status` | string, one of `"Tentative"`, `"Planning"`, `"PlanningDraft"`, `"NotStarted"`, `"InProgress"`, `"Paused"`, `"Completed"`, `"Cancelled"` | yes | The initial project status. Accepted values: 'Tentative', 'Planning', 'PlanningDraft', 'NotStarted', 'InProgress', 'Paused', 'Completed', 'Cancelled'. | | `currency` | string | yes | The project's default currency. Format ISO 4217, for example 'USD'. | | `budgetAmount` | string (decimal) | no | The project budget amount, in budgetCurrency. | | `budgetCurrency` | string | no | The currency of budgetAmount. Format ISO 4217, for example 'USD'. It should match the project's default currency. | | `departmentId` | number | no | The numeric identifier of the department to allocate the project to. GET departments/:id takes a UUID, so the number cannot be used to fetch the department. | | `customerId` | number | no | The numeric identifier of a customer to link directly to the project. It is ignored when salesArrangementId is set. GET accounting/customers/:id takes a UUID, so the number cannot be used to fetch the customer. | | `enforceActivityDateRange` | boolean | no | Whether time entries must fall within the project's startDate and endDate. | | `invoiced` | boolean | no | Whether to mark the project as invoiced. Defaults to false. | | `notes` | string | no | Free-form notes about the project. | | `opportunityId` | number | no | The numeric identifier of a CRM opportunity to link to the project. GET crm/opportunities/:id takes a UUID, so the number cannot be used to fetch the opportunity. | | `code` | string | no | A code for the project. | | `quoteId` | number | no | The numeric identifier of the CRM quote the project is created from. GET crm/quotes/:id takes a UUID, so the number cannot be used to fetch the quote. | | `salesArrangementId` | number | no | The numeric identifier of a sales arrangement that links the project to billing terms. When set, the direct customerId link is not persisted. There is no endpoint for it. | | `salesRepresentativeId` | number | no | The numeric identifier of the sales representative, an employee. There is no endpoint for it. | | `projectTypeId` | number | no | The numeric identifier of the project type. GET psa/project-types/:id takes a UUID, so the number cannot be used to fetch the project type. | | `externalLink` | string | no | A link to an external system for this project. | | `projectSettings` | object | no | The timetracking settings for the project. Created only when at least one setting is provided. | | `projectSettings.disableTimetrackingOnHolidays` | boolean | no | | | `projectSettings.disableTimetrackingOnWeekends` | boolean | no | | | `projectSettings.maxHoursPerDay` | string (decimal) | no | | | `projectSettings.maxHoursPerMonth` | string (decimal) | no | | | `projectSettings.maxHoursPerWeek` | string (decimal) | no | | | `projectSettings.billableHoursPerDay` | string (decimal) | no | | | `projectSettings.notifyNewMembers` | boolean | no | | | `projectSettings.requireNotes` | boolean | no | | Example: ```json { "name": "Acme Rollout", "startDate": "2026-09-01", "endDate": "2027-03-01", "teamStartDate": "2026-01-15", "teamEndDate": "2026-01-15", "teamLeadIds": [ 3845142 ], "notifyProjectLeads": true, "category": "Billable", "status": "NotStarted", "currency": "USD", "budgetAmount": "19.99", "budgetCurrency": "<budgetCurrency>", "departmentId": 0, "customerId": 0, "enforceActivityDateRange": true, "invoiced": true, "notes": "<notes>", "opportunityId": 0, "code": "<code>", "quoteId": 0, "salesArrangementId": 0, "salesRepresentativeId": 0, "projectTypeId": 0, "externalLink": "<externalLink>", "projectSettings": { "disableTimetrackingOnHolidays": true, "disableTimetrackingOnWeekends": true, "maxHoursPerDay": "19.99", "maxHoursPerMonth": "19.99", "maxHoursPerWeek": "19.99", "billableHoursPerDay": "19.99", "notifyNewMembers": true, "requireNotes": true } } ``` ## Response `200` Success Under `data` (createPsaProjectOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the created project. | ## 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 | | --- | --- | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure - `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 --- # Returns the PSA project with the given identifier. `GET /psa/projects/{id}` Group: PSA › Projects · Operation ID: `getPsaProject` Returns one PSA project with its budget, status, team, sales-arrangement links, and revenue to date. The item schema is identical to GET psa/projects. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getPsaProject) ## 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`. | | `id` | path | string | yes | The unique identifier for the project to return. | ## Response `200` Success Under `data` (getPsaProjectOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the project. | | `lastModifiedDate` | string (date-time) | yes | The date and time the project last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | | `name` | string | yes | The display name of the project. | | `code` | string | no | The project code, for example 'PSA-29767'. Absent when none has been set. | | `category` | string | yes | The project category. Possible values: 'Billable', 'Internal', 'NonBillable'. | | `status` | string | yes | The project status. Possible values: 'Tentative', 'Planning', 'PlanningDraft', 'NotStarted', 'InProgress', 'Paused', 'Completed', 'Cancelled'. | | `currency` | string | yes | The project's default currency. Format ISO 4217, for example 'USD'. It applies to revenueRecordedTimeToDate, revenueAccomplishedMilestones, revenueFixedFeeToDate, revenueToDate, and budgetRemaining. | | `invoiced` | boolean | yes | Whether the project is marked as invoiced. | | `startDate` | string (date) | yes | The project start date. Format YYYY-MM-DD. | | `endDate` | string (date) | yes | The project end date. Format YYYY-MM-DD. | | `notes` | string | no | Free-form notes about the project. | | `enforceActivityDateRange` | boolean | no | Whether time entries must fall within the project's startDate and endDate. | | `budgetAmount` | string (decimal) | no | The project budget amount, in budgetCurrency. | | `budgetCurrency` | string | no | The currency of budgetAmount. Format ISO 4217, for example 'USD'. | | `departmentId` | number | no | The numeric identifier of the department the project is allocated to. GET departments/:id takes a UUID, so the number cannot be used to fetch the department. | | `teamId` | number | yes | The numeric identifier of the team assigned to this project. GET people/teams/:id takes a UUID, so the number cannot be used to fetch the team. | | `teamName` | string | no | The display name of the team. | | `teamStartDate` | string (date) | no | The team's start date, which may differ from startDate. Format YYYY-MM-DD. | | `teamEndDate` | string (date) | no | The team's end date, which may differ from endDate. Format YYYY-MM-DD. | | `salesRepresentativeId` | number | no | The numeric identifier of the sales representative, an employee. There is no endpoint for it. | | `salesRepresentativeName` | string | no | The display name of the sales representative. | | `projectTypeId` | number | no | The numeric identifier of the project type. GET psa/project-types/:id takes a UUID, so the number cannot be used to fetch the project type. | | `projectTypeName` | string | no | The display name of the project type. | | `quoteId` | number | no | The numeric identifier of the CRM quote this project was created from. GET crm/quotes/:id takes a UUID, so the number cannot be used to fetch the quote. | | `quoteNumber` | string | no | The quote number. | | `opportunityId` | number | no | The numeric identifier of the CRM opportunity this project is linked to. GET crm/opportunities/:id takes a UUID, so the number cannot be used to fetch the opportunity. | | `opportunityName` | string | no | The display name of the opportunity. | | `opportunityCurrency` | string | no | The currency of the opportunity. Format ISO 4217, for example 'USD'. | | `salesArrangementId` | number | no | The numeric identifier of the sales arrangement that links the project to billing terms. There is no endpoint for it. | | `salesArrangementCurrency` | string | no | The functional currency of the sales arrangement. Format ISO 4217, for example 'USD'. | | `salesArrangementCustomerId` | number | no | The numeric identifier of the customer on the sales arrangement. GET accounting/customers/:id takes a UUID, so the number cannot be used to fetch the customer. | | `salesArrangementCustomerName` | string | no | The display name of the customer on the sales arrangement. | | `salesArrangementEntityId` | number | no | The numeric identifier of the legal entity on the sales arrangement. There is no endpoint for it. | | `salesArrangementEntityName` | string | no | The display name of the legal entity on the sales arrangement. | | `revenueRecordedTimeToDate` | string (decimal) | no | The revenue from recorded time up to today, in the project currency. Absent when the project has no time entries. | | `revenueAccomplishedMilestones` | string (decimal) | no | The revenue from accomplished and invoiced milestones, in the project currency. Absent when the project has no completed milestones. | | `revenueFixedFeeToDate` | string (decimal) | no | The fixed-fee revenue to date, in the project currency. Absent when the project has no fixed fees. | | `revenueToDate` | string (decimal) | no | The total revenue to date, in the project currency: revenueRecordedTimeToDate + revenueAccomplishedMilestones + revenueFixedFeeToDate. | | `budgetRemaining` | string (decimal) | no | The budget remaining, in the project currency: budgetAmount - revenueToDate. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Update Psa Project `PATCH /psa/projects/{id}` Group: PSA › Projects · Operation ID: `updatePsaProject` Updates the PSA project with the given identifier. Only the fields in the request body change. Changing or removing salesArrangementId fails with INVOICED_SALES_ARRANGEMENT_CHANGE_BLOCKED when a linked position, milestone, or fixed fee has already been invoiced. An invoiced expense budget does not block the change. Setting salesArrangementId, or leaving it set, forces the direct customerId link to null. The response is empty. Read the record with GET psa/projects/:id. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updatePsaProject) ## 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. | | `id` | path | string | yes | The unique identifier for the project to update. | ## Request body Required, `application/json`: updatePsaProjectInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string or null | no | The display name of the project. | | `startDate` | string (date) or null | no | The project start date. Format YYYY-MM-DD. | | `endDate` | string (date) or null | no | The project end date. Format YYYY-MM-DD. | | `teamStartDate` | string (date) or null | no | The team assignment start date. Format YYYY-MM-DD. | | `teamEndDate` | string (date) or null | no | The team assignment end date. Format YYYY-MM-DD. | | `teamLeadIds` | array of number or null | no | The numeric identifiers of the employees to set as project and team leads. This replaces the current set of leads. There is no endpoint for them. | | `active` | boolean or null | no | Whether the underlying timetracking project is active. | | `notifyProjectLeads` | boolean or null | no | Whether to notify the project leads. | | `category` | string, one of `"Billable"`, `"Internal"`, `"NonBillable"` | no | The project category. Accepted values: 'Billable', 'Internal', 'NonBillable'. | | `status` | string, one of `"Tentative"`, `"Planning"`, `"PlanningDraft"`, `"NotStarted"`, `"InProgress"`, `"Paused"`, `"Completed"`, `"Cancelled"` | no | The project status. Accepted values: 'Tentative', 'Planning', 'PlanningDraft', 'NotStarted', 'InProgress', 'Paused', 'Completed', 'Cancelled'. | | `currency` | string or null | no | The project's default currency. Format ISO 4217, for example 'USD'. | | `budgetAmount` | string (decimal) or null | no | The project budget amount, in budgetCurrency. | | `budgetCurrency` | string or null | no | The currency of budgetAmount. Format ISO 4217, for example 'USD'. It should match the project's default currency. | | `departmentId` | number or null | no | The numeric identifier of the department to allocate the project to. GET departments/:id takes a UUID, so the number cannot be used to fetch the department. | | `customerId` | number or null | no | The numeric identifier of a customer to link directly to the project. It is forced to null whenever a sales arrangement is set. GET accounting/customers/:id takes a UUID, so the number cannot be used to fetch the customer. | | `enforceActivityDateRange` | boolean or null | no | Whether time entries must fall within the project's startDate and endDate. | | `invoiced` | boolean or null | no | Whether the project is marked as invoiced. | | `notes` | string or null | no | Free-form notes about the project. | | `opportunityId` | number or null | no | The numeric identifier of a CRM opportunity linked to the project. GET crm/opportunities/:id takes a UUID, so the number cannot be used to fetch the opportunity. | | `code` | string or null | no | A code for the project. | | `quoteId` | number or null | no | The numeric identifier of a CRM quote linked to the project. GET crm/quotes/:id takes a UUID, so the number cannot be used to fetch the quote. | | `salesArrangementId` | number or null | no | The numeric identifier of a sales arrangement that links the project to billing terms. There is no endpoint for it. Changing or removing it fails with INVOICED_SALES_ARRANGEMENT_CHANGE_BLOCKED when a linked position, milestone, or fixed fee has already been invoiced. | | `salesRepresentativeId` | number or null | no | The numeric identifier of the sales representative, an employee. There is no endpoint for it. | | `projectTypeId` | number or null | no | The numeric identifier of the project type. GET psa/project-types/:id takes a UUID, so the number cannot be used to fetch the project type. | | `externalLink` | string or null | no | A link to an external system for this project. | | `projectSettings` | object or null | no | The timetracking settings for the project. Updated when projectSettingsId is given, otherwise a new settings record is created. | | `projectSettings.projectSettingsId` | number or null | no | | | `projectSettings.disableTimetrackingOnHolidays` | boolean or null | no | | | `projectSettings.disableTimetrackingOnWeekends` | boolean or null | no | | | `projectSettings.maxHoursPerDay` | string (decimal) or null | no | | | `projectSettings.maxHoursPerMonth` | string (decimal) or null | no | | | `projectSettings.maxHoursPerWeek` | string (decimal) or null | no | | | `projectSettings.billableHoursPerDay` | string (decimal) or null | no | | | `projectSettings.notifyNewMembers` | boolean or null | no | | | `projectSettings.requireNotes` | boolean or null | no | | Example: ```json { "name": "<name>", "startDate": "2026-01-15", "endDate": "2026-01-15", "teamStartDate": "2026-01-15", "teamEndDate": "2026-01-15", "teamLeadIds": [ 0 ], "active": true, "notifyProjectLeads": true, "category": "Billable", "status": "Tentative", "currency": "<currency>", "budgetAmount": "19.99", "budgetCurrency": "<budgetCurrency>", "departmentId": 0, "customerId": 0, "enforceActivityDateRange": true, "invoiced": true, "notes": "Kickoff moved to next sprint.", "opportunityId": 0, "code": "<code>", "quoteId": 0, "salesArrangementId": 0, "salesRepresentativeId": 0, "projectTypeId": 0, "externalLink": "<externalLink>", "projectSettings": { "projectSettingsId": 0, "disableTimetrackingOnHolidays": true, "disableTimetrackingOnWeekends": true, "maxHoursPerDay": "19.99", "maxHoursPerMonth": "19.99", "maxHoursPerWeek": "19.99", "billableHoursPerDay": "19.99", "notifyNewMembers": true, "requireNotes": true } } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `INVOICED_SALES_ARRANGEMENT_CHANGE_BLOCKED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, INVOICED_SALES_ARRANGEMENT_CHANGE_BLOCKED - `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 --- # Deletes the PSA project with the given identifier, archiving it. `DELETE /psa/projects/{id}` Group: PSA › Projects · Operation ID: `deletePsaProject` Deletes one PSA project and its timetracking data. This archives the project. To delete it permanently, use POST psa/projects/bulk-delete with irreversible, because a DELETE request has no body to carry that flag. The response is empty. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deletePsaProject) ## 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. | | `id` | path | string | yes | The unique identifier for the project to delete. The project is archived rather than permanently deleted. Use POST psa/projects/bulk-delete with irreversible to delete permanently. | ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Deletes PSA projects in bulk, archiving them by default. `POST /psa/projects/bulk-delete` Group: PSA › Projects · Operation ID: `deletePsaProjects` Deletes PSA projects and their timetracking data. By default this archives them, which you can undo with POST psa/projects/bulk-restore. Pass irreversible to delete permanently instead, which cannot be undone. This is all-or-nothing: if any id fails, nothing is deleted. To delete a single project use DELETE psa/projects/:id, which always archives, so irreversible is available only here. The response is empty. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deletePsaProjects) ## 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`: deletePsaProjectsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the projects to delete. If any id does not match a project, nothing is deleted. | | `irreversible` | boolean | no | Whether to delete the projects permanently. Defaults to false, which archives them instead. Archived projects can be restored with POST psa/projects/bulk-restore. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45", "e57a0d34-9f62-4b18-bc25-71a6e8039d4b" ], "irreversible": true } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Restores archived PSA projects in bulk. `POST /psa/projects/bulk-restore` Group: PSA › Projects · Operation ID: `restorePsaProjects` Restores PSA projects that were archived by POST psa/projects/bulk-delete without irreversible. A permanently deleted project cannot be restored. This is all-or-nothing: if any id fails, nothing is restored. The response is empty. [Open in the interactive reference](https://docs.api.lensing.ai/?op=restorePsaProjects) ## 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`: restorePsaProjectsInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the archived projects to restore. If any id does not match, nothing is restored. | Example: ```json { "ids": [ "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `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 --- # Returns a list of position assignments on PSA project teams. `GET /psa/team-member-positions` Group: PSA › Team Members · Operation ID: `teamMemberPositions` Each row is one position assignment: a person or a placeholder mapped to a project position. To read the team roster regardless of position, use GET psa/team-members. There is no endpoint to match an assignment by position name. [Open in the interactive reference](https://docs.api.lensing.ai/?op=teamMemberPositions) ## 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`. | ## Query surface - `psaProjectId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `projectPositionId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `teamMemberId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `placeholderId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `invoiced` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: psaProjectId, lastModifiedDate - Selectable: id, psaProjectId, projectPositionId, positionName, teamMemberId, placeholderId, invoiced, lastModifiedDate ## Response `200` Success Under `data`, an array of teamMemberPositionsRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the position assignment. | | `psaProjectId` | number | no | The id of the PSA project this record belongs to. It is a numeric package-layer key you can filter on here, not the UUID that GET psa/projects/:id takes. | | `projectPositionId` | number or null | no | The id of the project position this row assigns. It is a numeric package-layer key you can filter on here, not the UUID that GET psa/positions/:id takes. | | `positionName` | string or null | no | The name of the project position, paired with projectPositionId. It is returned but cannot be filtered or sorted on, and there is no endpoint to find an assignment by position name. | | `teamMemberId` | number or null | no | The id of the assigned person's team-roster row. Absent for placeholder-only assignments. It is a numeric package-layer key, not the UUID that GET psa/team-members/:id takes. | | `placeholderId` | number or null | no | The id of the placeholder filling this position, when it is filled by a placeholder rather than a person. It is a numeric package-layer key, not the UUID that GET psa/placeholders/:id takes. | | `invoiced` | boolean or null | no | Whether this assignment's work has been invoiced. An invoiced assignment cannot be deleted. | | `lastModifiedDate` | string (date-time) or null | no | The date and time this assignment last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the position assignment with the given identifier. `GET /psa/team-member-positions/{id}` Group: PSA › Team Members · Operation ID: `getTeamMemberPosition` A position assignment maps one person or placeholder to one project position. This route returns the assignment, not the team roster, which is a separate resource. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getTeamMemberPosition) ## 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`. | | `id` | path | string | yes | The unique identifier for the position assignment to return. | ## Response `200` Success Under `data` (getTeamMemberPositionOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the position assignment. | | `psaProjectId` | number | no | The id of the PSA project this record belongs to. It is a numeric package-layer key you can filter on here, not the UUID that GET psa/projects/:id takes. | | `projectPositionId` | number | no | The id of the project position this row assigns. It is a numeric package-layer key you can filter on here, not the UUID that GET psa/positions/:id takes. | | `positionName` | string | no | The name of the project position, paired with projectPositionId. It is returned but cannot be filtered or sorted on, and there is no endpoint to find an assignment by position name. | | `teamMemberId` | number | no | The id of the assigned person's team-roster row. Absent for placeholder-only assignments. It is a numeric package-layer key, not the UUID that GET psa/team-members/:id takes. | | `placeholderId` | number | no | The id of the placeholder filling this position, when it is filled by a placeholder rather than a person. It is a numeric package-layer key, not the UUID that GET psa/placeholders/:id takes. | | `invoiced` | boolean | no | Whether this assignment's work has been invoiced. An invoiced assignment cannot be deleted. | | `lastModifiedDate` | string (date-time) | yes | The date and time this assignment last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns a list of the people on PSA project teams. `GET /psa/team-members` Group: PSA › Team Members · Operation ID: `teamMembers` Each row is one person on a PSA project's team. To find who fills which position, use GET psa/team-member-positions. There is no endpoint to match a member by name. [Open in the interactive reference](https://docs.api.lensing.ai/?op=teamMembers) ## 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`. | ## Query surface - `psaProjectId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `employeeId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `isTeamLead` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `startDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `endDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: psaProjectId, startDate, endDate, lastModifiedDate - Selectable: id, psaProjectId, employeeId, isTeamLead, startDate, endDate, allocation, name, displayName, lastModifiedDate ## Response `200` Success Under `data`, an array of teamMembersRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the team-roster row. | | `psaProjectId` | number | no | The id of the PSA project this record belongs to. It is a numeric package-layer key you can filter on here, not the UUID that GET psa/projects/:id takes. | | `employeeId` | number or null | no | The id of the employee this roster row is for. It is a numeric package-layer key, not a UUID any public endpoint returns as a fetchable id. | | `isTeamLead` | boolean or null | no | Whether this team member is the team lead. | | `startDate` | string (date) or null | no | The first day of this person's membership of the team. Format YYYY-MM-DD. Absent when not set. | | `endDate` | string (date) or null | no | The last day of this person's membership of the team. Format YYYY-MM-DD. Absent when not set. | | `allocation` | string (decimal) or null | no | The share of the person's capacity allocated to this team, as a percentage. For example, 50 means 50%. | | `name` | string or null | no | The full name of the team member. It is returned but cannot be filtered or sorted on, and there is no endpoint to find a member by name. | | `displayName` | string or null | no | The preferred display name of the team member. Like name, it cannot be filtered or sorted on. | | `lastModifiedDate` | string (date-time) or null | no | The date and time this roster row last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Returns the team member with the given identifier. `GET /psa/team-members/{id}` Group: PSA › Team Members · Operation ID: `getTeamMember` An entry in the roster is one person on a PSA project's team. This route returns the roster row, not the person's position assignments, which are a separate resource. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getTeamMember) ## 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`. | | `id` | path | string | yes | The unique identifier for the team-roster row to return. | ## Response `200` Success Under `data` (getTeamMemberOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the team-roster row. | | `psaProjectId` | number | no | The id of the PSA project this record belongs to. It is a numeric package-layer key you can filter on here, not the UUID that GET psa/projects/:id takes. | | `employeeId` | number | yes | The id of the employee this roster row is for. It is a numeric package-layer key, not a UUID any public endpoint returns as a fetchable id. | | `isTeamLead` | boolean | no | Whether this team member is the team lead. | | `startDate` | string (date) | no | The first day of this person's membership of the team. Format YYYY-MM-DD. Absent when not set. | | `endDate` | string (date) | no | The last day of this person's membership of the team. Format YYYY-MM-DD. Absent when not set. | | `allocation` | string (decimal) | no | The share of the person's capacity allocated to this team, as a percentage. For example, 50 means 50%. | | `name` | string | no | The full name of the team member. It is returned but cannot be filtered or sorted on, and there is no endpoint to find a member by name. | | `displayName` | string | no | The preferred display name of the team member. Like name, it cannot be filtered or sorted on. | | `lastModifiedDate` | string (date-time) | yes | The date and time this roster row last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Assign Employees To Placeholders `POST /psa/team-members/assign-placeholder` Group: PSA › Team Members · Operation ID: `assignEmployeesToPlaceholders` Replaces placeholders with employees, keeping the placeholders' position assignments. Either all succeed, or none do. Each entry re-points a placeholder's position assignments to an employee. When the employee already holds the same position, the placeholder assignment is merged into it and removed, so it returns no id. The response holds only the resulting assignment ids, so it can be shorter than the input and is not aligned to it position by position. This needs edit permission on each project. [Open in the interactive reference](https://docs.api.lensing.ai/?op=assignEmployeesToPlaceholders) ## 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`: assignEmployeesToPlaceholdersInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `assignments` | array of object | yes | One entry per placeholder-to-employee conversion. Each entry re-points the placeholder's position assignments to the employee. | | `assignments[].employeeId` | number | yes | The employee taking over the placeholder's assignments. They are added to the team if not already a member. It is a numeric package-layer key. | | `assignments[].placeholderId` | number | yes | The placeholder being replaced. It is a numeric package-layer key. | | `assignments[].psaProjectId` | number | yes | The PSA project the placeholder belongs to. Needs edit permission on this project. It is a numeric package-layer key, not the UUID that GET psa/projects/:id takes. | Example: ```json { "assignments": [ { "employeeId": 3845142, "placeholderId": 91, "psaProjectId": 32341 } ] } ``` ## Response `200` Success Under `data` (assignEmployeesToPlaceholdersOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `assigned` | array of object | yes | The ids of the position assignments now held by the employee. A placeholder assignment merged into one the employee already held is removed and contributes no id, so this can be shorter than the input. Read a record back with GET psa/team-member-positions/:id. | | `assigned[].id` | string | yes | The unique identifier of the resulting position assignment. This is the id GET psa/team-member-positions returns. | ## 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 | | --- | --- | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure - `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 --- # Create Team Members `POST /psa/team-members/bulk` Group: PSA › Team Members · Operation ID: `createTeamMembers` Adds people or placeholders to a PSA project's team, optionally assigning each to a position. Either all are added, or none are. Each entry names exactly one of employeeId or placeholderId, and optionally a projectPositionId. An entry with a position creates a position assignment and returns its id. An entry without one adds the person to the team but creates no assignment, so it returns no id. The response holds only the created ids, so it can be shorter than the input and is not aligned to it position by position. Adding people needs edit permission on the project. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createTeamMembers) ## 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`: createTeamMembersInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `psaProjectId` | number | yes | The PSA project to add the people to. It is a numeric package-layer key, not the UUID that GET psa/projects/:id takes. Adding people needs edit permission on this project. | | `assignments` | array of object | yes | One entry per person or placeholder to add. Each entry needs exactly one of employeeId or placeholderId. Either all are added, or none are. | | `assignments[].employeeId` | number | no | The employee to add. Give either employeeId or placeholderId, not both. The employee joins the team even when projectPositionId is omitted. It is a numeric package-layer key. | | `assignments[].placeholderId` | number | no | The placeholder to assign. Give either employeeId or placeholderId, not both. A placeholder entry with no projectPositionId creates nothing. It is a numeric package-layer key. | | `assignments[].projectPositionId` | number or null | no | The project position to assign this person or placeholder to. Omit it to add the person to the team without a position assignment. It is a numeric package-layer key. | Example: ```json { "psaProjectId": 32341, "assignments": [ { "employeeId": 3845142, "projectPositionId": 7 }, { "placeholderId": 91 } ] } ``` ## Response `200` Success Under `data` (createTeamMembersOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `created` | array of object | yes | The ids of the position assignments created, in the order the entries were sent. An entry that created no position assignment contributes no id, so this can be shorter than the input. Read a record back with GET psa/team-member-positions/:id. | | `created[].id` | string | yes | The unique identifier of the created position assignment. This is the id GET psa/team-member-positions returns. | ## 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 | | --- | --- | | `INVALID_ASSIGNMENT` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: INVALID_ASSIGNMENT - `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 --- # Returns a list of time entries. `GET /psa/time-entries` Group: PSA › Time Entries · Operation ID: `timeEntries` Returns time entries for the projects where you are a team member. The person and project identifiers on each entry (memberId, memberEmployeeId, projectPositionId and psaProjectId) are returned but cannot be filtered or sorted on. There is no endpoint that lists a specific employee's time entries. [Open in the interactive reference](https://docs.api.lensing.ai/?op=timeEntries) ## 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`. | ## Query surface - `id` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `date` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `currency` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `submissionStatus` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `submissionDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `month` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `projectActivityId` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - `lastModifiedDate` — filter by eq, ne, gt, gte, lt, lte, in, notIn, between, like - Orderable: id, date, month, hours, submissionStatus, submissionDate, currency, lastModifiedDate - Selectable: id, date, hours, billableHours, nonBillableHours, internalValuableHours, internalNonValuableHours, invoicedHours, revenue, cost, currency, notes, submissionStatus, submissionDate, month, memberId, memberEmployeeId, memberEmployeeName, projectActivityId, projectActivityName, projectActivityCategory, projectPositionId, projectPositionName, psaProjectId, projectName, psaProjectStatus, lastModifiedDate ## Response `200` Success Under `data`, an array of timeEntriesRow: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | no | The unique identifier for the time entry. | | `date` | string (date) or null | no | The date the time was logged for. Format YYYY-MM-DD. | | `hours` | string (decimal) or null | no | The total hours logged in this entry. | | `billableHours` | string (decimal) or null | no | The hours classified as billable to a customer, derived from projectActivityCategory. | | `nonBillableHours` | string (decimal) or null | no | The hours classified as non-billable to a customer, derived from projectActivityCategory. | | `internalValuableHours` | string (decimal) or null | no | The hours classified as internal valuable work, derived from projectActivityCategory. | | `internalNonValuableHours` | string (decimal) or null | no | The hours classified as internal non-valuable work, derived from projectActivityCategory. | | `invoicedHours` | string (decimal) or null | no | The hours that have been invoiced, a subset of billableHours. | | `revenue` | string (decimal) or null | no | The revenue from this time entry, in the currency given by currency. | | `cost` | string (decimal) or null | no | The cost of this time entry, in the currency given by currency. | | `currency` | string or null | no | The currency of revenue and cost. Format ISO 4217, for example 'USD'. It is the currency of the project position's bill rate. | | `notes` | string or null | no | Free-form notes on the time entry. | | `submissionStatus` | string or null | no | The submission and approval status of the entry. Possible values: 'draft', 'pending', 'approved', 'rejected', 'invoiced'. | | `submissionDate` | string (date) or null | no | The date the entry was submitted for approval. Format YYYY-MM-DD. | | `month` | string (date) or null | no | The first day of the month the entry is reported under. Format YYYY-MM-DD. | | `memberId` | number or null | no | The numeric key of the PSA member assignment the time was logged for. GET psa/team-members/:id takes the member's UUID, not this key, so it cannot fetch the member. | | `memberEmployeeId` | number or null | no | The numeric key of the employee who logged the time. There is no endpoint for it. | | `memberEmployeeName` | string or null | no | The name of the employee who logged the time. | | `projectActivityId` | number or null | no | The numeric key of the activity the time was logged against. GET psa/activities/:id takes the activity's UUID, not this key, so it cannot fetch the activity. | | `projectActivityName` | string or null | no | The name of the activity the time was logged against. | | `projectActivityCategory` | string or null | no | The billability category of the activity the time was logged against. The billable, non-billable and internal-hour fields are derived from it. See category on GET psa/activities/:id. | | `projectPositionId` | number or null | no | The numeric key of the position the time was logged against. GET psa/positions/:id takes the position's UUID, not this key, so it cannot fetch the position. | | `projectPositionName` | string or null | no | The name of the position the time was logged against. | | `psaProjectId` | number or null | no | The numeric key of the PSA project the entry belongs to. GET psa/projects/:id takes the project's UUID, not this key, so it cannot fetch the project. | | `projectName` | string or null | no | The name of the PSA project the entry belongs to. | | `psaProjectStatus` | string or null | no | The status of the PSA project this entry belongs to. | | `lastModifiedDate` | string (date-time) or null | no | The date and time the entry last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Statuses - `400` — Invalid request or business failure - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Create Time Entries `POST /psa/time-entries` Group: PSA › Time Entries · Operation ID: `createTimeEntries` Creates time entries. A member can log time against an activity only once per date, and a duplicate fails with DUPLICATE_TIME_ENTRY. Creates time entries and changes nothing else. It never deletes or updates existing entries, so it is safe to call for a date that already has entries for the same member against other activities. A member can log time against a given activity on a given date only once. If an entry already exists for that member, activity and date, the request fails with DUPLICATE_TIME_ENTRY and nothing is created. Correct the existing entry with PATCH psa/time-entries instead. Entries repeated within one request are merged when they agree on hours and rejected with the same error when they disagree. Each entry's project is taken from its projectActivityId unless psaProjectId is given. An activity that resolves to no project fails with ACTIVITY_NOT_FOUND. A create also fails when the member's team is not active, and when the entries would break the project's time rules (required notes, no time on weekends or holidays, and per-member limits on hours per day, week and month). These failures are reported as generic errors, not as named diagnostics. [Open in the interactive reference](https://docs.api.lensing.ai/?op=createTimeEntries) ## 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`: createTimeEntriesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `timeEntries` | array of object | yes | The time entries to create. | | `timeEntries[].date` | string (date) | yes | The date the time is logged for. Use format YYYY-MM-DD. | | `timeEntries[].hours` | string (decimal) | yes | The total hours for the entry. | | `timeEntries[].projectActivityId` | number | yes | The numeric key of the project activity to log time against. | | `timeEntries[].memberId` | number | yes | The numeric key of the PSA member assignment to log time for. | | `timeEntries[].psaProjectId` | number | no | The numeric key of the PSA project the entry belongs to. If omitted, it is derived from projectActivityId. | | `timeEntries[].notes` | string | no | Free-form notes for the entry. Required when the project's settings require notes. | Example: ```json { "timeEntries": [ { "date": "2026-08-04", "hours": "7.5", "projectActivityId": 7001, "memberId": 5001, "notes": "Sprint 14 backend work" } ] } ``` ## Response `200` Success Under `data` (createTimeEntriesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `created` | array of object | yes | The created entries, in input order. Each is an id only. Read an entry back with GET psa/time-entries/:id. | | `created[].id` | string | yes | The unique identifier for the created time entry. | ## 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 | | --- | --- | | `ACTIVITY_NOT_FOUND` | 400 | | `DUPLICATE_TIME_ENTRY` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: ACTIVITY_NOT_FOUND, DUPLICATE_TIME_ENTRY - `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 --- # Updates the hours or notes of time entries. `PATCH /psa/time-entries` Group: PSA › Time Entries · Operation ID: `updateTimeEntries` Corrects the hours or notes of existing time entries and changes nothing else. Only the fields you supply for an entry change, and an omitted field is left as it was. If any id matches no time entry, the request fails with NOT_FOUND and nothing is changed. Hours and notes are the only fields you can change. A time entry is identified by who logged the time, when, and against what, so to move time to another date, member or activity you delete the entry and create a new one. The status is not settable here either: it moves through POST psa/time-entries/submit, approve and reject. Both hours and notes are content an approver signed off on, so correcting an approved entry resets its approval and returns it to draft, and it must be submitted and approved again. If any target entry has been invoiced, the request fails with ENTRY_INVOICED and nothing is changed. An entry counts as invoiced by its status or by a non-null invoiced-hours value, so an entry part-way through invoicing is rejected too. A correction also enforces the project's time rules (required notes, no time on weekends or holidays, and per-member limits on hours per day, week and month) and checks that the team is active. These failures are reported as generic errors, not as named diagnostics. [Open in the interactive reference](https://docs.api.lensing.ai/?op=updateTimeEntries) ## 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`: updateTimeEntriesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `timeEntries` | array of object | yes | The time entries to update. | | `timeEntries[].id` | string | yes | The unique identifier of the time entry to update. | | `timeEntries[].hours` | string (decimal) | no | New total hours for the entry. Omit to leave it unchanged. | | `timeEntries[].notes` | string | no | New free-form notes. Omit to leave them unchanged, or pass an empty string to clear them. | Example: ```json { "timeEntries": [ { "id": "9b1c7f42-3d8a-4e05-b6c1-27fa5e903d18", "hours": "7.5" } ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `ENTRY_INVOICED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, ENTRY_INVOICED - `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 --- # Returns the time entry with the given identifier. `GET /psa/time-entries/{id}` Group: PSA › Time Entries · Operation ID: `getTimeEntry` Returns one time entry, scoped to projects where you are a team member. Its fields are the same as a row of GET psa/time-entries. [Open in the interactive reference](https://docs.api.lensing.ai/?op=getTimeEntry) ## 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`. | | `id` | path | string | yes | The unique identifier for the time entry to return. | ## Response `200` Success Under `data` (getTimeEntryOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The unique identifier for the time entry. | | `date` | string (date) | no | The date the time was logged for. Format YYYY-MM-DD. | | `hours` | string (decimal) | no | The total hours logged in this entry. | | `billableHours` | string (decimal) | no | The hours classified as billable to a customer, derived from projectActivityCategory. | | `nonBillableHours` | string (decimal) | no | The hours classified as non-billable to a customer, derived from projectActivityCategory. | | `internalValuableHours` | string (decimal) | no | The hours classified as internal valuable work, derived from projectActivityCategory. | | `internalNonValuableHours` | string (decimal) | no | The hours classified as internal non-valuable work, derived from projectActivityCategory. | | `invoicedHours` | string (decimal) | no | The hours that have been invoiced, a subset of billableHours. | | `revenue` | string (decimal) | no | The revenue from this time entry, in the currency given by currency. | | `cost` | string (decimal) | no | The cost of this time entry, in the currency given by currency. | | `currency` | string | no | The currency of revenue and cost. Format ISO 4217, for example 'USD'. It is the currency of the project position's bill rate. | | `notes` | string | no | Free-form notes on the time entry. | | `submissionStatus` | string | no | The submission and approval status of the entry. Possible values: 'draft', 'pending', 'approved', 'rejected', 'invoiced'. | | `submissionDate` | string (date) | no | The date the entry was submitted for approval. Format YYYY-MM-DD. | | `month` | string (date) | no | The first day of the month the entry is reported under. Format YYYY-MM-DD. | | `memberId` | number | no | The numeric key of the PSA member assignment the time was logged for. GET psa/team-members/:id takes the member's UUID, not this key, so it cannot fetch the member. | | `memberEmployeeId` | number | no | The numeric key of the employee who logged the time. There is no endpoint for it. | | `memberEmployeeName` | string | no | The name of the employee who logged the time. | | `projectActivityId` | number | no | The numeric key of the activity the time was logged against. GET psa/activities/:id takes the activity's UUID, not this key, so it cannot fetch the activity. | | `projectActivityName` | string | no | The name of the activity the time was logged against. | | `projectActivityCategory` | string | no | The billability category of the activity the time was logged against. The billable, non-billable and internal-hour fields are derived from it. See category on GET psa/activities/:id. | | `projectPositionId` | number | no | The numeric key of the position the time was logged against. GET psa/positions/:id takes the position's UUID, not this key, so it cannot fetch the position. | | `projectPositionName` | string | no | The name of the position the time was logged against. | | `psaProjectId` | number | no | The numeric key of the PSA project the entry belongs to. GET psa/projects/:id takes the project's UUID, not this key, so it cannot fetch the project. | | `projectName` | string | no | The name of the PSA project the entry belongs to. | | `psaProjectStatus` | string | no | The status of the PSA project this entry belongs to. | | `lastModifiedDate` | string (date-time) | no | The date and time the entry last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. | ## Errors | Code | Status | | --- | --- | | `NOT_FOUND` | 400 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND - `401` — Missing or invalid access token - `403` — Not authorized - `404` — Not found - `429` — Too many requests - `500` — Internal server error --- # Approves submitted time entries. `POST /psa/time-entries/approve` Group: PSA › Time Entries · Operation ID: `approveTimeEntries` Approves submitted time entries, setting their status to approved. Only an entry in the 'pending' status can be approved. An entry in any other status fails with ENTRY_STATUS_INVALID, which reports each entry's current status, and nothing is approved. The request fails if the id list is empty, and fails with NOT_FOUND if any id matches no time entry. [Open in the interactive reference](https://docs.api.lensing.ai/?op=approveTimeEntries) ## 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`: approveTimeEntriesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the time entries to approve. | Example: ```json { "ids": [ "9b1c7f42-3d8a-4e05-b6c1-27fa5e903d18" ] } ``` ## Response `200` Success Under `data` (approveTimeEntriesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `approved` | array of object | yes | The entries whose status was set to approved. | | `approved[].id` | string | yes | The unique identifier for the entry. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `ENTRY_STATUS_INVALID` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, ENTRY_STATUS_INVALID - `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 --- # Deletes the time entries with the given identifiers. `POST /psa/time-entries/delete` Group: PSA › Time Entries · Operation ID: `deleteTimeEntries` Deletes the time entries you name and nothing else. If any id matches no time entry, the request fails with NOT_FOUND and nothing is deleted. If any target entry has already been invoiced, it fails with ENTRY_INVOICED and nothing is deleted. A delete also fails, with a generic error, when an entry belongs to a project whose team is not active. [Open in the interactive reference](https://docs.api.lensing.ai/?op=deleteTimeEntries) ## 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`: deleteTimeEntriesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the time entries to delete. | Example: ```json { "ids": [ "9b1c7f42-3d8a-4e05-b6c1-27fa5e903d18" ] } ``` ## Response `200` Success ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `ENTRY_INVOICED` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, ENTRY_INVOICED - `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 --- # Rejects submitted time entries. `POST /psa/time-entries/reject` Group: PSA › Time Entries · Operation ID: `rejectTimeEntries` Rejects submitted time entries, setting their status to rejected. Only an entry in the 'pending' status can be rejected. An entry in any other status fails with ENTRY_STATUS_INVALID, which reports each entry's current status, and nothing is rejected. The request fails if the list is empty, and fails with NOT_FOUND if any id matches no time entry. Only the rejectionReason of the first entry in the request is applied, and it is written into the notes field of every rejected entry. [Open in the interactive reference](https://docs.api.lensing.ai/?op=rejectTimeEntries) ## 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`: rejectTimeEntriesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `timeEntries` | array of object | yes | The time entries to reject. | | `timeEntries[].id` | string | yes | The unique identifier of the time entry to reject. | | `timeEntries[].rejectionReason` | string | yes | The reason for rejection. Only the value on the first entry is used, and it overwrites the notes field of every rejected entry. | Example: ```json { "timeEntries": [ { "id": "9b1c7f42-3d8a-4e05-b6c1-27fa5e903d18", "rejectionReason": "Hours exceed activity budget." } ] } ``` ## Response `200` Success Under `data` (rejectTimeEntriesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `rejected` | array of object | yes | The entries whose status was set to rejected. | | `rejected[].id` | string | yes | The unique identifier for the entry. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `ENTRY_STATUS_INVALID` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, ENTRY_STATUS_INVALID - `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 --- # Submits draft time entries for approval. `POST /psa/time-entries/submit` Group: PSA › Time Entries · Operation ID: `submitTimeEntries` Submits time entries for approval, setting their status to pending and stamping submissionDate. The lifecycle is draft, then pending, then approved, then invoiced, with rejected branching off pending. Only an entry that has not been through approval can be submitted: one that is draft, or rejected so a corrected entry can be sent again. An entry that is already pending, approved or invoiced fails with ENTRY_STATUS_INVALID, which reports each entry's current status, and nothing is submitted. Resubmitting does not undo an approval: an approved entry is never returned to pending. To change an approved entry, correct it with PATCH psa/time-entries, which resets its approval and returns it to draft. The request fails if the id list is empty, and fails with NOT_FOUND if any id matches no time entry. [Open in the interactive reference](https://docs.api.lensing.ai/?op=submitTimeEntries) ## 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`: submitTimeEntriesInput. | Field | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of string | yes | The unique identifiers of the time entries to submit for approval. | Example: ```json { "ids": [ "9b1c7f42-3d8a-4e05-b6c1-27fa5e903d18" ] } ``` ## Response `200` Success Under `data` (submitTimeEntriesOutput): | Field | Type | Required | Description | | --- | --- | --- | --- | | `submitted` | array of object | yes | The entries whose status was set to pending. | | `submitted[].id` | string | yes | The unique identifier for the entry. | ## 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 | | --- | --- | | `NOT_FOUND` | 400 | | `ENTRY_STATUS_INVALID` | 400 | | `IDEMPOTENCY_KEY_CONFLICT` | 409 | | `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 | ## Statuses - `400` — Invalid request or business failure. Codes: NOT_FOUND, ENTRY_STATUS_INVALID - `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