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

# Create a product

> Atomic-create endpoint -- accepts the complete nested resource graph in one transactional POST.
If any child fails to write, the entire request rolls back. The response includes every child
hydrated inline (variants, pricing, components, vendors) so no follow-up GET is required.

The `product_type` discriminator controls which nested children are accepted:
- `physical` / `service`: accepts `pricing[]` and `vendors[]`
- `kit` / `box`: accepts `components[]`, `pricing[]`, and `vendors[]`
- `variant_parent`: accepts `variants[]` (each variant may include its own `components[]`, `pricing[]`, `vendors[]`), plus top-level `pricing[]` and `vendors[]`

**Pricing tiers**: each `pricing[]` element uses `min_quantity` (the public-API key; internally
mapped to `qty_break`) to define the minimum order quantity that activates the tier. Use a unique
`min_quantity` within the same `pricing_level_id` scope to express qty-break discounts.

**Vendor cost tiers**: each `vendors[]` element may carry a `purchase_policies[]` array of
vendor cost tiers (`product_purchase_policies`) -- the per-quantity price that vendor charges.
These are wired atomically alongside the vendor relationship.

**Guide**: `https://arcuserp.mintlify.app/guides/creating-products-kits-variants`




## OpenAPI

````yaml /openapi.yaml post /products
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:
  /products:
    post:
      tags:
        - Products
      summary: Create a product
      description: >
        Atomic-create endpoint -- accepts the complete nested resource graph in
        one transactional POST.

        If any child fails to write, the entire request rolls back. The response
        includes every child

        hydrated inline (variants, pricing, components, vendors) so no follow-up
        GET is required.


        The `product_type` discriminator controls which nested children are
        accepted:

        - `physical` / `service`: accepts `pricing[]` and `vendors[]`

        - `kit` / `box`: accepts `components[]`, `pricing[]`, and `vendors[]`

        - `variant_parent`: accepts `variants[]` (each variant may include its
        own `components[]`, `pricing[]`, `vendors[]`), plus top-level
        `pricing[]` and `vendors[]`


        **Pricing tiers**: each `pricing[]` element uses `min_quantity` (the
        public-API key; internally

        mapped to `qty_break`) to define the minimum order quantity that
        activates the tier. Use a unique

        `min_quantity` within the same `pricing_level_id` scope to express
        qty-break discounts.


        **Vendor cost tiers**: each `vendors[]` element may carry a
        `purchase_policies[]` array of

        vendor cost tiers (`product_purchase_policies`) -- the per-quantity
        price that vendor charges.

        These are wired atomically alongside the vendor relationship.


        **Guide**:
        `https://arcuserp.mintlify.app/guides/creating-products-kits-variants`
      operationId: createProduct
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - title
                - product_type
              properties:
                title:
                  type: string
                  example: Riser Kit
                product_type:
                  type: string
                  enum:
                    - physical
                    - kit
                    - box
                    - service
                    - variant_parent
                  example: variant_parent
                description:
                  type: string
                  example: Modular riser kit with dome or flat options
                sku:
                  type: string
                list_price:
                  type: number
                packaging_classification:
                  type: string
                  enum:
                    - over_box
                    - sioc
                    - sipp
                    - prep_free
                  default: over_box
                  description: >
                    Amazon-FBA packaging classification. Defaults to `over_box`
                    (standard PackPilot path). Set to `sioc` or `sipp` for
                    single-item ship-ready packaging (label tapes on product, no
                    over-box, no prep -- cannot co-pack). Set to `prep_free`
                    when an over-box is still required but the prep step is
                    skipped (no bubble wrap / bagging / labels-on-product).
                    PackPilot reads this on every pack-order call. See the
                    `Product` schema for full enum semantics.
                  example: over_box
                components:
                  type: array
                  description: >-
                    Kit/box component assemblies (product_type=kit or box only).
                    Each element references a component product and the quantity
                    required to build one unit of this kit.
                  items:
                    type: object
                    required:
                      - component_product_id
                    properties:
                      component_product_id:
                        type: string
                        format: uuid
                        description: UUID of the component product
                      quantity:
                        type: number
                        description: >-
                          Quantity of component per kit unit. Alias for
                          quantity_per_kit.
                        example: 2
                      quantity_per_kit:
                        type: number
                        description: >-
                          Quantity of component per kit unit (canonical field
                          name).
                        example: 2
                      external_source:
                        type: string
                        nullable: true
                        description: >-
                          Source-system tag for historical migration (e.g.
                          'bubble_io').
                      external_id:
                        type: string
                        nullable: true
                        description: >-
                          Source-system row id, persisted for batch
                          traceability.
                      migration_batch_id:
                        type: string
                        format: uuid
                        nullable: true
                        description: >-
                          Optional migration-wave identifier, persisted on the
                          row.
                variants:
                  type: array
                  description: >
                    Inline variants (product_type=variant_parent only). Each
                    variant can itself carry

                    kit `components[]`, qty-break `pricing[]`, and `vendors[]`
                    that are wired

                    atomically in the same transaction.
                  items:
                    type: object
                    properties:
                      title:
                        type: string
                        example: Dome Riser Kit
                      sku:
                        type: string
                        example: RISER-DOME
                      product_type:
                        type: string
                        enum:
                          - kit
                          - physical
                          - service
                        description: >-
                          Variant subtype. Use kit if this variant is a
                          component assembly.
                      list_price:
                        type: number
                      components:
                        type: array
                        description: >
                          Kit components for this specific variant (wired when
                          variant product_type=kit).

                          Rows are stored in assembly_components with
                          parent_variant_id = this variant's id

                          (not parent_product_id), so the canonical kit-walker
                          CTE correctly resolves them.
                        items:
                          type: object
                          required:
                            - component_product_id
                          properties:
                            component_product_id:
                              type: string
                              format: uuid
                            quantity:
                              type: number
                              example: 2
                            component_variant_id:
                              type: string
                              format: uuid
                              nullable: true
                              description: >-
                                Optional: a specific variant of the component
                                product.
                            external_source:
                              type: string
                              nullable: true
                              description: >-
                                Source-system tag for historical migration (e.g.
                                'bubble_io').
                            external_id:
                              type: string
                              nullable: true
                              description: >-
                                Source-system row id, persisted for batch
                                traceability.
                            migration_batch_id:
                              type: string
                              format: uuid
                              nullable: true
                              description: >-
                                Optional migration-wave identifier, persisted on
                                the row.
                      pricing:
                        type: array
                        description: >-
                          Qty-break pricing tiers for this variant's product
                          record
                        items:
                          type: object
                          properties:
                            pricing_level_id:
                              type: string
                              format: uuid
                              description: >-
                                Pricing level UUID. Use GET /v1/pricing-levels
                                to list.
                            account_id:
                              type: string
                              format: uuid
                              description: >-
                                Account-specific price override (use instead of
                                pricing_level_id for account overrides)
                            min_quantity:
                              type: integer
                              description: >-
                                Minimum order qty to activate this tier. Must be
                                unique per pricing_level_id scope.
                              example: 5
                            list_price:
                              type: number
                              description: >-
                                Sell price at this tier. Returned as a JS number
                                (float).
                              example: 44.99
                      vendors:
                        type: array
                        description: Vendor relationships for this variant
                        items:
                          type: object
                          properties:
                            vendor_id:
                              type: string
                              format: uuid
                            vendor_sku:
                              type: string
                            default_unit_cost:
                              type: number
                pricing:
                  type: array
                  description: >
                    Sell-side pricing policies for the parent product (any
                    product_type). Each element

                    defines one qty-break tier on a pricing level. Use
                    `min_quantity` to set the

                    minimum order quantity that activates this tier (maps to
                    qty_break internally).

                    All money fields are returned as JS numbers (float).
                  items:
                    type: object
                    properties:
                      pricing_level_id:
                        type: string
                        format: uuid
                        description: >-
                          Pricing level UUID. Required if account_id not
                          provided.
                      account_id:
                        type: string
                        format: uuid
                        description: >-
                          Account-specific price override. Required if
                          pricing_level_id not provided.
                      min_quantity:
                        type: integer
                        description: >-
                          Minimum order quantity to activate this tier. Default
                          1. Must be unique per pricing_level_id scope.
                        example: 1
                      list_price:
                        type: number
                        example: 49.99
                      sell_price:
                        type: number
                        description: >-
                          Explicit sell price (overrides list_price if both
                          provided)
                      adjustment:
                        type: number
                        description: Adjustment amount (used with multiplier pricing_type)
                      margin_pct:
                        type: number
                        description: >-
                          Target margin percentage (used with margin
                          pricing_type)
                      pricing_type:
                        type: string
                        enum:
                          - regular
                          - multiplier
                        default: regular
                      is_default:
                        type: boolean
                        default: false
                vendors:
                  type: array
                  description: >-
                    Vendor relationships on the parent product (any
                    product_type)
                  items:
                    type: object
                    properties:
                      vendor_id:
                        type: string
                        format: uuid
                      vendor_sku:
                        type: string
                      default_unit_cost:
                        type: number
                      purchase_policies:
                        type: array
                        description: >
                          Vendor cost tiers (product_purchase_policies) -- the
                          per-quantity price

                          this vendor charges. Wired atomically in the same
                          transaction; a bad tier

                          rolls back the whole product. qty_break must be >= the
                          vendor's min_order_qty.
                        items:
                          type: object
                          properties:
                            qty_break:
                              type: integer
                              description: >-
                                Minimum order quantity that activates this cost
                                tier. Default 1.
                              example: 1
                            purchase_price:
                              type: number
                              description: >-
                                Per-unit cost from this vendor at this qty
                                break.
                              example: 195.6
                            pricing_type:
                              type: string
                              enum:
                                - regular
                                - multiplier
                              default: regular
                            multiplier:
                              type: number
                              description: >-
                                Cost multiplier (used with
                                pricing_type=multiplier)
                            is_default:
                              type: boolean
                              default: false
                            is_active:
                              type: boolean
                              default: true
            example:
              title: Riser Kit
              product_type: variant_parent
              description: Modular riser kit with dome or flat options
              variants:
                - title: Dome Riser Kit
                  sku: RISER-DOME
                  product_type: kit
                  components:
                    - component_product_id: prod_stone_uuid
                      quantity: 2
                    - component_product_id: prod_dome_lid_uuid
                      quantity: 1
                  pricing:
                    - pricing_level_id: pl_default_uuid
                      min_quantity: 1
                      list_price: 49.99
                    - pricing_level_id: pl_default_uuid
                      min_quantity: 5
                      list_price: 44.99
                    - pricing_level_id: pl_default_uuid
                      min_quantity: 10
                      list_price: 39.99
                  vendors:
                    - vendor_id: acct_acme_uuid
                      vendor_sku: RD-001
                      default_unit_cost: 18
                - title: Flat Riser Kit
                  sku: RISER-FLAT
                  product_type: kit
                  components:
                    - component_product_id: prod_stone_uuid
                      quantity: 2
                    - component_product_id: prod_flat_lid_uuid
                      quantity: 1
                  pricing:
                    - pricing_level_id: pl_default_uuid
                      min_quantity: 1
                      list_price: 44.99
                    - pricing_level_id: pl_default_uuid
                      min_quantity: 5
                      list_price: 39.99
                    - pricing_level_id: pl_default_uuid
                      min_quantity: 10
                      list_price: 34.99
              vendors:
                - vendor_id: acct_acme_uuid
                  vendor_sku: RISER-PARENT
                  default_unit_cost: 15
                  purchase_policies:
                    - qty_break: 1
                      purchase_price: 15
                      is_default: true
                    - qty_break: 100
                      purchase_price: 13.5
      responses:
        '201':
          description: Product created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Product'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'
        '403':
          $ref: '#/components/responses/Error'
        '409':
          $ref: '#/components/responses/Error'
      security:
        - ApiKeyAuth:
            - products:write
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.
      schema:
        type: string
        maxLength: 255
  schemas:
    Product:
      type: object
      description: Arcus ERP product (physical/kit/box/service/variant_parent).
      properties:
        id:
          type: string
          format: uuid
        object:
          type: string
          enum:
            - product
        entity_id:
          type: string
          format: uuid
          readOnly: true
        title:
          type: string
        description:
          type: string
          nullable: true
        sku:
          type: string
          nullable: true
        part_number:
          type: string
          nullable: true
        barcode:
          type: string
          nullable: true
        product_type:
          type: string
          enum:
            - physical
            - kit
            - box
            - service
            - variant_parent
        category_id:
          type: string
          format: uuid
          nullable: true
        vendor_id:
          type: string
          format: uuid
          nullable: true
        list_price:
          type: number
        cost:
          type: number
          readOnly: true
          description: >-
            WAC (Weighted Average Cost). ReadOnly. Writers:
            updateProductCostFromReceipt + recomputeKitCostForKit.
        is_active:
          type: boolean
        is_serialized:
          type: boolean
        charge_tax:
          type: boolean
        track_inventory:
          type: boolean
        weight:
          type: number
          nullable: true
        weight_uom:
          type: string
        length:
          type: number
          nullable: true
        width:
          type: number
          nullable: true
        height:
          type: number
          nullable: true
        length_uom:
          type: string
        quantity_uom:
          type: string
        product_tax_code:
          type: string
          nullable: true
        reorder_point:
          type: integer
          nullable: true
        reorder_qty:
          type: integer
          nullable: true
        safety_stock:
          type: integer
        packaging_classification:
          type: string
          enum:
            - over_box
            - sioc
            - sipp
            - prep_free
          default: over_box
          description: >
            Amazon-FBA packaging classification driving PackPilot's pack-order
            decisions. `over_box` (default): standard path, PackPilot picks an
            over-box from the catalog and the warehouse runs the full prep step.
            `sioc` (Ships In Own Container): item ships in its original
            manufacturer container; the shipping label tapes directly onto the
            product packaging; no over-box and no prep. Single-item package only
            (cannot co-pack). `sipp` (Ships In Product Packaging): Amazon's
            rebrand of SIOC with identical rules: retail packaging IS ship-ready
            and the label goes onto it. Single-item package only. `prep_free`
            (Prep-Free Packaging): item still requires an over-box (PackPilot
            picks one) but the warehouse skips the prep step (no bubble wrap, no
            bagging, no labels-on-product). Can co-pack with other prep-free or
            over_box items. Added 2026-05-18 in Phase 3 of
            FULFILLMENT-PRODUCTION-READINESS-CAMPAIGN; backed by the
            `packaging_classification` Postgres ENUM.
        has_variants:
          type: boolean
          readOnly: true
        metadata:
          type: object
          description: >-
            Operator-extensible JSONB. Reserved: _deleted (set by
            softDeleteProduct), _actor, _api.
        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.

````