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 Covr, 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.
Address verification
Ask whether USPS will deliver to an address, before you print a label. A parcel that comes back for a bad address is a replacement somebody has to fund, and catching one is cheaper than shipping it twice.
The answer is in Covr's words, not a verification vendor's: five verdicts, one action each. You never have to learn what a DPV match code is.
/v2/addresses/verify| Field | Type | Description |
|---|---|---|
| address.street_address1required | string | Street line. Send components, not one freeform line. |
| address.street_address2 | string | Apartment, suite, or unit, if you have one. |
| address.city | string | Required unless you send zip. |
| address.province | string | State name or two-letter abbreviation. Required unless you send zip. |
| address.zip | string | ZIP code. Required unless you send both city and province. |
| address.country_code | string | US, or omit. Anything else answers unknown without checking — we verify United States addresses only. |
Send the address object you already have
These are the same field names as shipping_details on an order, and shipping_details is accepted as a key here too. Names and phone numbers are ignored — they play no part in deliverability, so we would rather not receive them.
| Field | Type | Description |
|---|---|---|
| verdict | string | One of the five below. |
| deliverable | boolean | null | null when, and only when, the verdict is unknown. Test it with === false, never with !. |
| reason | string | A stable code, e.g. secondary_missing. |
| message | string | The same thing in plain words. Safe to show a support agent. |
| address | object | null | The address USPS will deliver to, in the same field names you sent, plus zip4. Present only on deliverable and corrected — write it back only on corrected. Null on every verdict you would hold on, because there is nothing deliverable to write back. |
| changes | string[] | What we changed, in your field names: street, city, state, zip, secondary. Empty unless the verdict is corrected. |
| flags | string[] | Warnings on a deliverable address: vacant, mail_receiving_agency, no_mail_service, no_street_delivery, po_box, military, general_delivery. |
| Field | Type | Description |
|---|---|---|
| deliverable | ship | USPS delivers to it as you sent it. |
| corrected | ship | USPS delivers to it, but not exactly as you sent it. Use the address field. changes says what moved. |
| unit_required | hold | The building is real; the apartment, suite, or unit number is missing or is not one USPS recognises there. Ask the customer for it. This is the most common reason a parcel comes back. |
| undeliverable | hold | USPS has no record of it. reason and message say why — a missing street number, a ZIP that does not go with the city, and so on. |
| unknown | ship | Covr could not check. This is never a statement about the address. |
{
"verdict": "corrected",
"deliverable": true,
"reason": "corrected",
"message": "USPS delivers to this address, but not exactly as it was given. Use the address in `address`.",
"address": {
"street_address1": "1441 Innovation Way",
"street_address2": null,
"city": "Lehi",
"province": "UT",
"zip": "84043",
"zip4": "6503",
"country_code": "US"
},
"changes": ["zip"],
"flags": []
}Never hold an order on `unknown`
unknown means Covr could not reach its verifier, or the address is outside the United States. It says nothing at all about the address, and deliverable is null rather than false so that a truthiness check cannot quietly turn our outage into your refused customer. The same goes for any non-200 from this endpoint: ship the order.
Nothing here changes your order
This endpoint has no side effects. It does not hold an order, stop a shipment, alter a premium, or store the address. What you do with a verdict is yours to decide.
Test keys never spend a lookup
With an ak_test_… key the answer is computed locally: any well-formed address comes back deliverable, and ZIP 00000 comes back undeliverable, so you can wire up both branches before you go live.
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-Covr-Mode | header | Every response carries live or test, so you can assert which key a deploy is using. The same value is also sent as legacy X-Arrive-Mode. |
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.
Address verification also has a daily ceiling
/v2/addresses/verify allows 120 requests per minute and 10,000 per day per store. The daily figure is several times the volume of our busiest store, so you will only meet it if something is looping. Identical addresses are deduplicated for 24 hours, which saves us a lookup — but every request counts against these limits, deduplicated or not.