Skip to main content

Syncing foundation entities

Before carrier invoices can match or customer invoices can bill, Upwell needs the data they reference: customers, carriers, shipments, companies, and (for AP) bills. This page covers how to push those records from your TMS.
If you haven’t read it yet, start with Core concepts & data model — it defines every object mentioned here and how they connect. For patterns that apply to every endpoint (integer-cents money, the input wrapper, the presigned upload), see Integration patterns.

Sync order

Entities reference each other, so push them in dependency order:
1

Companies (only if you invoice under more than one identity)

Your own legal entities. Most tenants already have the one they need and can skip this entirely — see Companies.
2

Customers and carriers

These two are independent — sync them in any order (or in parallel). Everything downstream needs at least one of them.
3

Shipments

A shipment references one customer and one carrier by their Upwell IDs (customerId, carrierId). Sync those first, store the returned IDs, then sync shipments.
4

Bills (AP only)

A bill references a shipment and a carrier. Optional for AR-only integrations.
5

Invoices, carrier invoices, payments

These are the transactional records — each guide has its own page: Customer invoices (AR), Carrier invoice submission (AP), Customer payments.

Customers

POST /api/rest/customers creates a customer (shipper). Store the returned id — you’ll need it on shipments and invoices.
To update an existing customer, use PUT /api/rest/customers/{id} (by Upwell ID) or PUT /api/rest/customers with sourceSystem + sourceSystemId in the body to update by your external key.
The customer’s billing address is a separate record. It isn’t a field on the customer — it’s an address row with type: "CUSTOMER_BILLING", and it’s what the invoice bill-to block and delivery routing read. You can nest it in this POST, but never in a PUT. A customer may hold at most one. See Working with addresses.
POST is not idempotent for customers — re-posting a (sourceSystem, sourceSystemId) pair that already exists returns a 400 Uniqueness violation. Use POST for first sync, PUT for updates. See Idempotency & retries.

Bill-to hierarchy and merging

A customer record carries a few fields beyond the basics above that model billing hierarchies and account cleanup:
There is no public endpoint for merging customers yourself — mergedIntoCustomerId is something you may see on a fetched record, not a field your integration should populate. Setting it yourself would incorrectly mark your own record as merged away.

Carriers

POST /api/rest/carriers creates a carrier.
Carriers do not follow the same duplicate-handling pattern as customers. Re-posting an existing (sourceSystem, sourceSystemId) pair for a customer hard-fails with 400 Uniqueness violation. Re-posting the same pair for a carrier silently succeeds instead — it’s a no-op upsert that only re-writes sourceSystem to itself, it does not update name, email, phone, or any other field you send. If you need to change an existing carrier’s details, use PUT, not a repeated POST.
Updates: PUT /api/rest/carriers/{id} (by Upwell ID) or PUT /api/rest/carriers (by sourceSystem + sourceSystemId in the request body).
Carriers are the one entity that holds its own address link. Unlike customers and companies — where the address points at the parent — a carrier carries addressId and billingAddressId, and both are settable on update. See Which direction the link runs.

Shipments

POST /api/rest/shipments creates a shipment (load). A shipment ties together a customer and a carrier and is the hub that invoices, bills, and documents hang off.
shipmentId is required and has no server default — omitting it returns a 400 constraint-violation. This is different from the id Upwell assigns (like shi_abc123); shipmentId is your own human-readable identifier.

Sending a numeric status code

