Skip to main content

Outbound webhooks

Upwell can POST a JSON event to a URL you host whenever something changes — a carrier invoice is created, matched to a shipment, approved, and so on. Webhooks are the push counterpart to polling.

Subscribing

Webhook subscriptions are configured in the Upwell dashboard (there’s no public REST endpoint to create one). A subscription has:
  • url — your HTTPS receiver.
  • triggers — the list of event types you want delivered.
  • authSettings — how Upwell proves the delivery came from Upwell (see Authenticating deliveries).
  • retryAttempts — how many times Upwell retries a failed delivery (0–10).

What you can subscribe to

The webhook event catalog is the single source of truth for every trigger string, across all resources — invoices, carrier invoices, documents, payments, exceptions, remittances, and approval requests. Two rules to internalize before subscribing:
  • Each change fires exactly one trigger — the most specific that applies. A tracked status transition fires only its .status.{STATUS} trigger, not also the generic update.{resource}; the generic trigger fires for changes that are neither a tracked status nor a named sub-event. Subscribe to every trigger you care about.
  • A few triggers exist in the dispatcher but are not self-service in the dashboard subscription form (they’re marked in the catalog) — contact support to enable those.
For carrier invoices specifically: “ingestion done” (matched + audited) is best observed via *.shipment_updated or the generic update.carrier_invoice; the *.status.APPROVED / *.status.EXCEPTION triggers reflect the later review lifecycle. See Knowing when a carrier invoice is processed.

The delivery envelope

Every delivery is a single JSON object — the event row. The fields you’ll read: Plus delivery bookkeeping: tenantId, webhookSubscriptionId, createdAt, updatedAt, scheduledAt, processedAt, retryOfId (set when this delivery is a retry of an earlier event — dedupe on it), and retries (retry events spawned from this one).

The payload shape varies by trigger

There is no single payload shape. Status-change events deliver a mapped object nested under carrierInvoice; row-level events (*.shipment_updated) deliver raw new/old column snapshots. The robust pattern is to read resourceId from the envelope and call GET /api/rest/carrier_invoices/{resourceId} for a canonical representation — then the payload is just a hint about what changed.
Status-change events (update.carrier_invoice.status.APPROVED / .EXCEPTION) — payload.carrierInvoice with the invoice and its matched relations nested:
Row-level events (update.carrier_invoice.shipment_updated) — raw new/old snapshots (snake_case columns):
Note new.source_system = "API" and new.integration_id are stamped by Upwell — confirming the invoice was API-submitted. source_system_id is null for API submissions.

Authenticating deliveries

You choose how Upwell authenticates to your receiver when you create the subscription. Three options:
Upwell sends Authorization: Basic base64(username:password) using the credentials you configure.
No auth header. Rely on URL secrecy / network controls. Not recommended for production.
Upwell always sends Content-Type: application/json and a User-Agent of UpwellWebhookService/<version> (https://www.upwell.com).
There is no payload signing (HMAC) or replay protection — authentication is the static header or Basic credentials you configure above. Treat the shared secret like a password: rotate it if it leaks, and always compare it on every delivery.

Reliability

  • What retries: deliveries that return 5xx, time out, or fail at the network level are retried, up to the subscription’s retryAttempts.
  • What does NOT retry: a 4xx response is treated as delivered — it is recorded as successful and never retried. A misconfigured receiver (broken auth check, wrong route) that 4xxes will silently lose every event while Upwell’s side shows success. Alert on your receiver’s 4xx rate, and reconcile with polling (below).
  • Retry schedule: exponential backoff starting at 1 minute and doubling each attempt (1, 2, 4, 8, … minutes).
  • retryAttempts counts retries beyond the first attemptretryAttempts: 3 (the default) means up to 4 total deliveries. The dashboard form accepts 0–10.
  • Each delivery attempt has a 15-second timeout. Return a 2xx quickly; do slow work asynchronously.
  • Build an idempotent receiver: dedupe on the delivery id, and treat a non-null retryOfId as a redelivery of that earlier event.
  • After retries exhaust there is no automatic replay — pair webhooks with a periodic reconciliation poll so a missed delivery never strands a record (see Knowing when a carrier invoice is processed).

Example receiver

See it end-to-end

Pair the receiver above with a polling fallback, and walk the full submit → attach → process flow in the submission guide.