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.
| Resource | Endpoint | Inline children |
|---|
| Package | POST /v1/packages | items[], optional label purchase, optional fulfill |
| Return (RMA) | POST /v1/returns | items[], 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
| Field | Type | Description |
|---|
order_id | string | Required. Parent sales order UUID. |
items[] | array | Line items to add. Each item: order_item_id (required), quantity (required), serial_numbers[] (optional, for serialized products). |
carrier_service | string | Compound carrier+service string (ups_ground, fedex_priority_overnight). Mapped to carrier + service fields automatically. |
carrier + service | strings | Alternative to carrier_service. Both must be provided. |
buy_label | boolean | If true, purchase a Shippo label after package creation. Requires rate_object_id. |
rate_object_id | string | Required with buy_label: true. Fetch rates via GET /v1/packages/:id/rates. |
fulfill_after_label | boolean | If 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
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
| Field | Type | Description |
|---|
original_order_id | string | Optional. Link to the originating sales order. |
reason | string | Free-text reason for the return. |
refund_amount | number | Total dollar amount to refund (before restocking fee). |
items[] | array | Line items to return. Fields: order_item_id (recommended), quantity (alias for quantity_authorized), condition (enum: new/used/damaged/defective). |
restocking_fee | number or object | Flat dollar amount OR { "type": "percentage", "value": 10 } (10% of refund_amount). Also accepts { "type": "flat", "value": 30 }. |
auto_refund | boolean | If true, issue the refund immediately after RMA creation. Requires refund_payment_id. |
refund_payment_id | string | Required with auto_refund: true. The original payment UUID to refund against. |
refund_method | string | Optional. original (default), store_credit, check, cash. |
auto_authorize | boolean | Ignored (RMA is always created in authorized state unless entity settings require inspection). |
override_window | boolean | If true, bypasses the entity’s return-window-days guard. |
customer_initiated | boolean | Set to true when the return was initiated via the customer portal. |
// 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
| Operation | Scope |
|---|
| List / read packages or returns | fulfillment:read / returns:read |
| Create / update / fulfill packages | fulfillment:write |
| Create / receive / disposition returns | returns:write |
Refund (via POST /v1/returns/:id/refund or auto_refund) | returns:write + payments:write |
| Create exchange order | returns:write + orders:write |
| Create return label | returns: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.