Submitting carrier invoices via API
If you’re an API-integrated TMS, you can push carrier invoices to Upwell programmatically instead of forwarding them by email. An invoice submitted this way gets identical processing to one received over email: shipment/carrier/bill matching, the exception audit, and AI classification of any documents you attach. This guide walks the end-to-end flow. For the full request/response schema of each endpoint, see the auto-generated API Reference — this page focuses on the workflow and the things that aren’t obvious from the schema alone.This flow reuses existing endpoints — there’s no separate “carrier invoice API” to learn. The same
POST /api/rest/carrier_invoices and POST /api/rest/generate-upload-presigned-url endpoints power the structured submission and its document uploads.Prerequisites
- An API key included on every request (
Authorization: YOUR_API_KEY, noBearerprefix). See Authentication. - Optionally, the Upwell
carrierId/shipmentIdif you already know the match. You don’t need them — Upwell matches the invoice to a shipment, carrier, and bill from the references you send (carrierProNumber,billOfLadingNumber,customerPoNumber,customerReferenceNumber).
Your API key is bound to an integration. Upwell uses that binding to stamp every invoice you submit as API-sourced — you don’t (and can’t) set the source yourself. See the warning under Step 1.
The flow
1
Create the carrier invoice
POST /api/rest/carrier_invoices with the invoice’s structured data wrapped in input. Store the id from the response.2
Attach documents (optional)
For each PDF (the invoice scan, BOL, POD, …) request a presigned URL, then
PUT the bytes to it.3
Let it process — then read the outcome
Upwell matches the invoice and runs the exception audit asynchronously (seconds). You learn the outcome by polling or by webhook — see Knowing when a carrier invoice is processed.
Step 1 — Create the carrier invoice
You send a bare invoice: the header fields plus whatever references help Upwell match it. Upwell fills in the matched shipment/carrier/bill and the provenance during processing.Response
shipmentId / billId / carrierId are null at this instant — matching runs a moment later (see Step 3). Note that exceptions is a comma-joined string (not an array); on a brand-new row it reflects the un-matched state and settles after processing.
Fields
Send these insideinput:
Document content populates the invoice. Any header field you leave blank — invoice number, amounts, currency, dates, references — and the line items, remit-to / factor, and carrier are filled from the documents you attach in Step 2, after they’re classified and parsed. Consumer-supplied values are authoritative: a field you set is never overwritten, and a genuine disagreement (e.g. a different total) is raised as an exception rather than silently changed. Line items aren’t settable directly on create — they come from the documents.This also holds for late-arriving documents: fields fill in whenever a document finishes parsing, even if it’s attached after creation — or after the invoice has been approved. On an already-approved invoice, blanks are still filled in place, but the status and any resolved exceptions are left untouched.
Re-submitting
When you send asourceSystemId, create is idempotent on (your integration, sourceSystemId): re-posting the same invoice does not create a duplicate — the re-posted record is discarded and the original stands. This is the recommended way to make submits safe to retry.
If you omit sourceSystemId, there’s no key to de-duplicate on, so a re-post does create a new record. Either way, store the returned id on first submit and use PUT /api/rest/carrier_invoices/{id} for later corrections.
Step 2 — Attach documents
Documents go through the standard two-step presigned-upload flow. Use it for every carrier document — the invoice scan, BOLs, PODs, and so on.1
Request a presigned URL
POST /api/rest/generate-upload-presigned-url with associationType: "CARRIER_INVOICE" and associationId set to the invoice id from Step 1. The response contains only uploadUrl and documentId.2
PUT the bytes
Upload the raw file to
uploadUrl. Send Content-Type; don’t add other headers (see the tip below).Document types and AI classification
If you know the document’s type, pass it (CARRIER_INVOICE, NOTICE_OF_ASSIGNMENT, BILL_OF_LADING, PROOF_OF_DELIVERY, …) and it’s used as-is. If you don’t — or you’re uploading a combined PDF — pass documentType: "UNKNOWN"; when AI classification is enabled for your tenant, Upwell splits and classifies the upload, assigning each resulting document a real type. The document stays UNKNOWN until classification completes. See Knowing when a carrier invoice is processed.
If the carrier factors their receivables, attach the
NOTICE_OF_ASSIGNMENT (the factoring notice). The invoice’s remit-to / factor is populated only from parsed documents — the NOTICE_OF_ASSIGNMENT, or factoring details printed on the invoice document itself. If neither carries a factoring signal, remit-to stays empty and remit-to checks can’t run.Step 3 — Let it process
Once the invoice exists, Upwell processes it asynchronously (typically within seconds): it stampssource_system = "API", matches the invoice to a shipment/carrier/bill from your references, and runs the exception audit. You don’t call anything for this — you observe the result.
Next: Knowing when it's processed
Poll the invoice, or subscribe to webhooks, to learn when matching, the exception audit, and document classification are done.

