Starship Rewards API
Core Concepts

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

Which completion-detection strategy should I use?
About to place an order
Quote via Charges API read product.delivery_time
delivery_time?
INSTANT
Expect DELIVERED on POST response
Skip polling · webhook arrives within seconds
Read vouchers · notify user
DELAYED
Expect PENDING on POST response
Subscribe to order.delivered webhook (PRIMARY completion signal)
Also poll GET /orders/:id every 60 s (FALLBACK — webhook may lag)
Either path fires → read vouchers

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:

  1. POST /orders, store order_id with status PENDING.
  2. On order.delivered webhook, mark DELIVERED, fetch vouchers, notify end-user.
  3. A background job every 5 minutes selects orders that have been PENDING for > 2 minutes and polls GET /orders/:id. If any have moved to DELIVERED, 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:

AttemptDelay after previousCumulative time
10 s
230 s30 s
32 min2.5 min
410 min12.5 min
51 hour~1.2 h
66 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. An INSTANT quote can legitimately return a PENDING order if the race closes against you.
  • The opposite is rarer: a DELAYED quote can return DELIVERED if 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 typeFallback poll intervalRationale
INSTANTNot needed — webhook usually wins; if polling anyway, every 10 s for up to 2 minUsually delivered by the first poll
DELAYEDEvery 60 s for the first 10 min; every 5 min thereafter; give up after 24 hMatches 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