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

# Record a receipt against a purchase order

> Records physical receipt of goods against a purchase order, creating inventory receive
transactions and incrementing on-hand balances for each received line. Partial receipts are
supported; the PO transitions to `status: partially_received` and finally to `received` when
all lines are fully received.

**Over-receipt tolerance (NEW-GAP-PO-RECEIVE-OVER-RECEIPT-ZERO-TOLERANCE-CONFLICTS-AP-5PCT,
2026-05-16):** the receive cap is `Math.floor(remaining * tolerance)` where `tolerance` is
resolved per-entity as `settings.po_overreceipt_tolerance` (dock-side knob) -> fall through
to `settings.ap_qty_tolerance` (AP-side knob) -> default 1.05 (5%). Bodies whose
`quantity_received + damaged_qty > cap` reject with HTTP 400 `over_receipt_blocked` and an
envelope of `{ ordered, already_received, remaining, attempted, tolerance_pct, cap, hint }`.
Industry parity: NetSuite Over-Receipt Tolerance / Acumatica Receipt Tolerance / SAP B1
Goods Receipt over-tolerance all expose a single tolerance % governing both dock + 3-way
match. Arcus exposes two knobs (dock falls through to AP) so entities can tighten dock
control without affecting 3-way match.

**Idempotency (NEW-GAP-AP-PO-RECEIVE-IDEMPOTENCY-GUARD, 2026-06-15):** the receive is a
financial mutation (inventory + GRNI + optional auto vendor bill). A server-side guard
dedups an identical retry (same PO + same line set: product / qty / damaged_qty) within a
120-second window and returns the original receipt with HTTP 200 + `idempotent: true` and
zero new side effects. A legitimately distinct follow-up partial (different qty / lines)
still creates a new receipt. You may additionally send a standard `Idempotency-Key` header
for cross-window safety.

Requires `purchasing:write` scope.




## OpenAPI

````yaml /openapi.yaml post /purchase-orders/{id}/receive
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.
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:
  /purchase-orders/{id}/receive:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags:
        - Purchasing
      summary: Record a receipt against a purchase order
      description: >
        Records physical receipt of goods against a purchase order, creating
        inventory receive

        transactions and incrementing on-hand balances for each received line.
        Partial receipts are

        supported; the PO transitions to `status: partially_received` and
        finally to `received` when

        all lines are fully received.


        **Over-receipt tolerance
        (NEW-GAP-PO-RECEIVE-OVER-RECEIPT-ZERO-TOLERANCE-CONFLICTS-AP-5PCT,

        2026-05-16):** the receive cap is `Math.floor(remaining * tolerance)`
        where `tolerance` is

        resolved per-entity as `settings.po_overreceipt_tolerance` (dock-side
        knob) -> fall through

        to `settings.ap_qty_tolerance` (AP-side knob) -> default 1.05 (5%).
        Bodies whose

        `quantity_received + damaged_qty > cap` reject with HTTP 400
        `over_receipt_blocked` and an

        envelope of `{ ordered, already_received, remaining, attempted,
        tolerance_pct, cap, hint }`.

        Industry parity: NetSuite Over-Receipt Tolerance / Acumatica Receipt
        Tolerance / SAP B1

        Goods Receipt over-tolerance all expose a single tolerance % governing
        both dock + 3-way

        match. Arcus exposes two knobs (dock falls through to AP) so entities
        can tighten dock

        control without affecting 3-way match.


        **Idempotency (NEW-GAP-AP-PO-RECEIVE-IDEMPOTENCY-GUARD, 2026-06-15):**
        the receive is a

        financial mutation (inventory + GRNI + optional auto vendor bill). A
        server-side guard

        dedups an identical retry (same PO + same line set: product / qty /
        damaged_qty) within a

        120-second window and returns the original receipt with HTTP 200 +
        `idempotent: true` and

        zero new side effects. A legitimately distinct follow-up partial
        (different qty / lines)

        still creates a new receipt. You may additionally send a standard
        `Idempotency-Key` header

        for cross-window safety.


        Requires `purchasing:write` scope.
      operationId: receivePurchaseOrderV1
      parameters:
        - name: X-Location-Id
          in: header
          required: false
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - items
              properties:
                items:
                  type: array
                  items:
                    type: object
                    required:
                      - product_id
                      - quantity_received
                    properties:
                      product_id:
                        type: string
                        format: uuid
                      quantity_received:
                        type: number
                      order_item_id:
                        type: string
                        format: uuid
                        nullable: true
                        description: >
                          Optional. The specific PO line (order_items.id) this
                          receive line fulfills.

                          When supplied, the received qty is stamped to that
                          exact line and the PO

                          status advances precisely. When OMITTED, the server
                          resolves it

                          automatically: if the received `product_id` (and
                          `variant_id`, when given)

                          maps to exactly ONE open PO line, that line is linked.
                          When the same product

                          appears on MULTIPLE open PO lines (e.g. two lines at
                          different costs), the

                          link is ambiguous and is left unlinked unless you
                          supply `order_item_id`

                          (or `po_item_id`). Supplying it is REQUIRED to advance
                          PO status and enable

                          line-level 3-way match in the ambiguous case.
                      po_item_id:
                        type: string
                        format: uuid
                        nullable: true
                        description: >
                          Alias for `order_item_id` (the PO line is an
                          `order_items` row). Use either.
                      damaged_qty:
                        type: number
                        nullable: true
                        description: >
                          Optional damaged qty at dock. Counts against the
                          over-receipt cap together

                          with `quantity_received` (sum must be <=
                          `Math.floor(remaining * tolerance)`).

                          When > 0, `condition` + `disposition` + `damage_notes`
                          are required.
                      condition:
                        type: string
                        nullable: true
                        enum:
                          - new
                          - damaged
                      disposition:
                        type: string
                        nullable: true
                        enum:
                          - restock
                          - quarantine
                          - return_to_vendor
                          - writeoff
                      damage_notes:
                        type: string
                        nullable: true
                create_bill:
                  type: boolean
                notes:
                  type: string
      responses:
        '200':
          description: >
            Idempotent replay. The identical receive was already recorded within
            the dedup window;

            the original receipt is returned with no new side effects.
          content:
            application/json:
              schema:
                type: object
                properties:
                  idempotent:
                    type: boolean
                  message:
                    type: string
                  receipt:
                    type: object
        '201':
          description: Receipt recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  receipt:
                    type: object
                  auto_bill:
                    type: object
                  auto_bill_error:
                    type: string
        '400':
          description: >
            Validation failure. `code='over_receipt_blocked'` returns envelope
            fields

            `{ ordered, already_received, remaining, attempted, tolerance_pct,
            cap, hint }`

            so the UI can surface the cap and the operator-tunable tolerance
            setting.
          content:
            application/json:
              schema:
                $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'
        '403':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
      security:
        - ApiKeyAuth: []
components:
  responses:
    Error:
      description: Error response (400/401/403/404/409/422/429/500)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
  schemas:
    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
  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.

````