Skip to main content

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.

Overview

Two resources — packages and returns — cover the post-order lifecycle. Both follow the atomic-create pattern: a single POST creates the parent resource plus all inline children in one transactional write, returning the fully hydrated response. You never need follow-up calls to add items or purchase a label.
ResourceEndpointInline children
PackagePOST /v1/packagesitems[], optional label purchase, optional fulfill
Return (RMA)POST /v1/returnsitems[], optional auto-refund, restocking fee

Packages

Canonical scenario: package with items + label + fulfill in one POST

This request creates the package, adds two line items, purchases a Shippo label, and fulfills the package (posts GL entries, sends shipment notification) in a single atomic call.
curl -s -X POST "https://api.arcuserp.com/v1/packages" \
  -H "Authorization: Bearer ark_live_ent_wss_abc123xyz" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "order_id": "ORDER_UUID",
    "carrier_service": "ups_ground",
    "items": [
      {
        "order_item_id": "ORDER_ITEM_UUID_1",
        "quantity": 2,
        "serial_numbers": ["SN001", "SN002"]
      },
      {
        "order_item_id": "ORDER_ITEM_UUID_2",
        "quantity": 1
      }
    ],
    "buy_label": true,
    "rate_object_id": "SHIPPO_RATE_OBJECT_ID",
    "fulfill_after_label": true
  }'

Response

{
  "data": {
    "id": "PKG_UUID",
    "package_id": "SO-1234-B1",
    "order_id": "ORDER_UUID",
    "entity_id": "ENTITY_UUID",
    "carrier": "ups",
    "service": "ground",
    "status": "packed",
    "fulfillment_status": "fulfilled",
    "items": [
      {
        "id": "PKG_ITEM_UUID_1",
        "order_item_id": "ORDER_ITEM_UUID_1",
        "quantity": 2,
        "serial_numbers": ["SN001", "SN002"]
      },
      {
        "id": "PKG_ITEM_UUID_2",
        "order_item_id": "ORDER_ITEM_UUID_2",
        "quantity": 1
      }
    ],
    "label_url": "https://api.goshippo.com/labels/...",
    "tracking_number": "1Z999AA10123456784",
    "label_purchased_at": "2026-05-12T14:32:00Z",
    "fulfilled_at": "2026-05-12T14:32:05Z",
    "weight": 2.5,
    "length": 12,
    "width": 9,
    "height": 6,
    "created_at": "2026-05-12T14:31:58Z"
  }
}

Inline create options

FieldTypeDescription
order_idstringRequired. Parent sales order UUID.
items[]arrayLine items to add. Each item: order_item_id (required), quantity (required), serial_numbers[] (optional, for serialized products).
carrier_servicestringCompound carrier+service string (ups_ground, fedex_priority_overnight). Mapped to carrier + service fields automatically.
carrier + servicestringsAlternative to carrier_service. Both must be provided.
buy_labelbooleanIf true, purchase a Shippo label after package creation. Requires rate_object_id.
rate_object_idstringRequired with buy_label: true. Fetch rates via GET /v1/packages/:id/rates.
fulfill_after_labelbooleanIf true, transition the package to fulfilled after label purchase. Posts GL entries, deducts inventory, queues shipment notification. Only fires if buy_label: true and label purchase succeeds.

Separate label purchase (two-step flow)

If you prefer to separate rate shopping from package creation:
# Step 1: Create package
PKG_ID=$(curl -s -X POST ".../v1/packages" \
  -d '{"order_id": "ORDER_UUID", "items": [...]}' | jq -r '.data.id')

# Step 2: Fetch rates
RATE_ID=$(curl -s ".../v1/packages/$PKG_ID/rates" | jq -r '.data[0].object_id')

# Step 3: Buy label
curl -s -X POST ".../v1/packages/$PKG_ID/buy-label" \
  -d "{\"rate_object_id\": \"$RATE_ID\"}"

# Step 4: Fulfill
curl -s -X POST ".../v1/packages/$PKG_ID/fulfill"

Package lifecycle

draft -> packed -> shipped (fulfilled)
                -> void (label voided, returns to packed)
  • POST /v1/packages/:id/buy-label — purchase label (transitions to packed + label attached)
  • POST /v1/packages/:id/void-label — void active label, request Shippo refund
  • POST /v1/packages/:id/fulfill — mark shipped, post GL fulfillment entries
  • GET /v1/tracking-events?package_id=:id — live tracking events

Shippo not configured

If the entity has no Shippo credentials, buy_label: true returns:
{
  "error": "connector_not_configured",
  "code": "connector_not_configured",
  "type": "connector_error",
  "hint": "Shippo is not configured for this entity. Add credentials in Settings > Integrations.",
  "package": { "id": "PKG_UUID", "..." }
}
The package is created and returned in the error body so you can retry buy-label after configuring Shippo.
Webhook delivery: the package.shipped event is queued when a package is fulfilled. Real-time webhook delivery to your endpoint is available in Q3 2026. In the interim, poll GET /v1/tracking-events to check shipment status.

Returns (RMAs)

