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

# Idempotency & retries

> The sourceSystem/sourceSystemId external key, per-resource POST duplicate semantics (including the carrier-invoice async dedup), and the fields that are required with no default.

## Idempotent upserts via sourceSystem + sourceSystemId

Every primary record (customers, carriers, shipments, invoices, bills) carries two fields:

* `sourceSystem` — a constant string identifying you (e.g. `"TMSEZ"`)
* `sourceSystemId` — your own primary key for the record

Together this pair is your stable external handle into Upwell. Send both on every record. Upwell uses the pair so that webhooks and callbacks can reference your records by your IDs.

**For the first sync of a record, use `POST`.** For updates, use **`PUT /<resource>/{id}`** with the Upwell id you stored on first sync — or `PUT /<resource>` (no id in the URL) with `sourceSystem` + `sourceSystemId` in the request body to update by your external key.

<Warning>
  There is **no** `/by-source-system-id/{id}` URL pattern — the by-external-key update goes to the **base resource path**, not a suffixed one: `PUT /api/rest/carriers` (not `PUT /api/rest/carriers/by-source-system-id/{id}`), with `sourceSystem` + `sourceSystemId` inside `input`.
</Warning>

<Warning>
  `POST` is **not** retry-safe across the full surface. `POST /api/rest/customers`, `POST /api/rest/shipments`, and `POST /api/rest/invoices` will return a `400 Uniqueness violation` if you re-post a record with an existing `(sourceSystem, sourceSystemId)` pair. Treat first-sync as `POST` and every subsequent change as `PUT`.

  `POST /api/rest/carrier_invoices` behaves differently, and less strictly than "no duplicate": the insert always succeeds synchronously and returns a **new** id, even when you re-post a `sourceSystemId` that already exists. De-duplication happens **afterward**, asynchronously: a background enrichment step tries to stamp the new row's identity and, on detecting the same `(tenant, sourceSystemId)` already live, soft-deletes the **newer** row and leaves the original standing. Two things follow:

  * **The dedup key is `(tenant, sourceSystemId)` — it is not scoped by integration.** Two different integrations on the same tenant that happen to reuse the same external id string will collide with each other, not just with themselves.
  * **There's a real window (seconds) where the duplicate is live** under its own id before the background pass soft-deletes it. If your client never received the response to an earlier submit and retries, you may end up holding the id from the losing (soon-to-be-deleted) copy rather than the surviving original. Don't treat the id from a retried `POST` as authoritative — `GET /api/rest/carrier_invoices` with a `where` filter on `sourceSystemId` to find the surviving (non-deleted) row is the safe way to reconcile after a possible retry.

  See the [carrier-invoice submission guide](/api-guides/carrier-invoice-submission#re-submitting) for the full mechanics.
</Warning>

## Required fields without server-side defaults

A few fields are `NOT NULL` in our schema and have no default — you need to provide them on insert.

| Endpoint                   | Field        | What to send                                                               |
| -------------------------- | ------------ | -------------------------------------------------------------------------- |
| `POST /api/rest/shipments` | `shipmentId` | A unique string. Generate from your own id, e.g. `"YOURSYS-SHIP-1001"`.    |
| `POST /api/rest/invoices`  | `balance`    | Outstanding balance in cents. For a new invoice this equals `totalAmount`. |

Omitting these returns a `400 constraint-violation` with the column name in the error message:

```
"error":"Not-NULL violation. null value in column \"shipment_id\" of relation \"shipments\" violates not-null constraint"
```
