API reference
Quotes, orders, and shipments.
The three write paths, plus the shared rules for money, errors, and test keys that apply to all of them.
Quotes
Prices protection for a cart and returns an id you pass to the order. Quoting is optional but recommended: it is the only way to guarantee the premium a shopper was shown is the premium they are charged.
/v2/quotes| Field | Type | Description |
|---|---|---|
| cart.covered.amountrequired | string | Insurable cart value as a string in major units, e.g. "189.34". Exclude digital and non-shippable items. |
| cart.covered.currency | string | ISO 4217. USD only today; defaults to USD. |
| cart.cart_ref | string | Your own cart identifier, echoed back for correlation. |
| cart.cart_items | array | Recorded for dispute resolution. Not re-summed — cart.covered.amount is authoritative. |
| merchant_id | string | Optional and redundant; your key already identifies the store. If sent and it names a different store, the request 404s. |
{
"id": "9146c08f-e0d0-4f0d-b7ca-7b416f8082f9",
"premium": { "currency": "USD", "amount": "4.35" },
"payment_responsible": { "type": "customer" }
}Quotes expire after 24 hours
Pass the id as quote_id when you create the order. If it has expired, belongs to another store, or is not a quote we issued, the order is still accepted — it is simply priced from your configured rate instead. Orders never fail because of a bad quote id.
Orders
Send an order once it has reached a final state before fulfilment. Doing it then means you only need the create call — no update or cancel logic.
/v2/orders| Field | Type | Description |
|---|---|---|
| source_order_idrequired | string | Your order id. Must be unique for your store — a repeat returns 409. |
| source_order_number | string | The number the customer sees. Defaults to source_order_id. |
| subtotalrequired | number | Aggregate value of insurable items, excluding the protection premium itself. This is what protection pays out against. |
| insurance_selectedrequired | boolean | Whether the shopper bought protection. False records the order without booking a premium. |
| quote_id | string | From POST /v2/quotes. Any string is accepted; unknown values simply re-price. |
| order_total | number | What the customer paid at checkout. |
| shipping_total | number | What the customer paid for shipping. |
| discounts_total | number | Total discounts applied to the order. |
| taxes | number | Total tax on the order. |
| currency | string | ISO 4217. USD only today. |
| customer_detailsrequired | object | first_name, last_name, email, and optional phone. Needed so the shopper can file a claim. |
| shipping_detailsrequired | object | street_address1, street_address2, city, province, zip, country_code. |
| billing_address | object | Same shape as shipping_details. |
| line_itemsrequired | array | At least one. Each needs source_id (unique within the order), name, price (pre-discount unit price), and quantity. If you charge the premium as a cart line, exclude it here and from the subtotal. |
| source_created_onrequired | date-time | ISO 8601. This decides which month the order lands in, so send the real placement time. |
{
"id": "order_0wvq7iblja5i6piw",
"merchant_id": "merch_2zta9l4uravokw42",
"order_number": "AR0WVQ7IBLJA5I6PIW",
"source_order_id": "11003271211",
"source_order_number": "#9929",
"status": "created",
"insured_status": "insured_selected",
"currency": "USD",
"exchange_rate": 1,
"subtotal": 145,
"amount_covered": 145,
"paid_to_insure": 4.35,
"order_total": 177.5,
"shipping_total": 20,
"taxes": 10.5,
"first_name": "John",
"last_name": "Smith",
"has_pii": true,
"platform_id": "api",
"source_created_on": "2026-08-11T09:27:35.000Z",
"created_on": "2026-08-14T15:33:57.182Z",
"updated_on": "2026-08-14T15:33:57.182Z",
"cancelled_on": null
}Save the id
Store id against your order record. It is how you tell which orders have reached Arrive, and it is what support will ask for.
/v2/orders/{source_order_id}Fields are merged, not replaced — send only what changed. The one exception is line_items, which replaces the whole array when present.
Changing subtotal or insurance_selected re-prices the premium and updates what you will be invoiced. Nothing else does.
/v1/orders/{source_order_id}/cancelNo body required. The order, its history, and any premium already booked are kept — cancelling is not a refund — but no new claim can be filed against it.
An order with an open claim cannot be cancelled and returns 409. Resolve or deny the claim first. Cancelling an already-cancelled order succeeds, so a retry is safe.
Shipments
A shipment links a tracking number to an order, so the order must exist first. Multi-shipment orders are supported; protection sits at the order level and covers all of them.
/v1/shipments| Field | Type | Description |
|---|---|---|
| tracking_numberrequired | string | Spaces and dashes are stripped and the value is upper-cased, so send whatever your platform stored. |
| source_order_idrequired | string | The order this shipment belongs to. |
| courier_id | string | Carrier slug — usps, ups, fedex. Optional; we resolve the carrier ourselves if you omit it. |
| source_product_ids | array | source_product_id values from the order. |
| line_items | array | source_id, source_product_id, and quantity — when a shipment carries only part of the order. |
{
"id": "ship_457lnyqag2oz9i5h",
"merchant_id": "merch_2zta9l4uravokw42",
"tracking_number": "9400111899223197428490",
"courier": "usps",
"delivery_status": "Active",
"tracking_registration_status": "registered",
"tracking_url": null,
"expected_delivery_date": null,
"checkpoints": [],
"merchant_messages": [],
"created_on": "2026-08-14T15:23:33.277Z",
"updated_on": "2026-08-14T15:23:33.277Z",
"cancelled_on": null
}/v1/shipments/{tracking_number}Returns the same object with checkpoints populated — up to 100 carrier scans, oldest first. delivery_status is one of Active, Delivered, Exception, or Cancelled.
/v1/shipments/{tracking_number}Only courier_id can change. Tracking state — delivered, in transit, exception — is driven by carrier scans and cannot be set through the API. To correct a tracking number, cancel the shipment and create a new one.
/v1/shipments/{tracking_number}/cancelFor a voided label or a fulfilment that never shipped. Tracking history is kept; polling stops. A shipment with an open claim returns 409.
Money and units
Read this once and the rest of the API is predictable. There are three encodings, and they are not interchangeable.
| Field | Type | Description |
|---|---|---|
| Quotes | string | Major units as a string: "189.34". Must match ^\d{1,10}(\.\d{1,2})?$. |
| Orders and shipments | number | Major units as a JSON number: 35.79. At most two decimal places — a third is rejected rather than rounded. |
| Webhooks and acknowledgements | integer | Minor units. 12000 means $120.00. This is the one that catches people: the same API speaks dollars on the REST side and cents on the webhook side. |
Sending dollars where cents are expected is rejected
On acknowledgements we check the raw request text, not just the parsed number — because 120.00 parses to the integer 120 and would otherwise be indistinguishable from $1.20. Any amount written with a decimal point returns 400.
Currency
USD only. exchange_rate is always 1 and every _usd field equals its base field. Sending another currency returns 400 rather than pretending a conversion happened.
Errors
Every error is JSON. error is a human-readable string, error_code is stable and safe to branch on, and errors lists every bad field — not just the first.
{
"error": "2 fields are invalid: subtotal, line_items[0].price",
"error_code": "invalid_request",
"errors": [
{ "field": "subtotal", "message": "must have at most 2 decimal places" },
{ "field": "line_items[0].price", "message": "must not be negative" }
]
}| Field | Type | Description |
|---|---|---|
| 400 | invalid_request | Missing or malformed fields. See errors[]. |
| 401 | unauthorized | Missing, malformed, revoked, or unknown API key. |
| 403 | forbidden | Valid key, but not one that can be used here — most often a key that is not scoped to a store. |
| 404 | not_found | No such order or shipment for your store. Also returned for objects belonging to another store. |
| 409 | conflict | Collides with something already stored — a duplicate source_order_id, or a cancel blocked by an open claim. |
| 422 | unprocessable | Duplicate id within the request itself, such as two line items sharing a source_id. |
| 429 | rate_limited | Slow down and retry with backoff. |
| 500 | internal_error | Our fault. Safe to retry. |
Retries are safe on orders
source_order_id is unique per store, so a retried create either succeeds once or returns 409 — it cannot double-book a premium. Treat 409 on create as "the first attempt landed".
Test mode
An ak_test_… key authenticates and validates exactly like a live key, and returns correctly-shaped objects — but writes nothing.
| Field | Type | Description |
|---|---|---|
| Writes | simulated | Quotes, orders, and shipments return a realistic object with a test_ id. Nothing is stored and no premium is booked. |
| Reads | empty | Reads, updates, and cancels return 404: test keys never see live data. |
| X-Arrive-Mode | header | Every response carries live or test, so you can assert which key a deploy is using. |
Rate limits
Keep orders and shipments under 120 requests per minute per store. Handle 429 with exponential backoff — a client that retries immediately on 429 will keep hitting it.