ZeroSettle User Offer API

Unified offer + subscription-state resolver (SDK 1.2+)

OpenAPI Specification

zerosettle-user-offer-api-openapi.yml Raw ↑
openapi: 3.0.3
info:
  title: ZeroSettle IAP Cancel Flow User Offer API
  description: The ZeroSettle IAP API powers the iOS, Android, and Flutter SDKs. It enables product catalog fetching, web checkout, entitlement management, subscription lifecycle operations, and StoreKit transaction syncing. All endpoints are authenticated via the `X-ZeroSettle-Key` header.
  version: 1.0.0
  contact:
    name: ZeroSettle Support
    email: support@zerosettle.io
    url: https://zerosettle.io
servers:
- url: https://api.zerosettle.io/v1
  description: Production
security:
- ApiKeyAuth: []
tags:
- name: User Offer
  description: Unified offer + subscription-state resolver (SDK 1.2+)
paths:
  /iap/user-offer:
    get:
      operationId: getUserOffer
      summary: Get User Offer
      description: 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.
      tags:
      - User Offer
      parameters:
      - name: user_id
        in: query
        required: true
        description: The user's external ID.
        schema:
          type: string
      responses:
        '200':
          description: Canonical user offer + subscription state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserOfferResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  schemas:
    SubscriptionStateActiveWeb:
      type: object
      description: User has an active web-sourced subscription (may be in grace period, past due, or paused).
      required:
      - type
      - product_id
      - status
      - is_trial
      - will_renew
      - stripe_subscription_id
      - entitlement_id
      properties:
        type:
          type: string
          enum:
          - active_web
        product_id:
          type: string
          description: Reference ID of the active product.
        subscription_group_id:
          type: integer
          nullable: true
          description: ZeroSettle subscription group ID, if any.
        status:
          type: string
          description: EntitlementStatus value (e.g., `active`, `grace_period`, `past_due`, `paused`, `cancelled`).
        expires_at:
          type: string
          format: date-time
          nullable: true
          description: When the current period ends / next renewal is due.
        trial_ends_at:
          type: string
          format: date-time
          nullable: true
          description: Trial end timestamp when `is_trial` is `true`.
        is_trial:
          type: boolean
        will_renew:
          type: boolean
          description: Whether the subscription is set to auto-renew at period end.
        stripe_subscription_id:
          type: string
        entitlement_id:
          type: integer
    SubscriptionStateCancelledActive:
      type: object
      description: Subscription is cancelled but access remains until `expires_at`.
      required:
      - type
      - source
      - product_id
      - expires_at
      - entitlement_id
      properties:
        type:
          type: string
          enum:
          - cancelled_active
        source:
          type: string
          description: EntitlementSource (e.g., `web_checkout`, `store_kit`).
        product_id:
          type: string
        subscription_group_id:
          type: integer
          nullable: true
        expires_at:
          type: string
          format: date-time
        stripe_subscription_id:
          type: string
          nullable: true
        storekit_original_transaction_id:
          type: string
          nullable: true
        entitlement_id:
          type: integer
    SubscriptionStateMigrationTrial:
      type: object
      description: '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.'
      required:
      - type
      - web_product_id
      - storekit_product_id
      - trial_ends_at
      - storekit_expires_at
      - stripe_subscription_id
      - storekit_original_transaction_id
      - entitlement_id
      properties:
        type:
          type: string
          enum:
          - migration_trial
        web_product_id:
          type: string
        storekit_product_id:
          type: string
        subscription_group_id:
          type: integer
          nullable: true
        trial_ends_at:
          type: string
          format: date-time
        storekit_expires_at:
          type: string
          format: date-time
        stripe_subscription_id:
          type: string
        storekit_original_transaction_id:
          type: string
        entitlement_id:
          type: integer
    SubscriptionStateActiveStorekit:
      type: object
      description: User has an active StoreKit-sourced subscription.
      required:
      - type
      - product_id
      - status
      - expires_at
      - is_trial
      - will_renew
      - storekit_original_transaction_id
      - entitlement_id
      properties:
        type:
          type: string
          enum:
          - active_storekit
        product_id:
          type: string
        subscription_group_id:
          type: integer
          nullable: true
        status:
          type: string
          description: EntitlementStatus value.
        expires_at:
          type: string
          format: date-time
        is_trial:
          type: boolean
        trial_ends_at:
          type: string
          format: date-time
          nullable: true
        will_renew:
          type: boolean
        storekit_original_transaction_id:
          type: string
        entitlement_id:
          type: integer
    UserOfferResponse:
      type: object
      description: 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.
      required:
      - user_id
      - app_id
      - is_sandbox
      - subscription
      - offer
      - server_time
      properties:
        user_id:
          type: string
          description: Echoes the `user_id` query parameter.
        app_id:
          type: integer
          description: Internal ZeroSettle app ID that owns the publishable key.
        is_sandbox:
          type: boolean
          description: Resolved environment — `true` when the request was authenticated with a sandbox publishable key.
        subscription:
          $ref: '#/components/schemas/UserOfferSubscription'
        offer:
          $ref: '#/components/schemas/OfferEligibility'
        server_time:
          type: string
          format: date-time
          description: Server-side timestamp at which the response was resolved (ISO 8601). Useful for client-side cache invalidation.
    UserOfferSubscription:
      description: Tagged-union describing the user's current subscription state. Discriminated by the `type` field. Exactly one variant is returned per request.
      oneOf:
      - $ref: '#/components/schemas/SubscriptionStateNone'
      - $ref: '#/components/schemas/SubscriptionStateActiveWeb'
      - $ref: '#/components/schemas/SubscriptionStateActiveStorekit'
      - $ref: '#/components/schemas/SubscriptionStateMigrationTrial'
      - $ref: '#/components/schemas/SubscriptionStateCancelledActive'
      discriminator:
        propertyName: type
        mapping:
          none: '#/components/schemas/SubscriptionStateNone'
          active_web: '#/components/schemas/SubscriptionStateActiveWeb'
          active_storekit: '#/components/schemas/SubscriptionStateActiveStorekit'
          migration_trial: '#/components/schemas/SubscriptionStateMigrationTrial'
          cancelled_active: '#/components/schemas/SubscriptionStateCancelledActive'
    AppleSubscriptionSummary:
      type: object
      description: 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`.
      required:
      - is_active
      - status_code
      - auto_renew_enabled
      properties:
        is_active:
          type: boolean
          description: Canonical active-entitlement flag, normalized server-side from the Apple status code.
        expires_at:
          type: string
          format: date-time
          nullable: true
        status_code:
          type: integer
          description: Raw Apple status code (1=active, 2=expired, 3=billing_retry, 4=grace_period, 5=revoked). Diagnostic only — prefer `is_active`.
        auto_renew_enabled:
          type: boolean
    SubscriptionStateNone:
      type: object
      description: User has no active subscription entitlement from any source.
      required:
      - type
      properties:
        type:
          type: string
          enum:
          - none
    OfferProration:
      type: object
      description: Stripe proration preview for web-to-web upgrades. Present only when `action_type` is `upgrade_web_to_web`.
      required:
      - amount_cents
      - currency
      properties:
        amount_cents:
          type: integer
          description: Credit applied from the old subscription, in minor currency units. Negative values represent money owed back to the user.
        currency:
          type: string
          description: ISO-4217 currency code for the proration amount.
        next_billing_date:
          type: string
          format: date-time
          nullable: true
          description: When the customer will next be billed on the upgraded plan.
    OfferEligibility:
      type: object
      description: The offer decision. Exactly one `action_type` is returned per user per request. `is_eligible` is `false` iff `action_type` is `no_action`.
      required:
      - action_type
      - is_eligible
      - checkout_product_id
      - savings_percent
      - free_trial_days
      - min_subscription_days
      - requires_apple_cancel
      properties:
        action_type:
          type: string
          enum:
          - no_action
          - migrate_storekit_to_web
          - upgrade_storekit_to_web
          - upgrade_web_to_web
          description: Which offer branch fired. `no_action` = nothing to show.
        is_eligible:
          type: boolean
          description: Convenience boolean; equivalent to `action_type != 'no_action'`.
        checkout_product_id:
          type: string
          description: Product ID the SDK should drive checkout toward. Empty string when `action_type` is `no_action`.
        from_product_id:
          type: string
          nullable: true
          description: The product the user is currently on (relevant for upgrade/migrate flows).
        savings_percent:
          type: integer
          description: Annualized savings percent vs. the user's current plan, 0 when not applicable.
        free_trial_days:
          type: integer
          description: Days of free trial the SDK should honor on the new subscription.
        min_subscription_days:
          type: integer
          description: Minimum subscribed days before the offer becomes eligible (for rollout/lock-in rules).
        display:
          $ref: '#/components/schemas/OfferDisplayV2'
          nullable: true
        proration:
          $ref: '#/components/schemas/OfferProration'
          nullable: true
        requires_apple_cancel:
          type: boolean
          description: Whether the user must cancel their StoreKit subscription after the new checkout completes (true for `migrate_*` / `upgrade_storekit_to_web`).
        apple_subscription:
          $ref: '#/components/schemas/AppleSubscriptionSummary'
          nullable: true
        checkout_presentation:
          type: string
          enum:
          - webview
          - native_pay
          - safari_vc
          - safari
          nullable: true
          description: How the SDK should present checkout for this offer.
        experiment_variant_id:
          type: integer
          nullable: true
          description: A/B experiment variant ID when this offer is part of an experiment.
    Error:
      type: object
      description: Error response.
      properties:
        error:
          type: string
          description: Human-readable error message.
        code:
          type: string
          description: Machine-readable error code (not always present).
      required:
      - error
    OfferDisplayV2:
      type: object
      description: 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.
      required:
      - title
      - body
      - cta_text
      - dismiss_text
      - accepted_title
      - accepted_body
      - completed_title
      - completed_body
      properties:
        title:
          type: string
        body:
          type: string
        cta_text:
          type: string
        dismiss_text:
          type: string
        accepted_title:
          type: string
        accepted_body:
          type: string
        completed_title:
          type: string
        completed_body:
          type: string
        apple_cancel_instructions:
          type: string
          description: Localized instructions shown in the accepted-state card when `requires_apple_cancel` is `true`. May be empty.
  responses:
    Unauthorized:
      description: Invalid or missing API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    BadRequest:
      description: Invalid request -- missing or malformed parameters.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-ZeroSettle-Key
      description: Your publishable API key. Use `zs_pk_test_*` for sandbox or `zs_pk_live_*` for production.