ProductOct 6, 2026 · 7 min

Webhooks for Your Ecommerce Returns API

DA
Defne Aksoy
Solutions Engineer

A returns dashboard that only updates when someone refreshes the page is a returns dashboard that lies to you. Support agents tell customers their refund 'should be processed by now' when it isn't. Warehouse staff scan a returned item and the WMS still shows it as 'in transit' an hour later. Finance closes the books on a day when three refunds are still stuck mid-flight because nobody polled the API in time to catch them. If you've built or bought an ecommerce returns API and you're still polling it every few minutes hoping to catch state changes, you're paying for real-time infrastructure and using it like a nightly batch job.

Webhooks fix this by inverting the relationship: instead of your systems asking the returns platform 'anything new?' on a timer, the returns platform tells your systems the instant something changes. That shift — from pull to push — is what makes a returns data model actually useful to the seven or eight downstream systems that care about it: your OMS, your WMS, your ERP, your CRM, your BI warehouse, your customer messaging tool, and your finance ledger.

Why polling breaks down at scale

Polling feels simple to build and that's exactly the trap. A cron job hitting GET /returns?updated_since=X every five minutes looks fine in a demo with ten returns a day. It falls apart in three predictable ways once volume grows. First, latency: a five-minute poll interval means your support team is, on average, two and a half minutes behind reality, and worse during traffic spikes when the polling job queues up behind itself. Second, cost: most polls return nothing new, so you're burning API rate limits and compute cycles on empty responses — pure overhead with zero information gained. Third, drift: if a poll fails silently (timeout, auth token expiry, a deploy that took the worker down for ten minutes), you don't find out until a customer complains that their refund never posted to Zendesk.

Event-driven integration patterns exist precisely to eliminate the polling tax — you pay once, in engineering time, to subscribe to state changes instead of paying continuously, in compute and latency, to ask for them.

This is a well-documented pattern in API design more broadly — see general guidance from engineering teams like Shopify's platform blog on webhook-driven architectures. Returns are a particularly good fit for webhooks because a single return object moves through six to ten discrete states, each of which triggers action in a different system, and none of which happen on a predictable schedule.

Mapping the return lifecycle to events

Before you write a single line of webhook-handling code, map every state your return object can be in to the event that should fire when it gets there. A return isn't one thing — it's a state machine, and each transition is a fact worth telling someone about immediately. Here's the mapping we recommend as a baseline, expandable per merchant vertical (apparel needs a 'quality_flagged' event; electronics needs 'serial_number_verified'):

Event nameTriggerPrimary consumer
return.requestedCustomer submits return via portalCRM, customer messaging
return.approvedMerchant rule engine or agent approvesOMS, customer messaging
return.label_generatedShipping label createdCustomer messaging, tracking dashboard
return.in_transitCarrier scan event receivedWMS, customer messaging
return.receivedWarehouse scans item on arrivalWMS, inventory system
return.inspectedQC decision recorded (pass/fail/partial)Inventory, finance
return.refund_issuedRefund processed at payment gatewayERP, finance, customer messaging
return.exchange_shippedReplacement item dispatchedOMS, customer messaging
return.closedTerminal state reached, no further actionBI warehouse, analytics

Notice that customer messaging appears in nearly every row. That's deliberate — proactive status updates are consistently one of the highest-leverage uses of return events, because they convert a support ticket into a notification the customer never had to ask for. The alternative, a customer emailing 'where's my refund,' costs you an agent's time and a point of trust; a webhook-triggered SMS costs you nothing per event.

Don't skip the negative-path events

Most integration guides focus on the happy path and leave failure states as an afterthought, which is backwards — failure states are where webhooks earn their keep, because they're exactly the moments a human would otherwise have to notice manually. Subscribe to `return.rejected`, `return.inspection_failed`, `return.refund_failed`, and `return.sla_breached` from day one. An SLA-breach webhook that fires when a return has sat in `received` status for more than 48 hours without an inspection decision is worth more operationally than half the happy-path events combined, because it's the one that prevents a return from silently rotting in a queue.

Designing the payload

A webhook payload should be self-sufficient enough that a consumer rarely needs to call back into the API just to find out what happened, but lean enough that you're not shipping the entire return object — with full line items, full address history, full audit trail — on every single event. The pattern that holds up well in practice is a thin envelope plus a resource reference:

  • A stable `event_id` (UUID) unique to this delivery, used for deduplication
  • An `event_type` string matching your documented event names exactly
  • A `created_at` ISO-8601 timestamp in UTC, never local merchant time
  • A `data` object with the minimum fields the consumer needs to act (return_id, order_id, status, amount, currency)
  • A `resource_url` pointing back to the full return object for consumers who need more

