Skip to main content

What happens if…?

Docs usually tell you the happy path. This page tells you what happens down the other branch — the retry, the duplicate, the delete, the misconfigured receiver. Each answer links its source. If the question blocking you isn’t answered here, ask support@upwell.com.

Writes and retries

The second POST fails with 400 Uniqueness violation on the (sourceSystem, sourceSystemId) pair. First sync is POST, every later change is PUT. Source: Idempotency & retries.
It silently succeeds and changes nothing — a no-op upsert that does not update name, email, phone, or anything else you sent. If you meant to update, use PUT. Source: Syncing foundation entities.
Both inserts succeed and return different ids. Seconds later, an async dedup pass soft-deletes the newer row, keyed on (tenant, sourceSystemId) — note: not scoped by integration, so two integrations reusing id strings collide. The id your retry returned may be the deleted one; reconcile by looking the invoice up by sourceSystemId afterward, and filter deletedAt null in the same where — the list returns soft-deleted rows too. Poll until the same single live row comes back a few seconds apart: two rows means dedup hasn’t run, and a lone row seen too early can still lose the race to the original insert. Source: Re-submitting. One caveat on the lookup itself — see filtering a list below.
Fails fast with 400 DOCUMENT_ALREADY_UPLOADED — synchronous, unlike carrier invoices. Exception: re-requesting one whose upload never completed returns a fresh URL for the same document. Source: Purchase orders & vendor invoices.
bulk-carriers: the entire batch fails (no upsert). bulk-bills: upserts like its singular endpoint. Neither reports partial success, and Upwell does not document which row caused the failure — validate batch contents before sending. Source: Syncing foundation entities.
Runtime rejection: "The value 2847.5 lies outside the bounds or is not an integer." All monetary fields are integer cents. Source: Money & payloads.
400 with the column name in a flat error string, e.g. "Not-NULL violation. null value in column \"shipment_id\"…". Known cases: shipments.shipmentId, invoices.balance — the OpenAPI spec marks these required, so generated clients catch them before the wire. Source: Integration patterns.

Errors, limits, and the reference

A flat object: {"error": "<message>"}, sometimes with a code such as constraint-violation or bad-request. An invalid or missing API key returns 401 with error, path, and code: "access-denied". There is no nested error envelope. The message→fix table lives in Error handling.
There is currently no rate limiting on the API — no 429, no X-RateLimit-* headers, no Retry-After. A 429 you observe came from your own proxy or gateway. Design your client politely (bounded concurrency, backoff on 5xx), but don’t build 429 machinery. Source: Error handling.
It now declares authentication (ApiKeyAuth in the Authorization header), the machine-enforced required fields, the real UPPER_SNAKE_CASE status enums, the nested invoiceShipments insert, and generic 400/401 responses. One remaining caveat: the where/orderBy filters that some list GETs accept are not in the spec, so they won’t appear in a generated client. Prefer the POST /search endpoints for filtering.

Reading data back

You get the first 10 records — the default limit is 10 (a handful of endpoints default to 100). Nothing marks the response as truncated. Always pass limit/offset explicitly. Source: API conventions.
Three cases:
  1. POST /<resource>/search exists for customers, carriers, shipments, shipment line items, invoices, bills, bill payments, customer payments (+ line items), vendors, purchase orders, vendor invoices — use it (Integration patterns).
  2. Addresses and companies have no search — page the collection or go through the parent.
  3. List GETs also accept a ?where= JSON filter, but the spec doesn’t declare it — so a client generated from the spec won’t expose it, and you’d be hand-rolling the request. Prefer POST /<resource>/search where it exists; otherwise page the collection.
You can’t get its id from the create response (POST /api/rest/companies returns no address ids), companies expose no address read path, and addresses carry no external key — the row is recoverable only by paging GET /api/rest/addresses. Create company addresses standalone instead. Source: Working with addresses.

Deletes

