Skip to main content

Knowing when a carrier invoice is processed

After you submit a carrier invoice, Upwell works on it asynchronously. This page explains how to learn when that work is done.
There’s no single “processed” flag. You read the existing fields on the invoice — its status, its exceptions, and the matched shipmentId — plus the type on each attached document. You can read them by polling the GET endpoints or by receiving webhooks. Most production integrations use webhooks for timeliness and polling as a reconciliation backstop.

Two phases of processing

It helps to separate two things:
  1. Ingestion (automatic, seconds after create). Upwell stamps provenance (source_system = "API"), matches the invoice to a shipment/carrier/bill from your references, and runs the exception audit. The status usually stays RECEIVED through this phase — ingestion populates shipmentId/billId/carrierId and updates exceptions, it doesn’t move the status.
  2. Review & approval (later; may involve your rules or a human). The invoice moves toward APPROVED or an exception state. This is where status changes.
So “is it done?” has two answers depending on what you’re waiting for. Both are observable from the same endpoints.

The signals

Status values

The generated Vocabulary page lists every enum value the API can produce, including the ones not defined below.
exceptions is a comma-joined string, not an array. Treat a non-empty string as “needs attention” even while status is still RECEIVED. And don’t judge exceptions until ingestion has run — a brand-new row always lists “no carrier / no matching shipment” findings that disappear once matching completes.

When a linked bill moves the status

Once a carrier invoice is linked to a bill, the bill’s status can move the invoice. The mapping depends on the bill’s source_system. Strings are exact, including case. Fully paid and partially paid bill statuses apply for every source: These bill statuses are removed for every source, and the carrier invoice goes to AWAITING_BILL: DELETED, VOID, Canceled. Approval that is not itself a payment is per source. A bill synced by one of Upwell’s TMS integrations moves the invoice to APPROVED only on that TMS’s own approval statuses. For example: TRANSPORT_PRO and transport-pro use the literal APPROVED on purpose. That is the same text Revenova stores; it is listed for Transport Pro rather than borrowed from Revenova. A status that only another TMS uses for approval does not approve: a Turvo bill whose status is APPROVED, or a Revenova bill whose status is Approved, leaves the invoice where it is (an invoice in AWAITING_BILL moves to RECEIVED, because it now has a bill). Bills you submit through the API (source_system = "API") use the combined list, because partner-authored bills keep bill status as the source of truth. So does a bill with no source_system, or one whose source_system has no list of its own. The combined list approves on AGING, SELECTED, Unpaid, APPROVED, Approved, Billed, and Transferred to Settlement; the paid and partially paid rows above apply as usual. A carrier invoice in AWAITING_INVOICE does not move because of the bill. The invoice document has to arrive first.

Approach 1 — Polling

Read the invoice and its documents on an interval. Wait for ingestion (the row’s updatedAt advances past createdAt), then read the outcome.
A minimal poller with backoff:
Start around a 1–2 s interval and back off exponentially. Ingestion usually settles within seconds; document classification can take longer for large multi-page PDFs.
The documents endpoint exposes the document’s type, not its internal classification status. A document still UNKNOWN past your timeout is ambiguous between “still classifying” and “couldn’t be classified” — treat it as needs manual review rather than a hard failure, and don’t block your pipeline on it.

Approach 2 — Webhooks

Subscribe once and let Upwell push the changes to you. Configure subscriptions in the Upwell dashboard (see Outbound webhooks for the model, payload shape, and auth). The webhook event catalog is the source of truth for trigger strings and dispatch rules (each change fires at most one trigger — the most specific that applies).
The only per-status webhooks are APPROVED and EXCEPTION — and those reflect the review lifecycle, not ingestion. To know that ingestion completed (matched + audited), listen to update.carrier_invoice / update.carrier_invoice.shipment_updated, or poll. Other status transitions (UNDER_REVIEW, PAID, …) don’t each get their own trigger.

Which should I use?

Webhooks

Best when you can host an HTTPS endpoint. Near-real-time, no polling load.

Polling

Best when you can’t receive inbound requests, or as a backstop. Read GET /carrier_invoices/{id} (+ /documents) with backoff.
Webhook delivery retries on 5xx and can be delayed under load. A robust integration uses webhooks for timeliness and reconciles with a periodic poll so a missed delivery never leaves an invoice stuck in your system. Both approaches are shown above, and Outbound webhooks covers the delivery and retry behavior in full.