> ## Documentation Index
> Fetch the complete documentation index at: https://docs.grantiva.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Subscription Claims

> Project your app's Apple IAP and Stripe subscription state into attestation JWTs — no API key required

Grantiva can ingest your app's own subscription state — from **Apple In-App Purchase** and/or **your Stripe account** — and surface it as a managed `subscription` object inside `custom_claims` in every attestation JWT. Your backend gates features by verifying the signed JWT it already receives; there is no entitlement API to call and **no API key involved**.

<Note>Subscription entitlement ingestion is an **Enterprise** feature.</Note>

## Sharing units (families)

Entitlements attach to a **Subject** — an app-owned sharing unit such as a family or household, not an individual device. One paying member entitles every device in the unit, because every member's device declares the same sharing-unit id at attestation time.

The join key is **one opaque id used in three places** — all three must carry the identical value:

| Where        | How                                                                                      |
| ------------ | ---------------------------------------------------------------------------------------- |
| Grantiva SDK | `grantiva.setSubjectId(familyId)` — sent as `subjectId` on validate/refresh (SDK 2.1.0+) |
| Apple IAP    | StoreKit `Product.PurchaseOption.appAccountToken(UUID(uuidString: familyId)!)`           |
| Stripe       | Checkout Session `client_reference_id = familyId`, plus `metadata.grantiva_price_id`     |

<Warning>Apple's `appAccountToken` requires a UUID, so your sharing-unit id **must be a UUID string**.</Warning>

The Subject id is opaque to Grantiva — mint a stable UUID per household in your own system and reuse it everywhere. It is distinct from `identify(_:)`, which scopes the *individual user* for feedback and flag targeting.

## The claim

The claim appears under `custom_claims.subscription` in the decoded JWT:

```json theme={null}
{
  "tier": "plus",
  "status": "active",
  "interval": "annual",
  "source": "apple",
  "product_id": "com.example.plus.annual",
  "expires_at": 1750000000,
  "auto_renew": true,
  "in_grace": false,
  "is_active": true,
  "environment": "production"
}
```

| Field         | Values                                                 | Notes                                                            |
| ------------- | ------------------------------------------------------ | ---------------------------------------------------------------- |
| `tier`        | your own strings                                       | Defined by your product/price mapping — not a Grantiva plan name |
| `status`      | `active`, `in_grace`, `expired`, `refunded`, `revoked` |                                                                  |
| `interval`    | `monthly`, `annual`                                    |                                                                  |
| `source`      | `apple`, `stripe`                                      | Which system the entitlement came from                           |
| `product_id`  | string                                                 | Apple product id or Stripe price id                              |
| `expires_at`  | epoch **seconds** (integer)                            | Omitted when the source reports no expiry                        |
| `auto_renew`  | boolean                                                |                                                                  |
| `in_grace`    | boolean                                                | Billing-retry grace from the source platform                     |
| `is_active`   | boolean                                                | **Gate on this single field**                                    |
| `environment` | `production`, `sandbox`                                | Filter out sandbox in production backends                        |

Rules worth knowing:

* **Absent means free.** If the device's sharing unit has no entitlement, the `subscription` key is simply not present.
* **Gate on `is_active`**, not on `status` — it already accounts for grace periods.
* When a unit holds multiple entitlements (two payers, or Apple + Stripe), the JWT surfaces the **best** one: active-like first, then higher tier, then later expiry.
* `subscription` is a **reserved claim key**, managed by Grantiva. Attempts to create a custom claim named `subscription` are rejected, and the managed claim is injected even if your custom-claims toggle is off.
* The JWT has a TTL of at most 1 hour and refreshes on every attest/refresh, so the claim self-heals — treat the JWT as the source of truth.

<Warning>
  Read `subscription` from the **decoded JWT**. The flat `customClaims` map in the validate/refresh HTTP response stringifies nested objects and is not suitable for structured claims.
</Warning>

## How state flows in

Apple and Stripe push subscription lifecycle events directly to Grantiva:

```
Apple App Store Server Notifications v2 ─► POST /webhooks/apple/app-store-notifications
Your Stripe account                     ─► POST /webhooks/stripe/entitlements/<orgID>
```

Grantiva resolves the sharing unit from the `appAccountToken` / `client_reference_id`, maps the product or price to a tier using your [entitlement configuration](/api-reference/entitlements/config), updates the entitlement, and from then on projects the claim into every JWT minted for any device in that unit. Ingestion trusts source events; there is no periodic re-sync against the App Store Server API or Stripe API in v1.

## Instant invalidation webhooks

JWTs live up to an hour, so Grantiva also fires outbound webhooks the moment an entitlement changes: `subscription.changed` (always), `subscription.expired`, and `subscription.refunded`. Use them as a cache-bust nudge — evict whatever you cached for that `subject_id` and re-read the next JWT. See [Webhooks](/concepts/webhooks#subscription-events) for payloads and signature verification.

## Setup checklist

1. [Configure your product/price → tier mapping](/api-reference/entitlements/config) (Enterprise, dashboard admin).
2. Point Apple App Store Server Notifications v2 and/or your Stripe webhook at Grantiva.
3. In your app: mint a household UUID, call `setSubjectId`, and pass the same UUID at purchase time.
4. In your backend: verify the JWT against [Grantiva's JWKS](/backend-verification) and gate on `custom_claims.subscription.is_active`.

Or follow the end-to-end [Subscription Claims Quick Start](/quickstart-subscription-claims).