Hard delete, and its addresses go with it — and per the AR overview, its invoices too. The external key pair is freed. Prefer the reversible soft delete (PUT with deletedAt), which frees the key equally — it is the documented way to deactivate a customer, and no separate deactivate endpoint is published. Source: Deleting and archiving.
Soft delete — the record disappears from the app, its (case-insensitively unique) name is freed, and its addresses are orphaned rather than removed. Source: Deleting and archiving.
It fails outright (restrict-on-delete). Reassign or remove the bills first. Vendor deletes are permanent — no soft-delete column. Source: Syncing foundation entities.
Gone permanently — rules never soft-delete. Set enabled: false instead. Source: Rules.

Webhooks misbehaving

The delivery retries — 5xx, timeouts, and network failures all count — up to the subscription’s retryAttempts, with exponential backoff (1 min, 2 min, 4 min, …). retryAttempts: 3 (the default) means up to 4 total deliveries. Each attempt has a 15-second timeout — ack fast, work async. Source: Outbound webhooks.
The event is recorded as delivered and never retried. This is the silent-loss case: Upwell’s side shows success while your side rejected everything. Alert on your receiver’s 4xx rate; reconcile by polling. Source: Outbound webhooks, Reliability.
After retries exhaust, there is no automatic replay and no delivery-inspection surface — recovery is polling the resource endpoints (GET /carrier_invoices/{id} etc.), which the status guide recommends running periodically anyway. Source: Outbound webhooks, Reliability.
Each change fires exactly one trigger — the most specific that applies. A tracked status transition (e.g. to APPROVED) fires only update.carrier_invoice.status.APPROVED, not also the generic update.carrier_invoice. Subscribe to every trigger you care about. Source: Webhook event catalog.
Those four triggers (create/update/delete.carrier_document, update.invoice.status.APPROVED) exist in the dispatcher but are not self-service — they’re marked † in the event catalog; contact support to enable them.
No webhook fires — foundation entities have no triggers by design; your system remains the system of record and reads state back by polling. Source: Webhook event catalog.

Lifecycle surprises

Expected. A brand-new row lists matching findings (“no matching shipment / bill”) that clear on their own when ingestion completes. Wait for updatedAt > createdAt before judging exceptions. Source: Carrier invoice status.
Every carrier-invoice status value is defined in the Vocabulary, which is generated from the API schema at each docs build rather than hand-maintained — start there, and handle an unrecognized value defensively rather than switching exhaustively. The three easiest to hit unexpectedly: AWAITING_BILL (matched to a shipment, no bill linked yet — it promotes itself to RECEIVED), and the terminal pair CANCELLED and REFUNDED, which drop the invoice out of the actionable queues.
The request is recorded as rejected and the invoice’s status is left exactly where it was — Upwell does not move it to REJECTED or EXCEPTION for you. Only SUCCEEDED moves an invoice, to APPROVED. If your workflow needs the invoice to reflect the rejection, write that status yourself. Source: How approvals work, Responding to approval requests.
Auto-approval runs on a schedule, not instantly; it only touches invoices with no open or in-progress exceptions, matched to a bill and carrier, inside your guardrails — and the master switch is off by default. The sweep is periodic rather than event-driven, so expect a lag of minutes between an invoice becoming eligible and it being approved. Source: How approvals work.
Yes, when you create a payment line item linked to the invoice. Upwell recalculates the invoice’s balance and derives PART_PAID or PAID in the standard API-managed model. A payment header alone has no invoice effect, and changing only a status does not reverse an application — update or delete the original line item instead. Source: Customer payments, AR integration requirements.
It is parsed and stored as what you labeled it — a carrier rate con typed CUSTOMER_RATE_CONFIRMATION pollutes customer-side validation and vice versa. Re-type and re-process rather than editing amounts; never let carrier cost reach the customer side. Source: Document conventions.
Almost always a type problem: pages render only specific types (CUSTOMER_BILLING, COMPANY_ADDRESS), and ADDRESS_BOOK is read by nothing. The write succeeded; the page filtered it out. Debug order and per-page field lists: Working with addresses.