Skip to main content
Addresses behave differently from every other object in the API, and the difference causes real integration bugs. Read Addresses on the data-model page for why; this section is the how. Three rules govern everything below:
  1. An address belongs to one parent. It is not shared master data and is never deduplicated by content. The same physical place used in two roles is two address records.
  2. There is no external key. Addresses have no sourceSystem/sourceSystemId, so there is no upsert-by-your-id. Store the addr_ id Upwell returns, keyed by parent and address type, or you lose your ability to update that address later.
  3. You can embed an address when you create its parent. You can never embed one on an update. Updates always take a separate call.
The nested addresses object exists on create inputs only. Sending it on a PUT/PATCH does nothing useful — the update inputs for customers, companies, and shipments have no address field at all. If an address “silently didn’t save” on an update, this is why.
This is the part that surprises people: the foreign key lives on a different side depending on the entity. So for a customer or company you patch the address; for a carrier you patch the carrier. Patching the customer itself does nothing, because there is no field there to accept it.
A shipment’s pickup and consignee addresses can only be linked when the shipment is created. At creation you can either embed a new address (pickupAddress / consigneeAddress) or point at an existing one (pickupAddressId / consigneeAddressId). After that the link is fixed: the shipment update input exposes neither id field, and there is no dedicated endpoint to relink them.This is rarely a problem in practice, because correcting the address record is almost always what you actually want — PUT /api/rest/addresses/{id} changes it in place and the shipment keeps pointing at it. You only hit the wall if you need to swap a shipment onto a different address row.

Task: create a record with its address

Embed the address under its parent. Use { "data": [ … ] } for a list of addresses (customers, companies) and { "data": { … } } for a single slot (a shipment’s pickup or consignee).
Don’t set customerId inside a nested address — the parent supplies it. Read the created address ids out of the response and store them, because this is the only moment they’re handed to you without a lookup.
Companies are the exception, and it’s a trap. POST /api/rest/companies returns the company’s own fields only — the nested addresses come back with no ids. Since a company exposes no address read path either, and addresses have no sourceSystem/sourceSystemId to look them up by, an address created that way is recoverable only by paging the unfiltered GET /api/rest/addresses list.If you need the id — and you do, to ever update that address — create it standalone instead: POST /api/rest/addresses with companyId and type: "COMPANY_ADDRESS", which returns the addr_ id directly. Nested creation is fine for customers and shipments, whose responses do include the new ids.

Task: add an address to a record that already exists

Two calls, and the second one depends on the entity (see the direction table above).
1

Create the address on its own

POST /api/rest/addresses with the address fields, its type, and — for a customer or company — the parent id (customerId / companyId) directly on the address.
2

Link it, if the parent holds the link

For a carrier, PUT /api/rest/carriers/{id} with addressId or billingAddressId. For a customer or company you’re already done: step 1 set the link.

Task: change an existing address

If you kept the addr_ id, this is one call:
If you didn’t keep it, fetch the parent and read the address off it first — GET /api/rest/customers/{id} returns the customer’s addresses, and GET /api/rest/carriers/{id} returns addressId / billingAddressId. Match on the address type to pick the right one.
Updating the address in place is almost always what you want. Because the parent points at the same addr_ id, every record using that address sees the change — no relinking needed.

Address types

type is required on every address — there is no default and no fallback. It records the role the address plays, and several product surfaces query it directly, so a valid address carrying the wrong type saves successfully and then does nothing. These are the ones an integration sets: Upwell also writes SUPPORTING_DOC_PARTY, DELIVERY_RECEIPT, and LUMPER_RECEIPT addresses from its own document-parsing pipelines. You’ll see them on reads; you shouldn’t create them.
One physical place, two roles, two records. If the same warehouse is both the consignee on one shipment and the pickup on another, that’s two address records with two different types. This is expected — there’s no way to share one row across two type slots, and no dedup will merge them.

Where an address shows up in the app

An address can save cleanly — 201, real addr_ id, readable back over the API — and still appear nowhere in the UI. That is not a bug and it produces no error: the pages filter on address type, and they select a narrow set of fields. Two consequences worth internalizing before you debug a “missing” address:
  • An ADDRESS_BOOK address on a customer or company is invisible. It exists, it’s linked, and no page will ever draw it. This is the single most common cause of “the API accepted it but I can’t see it.”
  • An address phoneNumber never reaches a company page. The company pages don’t load that field at all, so putting a phone number on a COMPANY_ADDRESS row is a dead end. The phone shown on a company record is the company’s own primaryPhone — a field on the company itself, set with PUT /api/rest/companies/{id}. (Customer pages do load the address phone number, so this asymmetry is company-specific.)
Debug in this order:
  1. GET /api/rest/addresses/{addressId} — confirms the row saved and returns its type. Check that against the table above: the type is wrong far more often than the write failed.
  2. Confirm the link from the parent — the address endpoints never return customerId or companyId, so an orphaned address is indistinguishable from a linked one when you fetch it directly. For a customer, GET /api/rest/customers/{id} and look for it in addresses. For a carrier, GET /api/rest/carriers/{id} and check addressId / billingAddressId.
Step 2 has no company equivalent. GET /api/rest/companies/{id} returns no addresses, and the address endpoints return no companyId, so the link is exposed by neither side. Step 1 still works — you can always read the type back — but if the type is right and the address still doesn’t render, you cannot confirm the link over the API. Two things to check before assuming it’s broken: that the address carries a companyId at all — if you created it standalone you had to send one, whereas the nested form on POST /api/rest/companies sets it for you — and, on the company list view only, the oldest-address rule above.

One billing address per customer

A customer can hold many addresses, but at most one CUSTOMER_BILLING address. This is a database constraint, not a convention — a second one is rejected outright:
So “create the billing address” is only safe the first time. The supported pattern for keeping one in sync — the same one Upwell’s own TMS integrations use — is match by type, then update in place:
1

Read the customer's existing addresses

GET /api/rest/customers/{id} returns its addresses, each with its type.
2

Find the CUSTOMER_BILLING row

Match on type, not on street or city — the address content is what changes.
3

Update it if present, create it if absent

Present → PUT /api/rest/addresses/{addressId}. Absent → POST /api/rest/addresses with customerId and type: "CUSTOMER_BILLING".
This is also why re-sending a full customer payload with a nested addresses block is not a safe “upsert” — the nested block always inserts. Use it on first creation only.
No other address type is constrained this way. A customer may hold any number of ADDRESS_BOOK or CONTACT rows, and a company any number of COMPANY_ADDRESS rows (though only the oldest shows in the company list view).

Reading addresses back

Read paths are narrower than write paths, which matters when you’re verifying a sync:
The list endpoints take no filters. GET /api/rest/addresses, GET /api/rest/companies and the other collection GETs accept only limit and offset — there is no where parameter. Server-side filtering exists only on the POST /…/search endpoints, and those exist for customers, carriers, shipments, shipment line items, invoices, bills, bill payments, customer payments, customer payment line items, vendors, purchase orders and vendor invoices — not for addresses or companies.Practically: store the addr_ id when you create an address. Recovering it later means paging the whole collection or going through the parent.