- 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.
- There is no external key. Addresses have no
sourceSystem/sourceSystemId, so there is no upsert-by-your-id. Store theaddr_id Upwell returns, keyed by parent and address type, or you lose your ability to update that address later. - You can embed an address when you create its parent. You can never embed one on an update. Updates always take a separate call.
Which direction the link runs
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.
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).
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.
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 theaddr_ id, this is one call:
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_BOOKaddress 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
phoneNumbernever reaches a company page. The company pages don’t load that field at all, so putting a phone number on aCOMPANY_ADDRESSrow is a dead end. The phone shown on a company record is the company’s ownprimaryPhone— a field on the company itself, set withPUT /api/rest/companies/{id}. (Customer pages do load the address phone number, so this asymmetry is company-specific.)
One billing address per customer
A customer can hold many addresses, but at most oneCUSTOMER_BILLING address. This is a
database constraint, not a convention — a second one is rejected outright:
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".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).