Many TMSs store load status as an integer. Upwell’s status is typed String, so send the label, not the code — translate on your side before the call:
If your codes are 1 = Available, 2 = Dispatched, 3 = In Transit, 4 = Delivered, keep that map in your integration layer and send "DELIVERED" for 4. Sending "4" is accepted — the column is unvalidated text — but it is strongly discouraged, for a concrete reason: the status is not just display data. It gates carrier-invoice approval. Upwell’s TMS Status Mismatch exception lets an operator nominate which statuses mean “this load is far enough along that its carrier invoice can be approved.” In Payables Settings those statuses come from two places: a dropdown listing the statuses registered for your integration, plus a free-text field for anything not registered. Registration is a step on our side — statuses are not discovered automatically from the loads you send, so a value we have not registered will never appear in that dropdown and has to be typed in by hand, matching exactly. That is the practical reason to send labels rather than codes. An operator choosing between AVAILABLE / DISPATCHED / IN_TRANSIT / DELIVERED can configure the rule correctly; one typing 1 / 2 / 3 / 4 from memory cannot, and a wrong pick either blocks valid invoices or approves loads that never delivered. Two practical notes:
  • Matching is case-insensitive and whitespace-trimmed, but otherwise exact — "Delivered", "DELIVERED", and "delivered " are the same status; "Delivered to consignee" is a different one. Pick one spelling per status and stay consistent, or the exception rule will silently stop matching.
  • A shipment with no status at all trips the same exception once the rule is enabled, so populate status on create and keep it current with PUT /api/rest/shipments.
Send us the finished list of status strings your integration emits and we’ll register them against your integration, so they appear in that dropdown ready to select rather than having to be typed in by hand. Send it once the list is stable, ideally before go-live.

One customer per shipment

This is the modeling question that trips up most TMS integrations, so it’s worth stating flatly:
  • A shipment has exactly one customer. customerId is a single field, not a list. It is the party you will invoice — the bill-to. There is no “customers” collection on a shipment.
  • The shipper and the consignee are addresses, not customers. They go in the shipment’s own slots: pickupAddress / pickupAddressId (origin) and consigneeAddress / consigneeAddressId (destination), typed SHIPMENT_PICKUP and SHIPMENT_CONSIGNEE.
  • Additional stops go in stops, each with its own address. A stop has two type fields and they behave differently: stopType is free text — nothing validates it, so a typo is accepted silently — while type is constrained by an enum (ORIGIN, PICKUP, DELIVERY, DROP_OFF, TRANSFER, CROSS_DOCK, TERMINAL, CUSTOMS, LAYOVER, RETURN, STORAGE, FUEL_STOP, CONSOLIDATION, DECONSOLIDATION, OTHER). Both are read, by different surfaces — proof-of-delivery exception evaluation and the carrier-invoice shipments view read the constrained type, while some outbound EDI and partner payloads read stopType — so populate both, drawing the same value from the enum. One caveat: this endpoint (POST /api/rest/shipments) accepts only stopType, so a stop created here cannot carry the constrained type until it is set through another path. The Vocabulary page defines every value.
So a load that picks up at one facility and delivers to another is one shipment, with one customerId and two address records — not two customer records. Only create a customer for a party you actually invoice.
Because shipper/consignee is a per-shipment role rather than a property of a business, the same warehouse being the consignee on one load and the pickup on another is completely normal. That’s two address records with two types, and no deduplication will merge them. See Addresses.
Updates: PUT /api/rest/shipments/{id}. Note that the pickup and consignee links are fixed at creation — see Working with addresses.

Bills (AP only)

A bill is the expected payable — what your TMS says you should owe a carrier for a shipment. When a carrier invoice arrives, Upwell audits it against the bill and flags mismatches as exceptions. POST /api/rest/bills creates a bill:
Updates: PUT /api/rest/bills/{id}.
Bills are optional for carrier-invoice processing — Upwell can still receive and classify carrier invoices without a matching bill. But without a bill, the exception audit can’t compare expected vs. actual amounts, so you won’t get amount-mismatch exceptions. If your TMS tracks expected carrier costs, push them as bills.

Companies

