Starship Rewards API
Core Concepts

Fulfillment Model

How Starship fulfills orders — inventory-first, admin-gated vendor fallback — and what that means for your integration's polling strategy

Fulfillment Model

Starship fulfills orders inventory-first. On every POST /orders, the API checks an in-memory voucher cache. A cache hit means the voucher is delivered synchronously, in the same request. A cache miss means the order is parked in PENDING status, and — critically — Starship does not call the vendor API on your behalf. The vendor is only contacted after explicit admin action.

This design has two direct consequences for your integration:

  1. Some orders complete in milliseconds, others take hours. The same product can fulfill instantly at 10:00 AM and park in PENDING at 10:05 AM, depending on inventory state. Your client code must handle both paths.
  2. PENDING does not mean "we're working on it." It means "we're waiting for a human or a future inventory pump." Don't build retry loops that assume the server will eventually push the order through.

The Happy Path: Cache Hit

Inventory-First Fulfillment — Cache Hit
POST /api/v1/orders
Auth + HMAC verify
Validate payload Resolve product · FX · wallet
Inventory cache has (product, denom)?
HIT
ZRem voucher from pool DEL metadata
Debit wallet · persist order Mark DELIVERED
HTTP 200 · DELIVERED
Webhook: order.delivered Vouchers retrievable now
MISS
Persist order · no vendor call
HTTP 200 · PENDING
Webhook: order.pending Awaits admin action

Every client POST /orders takes exactly one of these two paths at creation time. The inventory cache is refreshed periodically by a background pump — see 'Why inventory misses happen' below.

What the cache hit means for you:

  • Your POST /orders response arrives with status: "DELIVERED" already set.
  • Voucher codes are immediately retrievable via GET /orders/:id.
  • The order.delivered webhook fires within seconds — before your retry logic would have tried again.

The Miss Path: PENDING

When inventory is not cached for a given (product_id, denomination, currency) tuple, the order is persisted in PENDING status and does nothing further automatically. No vendor API is called. No background retry is triggered by default.

From here, one of three things happens:

  1. Inventory arrives and a pump runs. The next 30-minute inventory pump (or a manual admin pump) loads the voucher into cache. An admin then triggers a retry, which fulfills the order from cache — same path as a cache hit.
  2. Admin triggers vendor fallback. An operator explicitly approves vendor-API fulfillment. Starship calls the vendor (e.g., VoucherKart, Bamboo, PineLabs), receives the voucher, and marks the order DELIVERED. You receive the order.delivered webhook.
  3. Admin cancels. If the voucher can't be sourced, the order is moved to CANCELLED and the wallet is refunded. You receive the order.cancelled webhook.

The auto-retry cron is disabled by default (DISABLE_AUTO_ORDER_RETRY=true). Even if enabled, it only re-checks inventory — it never calls vendor APIs automatically. If you have a product category that absolutely must auto-fulfill via vendor, ask your Starship contact about enabling ALLOW_AUTO_VENDOR_FULFILL on specific products.

Why Inventory Misses Happen

The inventory cache is not a "live" lookup against vendors — it's a locally-warmed pool of pre-purchased, pre-validated vouchers. Misses happen for a handful of reasons:

  • Cold start. No voucher for this (product, denom, currency) has been loaded yet.
  • Burst depletion. A bulk order drained the pool faster than the pump can refill.
  • Expired or blocked units. Vouchers past their validity window, or flagged by vendor audit, are removed from the pool.
  • New product. A recently added product has no inventory yet — everything will miss until the first pump.

Call the Charges API or Product availability endpoint before order creation to see the live delivery_time signal (INSTANT if cache is warm, DELAYED if not). This is the same signal Starship uses internally to decide the fulfillment path, so what you see is what you get.

Async Orders (Large Quantities)

One wrinkle: for large bulk orders (qty > 50), the API returns HTTP 200 immediately with status: PENDING and no order items yet. Order items are created on the first retry (admin-triggered or inventory-pump-triggered). This prevents a single bulk order from holding the API request open for minutes while Starship allocates dozens of vouchers.

What this means for your client:

  • Don't read order.items[] from the initial POST /orders response on large orders — it will be empty.
  • Wait for the order.delivered webhook (or poll GET /orders/:id) to see the allocated vouchers.
  • Treat pendingItems == 0 as "done" only if fulfilledFromCache > 0. The combination of both tells you the order is complete vs. the order never had anything allocated.

What You Should Build

Design for both paths from day one

Even if most of your orders fulfill from cache, some will go PENDING. A production integration must handle status: PENDING on initial POST without treating it as an error.

Subscribe to order.* webhooks

Webhooks are how you learn about PENDINGDELIVERED transitions without polling every order every minute. See the Webhooks guide.

Use delivery_time to set expectations in your UI

If your cart shows "Delivered instantly" to end users, only show that copy when the product's delivery_time is INSTANT at quote time. For DELAYED products, surface "Delivery within a few minutes to a few hours" so customers aren't surprised.

Reconcile daily

Run a daily job that lists orders in PENDING older than 24 hours and alerts. These are stuck and require human attention; they should not silently accumulate.

Next