> ## Documentation Index
> Fetch the complete documentation index at: https://docs.upwell.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Submitting carrier invoices via API

> Create a carrier invoice, attach its documents, and let Upwell process it — the same way the email pipeline does.

# 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](/api-reference/introduction) — this page focuses on the workflow and the things that aren't obvious from the schema alone.

<Note>
  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.
</Note>

## Prerequisites

* An **API key** included on every request (`Authorization: YOUR_API_KEY`, no `Bearer` prefix). See [Authentication](/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`).

<Info>
  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.
</Info>

## The flow

<Steps>
  <Step title="Create the carrier invoice">
    `POST /api/rest/carrier_invoices` with the invoice's structured data wrapped in `input`. Store the `id` from the response.
  </Step>

  <Step title="Attach documents (optional)">
    For each PDF (the invoice scan, BOL, POD, …) request a presigned URL, then `PUT` the bytes to it.
  </Step>

  <Step title="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](/api-guides/carrier-invoice-status).
  </Step>
</Steps>

## 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.

<Warning>
  * **Don't send `sourceSystem` or `sourceSystemId`.** Upwell stamps `source_system = "API"` (and your integration) server-side during processing. (Those two fields *are* yours to set on **documents** — see Step 2 — just not on the invoice itself.)
  * **`balance` and `status` are required** (along with `invoiceNumber` and `totalAmount`). For a new invoice, `balance` equals `totalAmount`; `status` is `"RECEIVED"`.
  * **Money is integer cents.** `$2,847.50` → `284750`.
  * The create response does **not** echo a client id — **store the returned `id`** as your handle.
</Warning>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.upwell.com/api/rest/carrier_invoices \
    -H "Authorization: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "input": {
        "invoiceNumber": "INV-99821",
        "carrierName": "Acme Freight",
        "totalAmount": 284750,
        "balance": 284750,
        "status": "RECEIVED",
        "currency": "USD",
        "issueDate": "2026-06-20",
        "dueDate": "2026-07-20",
        "billOfLadingNumber": "BOL-77123",
        "carrierProNumber": "PRO-44120"
      }
    }'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch('https://api.upwell.com/api/rest/carrier_invoices', {
    method: 'POST',
    headers: { Authorization: 'YOUR_API_KEY', 'Content-Type': 'application/json' },
    body: JSON.stringify({
      input: {
        invoiceNumber: 'INV-99821',
        carrierName: 'Acme Freight',
        totalAmount: 284750, // integer cents — $2,847.50
        balance: 284750, // required; equals totalAmount for a new invoice
        status: 'RECEIVED', // required
        currency: 'USD',
        issueDate: '2026-06-20',
        dueDate: '2026-07-20',
        billOfLadingNumber: 'BOL-77123',
        carrierProNumber: 'PRO-44120',
        // NOTE: do not send sourceSystem / sourceSystemId here — server-stamped.
      },
    }),
  });

  const { createCarrierInvoice } = await res.json();
  const carrierInvoiceId = createCarrierInvoice.id; // store this
  ```
</CodeGroup>

### Response

```json theme={null}
{
  "createCarrierInvoice": {
    "id": "cari_a1b2c3d4e5",
    "invoiceNumber": "INV-99821",
    "status": "RECEIVED",
    "balance": 284750,
    "currency": "USD",
    "shipmentId": null,
    "billId": null,
    "carrierId": null,
    "exceptions": "NO MATCHING SHIPMENT, INVOICE AMOUNT MISMATCH",
    "carrierInvoiceLineItems": []
  }
}
```

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`:

| Field                                                                                   | Notes                                                                                     |
| --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `invoiceNumber`                                                                         | **Required.** The carrier's invoice number.                                               |
| `totalAmount`                                                                           | **Required.** Integer cents.                                                              |
| `balance`                                                                               | **Required.** Outstanding amount in cents (`= totalAmount` for a new invoice).            |
| `status`                                                                                | **Required.** Use `"RECEIVED"`.                                                           |
| `currency`                                                                              | ISO code, e.g. `"USD"`.                                                                   |
| `carrierName`                                                                           | Carrier name as printed — helps matching and avoids a spurious "no carrier name" finding. |
| `carrierProNumber`, `billOfLadingNumber`, `customerPoNumber`, `customerReferenceNumber` | Matching references. `carrierProNumber` is the strongest.                                 |
| `shipmentId`, `carrierId`                                                               | Pin the match yourself if you already know it.                                            |
| `loadNumber`, `issueDate`, `dueDate`, `totalTaxAmount`, `metadata`, `referenceNumbers`  | Optional.                                                                                 |

<Warning>
  Line items are **not** settable on create via the API (the `carrierInvoiceLineItems` insert isn't exposed to API callers). Send the header `totalAmount`; Upwell derives line-level detail from the documents you attach.
</Warning>

<Tip>
  See [Integration patterns](/integration-patterns) for the cross-cutting rules: integer-cents money, the `input` wrapper, and the presigned-upload header gotcha.
</Tip>

### Re-submitting

There's **no server-side idempotency** on create — re-posting the same invoice creates a **new** record. Store the returned `id` on first submit and use `PUT /api/rest/carrier_invoices/{id}` for any later corrections; guard against accidental re-submits on your side.

## 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.

<Steps>
  <Step title="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`.
  </Step>

  <Step title="PUT the bytes">
    Upload the raw file to `uploadUrl`. Send `Content-Type`; don't add other headers (see the tip below).
  </Step>
</Steps>

<CodeGroup>
  ```javascript JavaScript theme={null}
  // 1) ask Upwell for a presigned URL for a CARRIER_INVOICE document
  const presign = await fetch(
    'https://api.upwell.com/api/rest/generate-upload-presigned-url',
    {
      method: 'POST',
      headers: { Authorization: 'YOUR_API_KEY', 'Content-Type': 'application/json' },
      body: JSON.stringify({
        input: {
          associationType: 'CARRIER_INVOICE',
          associationId: carrierInvoiceId, // the id from Step 1
          documentType: 'CARRIER_INVOICE', // or BILL_OF_LADING, PROOF_OF_DELIVERY, UNKNOWN, …
          fileName: 'invoice-99821.pdf',
          mimeType: 'application/pdf',
          // On DOCUMENTS, sourceSystem + sourceSystemId ARE yours to set (both or neither):
          sourceSystem: 'YOURSYS',
          sourceSystemId: 'YOURSYS-CI-99821-DOC1',
        },
      }),
    },
  ).then((r) => r.json());

  // the result is wrapped under `generateUploadPresignedUrl`; only these two fields are returned
  const { uploadUrl, documentId } = presign.generateUploadPresignedUrl;

  // 2) PUT the raw bytes (an ArrayBuffer, not a typed Blob)
  await fetch(uploadUrl, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/pdf' },
    body: fileArrayBuffer,
  });
  ```

  ```bash cURL theme={null}
  # 1) presigned URL
  curl -X POST https://api.upwell.com/api/rest/generate-upload-presigned-url \
    -H "Authorization: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "input": {
        "associationType": "CARRIER_INVOICE",
        "associationId": "cari_a1b2c3d4e5",
        "documentType": "CARRIER_INVOICE",
        "fileName": "invoice-99821.pdf",
        "mimeType": "application/pdf",
        "sourceSystem": "YOURSYS",
        "sourceSystemId": "YOURSYS-CI-99821-DOC1"
      }
    }'
  # -> { "generateUploadPresignedUrl": { "uploadUrl": "https://...s3...", "documentId": "doc_..." } }

  # 2) upload the bytes
  curl -X PUT "https://...s3..." \
    -H "Content-Type: application/pdf" \
    --data-binary @invoice-99821.pdf
  ```
</CodeGroup>

<Tip>
  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](/integration-patterns).
</Tip>

### Document types and AI classification

If you know the document's type, pass it (`CARRIER_INVOICE`, `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](/api-guides/carrier-invoice-status).

## 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.

<Card title="Next: Knowing when it's processed" icon="circle-check" href="/api-guides/carrier-invoice-status">
  Poll the invoice, or subscribe to webhooks, to learn when matching, the exception audit, and document classification are done.
</Card>
