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

# Receive 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, and receiving beyond the entity's tolerance cap is refused with 400 `over_receipt_blocked`. Requires `purchasing:write` scope, and an identical retry within a short window returns the original receipt with `idempotent: true` instead of creating a duplicate.



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

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


    **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:
  /purchase-orders/{id}/receive:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags:
        - Purchasing
      summary: Receive 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, and receiving beyond
        the entity's tolerance cap is refused with 400 `over_receipt_blocked`.
        Requires `purchasing:write` scope, and an identical retry within a short
        window returns the original receipt with `idempotent: true` instead of
        creating a duplicate.
      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
                vendor_shipment:
                  type: object
                  nullable: true
                  description: >
                    DROP-SHIP "Mark shipped by vendor" (2026-07-11). For a
                    drop-ship PO the receive

                    IS the vendor-shipped event: supplying `vendor_shipment`
                    captures the vendor's

                    tracking and creates a real shipped package on the linked
                    sales order so the

                    customer shipment email carries a working tracking link.
                    Tracking is OPTIONAL

                    ("we might have a tracking"); with no tracking a package is
                    still created and the

                    email renders without a tracking block. `carrier` is
                    required only when

                    `tracking_number` is supplied.
                  properties:
                    tracking_number:
                      type: string
                      nullable: true
                      maxLength: 255
                    carrier:
                      type: string
                      nullable: true
                      description: >-
                        Required when tracking_number is supplied (e.g. UPS,
                        FedEx, USPS).
                    shipped_at:
                      type: string
                      format: date-time
                      nullable: true
                      description: Vendor ship date. Defaults to now.
                    note:
                      type: string
                      nullable: true
      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.

````

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