Keep the payload schema versioned from the start — `event_version: 1` in the envelope — because you will add fields later, and a consumer with a strict deserializer should never break because you added an optional field. This is the same discipline that governs idempotent retry handling on the request side; webhooks need the mirror-image discipline on the delivery side.

Delivery guarantees: at-least-once, not exactly-once

Every honest webhook system is at-least-once, not exactly-once. Networks fail, consumers time out, retries happen, and duplicates are the cost of reliability. Build your consumer to be idempotent from the first line of code, not as a fix after a duplicate refund notification embarrasses someone. The practical recipe:

  1. 1Persist the `event_id` of every processed webhook in a dedup table with a unique constraint
  2. 2On receipt, attempt an insert; if it violates the unique constraint, acknowledge with 200 and stop — you've already handled this event
  3. 3Respond within 5 seconds even if downstream processing takes longer — queue the actual work asynchronously
  4. 4Return a 2xx only after the event is durably queued, never before validating the signature
  5. 5Expect retries on 4xx/5xx or timeout, typically with exponential backoff over 24-72 hours

Signature verification is not optional

A webhook endpoint is a public URL that triggers real actions — issuing refunds, updating inventory, messaging customers. If you don't verify that the payload actually came from your returns platform, you've built an open door. The standard approach is HMAC signing: the returns platform computes an HMAC-SHA256 hash of the raw request body using a shared secret, sends it in a header like `X-ResReturn-Signature`, and your endpoint recomputes the same hash and compares. Always compare with a constant-time function, never a plain string equality check, to avoid timing attacks. Reject anything with a timestamp header older than five minutes to block replay attacks even with a leaked secret. This same signing discipline is what regulators increasingly expect for any automated financial event pipeline — refund and payment webhooks fall squarely under the operational-resilience expectations described in guidance from bodies like the European Commission on digital financial services.

RiskMitigationWhere it's enforced
Spoofed payloadHMAC-SHA256 signature verificationConsumer endpoint
Replay attackTimestamp header + 5-minute windowConsumer endpoint
Duplicate processingevent_id dedup table with unique constraintConsumer database
Silent delivery failureRetry with exponential backoff, dead-letter queueWebhook publisher
Payload drift breaking consumersVersioned event schema (event_version field)Publisher + consumer contract

Monitoring what you can't see

The failure mode unique to webhooks is that they fail silently by default — there's no user staring at a spinner, no error page, just a notification that never arrived and nobody noticed for three days. Treat your webhook pipeline as a first-class monitored system: track delivery success rate per event type, alert when a consumer's failure rate crosses a threshold over a rolling window, and keep a dead-letter queue with a replay mechanism so an outage on your side doesn't mean permanently lost events. A dashboard showing 'last successful delivery per event type' catches configuration drift — an expired API key, a certificate rotation nobody told the returns team about — long before a customer does.

How many events should I subscribe to when I first integrate a returns API?

Start with the five that touch customer communication directly: return.approved, return.label_generated, return.in_transit, return.refund_issued, and return.rejected. These give you real-time customer messaging with minimal engineering surface, and you can add inventory- and finance-facing events once that pipeline is stable.

What happens if my endpoint is down when a webhook fires?

A well-built returns platform retries failed deliveries with exponential backoff, typically for 24-72 hours, and logs each attempt. If your endpoint is down longer than the retry window, you need a reconciliation job that polls the API on recovery to catch anything missed — webhooks should be the primary channel, with polling as a fallback safety net, not the other way around.

Should webhook payloads include the full return object?

No. Keep the payload thin — event metadata plus the minimum fields to act — and include a resource_url so consumers can fetch the full object when they actually need it. Fat payloads increase bandwidth, increase the chance of schema-change breakage, and rarely get fully used by any single consumer.

How do we avoid duplicate refund notifications going to customers?

Idempotency on the consumer side. Persist every processed event_id in a dedup table before taking any customer-facing action, and treat a repeat delivery of the same event_id as a no-op. This is standard at-least-once delivery hygiene, not a returns-specific quirk.

Webhooks are the difference between a returns API that's technically real-time and one that's operationally real-time. The first just means the data is fresh if you ask for it fast enough; the second means every system that needs to know, knows the moment it happens — without a single polling job, without a support agent guessing, and without a finance close that has to chase down returns still mid-flight.

See it on your own returns.

Start free