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.

POST/v2/quotes
Create a quote for the current cart.
FieldTypeDescription
cart.covered.amountrequiredstringInsurable cart value as a string in major units, e.g. "189.34". Exclude digital and non-shippable items.
cart.covered.currencystringISO 4217. USD only today; defaults to USD.
cart.cart_refstringYour own cart identifier, echoed back for correlation.
cart.cart_itemsarrayRecorded for dispute resolution. Not re-summed — cart.covered.amount is authoritative.
merchant_idstringOptional and redundant; your key already identifies the store. If sent and it names a different store, the request 404s.
Response 200
{
      "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.

POST/v2/orders
Record an order and book its premium.
FieldTypeDescription
source_order_idrequiredstringYour order id. Must be unique for your store — a repeat returns 409.
source_order_numberstringThe number the customer sees. Defaults to source_order_id.
subtotalrequirednumberAggregate value of insurable items, excluding the protection premium itself. This is what protection pays out against.
insurance_selectedrequiredbooleanWhether the shopper bought protection. False records the order without booking a premium.
quote_idstringFrom POST /v2/quotes. Any string is accepted; unknown values simply re-price.
order_totalnumberWhat the customer paid at checkout.
shipping_totalnumberWhat the customer paid for shipping.
discounts_totalnumberTotal discounts applied to the order.
taxesnumberTotal tax on the order.
currencystringISO 4217. USD only today.
customer_detailsrequiredobjectfirst_name, last_name, email, and optional phone. Needed so the shopper can file a claim.
shipping_detailsrequiredobjectstreet_address1, street_address2, city, province, zip, country_code.
billing_addressobjectSame shape as shipping_details.
line_itemsrequiredarrayAt 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_onrequireddate-timeISO 8601. This decides which month the order lands in, so send the real placement time.
Response 201
{
      "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.

POST/v2/orders/{source_order_id}
Amend an order already sent to Arrive.

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.

POST/v1/orders/{source_order_id}/cancel
Stop protection applying to an order.

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

POST/v1/shipments
Register a tracking number.
FieldTypeDescription
tracking_numberrequiredstringSpaces and dashes are stripped and the value is upper-cased, so send whatever your platform stored.
source_order_idrequiredstringThe order this shipment belongs to.
courier_idstringCarrier slug — usps, ups, fedex. Optional; we resolve the carrier ourselves if you omit it.
source_product_idsarraysource_product_id values from the order.
line_itemsarraysource_id, source_product_id, and quantity — when a shipment carries only part of the order.
Response 201
{
      "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
    }
GET/v1/shipments/{tracking_number}
Read a shipment and its scan history.

Returns the same object with checkpoints populated — up to 100 carrier scans, oldest first. delivery_status is one of Active, Delivered, Exception, or Cancelled.

POST/v1/shipments/{tracking_number}
Correct the carrier on a shipment.

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.

POST/v1/shipments/{tracking_number}/cancel
Void a shipment.

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

FieldTypeDescription
QuotesstringMajor units as a string: "189.34". Must match ^\d{1,10}(\.\d{1,2})?$.
Orders and shipmentsnumberMajor units as a JSON number: 35.79. At most two decimal places — a third is rejected rather than rounded.
Webhooks and acknowledgementsintegerMinor 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.

Response 400
{
      "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" }
      ]
    }
Status codes
FieldTypeDescription
400invalid_requestMissing or malformed fields. See errors[].
401unauthorizedMissing, malformed, revoked, or unknown API key.
403forbiddenValid key, but not one that can be used here — most often a key that is not scoped to a store.
404not_foundNo such order or shipment for your store. Also returned for objects belonging to another store.
409conflictCollides with something already stored — a duplicate source_order_id, or a cancel blocked by an open claim.
422unprocessableDuplicate id within the request itself, such as two line items sharing a source_id.
429rate_limitedSlow down and retry with backoff.
500internal_errorOur 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.

FieldTypeDescription
WritessimulatedQuotes, orders, and shipments return a realistic object with a test_ id. Nothing is stored and no premium is booked.
ReadsemptyReads, updates, and cancels return 404: test keys never see live data.
X-Arrive-ModeheaderEvery 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.