GuidesRecipesSubscribe & save

Recipe: subscribe & save

Goal: let a shopper opt into recurring delivery at checkout, so the order both places now and creates a subscription for future renewals.

Audience: storefront (server-side, via the storefront SDK).

Subscriptions on Flightdeck are a real recurring-billing engine — a schedule, an exactly-once renewal sweep, and a dunning ladder driven off a vaulted merchant-initiated credential. This recipe covers only the storefront opt-in; the engine and its lifecycle routes are in the subscriptions reference.

Prerequisites

  • The store’s manifest must have subscribe-and-save enabled: behavior.subscriptions.enabled (default false). Interval discounts live in behavior.subscriptions.discounts (interval-days to percent off, e.g. "30", "60").
  • Checkout is one-shot: you submit the cart and card together. There is no separate “create subscription” call from the storefront — it rides the checkout.

Step 1 — render the auto-renewal disclosure before the opt-in

This is not optional and not cosmetic. A subscription created from the storefront requires recorded consent evidence, enforced at the route: a checkout with subscribe: true but no consent block is refused with 400 subscription_consent_required.

Render the auto-renewal disclosure (its terms: cadence, price, how to cancel) before the opt-in control, and capture:

  • disclosureRef — the disclosure version the shopper saw (e.g. arl-disclosure-v1).
  • disclosureSha256 — a SHA-256 hex of the exact rendered disclosure text.
  • ipHash (optional) — a SHA-256 hash of the shopper’s IP.
  • userAgent (optional) — the shopper’s user agent.

The hosted checkout sends this automatically. A custom frontend must supply it.

// Server-side Route Handler — card fields never touch the browser.
const result = await store.checkout.submit(
  {
    customer_ref: "guest:shopper@example.com",
    items: [{ variant_id: "var_123", quantity: 1 }],
    card: { ccnumber: "4111111111111111", ccexp: "12/28", cvv: "123" },
    email: "shopper@example.com",   // the order-confirmation + ARL-terms address
    subscribe: true,                // opt into subscribe-and-save (if eligible)
    subscriptionConsent: {
      disclosureRef: "arl-disclosure-v1",
      disclosureSha256: "…",        // SHA-256 hex of the rendered disclosure
      ipHash: "…",                  // optional
      userAgent: "…",               // optional
    },
  },
  { idempotencyKey: `cart-${cartId}:attempt-1` }, // required Idempotency-Key
);

subscriptionConsent is enforced at the route but is not yet part of the generated request types — include it alongside the typed fields. The shopper’s typed email rides the order event as customer_contact_email and is where the confirmation (and the auto-renewal terms acknowledgment) is delivered.

Step 3 — read the outcome

On a placed order the response carries a subscriptions array alongside the usual checkout outcome fields (order, card_charged_cents, replayed, …). Handle every branch the same way you would a normal checkout:

  • Placed (201) — the order placed and the subscription was created.
  • Replayed (200) — the same idempotency key returns the original order with replayed: true. Treat as success; do not re-submit.
  • 400 subscription_consent_required — you did not send a valid consent block. This is a real error, not a decline; fix the flow, do not retry blind.
  • Declined / error — the SDK throws FlightdeckError; show a decline state, do not auto-retry (double-charge risk).

What the platform guarantees after opt-in

You do not manage renewals — the engine does, with an exactly-once renewal invariant (a UNIQUE (subscription, period) invoice, the deterministic key sub-renewal:{sub_id}:{period}, and next_bill_at advancing only inside the settle transaction). Note for compliance:

  • Every store sends a pre-billing renewal reminder. The cadence (renewalReminderDaysBeforeBilling) is per-store (default 7) with an enforced floor of 3 — reminders cannot be turned off, and a marketing unsubscribe never blocks them (they are transactional).
  • The consent evidence you captured is persisted on the subscription row.

Shopper self-serve

Give shoppers a way to manage what they signed up for. When behavior.accounts.subscriptionSelfServe is on (default true), the account page can list and cancel/pause/resume a shopper’s own subscriptions through the shopper-scoped routes (see subscriptions reference). A self-serve cancel is immediate and sends a confirmation; a resume recomputes next_bill_at forward and never back-bills.