Skip to main content

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, no Bearer prefix). See Authentication.
  • Optionally, the Upwell carrierId / shipmentId if 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.
  • Don’t send sourceSystem — Upwell always stamps source_system = "API" (and your integration) server-side.
  • Do send sourceSystemId — set it to your stable external id for this invoice (e.g. your TMS’s invoice id). It’s how Upwell de-duplicates re-posts: a second create with the same sourceSystemId will not create a duplicate (see Re-submitting). Omit it only if you have no external id to key on.
  • balance and status are the only required fields. Send invoiceNumber and totalAmount when you have them (your values are authoritative), but they’re optional — anything you leave blank is filled from the documents you attach in Step 2. For a new invoice, balance equals the invoice total; use status: "RECEIVED" when you’re sending the invoice data, or "AWAITING_INVOICE" if the invoice document will follow (it auto-promotes to RECEIVED once that document arrives and parses).
  • Money is integer cents. $2,847.50284750.
  • The create response does not echo a client id — store the returned id as your handle.

Response

The matched 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 inside input:
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.
See Integration patterns for the cross-cutting rules: integer-cents money, the input wrapper, and the presigned-upload header gotcha.

Re-submitting

When you send a sourceSystemId, 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).
The presigned URL is signed for host-only headers. Send Content-Type on the PUT, but avoid extra headers that aren’t in X-Amz-SignedHeaders. Wrapping the body in a typed Blob can make some Node fetch implementations add headers that break the signature — pass an ArrayBuffer. The (sourceSystem, sourceSystemId) pair on a document must be both set or both null. See Integration patterns.

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 stamps source_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.