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

# Cancel a purchase order

> Cancels a purchase order, transitioning it to `status: cancelled`. Only orders that have NOT
yet been received can be cancelled; partially or fully received orders must be closed via the
Close Short workflow on the staff app. Any inventory quantities on order are released back
to ATP via the canonical handler.

**Separation of Duties (SoD) gate (NEW-GAP-PURCHASING-CANCEL-PERMISSION-SOD-PHASE-2 2026-05-16):**
Beyond the `purchasing:write` API-key scope, the user who created the API key (the
`on_behalf_of_user_id` recorded on the key) MUST hold the `purchasing.cancel` permission key
(granted by default to owner / admin / manager / accountant; staff / warehouse / sales /
auditor / viewer denied). This mirrors Stripe's restricted-keys design (destructive ops
require their own scope), NetSuite's "Cancel Order" capability (the API caller's role must
carry it), Acumatica's "Cancel Order" form access rights, and AICPA SOX SoD framework
(destructive financial actions require separate authorization from creation). A 403
`permission_denied` response is returned when the key's owning user lacks the RBAC; the
denied attempt is recorded in the activity log (SoC2 CC6.1 + PCAOB AS 2201 §17).

Requires `purchasing:write` API-key scope AND the key-creator's user role must carry
`purchasing.cancel`.




## OpenAPI

````yaml /openapi.yaml post /purchase-orders/{id}/cancel
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}/cancel:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags:
        - Purchasing
      summary: Cancel a purchase order
      description: >
        Cancels a purchase order, transitioning it to `status: cancelled`. Only
        orders that have NOT

        yet been received can be cancelled; partially or fully received orders
        must be closed via the

        Close Short workflow on the staff app. Any inventory quantities on order
        are released back

        to ATP via the canonical handler.


        **Separation of Duties (SoD) gate
        (NEW-GAP-PURCHASING-CANCEL-PERMISSION-SOD-PHASE-2 2026-05-16):**

        Beyond the `purchasing:write` API-key scope, the user who created the
        API key (the

        `on_behalf_of_user_id` recorded on the key) MUST hold the
        `purchasing.cancel` permission key

        (granted by default to owner / admin / manager / accountant; staff /
        warehouse / sales /

        auditor / viewer denied). This mirrors Stripe's restricted-keys design
        (destructive ops

        require their own scope), NetSuite's "Cancel Order" capability (the API
        caller's role must

        carry it), Acumatica's "Cancel Order" form access rights, and AICPA SOX
        SoD framework

        (destructive financial actions require separate authorization from
        creation). A 403

        `permission_denied` response is returned when the key's owning user
        lacks the RBAC; the

        denied attempt is recorded in the activity log (SoC2 CC6.1 + PCAOB AS
        2201 §17).


        Requires `purchasing:write` API-key scope AND the key-creator's user
        role must carry

        `purchasing.cancel`.
      operationId: cancelPurchaseOrderV1
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  description: Optional cancellation reason recorded on the activity log.
      responses:
        '200':
          description: Cancelled purchase order
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PurchaseOrder'
        '400':
          description: >
            Bad request. Examples: PO already cancelled / fulfilled / archived;
            PO has existing

            receipts (use Close Short instead); entity_id missing.
        '401':
          $ref: '#/components/responses/Error'
        '403':
          description: >
            Permission denied. Either the API key lacks `purchasing:write` scope
            OR the key-creator

            user lacks the `purchasing.cancel` permission key (SoD). Response
            body shape:

            `{ error: "permission_denied", code: "permission_denied",
            permission_key: "purchasing.cancel", hint: ... }`.

            The denied attempt is recorded in activity_log (Rule 20 + SoC2
            CC6.1).
        '404':
          $ref: '#/components/responses/Error'
      security:
        - ApiKeyAuth: []
components:
  schemas:
    PurchaseOrder:
      type: object
      description: A purchase order issued to a vendor.
      properties:
        id:
          type: string
          format: uuid
        object:
          type: string
          enum:
            - purchase_order
        entity_id:
          type: string
          format: uuid
          readOnly: true
        po_number:
          type: string
          readOnly: true
        vendor_id:
          type: string
          format: uuid
        location_id:
          type: string
          format: uuid
          nullable: true
        status:
          type: string
          enum:
            - draft
            - approved
            - sent
            - partially_received
            - received
            - closed
            - cancelled
          readOnly: true
        po_date:
          type: string
          format: date-time
        expected_date:
          type: string
          format: date-time
          nullable: true
        subtotal:
          type: number
        tax_total:
          type: number
        shipping_total:
          type: number
        po_total:
          type: number
        currency:
          type: string
          default: USD
        notes:
          type: string
          nullable: true
        internal_notes:
          type: string
          nullable: true
        metadata:
          type: object
          nullable: true
        created_at:
          type: string
          format: date-time
          readOnly: true
        updated_at:
          type: string
          format: date-time
          readOnly: true
    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
  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.

````