Skip to main content

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

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}).
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.

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.
EndpointFieldWhat to send
POST /api/rest/shipmentsshipmentIdA unique string. Generate from your own id, e.g. "YOURSYS-SHIP-1001".
POST /api/rest/invoicesbalanceOutstanding 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.
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.

Wrapping POST payloads

For most insert endpoints, the request body shape is:
{ "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 for full details.