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

# Outbound webhooks

> Subscribe to Upwell events, authenticate the deliveries, and read the payload envelope.

# Outbound webhooks

Upwell can POST a JSON event to a URL you host whenever something changes — a carrier invoice is created, matched to a shipment, approved, and so on. Webhooks are the push counterpart to [polling](/api-guides/carrier-invoice-status).

## Subscribing

Webhook subscriptions are configured in the **Upwell dashboard** (there's no public REST endpoint to create one). A subscription has:

* **`url`** — your HTTPS receiver.
* **`triggers`** — the list of event types you want delivered.
* **`authSettings`** — how Upwell proves the delivery came from Upwell (see [Authenticating deliveries](#authenticating-deliveries)).
* **`retryAttempts`** — how many times Upwell retries a failed delivery (0–10).

## Carrier-invoice triggers

| Trigger                                                                               | Fires when                                                             |
| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `create.carrier_invoice`                                                              | A carrier invoice is created.                                          |
| `update.carrier_invoice`                                                              | Any field/status change on a carrier invoice.                          |
| `update.carrier_invoice.shipment_updated` / `create.carrier_invoice.shipment_updated` | The invoice was matched/linked to a shipment (fires during ingestion). |
| `update.carrier_invoice.status.APPROVED`                                              | The invoice reached `APPROVED` (review lifecycle).                     |
| `update.carrier_invoice.status.EXCEPTION`                                             | The invoice hit an exception status (review lifecycle).                |
| `create.carrier_document` / `update.carrier_document` / `delete.carrier_document`     | A document was added, (re)classified, or removed.                      |

<Note>
  "Ingestion done" (matched + audited) is best observed via `*.shipment_updated` or the generic `update.carrier_invoice`. The `*.status.APPROVED` / `*.status.EXCEPTION` triggers reflect the later **review lifecycle**. See [Knowing when a carrier invoice is processed](/api-guides/carrier-invoice-status).
</Note>

## The delivery envelope

Every delivery is a single JSON object — the event row. The fields you'll read:

| Field        | Description                                                                                                                                         |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `trigger`    | The trigger string, e.g. `update.carrier_invoice.status.APPROVED`. **Branch on this.**                                                              |
| `table`      | The resource table, e.g. `carrier_invoices`.                                                                                                        |
| `operation`  | `INSERT`, `UPDATE`, or `DELETE`.                                                                                                                    |
| `resourceId` | The id of the affected carrier invoice. **Use it to correlate** (and to fetch a canonical representation via `GET /carrier_invoices/{resourceId}`). |
| `payload`    | The event body — **its shape depends on the trigger** (see below).                                                                                  |
| `status`     | The **delivery** status of this webhook event (e.g. `SUCCESS`), **not** the invoice status. Don't confuse the two.                                  |
| `id`         | The delivery's own id. Use it to dedupe retries.                                                                                                    |

Plus delivery bookkeeping (`tenantId`, `retries`, `createdAt`, `processedAt`, …).

### The `payload` shape varies by trigger

<Warning>
  There is **no single payload shape**. Status-change events deliver a mapped object nested under `carrierInvoice`; row-level events (`*.shipment_updated`) deliver raw `new`/`old` column snapshots. The robust pattern is to read **`resourceId`** from the envelope and call **`GET /api/rest/carrier_invoices/{resourceId}`** for a canonical representation — then the payload is just a hint about what changed.
</Warning>

**Status-change events** (`update.carrier_invoice.status.APPROVED` / `.EXCEPTION`) — `payload.carrierInvoice` with the invoice and its matched relations nested:

```json theme={null}
{
  "trigger": "update.carrier_invoice.status.APPROVED",
  "resourceId": "cari_f68wIfYF",
  "payload": {
    "carrierInvoice": {
      "id": "cari_f68wIfYF",
      "status": "APPROVED",
      "invoiceNumber": "INV-99821",
      "totalAmount": 220000,
      "currency": "USD",
      "carrierProNumber": "SH-100015",
      "issueDate": "2026-06-29",
      "shipment": { "id": "shi_...", "shipmentNumber": "SH-100015" },
      "bill": {
        "id": "bil_...", "proNumber": "SH-100015", "totalAmount": 914917,
        "carrier": { "id": "car_...", "name": "AAA FREIGHT INC" },
        "vendor": null
      }
    }
  }
}
```

**Row-level events** (`update.carrier_invoice.shipment_updated`) — raw `new`/`old` snapshots (snake\_case columns):

```json theme={null}
{
  "trigger": "update.carrier_invoice.shipment_updated",
  "resourceId": "cari_dM-zp1Fk",
  "payload": {
    "new": {
      "id": "cari_dM-zp1Fk", "status": "RECEIVED", "shipment_id": "shi_...",
      "carrier_id": "car_...", "bill_id": "bil_...", "source_system": "API",
      "integration_id": "int_...", "invoice_number": "LIVE-WH-1782703087", "...": "..."
    },
    "old": { "...": "..." }
  }
}
```

<Note>
  Note `new.source_system` = `"API"` and `new.integration_id` are stamped by Upwell — confirming the invoice was API-submitted. `source_system_id` is `null` for API submissions.
</Note>

## Authenticating deliveries

You choose how Upwell authenticates to your receiver when you create the subscription. Three options:

<AccordionGroup>
  <Accordion title="Custom header (recommended)">
    You pick a header name and a secret value; Upwell sends that header on every delivery. A common choice is a token header:

    ```
    X-Upwell-Token: <your-shared-secret>
    ```

    Your receiver compares the header against the secret you configured and rejects mismatches. **The header name is your choice** — `X-Upwell-Token` is a convention, not a built-in.
  </Accordion>

  <Accordion title="Basic auth">
    Upwell sends `Authorization: Basic base64(username:password)` using the credentials you configure.
  </Accordion>

  <Accordion title="None">
    No auth header. Rely on URL secrecy / network controls. Not recommended for production.
  </Accordion>
</AccordionGroup>

Upwell always sends `Content-Type: application/json` and a `User-Agent` of `UpwellWebhookService/<version> (https://www.upwell.com)`.

## Reliability

* Deliveries that return **`5xx`** are retried, up to the subscription's `retryAttempts`.
* Each delivery has a **15-second** timeout.
* Return a `2xx` quickly; do slow work asynchronously.
* Build an **idempotent** receiver: dedupe on the delivery `id` (or `resourceId` + `trigger`).

## Example receiver

```javascript theme={null}
import express from 'express';
const app = express();
app.use(express.json());

app.post('/webhooks/upwell', (req, res) => {
  if (req.get('X-Upwell-Token') !== process.env.UPWELL_WEBHOOK_TOKEN)
    return res.status(401).send('unauthorized');

  const event = req.body; // the envelope
  // The invoice status lives in the (trigger-specific) payload, NOT event.status.
  const invoice =
    event.payload?.carrierInvoice ?? event.payload?.new ?? null;

  switch (event.trigger) {
    case 'update.carrier_invoice.status.APPROVED':
      onApproved(event.resourceId, invoice);
      break;
    case 'update.carrier_invoice.status.EXCEPTION':
      onException(event.resourceId, invoice);
      break;
    case 'update.carrier_invoice.shipment_updated':
      onMatched(event.resourceId, invoice);
      break;
  }
  res.json({ ok: true }); // ack fast; 5xx would be retried
});
```

<Card title="See it end-to-end" icon="code" href="/api-guides/carrier-invoice-submission">
  The sample TMS app implements this receiver alongside a polling fallback, and walks the full submit → attach → process flow. Start from the [submission guide](/api-guides/carrier-invoice-submission).
</Card>
