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

# Integration patterns

> Patterns and gotchas that aren't obvious from the API reference alone.

# Integration patterns

The endpoint reference describes *what* each endpoint accepts. This page covers the *how* — patterns we've seen customers hit when building against Upwell. Reach for this page when you're wiring up an integration and something the reference seems to imply doesn't quite match what the API does at runtime.

## 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 one of the by-source-system-id endpoints (e.g. `PUT /api/rest/carriers/by-source-system-id/{id}`).

<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`.
</Warning>

## All monetary fields are integer cents

Money is stored as integers in the smallest currency unit (US cents for USD). A \$2,847.50 shipment rate goes over the wire as `284750`. This applies to `customerTotalRate`, `carrierTotalRate`, `totalAmount`, `balance`, `totalTaxAmount`, and similar fields.

Float values are rejected at runtime:

```
"error":"The value 2847.5 lies outside the bounds or is not an integer. Maybe it is a float, or is there integer overflow?"
```

## 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"
```

## Document sourceSystem / sourceSystemId pair constraint

On `documents`, the check constraint `documents_source_system_pair_nullity_chk` requires both fields to be set or both to be null. Setting only `sourceSystem` without `sourceSystemId` fails with:

```
"Check constraint violation. ... documents_source_system_pair_nullity_chk"
```

The simplest pattern: always pass a `sourceSystemId` alongside `sourceSystem`. If you don't have a meaningful external id for a document, generate one (e.g. a UUID, or `<your-system>_doc_<timestamp>`).

## Document upload: two-step presigned URL

For any document larger than \~3 MB (typical for scanned PODs and BOLs), don't use the inline-base64 endpoints. Use the two-step flow:

1. `POST /api/rest/generate-upload-presigned-url` with `{ associationType, associationId, documentType, fileName, mimeType, sourceSystem, sourceSystemId }` returns `{ documentId, uploadUrl }`.
2. `PUT` the file bytes directly to `uploadUrl`.

<Tip>
  The presigned URL is signed for `host`-only headers. You can send `Content-Type` on the PUT, but avoid adding headers that aren't in `X-Amz-SignedHeaders`. Wrapping the body in a `Blob` with an explicit type can cause some Node fetch implementations to add headers that break the signature; passing an `ArrayBuffer` is the safe path.
</Tip>

## Wrapping POST payloads

For most insert endpoints, the request body shape is:

```json theme={null}
{ "input": { "field": "value", "...": "..." } }
```

A few endpoints (the bulk inserts) use `{ "inputs": [...] }`. If you get a `bad-request: Unexpected variable <fieldName>` error, your payload likely needs to be wrapped in `input`.

## Authentication header format

Send your API key directly in the `Authorization` header, **with no `Bearer` prefix**:

```
Authorization: YOUR_API_KEY
```

See [Authentication](/authentication) for full details.
