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 Covr, and it is what support will ask for.

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

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.

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.

POST/v2/addresses/verify
Check one US shipping address.
FieldTypeDescription
address.street_address1requiredstringStreet line. Send components, not one freeform line.
address.street_address2stringApartment, suite, or unit, if you have one.
address.citystringRequired unless you send zip.
address.provincestringState name or two-letter abbreviation. Required unless you send zip.
address.zipstringZIP code. Required unless you send both city and province.
address.country_codestringUS, 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.

What comes back
FieldTypeDescription
verdictstringOne of the five below.
deliverableboolean | nullnull when, and only when, the verdict is unknown. Test it with === false, never with !.
reasonstringA stable code, e.g. secondary_missing.
messagestringThe same thing in plain words. Safe to show a support agent.
addressobject | nullThe 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.
changesstring[]What we changed, in your field names: street, city, state, zip, secondary. Empty unless the verdict is corrected.
flagsstring[]Warnings on a deliverable address: vacant, mail_receiving_agency, no_mail_service, no_street_delivery, po_box, military, general_delivery.
The five verdicts
FieldTypeDescription
deliverableshipUSPS delivers to it as you sent it.
correctedshipUSPS delivers to it, but not exactly as you sent it. Use the address field. changes says what moved.
unit_requiredholdThe 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.
undeliverableholdUSPS 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.
unknownshipCovr could not check. This is never a statement about the address.
Response 200 — corrected
{
  "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.

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-Covr-ModeheaderEvery 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.