---
title: "Get User Offer"
method: GET
path: "/iap/user-offer"
tags: ["User Offer"]
---

# Get User Offer

`GET /iap/user-offer`

Canonical offer + subscription-state resolver (SDK 1.2+). Returns the user's current subscription variant plus exactly one offer decision (`no_action`, `migrate_storekit_to_web`, `upgrade_storekit_to_web`, or `upgrade_web_to_web`). Replaces the overlapping `/iap/products/` offer config, `/iap/upgrade-offer/`, and ad-hoc status parsing with a single typed contract. Legacy endpoints remain supported for older SDKs.

## Query parameters

- `user_id` string, required

## Response `200`

Canonical user offer + subscription state

- UserOfferResponse — Canonical response from `GET /iap/user-offer/`. Wraps the user's current subscription state (as a tagged union keyed by `subscription.type`) together with exactly one offer decision.
  - `user_id` string, required — Echoes the `user_id` query parameter.
  - `app_id` integer, required — Internal ZeroSettle app ID that owns the publishable key.
  - `is_sandbox` boolean, required — Resolved environment — `true` when the request was authenticated with a sandbox publishable key.
  - `subscription` union, required — Tagged-union describing the user's current subscription state. Discriminated by the `type` field. Exactly one variant is returned per request.
    - SubscriptionStateNone — User has no active subscription entitlement from any source.
      - `type` 'none', required
    - SubscriptionStateActiveWeb — User has an active web-sourced subscription (may be in grace period, past due, or paused).
      - `type` 'active_web', required
      - `product_id` string, required — Reference ID of the active product.
      - `subscription_group_id` integer, nullable — ZeroSettle subscription group ID, if any.
      - `status` string, required — EntitlementStatus value (e.g., `active`, `grace_period`, `past_due`, `paused`, `cancelled`).
      - `expires_at` string, date-time, nullable — When the current period ends / next renewal is due.
      - `trial_ends_at` string, date-time, nullable — Trial end timestamp when `is_trial` is `true`.
      - `is_trial` boolean, required
      - `will_renew` boolean, required — Whether the subscription is set to auto-renew at period end.
      - `stripe_subscription_id` string, required
      - `entitlement_id` integer, required
    - SubscriptionStateActiveStorekit — User has an active StoreKit-sourced subscription.
      - `type` 'active_storekit', required
      - `product_id` string, required
      - `subscription_group_id` integer, nullable
      - `status` string, required — EntitlementStatus value.
      - `expires_at` string, date-time, required
      - `is_trial` boolean, required
      - `trial_ends_at` string, date-time, nullable
      - `will_renew` boolean, required
      - `storekit_original_transaction_id` string, required
      - `entitlement_id` integer, required
    - SubscriptionStateMigrationTrial — Dual-active migration trial window: the user has a web subscription inside its free trial plus an overlapping StoreKit entitlement that has not yet expired.
      - `type` 'migration_trial', required
      - `web_product_id` string, required
      - `storekit_product_id` string, required
      - `subscription_group_id` integer, nullable
      - `trial_ends_at` string, date-time, required
      - `storekit_expires_at` string, date-time, required
      - `stripe_subscription_id` string, required
      - `storekit_original_transaction_id` string, required
      - `entitlement_id` integer, required
    - SubscriptionStateCancelledActive — Subscription is cancelled but access remains until `expires_at`.
      - `type` 'cancelled_active', required
      - `source` string, required — EntitlementSource (e.g., `web_checkout`, `store_kit`).
      - `product_id` string, required
      - `subscription_group_id` integer, nullable
      - `expires_at` string, date-time, required
      - `stripe_subscription_id` string, nullable
      - `storekit_original_transaction_id` string, nullable
      - `entitlement_id` integer, required
  - `offer` OfferEligibility, required — The offer decision. Exactly one `action_type` is returned per user per request. `is_eligible` is `false` iff `action_type` is `no_action`.
    - `action_type` 'no_action' | 'migrate_storekit_to_web' | 'upgrade_storekit_to_web' | 'upgrade_web_to_web', required — Which offer branch fired. `no_action` = nothing to show.
    - `is_eligible` boolean, required — Convenience boolean; equivalent to `action_type != 'no_action'`.
    - `checkout_product_id` string, required — Product ID the SDK should drive checkout toward. Empty string when `action_type` is `no_action`.
    - `from_product_id` string, nullable — The product the user is currently on (relevant for upgrade/migrate flows).
    - `savings_percent` integer, required — Annualized savings percent vs. the user's current plan, 0 when not applicable.
    - `free_trial_days` integer, required — Days of free trial the SDK should honor on the new subscription.
    - `min_subscription_days` integer, required — Minimum subscribed days before the offer becomes eligible (for rollout/lock-in rules).
    - `display` OfferDisplayV2 — Presentation copy for the user-offer card. Distinct from the legacy `OfferDisplay` schema used by `/iap/products/` offer config — field names are simplified and the copy is localized + interpolated server-side.
      - `title` string, required
      - `body` string, required
      - `cta_text` string, required
      - `dismiss_text` string, required
      - `accepted_title` string, required
      - `accepted_body` string, required
      - `completed_title` string, required
      - `completed_body` string, required
      - `apple_cancel_instructions` string — Localized instructions shown in the accepted-state card when `requires_apple_cancel` is `true`. May be empty.
    - `proration` OfferProration — Stripe proration preview for web-to-web upgrades. Present only when `action_type` is `upgrade_web_to_web`.
      - `amount_cents` integer, required — Credit applied from the old subscription, in minor currency units. Negative values represent money owed back to the user.
      - `currency` string, required — ISO-4217 currency code for the proration amount.
      - `next_billing_date` string, date-time, nullable — When the customer will next be billed on the upgraded plan.
    - `requires_apple_cancel` boolean, required — Whether the user must cancel their StoreKit subscription after the new checkout completes (true for `migrate_*` / `upgrade_storekit_to_web`).
    - `apple_subscription` AppleSubscriptionSummary — Normalized view of the user's Apple (StoreKit) subscription. Present only when the user currently has — or recently had — a StoreKit entitlement. Clients should rely on `is_active` rather than deriving activeness from `status_code`.
      - `is_active` boolean, required — Canonical active-entitlement flag, normalized server-side from the Apple status code.
      - `expires_at` string, date-time, nullable
      - `status_code` integer, required — Raw Apple status code (1=active, 2=expired, 3=billing_retry, 4=grace_period, 5=revoked). Diagnostic only — prefer `is_active`.
      - `auto_renew_enabled` boolean, required
    - `checkout_presentation` 'webview' | 'native_pay' | 'safari_vc' | 'safari', nullable — How the SDK should present checkout for this offer.
    - `experiment_variant_id` integer, nullable — A/B experiment variant ID when this offer is part of an experiment.
  - `server_time` string, date-time, required — Server-side timestamp at which the response was resolved (ISO 8601). Useful for client-side cache invalidation.

## Other responses

- `400` — Invalid request -- missing or malformed parameters.
- `401` — Invalid or missing API key.
- `404` — Resource not found.

---

[API](https://skmtc.net/zerosettle/apis/zerosettle-iap-api.md) · [All operations](https://skmtc.net/zerosettle/apis/zerosettle-iap-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/zerosettle/zerosettle-iap-api/revisions/1a7aa12f3d5e/schema)
