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/ matching, the 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— aBearerprefix also works). 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
POST /api/rest/carrier_invoices is a plain insert — it does not check for an existing sourceSystemId before creating the row, and it always returns a new id, even on a re-post. De-duplication happens afterward, asynchronously, once the background enrichment pass runs on the new row: if it finds another live invoice already holding the same sourceSystemId for your tenant, it soft-deletes the newer row (the one you just posted) and leaves the original untouched. The end state is one live invoice, but getting there is not synchronous, and it’s not scoped the way you might expect:
Don’t treat the id from a retried POST as automatically correct. If you’re not certain whether an earlier submit with the same sourceSystemId already succeeded, look up the surviving row instead of trusting the response you just got:
sourceSystemId, there’s no key to de-duplicate on, so a re-post does create a permanent duplicate. Either way, store the returned id on first submit and use PUT /api/rest/carrier_invoices/{id} for later corrections — just be prepared to reconcile via the lookup above if you’re re-submitting after an uncertain prior attempt.
Bulk create
POST /api/rest/bulk-carrier-invoices accepts { "inputs": [...] } — an array of the same objects you’d send individually to /api/rest/carrier_invoices. Every row goes through the same asynchronous enrichment and sourceSystemId de-duplication described above, independently per row — there’s no batch-level atomicity guarantee beyond what a single row gets.
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).The
(sourceSystem, sourceSystemId) pair on a document must be both set or both null — see
Document conventions.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.
Recording carrier payments
Once a carrier invoice is approved, a carrier payment line item records that a payment was made against it — the AP counterpart to a customer payment’s line items on the AR side.POST /api/rest/carrier_payment_line_items (bulk variant: POST /api/rest/bulk-carrier-payment-line-items, same { "inputs": [...] } shape as elsewhere) creates one.
A carrier payment line item’s real parent is technically a bill payment (
blp_) via billPaymentId, not billId. billPaymentId is accepted on create/update, but it is not included in the response body, and it is rarely populated in practice — billId is the link that carrier payment line items are reliably keyed on. Treat billId as the field to build against; don’t assume billPaymentId will be populated or plan a workflow around reading it back.