Canonical scenario: return with items + restocking fee + auto-refund

This creates the RMA, adds line items, and issues the refund in a single atomic call.
curl -s -X POST "https://api.arcuserp.com/v1/returns" \
  -H "Authorization: Bearer ark_live_ent_wss_abc123xyz" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "original_order_id": "ORDER_UUID",
    "reason": "wrong_item",
    "refund_amount": 150.00,
    "items": [
      {
        "order_item_id": "ORDER_ITEM_UUID_1",
        "quantity": 1,
        "condition": "unused"
      }
    ],
    "restocking_fee": { "type": "percentage", "value": 10 },
    "auto_refund": true,
    "refund_payment_id": "PAYMENT_UUID"
  }'

Response

{
  "data": {
    "id": "RETURN_UUID",
    "return_number": "RMA-00042",
    "entity_id": "ENTITY_UUID",
    "original_order_id": "ORDER_UUID",
    "status": "authorized",
    "reason": "wrong_item",
    "refund_amount": 150.0,
    "restocking_fee": 15.0,
    "restocking_fee_auto_applied": false,
    "items": [
      {
        "id": "RETURN_ITEM_UUID",
        "order_item_id": "ORDER_ITEM_UUID_1",
        "quantity_authorized": 1,
        "condition": "unused",
        "disposition": "pending"
      }
    ],
    "refund": {
      "id": "REFUND_UUID",
      "amount": 135.0,
      "payment_method": "card",
      "status": "succeeded"
    },
    "created_at": "2026-05-12T14:32:00Z"
  }
}

Inline create options

FieldTypeDescription
original_order_idstringOptional. Link to the originating sales order.
reasonstringFree-text reason for the return.
refund_amountnumberTotal dollar amount to refund (before restocking fee).
items[]arrayLine items to return. Fields: order_item_id (recommended), quantity (alias for quantity_authorized), condition (enum: new/used/damaged/defective).
restocking_feenumber or objectFlat dollar amount OR { "type": "percentage", "value": 10 } (10% of refund_amount). Also accepts { "type": "flat", "value": 30 }.
auto_refundbooleanIf true, issue the refund immediately after RMA creation. Requires refund_payment_id.
refund_payment_idstringRequired with auto_refund: true. The original payment UUID to refund against.
refund_methodstringOptional. original (default), store_credit, check, cash.
auto_authorizebooleanIgnored (RMA is always created in authorized state unless entity settings require inspection).
override_windowbooleanIf true, bypasses the entity’s return-window-days guard.
customer_initiatedbooleanSet to true when the return was initiated via the customer portal.

Restocking fee formats

// Flat dollar amount
"restocking_fee": 25.00

// 10% of refund_amount
"restocking_fee": { "type": "percentage", "value": 10 }

// Explicit flat type (same as flat number)
"restocking_fee": { "type": "flat", "value": 25.00 }
All three formats store the computed flat amount in returns.restocking_fee and return it as a JS number.

RMA lifecycle

authorized -> expected (label sent to customer) -> received -> inspecting? -> restocked / written_off / sent_to_vendor -> closed
authorized -> cancelled
  • POST /v1/returns/:id/receive — record inbound goods, restore inventory
  • POST /v1/returns/:id/inspect — flag for QC inspection (if entity settings require)
  • POST /v1/returns/:id/disposition — restock / write off / send to vendor
  • POST /v1/returns/:id/refund — issue explicit refund (if not done inline)
  • POST /v1/returns/:id/cancel — cancel the RMA, release holds

Receiving and disposition (separate calls)

After the physical goods arrive:
# Receive the goods (restores inventory)
curl -X POST ".../v1/returns/$RMA_ID/receive" \
  -d '{ "items": [{ "return_item_id": "ITEM_UUID", "quantity_received": 1 }] }'

# Disposition (posts COGS reversal GL entries)
curl -X POST ".../v1/returns/$RMA_ID/disposition" \
  -d '{ "items": [{ "return_item_id": "ITEM_UUID", "outcome": "restock" }] }'

Return-window enforcement

If entities.settings.return_window_days is set, returns outside the window are rejected with 422 outside_return_window. Pass override_window: true to bypass (requires returns:write scope).

Scopes required

OperationScope
List / read packages or returnsfulfillment:read / returns:read
Create / update / fulfill packagesfulfillment:write
Create / receive / disposition returnsreturns:write
Refund (via POST /v1/returns/:id/refund or auto_refund)returns:write + payments:write
Create exchange orderreturns:write + orders:write
Create return labelreturns:write + fulfillment:write

Error handling

All errors follow the { error, code, type, hint } envelope. Partial-success responses (e.g. package created but label failed) include the created resource in the response body alongside the error:
{
  "error": "label_purchase_failed",
  "code": "label_purchase_failed",
  "type": "connector_error",
  "hint": "Package was created but label purchase failed. Retry via POST /v1/packages/:id/buy-label.",
  "package": { "id": "PKG_UUID", "..." }
}
This lets you recover without duplicating the package — just retry buy-label using the package.id from the error body.