A company is one of your own legal entities or divisions — not an external business. It carries the identity that appears on the invoices you issue: name, logo, terms of service, MC and DUNS numbers, primary contact details. Your tenant is seeded with one from your account settings, and most integrations never need to create another.
Your customers are not companies. This is the most common mis-mapping. If your TMS calls the businesses you bill “companies”, they map to Upwell customers — see Customers above. Reach for a company only when you operate under more than one brand or legal entity and an invoice needs to go out under a specific one.A useful test: if you’d ever send it an invoice, it’s a customer. If it’s the name on the invoice, it’s a company.
You need more than one company when you invoice under multiple identities — separate subsidiaries, acquired brokerages still billing under their own name, or divisions with distinct remit-to branding. If you invoice everything under one name, one company is correct.
A company address must be typed COMPANY_ADDRESS. The company pages query that value exactly; an address of any other type is saved and linked but never displayed. The list view shows only the oldest one. See Where an address shows up in the app.
Updates: PUT /api/rest/companies/{id}. As with customers, the update body takes no nested addresses — write the address with POST/PUT /api/rest/addresses and its companyId.
DELETE /api/rest/companies/{id} is a soft delete, unlike customers and carriers. The record is retained with deletedAt set and disappears from the app, which frees its name for reuse. Its addresses are not removed. See Deleting and archiving.
Reading them back is limited, and it’s worth understanding before you pick a create path. GET /api/rest/companies and GET /api/rest/companies/{id} return the company’s own fields but not its addresses, and neither accepts a filter.
POST /api/rest/companies does not return the ids of nested addresses — its response carries the company’s scalar fields only. So the convenient nested form above hands you a company id and nothing else, and because a company has no address read path and addresses carry no sourceSystem/sourceSystemId, the only way back to that addr_ id is paging the unfiltered GET /api/rest/addresses list.Prefer creating a company address standalone: POST /api/rest/addresses with companyId and type: "COMPANY_ADDRESS" returns the addr_ id in its response. Store it — you need it to update the address later. (Customers and shipments don’t have this problem; their create responses do include nested address ids.)

Vendors

A vendor is a payable party that isn’t a hauling carrier — a lumper service, a factor, a utility, a non-freight supplier. POST /api/rest/vendors creates one:
Updates: PUT /api/rest/vendors/{id}.
DELETE /api/rest/vendors/{id} is a permanent hard delete — vendors have no soft-delete column, unlike most other Upwell resources. It will also fail outright if any bill still references the vendor (bills.vendor_id is a restrict-on-delete foreign key) — reassign or remove those bills first.

Keeping records in sync

Most TMS integrations run a two-phase sync:
  1. Initial load — push all active customers, carriers, and shipments via POST. Store the returned Upwell IDs in your system.
  2. Ongoing sync — when a record changes in your TMS, push the update via PUT. You can update by Upwell ID (PUT /api/rest/<resource>/{id}) or by source-system key (PUT /api/rest/<resource> with sourceSystem + sourceSystemId in the body).
Your TMS fires a webhook on every create/update, and your integration pushes the change to Upwell immediately. This is the lowest-latency option — records are usually in Upwell within seconds. Best for TMS platforms with outbound webhook support.
A cron job queries your TMS for records changed since the last run and pushes them to Upwell in bulk. Simpler to build; latency depends on your schedule (typically 5–15 minutes). Best for TMS platforms with a “changed since” query API.
Upwell calls your TMS’s API on a schedule to pull new/changed records. Requires your TMS to expose a query API. Contact Upwell to set this up.

Bulk create

For the initial load, bulk-customers, bulk-carriers, and bulk-bills let you create many records in one request instead of looping individual POST calls. Each accepts { "inputs": [...] } (plural — see Wrapping POST payloads):
Bulk endpoints don’t all share the same duplicate-handling behavior as their singular counterparts:
  • bulk-carriers has no upsert at all — unlike the singular POST /api/rest/carriers (which silently no-ops on a duplicate key), a duplicate (sourceSystem, sourceSystemId) anywhere in a bulk-carriers batch fails the entire batch.
  • bulk-bills does upsert on the same key as the singular endpoint, matching its behavior.
  • Neither bulk endpoint reports partial success — a batch either fully succeeds or fully fails.

What’s next

Once foundation entities are in Upwell, you can build on top of them:

Customer invoices (AR)

Create customer invoices tied to shipments.

Carrier invoice submission (AP)

Submit carrier invoices for audit and matching.

Shipment documents

Attach BOLs, PODs, and other documents to shipments.

Customer payments

Record remittances from customers.