> ## Documentation Index
> Fetch the complete documentation index at: https://docs.arcuserp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Change a return line's reason

> Changes one line's reason. `reason_code` becomes the line's reason, one of the reason codes; `reason_words` says why, kept as an internal note of the return and never shown to the customer (required for `other`). Sent alone, `reason_words` adds the note and the line's reason stays. While the return is open its automatic fees follow the new reason (a fee a person set stays); a settled return keeps its figures. Once a credit, a debit, a credit memo or a refund of the return is issued the reason is locked: a change is refused 409 `reason_locked_after_money` (add a note to explain one instead). A changed reason is written on the return's activity with the line's reason before and after, under the key's creator. Requires the `returns:write` scope and the `returns.edit` permission for the key's creator. The return's own reason changes with `PATCH /returns/{id}`.



## OpenAPI

````yaml /openapi.yaml patch /returns/{id}/items/{item_id}
openapi: 3.1.0
info:
  title: Arcus ERP Public API
  version: 1.0.0
  description: >
    Arcus ERP public REST API. Designed for external integrations and data
    migration.


    **Authentication.** Bearer token (API key) via the `Authorization` header.

    Format: `Authorization: Bearer ark_live_ent_<code>_<random>` (or
    `ark_test_*` for sandbox).

    API keys are issued per-entity in **Settings > Developers > API Keys**.


    **Entity scoping.** The entity is encoded in the API key prefix; routes are
    flat

    (e.g. `/v1/accounts`, `/v1/orders`, `/v1/products`). A small set of platform
    endpoints

    (migration, reconciliation, events, webhook endpoints, API keys) use the

    `/v1/entities/{entity_id}/...` form -- those are noted in their tags.


    **Key capabilities.**
      - Related-resource hydration via `?expand[]=` (see `x-arcus-expand` on each resource).
      - Cursor-based pagination (`starting_after` / `ending_before` / `limit`).
      - Idempotency via the `Idempotency-Key` header.
      - Webhook events for asynchronous notification.
      - Conditional requests / ETag for cache validation.

    **Changelog.** Entries are dated and name every published contract whose
    MEANING moved, not

    only the ones whose field names changed. The narrative version of the same
    entries, written

    for integrators, is published at https://arcuserp.mintlify.app/changelog.


    **October 2026: the return list's location filter reads a return's own
    return location.** `GET /v1/returns` with

    `X-Location-Id` (or `location_id`) scopes a return by its own return
    location when one is set, else by its

    originating order's location, the way the returns list in the app scopes it:
    a return whose goods come back to

    another warehouse than its order shipped from is listed at that warehouse
    and no longer at the order's. A return

    with neither stays visible at every location. The Return now documents
    `return_location_id` and

    `return_location_source`, which every return read already answered, and
    `original_order_id` as nullable (a return

    that names no order).


    **October 2026: the adjustment and location pages name the refusals of the
    locations the returns own.**

    `POST /v1/inventory/adjustments` and `POST /v1/adjustments` (adjustment_type
    inventory) name what they answer at the

    entity's system Holding or Write-off: 422 `location_owned_by_documents` for
    a decrease (`owner`, `location`, `holders`

    and `units_held` name the returns that hold the product there), 422
    `system_location_note_required` for an increase

    without a reason and a note, and 422
    `system_location_override_reason_required` and 403

    `system_location_override_requires_user` for `override` (adjusting anyway is
    a signed-in person's act, never an API

    key's). `POST /v1/locations` names 422 `system_location_exists` (a second
    Holding), and `PATCH /v1/locations/{id}`

    names 422 `system_location_exists` (a second Holding or Write-off),
    `system_location_type_fixed` and

    `holding_location_holds_units`. A new entity's default adjustment reasons
    damage, writeoff and other require a

    note (`adjustment_note_required`). The operations already answered these;
    the pages now say so.


    **October 2026: an API key writes on a return only a note it can read
    back.** `POST /v1/notes` on a return or a

    return line (`parent_type` return or return_item) answers 422
    `note_internal_on_return` unless the body sends

    `is_internal` false: a body that left it out used to write an internal note,
    which no API key can list, read, edit

    or delete. `PATCH /v1/notes/{id}` answers the same refusal for `is_internal`
    sent as anything but false on a note of a

    return or a return line. Notes on every other parent are unchanged. The
    create's request body names `content`, the

    field it always read (it listed `body`), and its 201 answer is the note
    itself; the `Note` schema no longer lists

    `object`, which no note answer carries.


    **October 2026: a return read with an API key names no member of staff.**
    `GET /v1/returns/{id}` and the operations

    that answer the return as it reads (`POST /v1/returns/{id}/receive`,
    `/inspect`, `/disposition`,

    `/receive-and-dispose`, `/unreceive`, `/receipts/{receipt_id}/unreceive`,

    `/receipt-items/{receipt_item_id}/repair-stock` and `/return-location`) no
    longer answer `voided_by_name`, the name of

    the person who voided the return, which no page of this reference listed. An
    answer replayed for an

    `Idempotency-Key` leaves it out too. Nothing else in these answers changes.


    **October 2026: a return's journal entries, and which of them were reversed,
    on the public API.**

    `GET /v1/journal-entries/expand` takes `for_return`, a return's id: the rows
    are every journal entry the return caused

    and every reversal of any of them, oldest first, and each row answers
    `reversed_by` (the `id`, `entry_number` and

    `entry_date` of the entry that reversed it, or null) and `reversed_at` (when
    that entry posted, or null). A value that

    is not a UUID is refused 400 `invalid_id_format`. The page now documents its
    rows as they are answered: journal entry

    headers without their lines, and the list's `next_cursor` and `url`.


    **October 2026: a return says what the restocking fee bounds held.** `GET
    /v1/returns/{id}` answers `restocking_bound`

    when the minimum or maximum restocking fee (Settings > Returns) holds the
    return's computed restocking fee: `code`

    (`restocking_fee_above_entity_max` or `restocking_fee_below_entity_min`),
    `entity_fee_min`, `entity_fee_max`, the fee

    before the bound (`from`) and after it (`to`), `source`, and `words`, the
    sentence to show, for example "Restocking fee

    $15.00, limited to the entity maximum of $10.00 (Settings > Returns)." It is
    absent when the bounds hold nothing. A fee

    line of `POST /v1/returns/{id}/fee-intents` carries the same object. The
    page of `GET /v1/returns/{id}` now documents its

    answer as it is given: the return under `data`.


    **October 2026: a return's receipt is previewed, undone and repaired, and
    its location changed, on the public API.**

    `GET /v1/returns/{id}/receive-preview` shows where a receipt by product
    would place its units and every refusal the

    receive would answer for it, writing nothing. `POST
    /v1/returns/{id}/receipts/{receipt_id}/unreceive` takes units of

    one receipt back out of Holding (its `items`, or every unit of it not put
    away) and

    `GET /v1/returns/{id}/receipts/{receipt_id}/unreceive-preview` shows what it
    would take back (`items`, `materialized`,

    `receive_key_admits`, `journal_entries`); `POST /v1/returns/{id}/unreceive`
    does it for every receipt of the return.

    Each takes `reason` (10 characters or more) and `request_key` (a retry
    answers the first act; another request under

    the key is refused 409 `request_key_reused`), refuses units already put away
    (422 `units_already_decided`, or

    `disposition_present`) and units no longer in Holding (422
    `units_not_in_holding`), and answers 409

    `unreceive_conflict_retry` when another change to the same stock was saved
    at the same moment.

    `POST /v1/returns/{id}/receipt-items/{receipt_item_id}/repair-stock`
    resolves a receipt recorded without stock

    (`mode` `book_missing_stock` or `reverse_unbacked`, with a count `note`, 422
    `count_note_required`), and

    `POST /v1/returns/{id}/return-location` sets the warehouse the goods come
    back to (`location_id`, and a `reason` once

    goods are received; 422 `return_location_not_a_warehouse`, 409
    `return_not_open` and `return_location_read_only`).

    The writes take the `returns:write` scope and the `Idempotency-Key` header;
    the previews `returns:read`.


    **October 2026: the receipt, grade and put-away pages of a return say what
    the goods steps answer.**

    `POST /v1/returns/{id}/receive` books every unit into the entity's Holding
    location (`receiving_location_id` names the

    warehouse the goods arrived at); an entity without exactly one active
    Holding location is refused 422

    `holding_location_missing`, and `no_receiving_location` is no longer given,
    here or on

    `POST /v1/returns/{id}/receive-and-dispose`. The receive takes a receipt by
    product (`by_product`, with `split`,

    `split_reason`, `allocation_note_id`, `serial_numbers` and `entered_costs`),
    a line's `serial_numbers` and

    `entered_cost`, and `client_request_id`; a further lot on a return that
    already holds received units is refused 400

    `rma_already_received` only when it carries no `client_request_id` and no
    `Idempotency-Key`. Both receive operations

    name 409 `return_order_set_changed`: the orders the return touches changed
    while it was being locked, and nothing was

    written. `POST /v1/returns/{id}/receive-and-dispose` takes `force_restock`
    with `force_restock_reason` (422

    `force_restock_reason_required` without the reason). `POST
    /v1/returns/{id}/inspect`, `/disposition` and

    `/quarantine/resolve` document the grade, put-away and hold bodies they read
    and every refusal they answer, and

    `POST /v1/returns/{id}/exchange` and `/advance-replacement` name 422
    `return_has_no_account` and

    `tax_exempt_requires_exemption_code`.


    **October 2026: a return's reason, and a line's, change on the public API.**
    `PATCH /v1/returns/{id}` takes

    `reason` (or `reason_code`), one of the reason codes, and `reason_words`:
    the lines that carried the return's

    reason follow it, the words are kept as an internal note of the return, and
    while the return is open its

    automatic fees follow; the answer carries `reason_change`. `PATCH
    /v1/returns/{id}/items/{item_id}` (scope

    `returns:write`, the `Idempotency-Key` header honoured) changes one line's
    `reason_code` the same way. A reason that

    is not one of the codes is refused 422 `invalid_reason_code`, the reason
    `other` with nothing saying why 422

    `reason_words_required`, words over 2,000 characters 422 `note_too_long`,
    and a change once a credit, a debit, a

    credit memo or a refund of the return is issued 409
    `reason_locked_after_money` (`meta.issued` names each).


    **October 2026: a return's fees change one at a time, and the update's fee
    array is deprecated.**

    `POST /v1/returns/{id}/fee-intents` (scope `returns:write`, the
    `Idempotency-Key` header honoured) takes

    `{ intents, expected_updated_at }`: each intent picks a saved fee rule, adds
    a one-off fee or credit with its

    reason, sets a line's amount or decides its tax, waives a line or removes
    one, and the return's fee plan after

    the change is the answer; `POST /v1/returns/{id}/fee-intents/preview`
    answers the same and writes nothing. A save

    made from an older copy of the return is refused 409 `rma_changed`, a
    closed, voided or cancelled return 409

    `fees_locked_*`, a change the fee plan refuses 422 with `meta.offers` (403
    `fee_tax_override_not_permitted` for a

    tax decision without `returns.override_condition`). On `PATCH
    /v1/returns/{id}`, `return_adjustments` is

    deprecated (paired with the stored lines by key and translated into fee
    intents) and answered with a

    `Deprecation: true` header; the update also takes `adjustment_reason`,
    `restocking_fee_reason` and

    `expected_updated_at` (409 `rma_changed`), refuses `adjustment_lines` with
    422 `return_adjustments_use_fee_intents`,

    and its page says that `notes` and `metadata` are stored nowhere today.


    **October 2026: the return cancel page names every refusal it answers.**
    `POST /v1/returns/{id}/cancel` now

    documents its 422: a return a mis-shipment correction created is refused
    `misshipment_correction_open` while the

    wrong item is still held for it and `misshipment_correction_written_off`
    after its hold was written off, with the

    held units, the reopened order line and the three choices as fields (choose
    one with

    `POST /v1/returns/{id}/misshipment-intent` instead). Its 409 now names
    `spawned_order_open` (an exchange or

    advance-replacement order the return created is still open; `order_id`,
    `order_number` and `door` name it and

    what comes first), and says that a return in a status a cancel does not take
    is refused 409 with no `code`. The

    page says which statuses a cancel takes (pending approval, authorized,
    expected) and that it moves no stock; it

    no longer names a `draft` status, which a return does not have.


    **October 2026: a mis-shipment correction's preview no longer uses up its
    Idempotency-Key.** On

    `POST /v1/orders/{id}/correct-misshipment`, a body with `preview: true` is
    answered without reading the

    `Idempotency-Key` header, and its answer is never stored: the correction
    sent next with the same key is made

    (before, the preview's answer was stored under the key, so that correction
    was refused 409

    `idempotency_key_collision` and nothing was corrected), and a preview sent
    again reads the order anew instead of

    replaying the first one. Every other body is keyed as before. The page no
    longer says a repeated call is safe

    without the key: the same correction sent again without an `Idempotency-Key`
    is a further correction while the

    line still has shipped units, and once none is left it is refused 422
    `quantity_exceeds_shipped`, now named there.


    **October 2026: a return a mis-shipment correction created says what
    replaces a void of it.**

    `GET /v1/returns/{id}/misshipment-intent-preview` (scope `returns:read`)
    reads what the return still holds and

    what each of three choices would book; `POST
    /v1/returns/{id}/misshipment-intent` (scope `returns:write`, the

    `Idempotency-Key` header honoured) makes one of them: `no_replacement`
    closes the reopened units short on the order

    and takes back the discount and tax booked when they shipped (the answer's
    `taken_back` and `books`; nothing is

    refunded), `replacement_later` keeps the return open, and `not_coming_back`
    writes the held wrong item off. A choice

    the return's state does not allow is refused 409
    `misshipment_intent_unavailable`, its reason in `hint`.


    **October 2026: a restocking fee the return computes is held to the entity's
    bounds instead of refused.** On

    `POST /v1/returns/{id}/disposition`, `POST
    /v1/returns/{id}/receive-and-dispose` and `POST /v1/returns/{id}/refund`,

    a restocking fee computed from a percentage that falls outside the entity's
    configured minimum or maximum restocking

    fee is charged at that bound, and the return's fee line names the bound it
    was held to; the 422 answers

    `restocking_fee_below_minimum_at_disposition` and
    `restocking_fee_above_maximum_at_disposition` are no longer given.


    **October 2026: the reason Other says why.** `POST /v1/returns` and `POST
    /v1/returns/{id}/items` take

    `reason_words`. A return or a line whose reason is `other` with nothing
    saying why is refused 422

    `reason_words_required` (a `reason` sent as words counts, and so do words
    the return already holds when lines are

    added); words over 2,000 characters are refused 422 `note_too_long`. The
    words are kept as an internal note of the

    return.


    **October 2026: a line refused for where it belongs names its order, a
    return that refunds nothing is not refused

    for a cash refund, and a fee line cannot mark itself as a rule's.** On `POST
    /v1/returns` and

    `POST /v1/returns/{id}/items`, `multi_order_not_enabled` reads "Each return
    covers one order. Make a separate return

    for <the line's order number>.", `channel_orders_mixed` and
    `mixed_return_destinations` name the line's channel and

    where it ships back, and none of the three starts its hint with the line's
    position (`param` and `index` still name

    it). `POST /v1/returns` with `refund_method` `cash` on an `exchange`,
    `advance_replacement` or `warranty_repair`

    return of an order that holds no payment while an invoice of it is open is
    created with `credit_memo` instead of

    the 422 `nothing_paid_credit_the_invoice`. A `return_adjustments` line no
    longer marks itself as a rule's line: on

    `POST /v1/returns` its `_auto_rule`, a `coupon_id` beside a typed `value`
    and the rule values are ignored and it is

    judged as a typed fee (`fee_exceeds_goods`, `fee_reason_required`); on
    `PATCH /v1/returns/{id}` a line keeps them

    only as the return's own stored line, sent back as it stands.


    **October 2026: a line that makes a return span orders meets the create's
    checks, a return on an invoice never paid

    credits the invoice, and a return's drop-ship destination is its lines'.**
    `POST /v1/returns/{id}/items` answers the

    create's refusals of a return across orders when the added lines make the
    return span orders: 422

    `return_location_required` while the return names no return location and its
    lines shipped from more than one

    warehouse, 422 `refund_method_required_multi_order` while its stored refund
    method is `auto`, and 422

    `multi_order_tax_filing_not_ready`; before, such lines were added. `PATCH
    /v1/returns/{id}` refuses `refund_method`

    `auto` on a return whose lines span orders (422
    `refund_method_required_multi_order`). On `POST /v1/returns` and

    `PATCH /v1/returns/{id}`, `refund_method` `cash` on an order that holds no
    payment while an invoice of it is open is

    refused 422 `nothing_paid_credit_the_invoice`, naming the invoice and the
    open balance; `credit_memo` (the credit

    against the open invoice) and `store_credit` are accepted, and a create that
    sends no method where the entity's

    default is `cash` is created with `credit_memo`. A return of a line that
    ships back to the warehouse is no longer

    marked as shipping back to a drop-ship vendor because another line of its
    order was drop-shipped: a return's

    destination is its lines'.


    **October 2026: a return credits a service line instead of returning it,
    carries one reason for every line, and

    works out the tax on the shipping it gives back.** On `POST /v1/returns` and
    `POST /v1/returns/{id}/items`, a line

    of a service (a product whose type is service, which moves no stock) is
    credited with `items[].credit_only: true`

    and no quantity: it takes the units not yet credited, its credit is its net
    and its tax, and nothing is received.

    A service line sent as goods is refused 422 `service_line_not_returnable`,
    `credit_only` on a line whose goods come

    back 422 `credit_only_not_a_service_line`, and a service line with nothing
    left to credit 422

    `service_line_already_credited`; before, a service line was taken as goods
    coming back. A line of an order that a

    mis-shipment correction re-ships is refused 422 `misshipment_expects_units`
    while that correction's return still

    expects the item shipped in its place. `POST /v1/returns` reads
    `reason_code` (and `reason` when it is one of the

    reason codes) as the reason of every line that sends none. `shipping_refund`
    is capped at the shipping the customer

    paid and has not been given back, and its tax is worked out from the order;
    a `shipping_refund_tax` sent is no

    longer stored. The hints of `return_location_required`,
    `refund_method_required_multi_order` and

    `multi_order_tax_filing_not_ready` read as sentences with no field names.


    **October 2026: free text on an inventory adjustment posts from the reason
    its words mean, and every adjustment

    posts to the income statement.** On `POST /v1/inventory/adjustments`, a
    request (or an item) that sends free text

    `reason` and no `reason_code` or `adjustment_reason_id` now posts from the
    entity's adjustment reason its words

    mean: the text names a reason code of the entity exactly, or its words match
    a reason (damage, cycle count,

    write-off, revaluation, found stock), and text that matches none is the
    Other reason. The adjustment posts to the

    account that reason names for the movement's direction, and the answer names
    the reason and the account

    (`adjustment_reason`, `gl_account_source` `free_text`, `posted_to`). A
    reason with no account for the direction is

    refused 422 `adjustment_reason_needs_account` with `param` `reason` (or
    `items[n].reason`), and nothing is written.

    A reason that requires a note takes the text sent in `reason` as the note; a
    request that sends neither text nor a

    note (`notes`, or a multi-item item's `notes` or the root `memo`) is refused
    422 `adjustment_note_required` with

    `param` `notes` (or `items[n].notes`) on both the single-item and the
    multi-item request.

    The account an adjustment posts to is always an income-statement account
    (cost of goods, expense, other income or

    other expense); any other is refused 422
    `adjustment_account_not_income_statement`. Before, text that matched no

    rule posted to the entity's inventory adjustments holding account on the
    balance sheet. The movement's notes and the

    entry's description still carry the text exactly as sent. This replaces the
    deprecation note below: free text no

    longer chooses an account by itself.


    **October 2026: an inventory adjustment names its reason, a key never names
    a document's uploader, and the flag

    refusals are documented.** `POST /v1/inventory/adjustments` accepts
    `reason_code` (or `adjustment_reason_id`) on a

    single-item request, at the root of a multi-item request (the reason of
    every item that sends none of its own) and

    on each item. The adjustment posts to the account the entity's adjustment
    reason names for the movement's direction,

    exactly as the adjustment screen posts it, and the answer names the reason
    and the account (`adjustment_reason`,

    `gl_account_source`, `posted_to`). A code that names none of the entity's
    active reasons is refused 422

    `adjustment_reason_not_found` with `param` and the active codes in
    `allowed_codes`, and nothing is written; a

    turned-off reason is refused 422 `adjustment_reason_inactive`; a key cannot
    choose an account other than the

    reason's (403 `adjustment_account_choice_requires_user`, or 422 with `param`
    `gl_account_id` for an account the

    adjustment picker does not offer) and cannot post above a reason's approval
    limit (403

    `adjustment_over_approval_threshold`). Before, `reason_code` was not read:
    such an adjustment, and a multi-item

    item's `adjustment_reason_id`, posted where the free text sent them.
    Deprecated: choosing the account through the

    free-text `reason`, which picks it by matching words in the text (text that
    matches none posts to the entity's

    inventory adjustments account); free text keeps choosing the account as it
    does today, and no removal date is set. A

    single-item adjustment answers 200 and a multi-item adjustment 201, as they
    always have (the page listed only 201).

    A retry with the same `Idempotency-Key` now answers the first request's
    response even when a reason, a product, a

    permission or the posting window changed in between; before, such a retry
    could answer a refusal for an

    adjustment that had already been applied. A refusal the adjustment answers
    (a turned-off reason, a product that

    holds no stock, the key creator's permission, the posting window) is
    likewise the answer of its key: once the

    cause is fixed, send the request with a new key.


    `POST /v1/documents` and `POST /v1/documents/confirm` never record a person
    from the request body: a document an API

    key creates names no uploader, and its activity rows name no user (before, a
    `user_id` or `uploaded_by` in the body

    naming an active member of the entity was recorded as that member).


    Fifteen operations answer 422 `invalid_boolean_field`, with `param` naming
    the field and nothing written, when a

    flag is not a JSON `true` or `false`, and each page now says so, naming the
    flags and the values it refuses:

    `POST /v1/locations`, `PATCH /v1/locations/{id}`, `POST` and `PATCH
    /v1/payment-terms`,

    `PATCH /v1/gl-accounts/{id}`, `PATCH /v1/accounts/{id}`, `POST
    /v1/accounts/{id}/addresses`,

    `PATCH /v1/accounts/{id}/addresses/{addr_id}`, `PATCH
    /v1/accounts/{id}/contacts/{cnt_id}`, `PATCH /v1/kits/{id}`,

    `POST` and `PATCH /v1/pricing-policies`, `POST /v1/products` (a `pricing` or
    `purchase_policies` entry),

    `PATCH /v1/products/{id}` and `POST /v1/products/{id}/pricing`. The text
    "true" or "false" and 1 are refused on

    all fifteen (on `PATCH /v1/gl-accounts/{id}` when the body carries no
    `opening_balance`), and 0 and null on all

    but `POST /v1/accounts/{id}/addresses`, which reads 0 and null as `false`
    and creates the address.

    `PATCH /v1/gl-accounts/{id}` reads its flag only when the body carries no
    `opening_balance`: a body that carries

    one posts the account's opening balance and reads no other field. The
    request bodies of

    `PATCH /v1/locations/{id}` and `POST /v1/payment-terms`, and the `pricing`
    entries of `POST /v1/products` and

    `POST /v1/products/{id}/pricing`, now list those flags as booleans. `POST
    /v1/locations` lists the fields it reads

    (`code` is required, the address is one `address` object, and `type`,
    `description`, `sort_order` and the four

    flags are listed); `POST /v1/payment-terms` reads `days` and
    `discount_percent`, not `days_due` and

    `discount_pct`, and only `name` is required; a listed field the operation
    does not read now says so.


    **October 2026: the return create and update contract says what the API
    does.** On `POST /v1/returns`,

    `original_order_id` is derived from the lines when it is omitted, and every
    line names the sales order line it

    comes back from (`items[].order_item_id`; a line without one is refused 422
    `order_line_required`, and that

    refusal is already live). Deprecated now, and answered on a 201, 400, 404 or
    422 answer with a `Deprecation: true`

    response header and a `Link` to this changelog (plus a `Sunset` header once
    the removal date is set):

    `auto_refund: true` and `refund_payment_id` (no longer honored: a return
    never refunds on its own; issue the

    refund with `POST /v1/returns/{id}/refund`; at the removal release
    `auto_refund: true` is refused 422

    `auto_refund_removed`; `auto_refund: false` asks for nothing and is not a
    deprecated use, and the field has no

    default), and the standalone return, a line that sends `product_id` and no
    `order_item_id` (already refused; its

    description leaves the spec at the removal release). `refund_method` lists
    the values the API accepts (`auto`,

    `cash`, `credit_memo`, `store_credit`); `original` and `check`, listed
    before, are refused 400

    `invalid_refund_method`. A return line's `condition` lists the values a line
    stores (`new`, `used`, `damaged`,

    `defective`); `like_new`, `good` and `fair` were never sent and leave the
    `ReturnItem` schema one release later.

    Each return operation's page now names, under the status it is answered
    with, the refusals with their own `code`

    that a request sending the fields the page documents can meet, read from the
    operation's code at every call

    depth and checked by running it: the operation's own checks (a location or a
    vendor the request names that is

    not this entity's among them), the steps it runs (the order line, the
    replacement order line, the carrier label,

    the card refund, the vendor-return line) and the ledger and stock refusals
    it can meet through the return's

    state or the entity's settings. Not repeated on every page: the refusals
    every operation shares (the API key,

    its scopes, its creator's permissions, its network rules, the rate limit and
    the `Idempotency-Key` header, whose

    two answers that header's description names); the answers the database gives
    a value its column does not take,

    on any field of the path, the query or the body: 400 `invalid_id_format`
    (not a valid id), 400

    `invalid_enum_value` (not a value of the field's type, text where a number
    belongs among them), 422 `value_too_long`

    (a text longer than the field holds), 422 `value_out_of_range` (a number
    larger than the field holds), 400

    `not_null_violation`, 400 `check_violation`, 400 `constraint_violated`, 409
    `fk_violation` and 409

    `uniq_violation` (a value a rule of the database refuses), and 409
    `serialization_failure` or 409 `deadlock` (a

    parallel update; a retry clears it), which a page also names where its own
    fields were measured to draw one; and

    an integrity refusal, which reports that records the operation reads
    disagree with each other and is answered

    with its own `code`. The two read operations now document their 400 answer,
    and the add-lines, exchange, advance

    replacement and vendor return operations the 201 they answer (their pages
    said 200). The receive-and-dispose

    lines, the exchange body and the advance replacement lines now list the
    fields of the contract those operations

    read for a caller (receive-and-dispose also reads
    `items[].serial_number_returned`, which is not yet listed); the

    disposition line lists `vendor_id`, and its `condition` lists the four
    values a return line stores (`like_new`,

    `good` and `fair` are still accepted there on a line that sends no `leaves`,
    where they never block a restock, and

    leave the list one release later; a kit line that sends `leaves` refuses
    them 400 `invalid_enum_value`); a

    documented field an operation does not read now says so.
    `estimated_refund_amount`, `money_suppression` and

    `is_pre_settlement` say what the API already did: the estimate is present
    while neither the lines nor the header

    hold a credit (the schema said "while no line holds a credit"). A bulk
    import with `conflict_mode: replace` on

    returns now answers 409 `return_replace_not_supported` naming the existing
    return and writes nothing

    (`MigrationConflictMode` says so); before, it removed the import record from
    the existing return and created a

    second return for the same goods. The webhook endpoint's event list counts
    153 events in 15 families. The

    correction documents, the refile, the returns of an order, the receive
    order, the unexpected arrival and the

    staff RA PDF stay staff-only for now: they have no api-v1 endpoint in this
    release.


    Returns: override_window now needs override_window_reason when a line is
    outside its return window; returns can

    span several orders of one customer where Settings > Returns allows it. On
    `POST /v1/returns`, `return_location_id`

    names the warehouse the goods come back to (needed when the lines span
    orders shipped from more than one

    warehouse), a return across orders names its `refund_method` (`cash`,
    `credit_memo` or `store_credit`), and a sent

    `original_order_id` must be the order of one of the lines; `POST
    /v1/returns/{id}/items` reads `override_window`

    and `override_window_reason` for a line of an order new to the return. The
    pages name the refusals:

    `original_order_not_among_lines`, `return_location_required`,
    `return_location_not_a_warehouse`,

    `refund_method_required_multi_order`, `multi_order_tax_filing_not_ready`,
    `override_window_reason_required`,

    `approval_required_for_added_lines`, and 404 `location_not_found` for a
    `return_location_id` that is not one of

    the entity's locations.


    **October 2026: credit memos in search, and the cross-resource search
    answers by scope.**

    `GET /v1/search/{object}` accepts `credit_memos`: customer credit memos by
    memo number or customer

    name, the exact memo number first, each result naming the memo's status, its
    customer and its return

    when it has one; it requires the `ar:read` scope in addition to
    `search:read` (a key without it is

    refused with 403 `insufficient_scope`), and its cursor (`starting_after`)
    continues exactly where the

    previous page ended. `GET /v1/search` now returns matches: each of its five
    buckets holds results when

    the key also holds that resource's read scope (`orders:read` for orders and
    invoices, `products:read`

    for products, `accounts:read` for accounts, `inventory:read` for serial
    numbers) and stays empty

    otherwise; until now every bucket was empty for every key.
    `meta.did_you_mean` now carries the search

    term when the results are close matches rather than exact ones. No field is
    renamed.


    **October 2026: the order status values are the ones the API sends.** The
    `Order` schema's

    `order_status`, `payment_status`, `fulfillment_status` and `document_type`
    now list the values

    the order endpoints actually return (`order_status`: draft, open,
    processing, fulfilled,

    cancelled, archived, expired, awaiting_ach_clearance; `payment_status`:
    unpaid, partial,

    processing, paid, refunded, terms, overdue, overpaid, partially_refunded;
    `fulfillment_status`:

    unfulfilled, partial, fulfilled, backordered, awaiting_pickup;
    `document_type` adds

    late_fee_invoice). The values themselves did not change; the schema had
    listed names the API

    never sends (`confirmed`, `partially_paid`, `partially_fulfilled`, `voided`,
    `on_hold`), so a

    client generated from it could reject a real order. On `POST /v1/orders`,
    `order_status` is

    limited to `draft` only for a `sales_order` or `invoice`. The refusals of
    the order, refund,

    settings and deposit endpoints are now listed with their `code` values on
    each response.


    **October 2026: a pair of bank lines in transit at a month end is an open
    item there.** `GET /v1/entities/{entity_id}/reconciliation/account-audit`,
    `answers.books_against_bank`,

    each `month_ends` row: when the two bank lines of a pair matched together to
    one bill payment (a

    wire and its return) fall on either side of that date,
    `bank_unbooked_amount` and

    `bank_unbooked_count` now include the line already in the bank, and
    `gl_uncleared_amount` and

    `gl_uncleared_count` include the payment's bank leg;
    `explained_by_open_items` therefore explains

    that date instead of leaving the part in transit unexplained. No field is
    renamed and no other

    date moves; once both lines are in the bank the pair counts nowhere, as
    every other match.


    **2026-09-22 (planned 2026-09-21), REORDER-BUYER-TRUTH: demand changed what
    it MEANS on three

    published contracts, with no field renamed.** An integrator that pins field
    names sees no

    breakage and different numbers, which is why this entry exists.

      - **Demand now counts build consumption.** `demand_avg_per_day` on the public product, kit
        and inventory-balance objects, and `daily_demand` / `demand_basis` / `net_suggested_qty`
        on `GET /v1/purchasing/reorder-report`, are composed from fulfilled sales lines PLUS
        posted `build_consume` inventory draws: each physical decrement of a product counts
        exactly once. The previous rule adopted an internal-consumption basis only when the sales
        blend was exactly zero, so a product both sold AND consumed into work orders planned as
        if the build draws did not exist. Products drawn into work orders move; on one
        production-shaped dataset five did, the largest from 0.05/day to 14.95/day.
      - **`demand_basis` now carries four values, not two:** `sales`, `sales_and_builds`,
        `builds` and `consumption`. A consumer with a two-branch reader (anything that is not
        `consumption` is `sales`) silently hides the two new ones.
      - **`current_demand_units` on `GET /v1/inventory/balances/:id` moved, by a second rule.**
        It counts committed-but-unshipped CUSTOMER demand, and it now counts a kit component's
        own line rather than its parent kit line, and excludes non-sales documents. On one
        production dataset 269 of 1,043 balance rows changed, 227 of them downward; the largest
        single move was 1,941 to 25, on a product whose open PURCHASE order line had been
        reported as customer demand. Re-baseline anything that alerts or reorders off this field.
      - `run_rate_30/90/180/365` and `blended_daily` on the same balance object move for the
        first reason above.
      - **Added, not changed:** `GET /v1/purchasing/reorder-report` accepts `demand_window` and
        returns the unit, cover-through and reorder-point-provenance fields documented on that
        operation; `order_multiple` is accepted and returned on the product-vendor doors.
      - **Volume note.** The `inventory.low_stock`, `inventory.out_of_stock` and
        `inventory.back_in_stock` webhook events are population-gated on
        `on_hand <= reorder_point`, and reorder points move with the demand above, so
        subscribers should expect a one-time step change in event volume around the release.
servers:
  - url: https://api.arcuserp.com/v1
    description: Arcus ERP API (accepts both live `ark_live_*` and test `ark_test_*` keys)
  - url: https://dev-api.arcuserp.com/v1
    description: >-
      Dev sandbox API (test-only data, accepts `ark_test_*` keys against dev
      RDS)
security: []
paths:
  /returns/{id}/items/{item_id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: item_id
        in: path
        required: true
        description: 'The return line: an `id` of the return''s `items`.'
        schema:
          type: string
          format: uuid
    patch:
      tags:
        - Returns
      summary: Change a return line's reason
      description: >-
        Changes one line's reason. `reason_code` becomes the line's reason, one
        of the reason codes; `reason_words` says why, kept as an internal note
        of the return and never shown to the customer (required for `other`).
        Sent alone, `reason_words` adds the note and the line's reason stays.
        While the return is open its automatic fees follow the new reason (a fee
        a person set stays); a settled return keeps its figures. Once a credit,
        a debit, a credit memo or a refund of the return is issued the reason is
        locked: a change is refused 409 `reason_locked_after_money` (add a note
        to explain one instead). A changed reason is written on the return's
        activity with the line's reason before and after, under the key's
        creator. Requires the `returns:write` scope and the `returns.edit`
        permission for the key's creator. The return's own reason changes with
        `PATCH /returns/{id}`.
      operationId: updateReturnItemReasonV1
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                reason_code:
                  type: string
                  enum:
                    - defective
                    - damaged_in_transit
                    - wrong_item_shipped
                    - not_as_described
                    - no_longer_needed
                    - found_better_price
                    - arrived_late
                    - other
                    - unspecified
                  description: >-
                    The line's new reason. Any other value is refused 422
                    `invalid_reason_code`. Required unless `reason_words` is
                    sent (400 `missing_field`). Sending the reason the line
                    already has changes nothing.
                reason_words:
                  type: string
                  maxLength: 2000
                  description: >-
                    Why, in words, kept as an internal note of the return and
                    never shown to the customer. Required when `reason_code` is
                    `other` (422 `reason_words_required`); words that are only
                    spaces are no words. At most 2,000 characters (422
                    `note_too_long`).
      responses:
        '200':
          description: The line's reason after the edit, and what the edit changed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReturnLineReasonResult'
        '400':
          description: >
            The request is malformed. `code` includes:

            - `invalid_id_format`: the return id or the line id in the path is
            not a valid id (`param` names it).

            - `missing_field`: the body sends no `reason_code` and no words
            (`param: reason_code`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          $ref: '#/components/responses/Error'
        '403':
          description: >
            The key may not make this change; nothing is written. `code`
            includes:

            - `insufficient_scope`: the key does not carry `returns:write`.

            - `permission_denied`: the key's creator does not hold
            `returns.edit` (`permission_key` names it); a key
              with no creator is refused too.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ErrorEnvelope'
                  - type: object
                    properties:
                      message:
                        type: string
                        description: >-
                          On `permission_denied` for a key with a creator: the
                          permission refused, in words.
                      permission_key:
                        type: string
                        description: >-
                          On `permission_denied`: the permission refused
                          (`returns.edit`).
                      role:
                        type: string
                        nullable: true
                        description: >-
                          On `permission_denied` for a key with a creator: the
                          creator's role in this entity (null for a custom
                          role).
        '404':
          description: >
            Not found. `code` includes:

            - `return_not_found`: no return with this id exists in this entity.

            - `return_item_not_found`: the line is not on this return (`param:
            item_id`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '409':
          description: >
            The reason is locked; nothing is written. `code` includes:

            - `reason_locked_after_money`: the request changes the line's reason
            and a credit, a debit, a credit memo or
              a refund of the return is issued (`meta.issued` names each, `param: reason_code`); add a note to explain a
              change instead.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ErrorEnvelope'
                  - type: object
                    properties:
                      meta:
                        $ref: '#/components/schemas/ReturnReasonLockedMeta'
        '422':
          description: >
            The reason cannot be taken as sent; nothing is written. `code`
            includes:

            - `invalid_reason_code`: `reason_code` is not one of the reason
            codes (`param: reason_code`).

            - `reason_words_required`: `reason_code` is `other` and nothing says
            why; send `reason_words`
              (`param: reason_words`).
            - `note_too_long`: `reason_words` is longer than 2,000 characters.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
      security:
        - ApiKeyAuth: []
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >
        Client-generated unique key for idempotent POST/PATCH/DELETE operations.

        Alias for the Idempotency parameter. Max 255 chars. On retry with the

        same key, the original response is returned without re-executing the

        operation. Keys expire after 24 hours. A second request with the same

        key sent while the first is still running waits for it and returns its

        answer; if the first is still running after 20 seconds the second

        answers 409 `idempotency_key_in_use` (retryable, nothing done). An

        answer carrying `retryable: true` is not cached. Reusing a key with a

        different request body answers 409 `idempotency_key_collision` (nothing

        done; send the new request with a fresh key). The replay, the wait and

        these two 409 codes come from the idempotency layer, so they apply to

        the operations that run it, each of which can answer the two codes

        whether or not its own 409 response lists them. The handlers of these

        operations take the header but do not run the layer, so on them the key

        neither replays nor answers either code: createPaymentPlan,

        chargePaymentPlanInstallment, setProductLocations, updateOrder,

        emailOrder, updateOrderItem, confirmPickup, markOrderNoShow,

        applyExistingPaymentToInvoices, updatePackage, movePackageItem,

        changePackageBox, changePackageCarrier, voidPackageLabel,

        printPackageLabel, emailPackage, setProNumberV1, setPackageProNumber,

        sendPurchaseOrderToVendorV1, notifyRevisionPurchaseOrderV1,

        addMaintenanceLog, generateBOLV1,

        createReportJobV1 and customerAcknowledgeRevision;
        recordVendorPrepaymentV1

        and recordCustomerDepositV1 hand the key to their own write instead.
      schema:
        type: string
        maxLength: 255
  schemas:
    ReturnLineReasonResult:
      type: object
      required:
        - data
      properties:
        data:
          type: object
          required:
            - return_item_id
            - reason_code
            - reason_change
          properties:
            return_item_id:
              type: string
              format: uuid
            reason_code:
              type: string
              nullable: true
              description: >-
                The line's reason code after the change (as it stood when only
                words were sent).
            reason_change:
              $ref: '#/components/schemas/ReturnReasonChange'
    ErrorEnvelope:
      type: object
      description: |
        Canonical error response envelope. All API errors use this shape.
      required:
        - error
        - code
      properties:
        error:
          type: string
          description: Machine-readable error key
          example: not_found
        code:
          type: string
          description: Machine-readable error code (often same as error)
          example: not_found
        type:
          type: string
          enum:
            - validation_error
            - permission_error
            - not_found
            - conflict
            - rate_limit
            - internal
            - expand_error
            - not_implemented
          example: not_found
        hint:
          type: string
          description: Human-readable one-sentence explanation (English)
          example: >-
            The requested order does not exist or does not belong to this
            entity.
        param:
          type: string
          description: The parameter that caused the error, if applicable
          example: expand[0]
        required:
          type: string
          description: The scope required (only on insufficient_scope errors)
          example: accounts:read
        request_id:
          type: string
          description: >-
            Unique request ID for support tracing (maps to CloudWatch log
            stream)
          example: req_abc123
    ReturnReasonLockedMeta:
      type: object
      description: >-
        On `reason_locked_after_money`: the money documents of the return that
        lock its reason.
      required:
        - issued
      properties:
        issued:
          type: array
          items:
            type: object
            required:
              - kind
              - number
            properties:
              kind:
                type: string
                enum:
                  - credit
                  - debit
                  - credit_memo
                  - refund
              number:
                type: string
                nullable: true
                description: The document's number, when it has one.
    ReturnReasonChange:
      type: object
      description: >-
        What a reason edit changed: the reason before and after, the lines that
        followed it, the notes its words were kept in, and whether the automatic
        fees followed. Answered by `PATCH /returns/{id}` (beside `data`, when
        the request sends `reason`, `reason_code` or `reason_words`) and by
        `PATCH /returns/{id}/items/{item_id}`.
      required:
        - field
        - changed
        - before
        - after
        - words_note_id
        - kept_words_note_id
        - lines
        - fees
      properties:
        field:
          type: string
          enum:
            - reason
            - items.reason_code
          description: '`reason` for the return''s reason, `items.reason_code` for a line''s.'
        changed:
          type: boolean
          description: >-
            False when the request sent the reason the return (or the line)
            already had, or only words.
        before:
          type: string
          nullable: true
          description: >-
            The reason before. A return's reason may be words a return was
            created with; a line's is a reason code, or null.
        after:
          type: string
          nullable: true
          description: >-
            The reason after: the new code, or the reason as it stood when
            nothing changed.
        return_item_id:
          type: string
          format: uuid
          description: 'On a line''s edit: the line.'
        words_note_id:
          type: string
          format: uuid
          nullable: true
          description: >-
            The internal note that keeps `reason_words`; null when none was
            sent.
        kept_words_note_id:
          type: string
          format: uuid
          nullable: true
          description: >-
            On a return's edit: the internal note that keeps the reason the
            return held in words before a code replaced it; null otherwise.
        lines:
          type: array
          description: >-
            The lines whose reason moved: on a return's edit, every line whose
            reason was the return's (none of its own, `unspecified`, or the old
            code); on a line's edit, that line. Empty when nothing changed.
          items:
            type: object
            required:
              - return_item_id
              - before
              - after
            properties:
              return_item_id:
                type: string
                format: uuid
              before:
                type: string
                nullable: true
                description: The line's reason code before (null when it had none).
              after:
                type: string
                description: The line's reason code after.
        fees:
          type: object
          nullable: true
          description: >-
            The automatic fees after a changed reason; null when the reason did
            not change.
          required:
            - recomputed
          properties:
            recomputed:
              type: boolean
              description: >-
                Whether the fees were worked out again. False on a return that
                is closed, restocked, written off, sent to the vendor, voided or
                cancelled (`reason: settled`), which keeps its figures, and when
                the fees could not be worked out (`reason: failed`; the reason
                still changed).
            reason:
              type: string
              enum:
                - settled
                - failed
              description: 'Beside `recomputed: false`: why.'
            changed:
              type: boolean
              description: 'Beside `recomputed: true`: whether a fee moved.'
            restocking_fee_before:
              type: number
              description: 'Beside `changed: true`: the restocking fee before.'
            restocking_fee_after:
              type: number
              description: 'Beside `changed: true`: the restocking fee after.'
            fee_lines_before:
              type: integer
              description: 'Beside `changed: true`: the fee lines before.'
            fee_lines_after:
              type: integer
              description: 'Beside `changed: true`: the fee lines after.'
            kept_by_hand:
              type: object
              description: >-
                The fees a person set that the new reason left as they were: the
                restocking fee, and the rules a person removed or changed.
              required:
                - restocking_fee
                - rules
              properties:
                restocking_fee:
                  type: boolean
                rules:
                  type: array
                  items:
                    type: string
  responses:
    Error:
      description: Error response (400/401/403/404/409/422/429/500)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      description: |
        API key issued per entity via Settings > Developers > API Keys.
        Each key carries scopes (e.g. orders:read, products:write).
        Bearer token format: Authorization: Bearer ark_live_ent_<code>_<random>
        Test keys use ark_test_ent_<code>_<random>. Both are issued per entity
        via Settings > Developers > API Keys.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.