---
title: "Checkout Complete"
method: POST
path: "/subscription/checkout-complete"
tags: ["subscription"]
---

# Checkout Complete

`POST /subscription/checkout-complete`

## Request

- Base URL: `https://api.projectindi.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.projectindi.com/subscription/checkout-complete`
- Auth: none declared

Settle state after a successful Stripe checkout, then return the status.

Stamps a short grace snooze (so a status read in the redirect→webhook
window doesn't bounce a fresh payer to /paywall) and optimistically sets
the store to STRIPE, then returns the freshly-settled status from the same
projection ``GET /status`` uses - so the post-checkout caller keeps its
single round-trip.

The grace is gated on RevenueCat - the entitlement broker that ingests
Stripe and feeds our ``/webhooks/revenuecat`` handler - confirming the
caller's *own* entitlement (keyed on the JWT user id, so nothing here is
forgeable). Without that gate any authenticated free account could POST
here on a loop to self-grant a permanent paywall snooze, bypassing even the
hard lockout (#4623):

- RevenueCat confirms an active entitlement → stamp the grace (verified).
- RevenueCat unreachable → stamp it anyway, logged loudly, so an RC outage
  never re-paywalls a real payer. Bounded to ``CHECKOUT_GRACE_MINUTES``;
  the webhook reconciles authoritatively.
- RevenueCat reachable but no active entitlement → the self-grant case.
  Behaviour here depends on ``CHECKOUT_GRACE_ENFORCE_REVENUECAT``:

**Two-phase rollout.** The gate ships in *observe* mode by default: the
RevenueCat lookup runs and every would-be denial is logged, but the grace
is still stamped - so the deploy changes nothing a returning payer sees and
cannot regress the redirect→webhook bridge. The logs measure the real
false-negative rate (genuine payers still inside RC's Stripe-ingestion lag)
in prod. Flip ``CHECKOUT_GRACE_ENFORCE_REVENUECAT=true`` to *enforce* -
then a no-entitlement caller gets no grace, closing the self-grant. The
flip is a config change, not a deploy, and instantly reversible. Even under
enforcement the blast radius is one ``CHECKOUT_GRACE_MINUTES`` window for a
fresh payer, cleared by the webhook within minutes.

Re-POSTing just re-stamps the same window (idempotent).

## Response `200`

Successful Response

- SubscriptionResponse: Unified subscription status response.
  - `subscription_status` string, required
  - `subscription_store` string, nullable, required
  - `has_active_subscription` boolean, required
  - `subscription_expires_at` string, nullable, required
  - `is_billing_contact` boolean, required
  - `is_locked_out` boolean, required
  - `trial_expired` boolean, required
  - `message_count` integer, required
  - `requires_payment` boolean, required
  - `is_snoozed` boolean
  - `snoozed_until` string, nullable
  - `is_clinic_referred` boolean
  - `clinic_name` string, nullable
  - `is_sponsored` boolean
  - `sponsoring_clinic_name` string, nullable
  - `sponsorship_ends_at` string, nullable
  - `has_own_subscription` boolean
  - `trial_eligible` boolean, required
  - `jurisdiction_code` string, nullable
  - `has_pending_ndis_request` boolean
  - `plan_type` string, nullable
  - `prefill_clinician` string, nullable
  - `pending_user_action_store` string, nullable
  - `pending_user_action_deep_link` string, nullable
  - `promo` PromoDeal
    - `code` string, required
    - `percent_off` integer, required
    - `months` integer, required
    - `play_offer_id` string, required
    - `apple_redeem_url` string, required

---

[API](https://skmtc.dev/projectindi/apis/indi-agent-api-first-principles.md) - [All operations](https://skmtc.dev/projectindi/apis/indi-agent-api-first-principles/llms.txt) - [OpenAPI document](https://skmtc.dev/projectindi/apis/indi-agent-api-first-principles/revisions/25a7813e21fd?raw)
