Delivery Semantics
When to poll, when to wait on a webhook, and how to handle retries — the correctness rules for finding out "is this order done yet?"
Delivery Semantics
Orders finish on one of two tracks: synchronous (you know it's done by the time the POST /orders response returns) or asynchronous (you find out later via webhook, or by polling). Picking the wrong track is the single most common source of integration bugs. This page tells you which one to use and when.
The Decision
The key signal is the product's delivery_time at quote time. Build both paths; choose per-order.
Webhook First, Polling as Safety Net
Webhooks are cheaper, faster, and more scalable than polling. Your integration should treat order.delivered as the primary completion signal, with polling as a fallback for cases where:
- Your webhook endpoint was down during delivery (Starship retries, but only a few times — see below)
- A network partition dropped the event
- You haven't set up webhooks yet (development only — don't ship this to prod)
A reasonable production pattern:
POST /orders, storeorder_idwith statusPENDING.- On
order.deliveredwebhook, markDELIVERED, fetch vouchers, notify end-user. - A background job every 5 minutes selects orders that have been
PENDINGfor > 2 minutes and pollsGET /orders/:id. If any have moved toDELIVERED, process them as if a webhook had arrived.
This gives you sub-second latency on the happy path (webhook) and guaranteed convergence on the unhappy path (poll).
Webhook Retry Policy
When Starship sends an event and your endpoint doesn't return 2xx, Starship retries with exponential backoff. The schedule is:
| Attempt | Delay after previous | Cumulative time |
|---|---|---|
| 1 | — | 0 s |
| 2 | 30 s | 30 s |
| 3 | 2 min | 2.5 min |
| 4 | 10 min | 12.5 min |
| 5 | 1 hour | ~1.2 h |
| 6 | 6 hours | ~7.2 h |
After the 6th attempt fails, the event is marked failed in Starship's webhook log and is not retried further. An admin can manually re-deliver from the Starship admin console if you discover the failure later.
Tolerate replays. Starship may retry a webhook you already successfully received — your 2xx response might have been lost on the network. Dedupe on event_id in the payload: if you've already processed this event_id, return 200 and do nothing. Don't process the event a second time.
What "INSTANT" Actually Guarantees
delivery_time: INSTANT means inventory was cached at quote time. It's a strong hint, not a hard guarantee:
- Between your quote and your
POST /orders, a different client might deplete the pool. AnINSTANTquote can legitimately return aPENDINGorder if the race closes against you. - The opposite is rarer: a
DELAYEDquote can returnDELIVEREDif an inventory pump landed between quote and order.
In practice, quote-to-order races are rare (seconds-wide window, most pools have dozens to hundreds of units). Build the DELAYED path anyway — it's a correctness property, not a performance one.
Polling Cadence
If you must poll (development, or as the fallback described above), use these intervals:
| Delivery type | Fallback poll interval | Rationale |
|---|---|---|
INSTANT | Not needed — webhook usually wins; if polling anyway, every 10 s for up to 2 min | Usually delivered by the first poll |
DELAYED | Every 60 s for the first 10 min; every 5 min thereafter; give up after 24 h | Matches the typical vendor turnaround envelope |
Polling faster than 10 s will trip the rate limiter (429 E_REQUEST_THROTTLED) and accomplish nothing — Starship's worker pool doesn't refresh order state faster than that.
Timeouts and the Idempotency-Key
If your POST /orders call times out before you get a response, you don't know whether the order was created or not. Do not retry without an Idempotency-Key.
With an Idempotency-Key:
- First request succeeded but response was lost → retry returns the cached response, no duplicate charge.
- First request failed cleanly → retry processes fresh.
- First request in-flight when you retry → the second request waits (briefly) and returns the same result.
Without an Idempotency-Key, a retry can double-charge. Never skip it on write endpoints.
What You Should Build
One webhook handler, idempotent, fast
Your POST /webhooks/starship endpoint should acknowledge in under 3 seconds, dedupe on event_id, and queue the actual work for async processing. Verify the HMAC signature on the webhook body before trusting it.
A poller for stuck orders
A separate job that selects orders PENDING > 2 min and calls GET /orders/:id. Cheap, simple, catches webhook failures without you noticing.
A dashboard for orders PENDING > 24 h
Orders stuck that long require human attention. Surface them to ops; do not silently accumulate them.
Always use an Idempotency-Key on writes
Even for reads where it isn't required, if you have a natural unique ID (cart_hash, user_checkout_id), pass it. Future you will thank past you.
Next
- Webhooks guide — signature verification and event payloads
- Idempotency guide — choosing a key strategy
- Error Codes — retry guidance per status class
Order Lifecycle
Every status and sub-status an order can move through, who triggers each transition, and what your integration should do at each stage
Pricing & Charges
How the final amount debited from your wallet is computed — base price, FX, client discount, vendor discount, fees — and how to quote it before placing an order