Subscription Plans

Charge your app's users a recurring price through Hypery and give each payment credit grants that can only be spent in your app — price and grants configured independently.

Overview

Subscription plans let your app sell a recurring subscription (a monthly price, plus an optional annual price) and give subscribers credit grants to spend on AI inside your app.

  • You're the merchant of record. Payments go to your Stripe Connect account. Hypery takes its platform fee.
  • Price and grants are separate. A plan can charge $10/month and grant $5, $25, several grants of different types, or nothing at all.
  • Grants stay in your app. A grant can only pay for requests made through your app. Other apps, and the user's own direct API usage, can't spend it.
  • Grants expire and never roll over. Subscribers get fresh grants every month, and each grant expires after the number of days you set.
  • Annual billing, monthly grants. Annual subscribers pay once a year but still receive the plan's grants every month, so credits never land all at once.
  • Usage keeps working when a grant runs out. Once a grant is used up or expired, the user's usage bills the normal way (their balance or card).
  • No re-entering a card. Subscribing uses a card the user already gave Hypery: their wallet card, or the card they added when they set up billing.

Authentication:

  • Plan management: your personal API key (ak_...) or a session/OAuth token with write (read for GET). Only the app's owner or a billing admin can manage plans.
  • Subscribing: an OAuth token for your app with the billing:charge scope.

Plans require Stripe Connect onboarding to be complete for the app (chargesEnabled). Until then, creating a plan returns 409 MARKETPLACE_NOT_ONBOARDED.


Grant types

Every grant on a plan is one of two types:

TypeWhat the subscriber receivesCovers
hyperyHypery credits held on the grant itself (100 credits = $1). Requests from your app draw from it before the user's balance.Chat completions (streaming and non-streaming), OpenAI-compatible media endpoints, and /api/v1/usage/charge.
stripeA Stripe Billing credit grant on a Stripe customer dedicated to this user and your app.Metered AI usage billed through Vercel AI Gateway on /api/v1/chat/completions.

How each type is spent:

  • Hypery grants:
    • A request is paid from the grant first; anything left over is charged to the user's balance.
    • A request normally billed through Vercel AI Gateway is routed through the credit ledger instead, but only when the grant covers the whole request's estimate.
    • Hypery grants don't cover /v1/ai (the Vercel AI Gateway protocol endpoint) or predictions.
  • Stripe grants:
    • While the grant has value left, your app's metered usage is billed to the dedicated customer, where the credit grant offsets it.
    • Once it's spent, usage goes back to the user's regular Stripe customer.

Funding: you pay for every grant.

  • When a grant is issued, its USD value is debited from your team's Hypery balance.
  • If the balance can't cover it, the amount is added to your team's Stripe invoice instead.
  • If neither works, the grant is marked funding_failed (not spendable) and retried automatically.

Plans

List plans

Endpoint: GET /api/apps/{appId}/plans

curl https://hypery.ai/api/apps/{appId}/plans \
  -H "Authorization: Bearer ak_your_key"
{
  "success": true,
  "data": {
    "connectReady": true,
    "plans": [
      {
        "_id": "66f…",
        "name": "Pro",
        "status": "active",
        "priceCents": 1000,
        "interval": "month",
        "prices": [
          { "interval": "month", "priceCents": 1000, "stripePriceId": "price_…", "active": true },
          { "interval": "year", "priceCents": 10000, "stripePriceId": "price_…", "active": true }
        ],
        "grants": [
          { "id": "g_1a2b…", "type": "hypery", "amountUsd": 10, "expiresAfterDays": 31, "issueOn": "each_payment" }
        ],
        "activeSubscribers": 42,
        "economics": {
          "priceUsd": 10, "monthlyPriceUsd": 10, "annualPriceUsd": 100, "annualEffectiveMonthlyUsd": 8.33,
          "annualSavingsPct": 17, "recurringGrantUsd": 10, "firstPaymentGrantUsd": 10, "grantsExceedPrice": true
        }
      }
    ]
  }
}

Create a plan

Endpoint: POST /api/apps/{appId}/plans

curl -X POST https://hypery.ai/api/apps/{appId}/plans \
  -H "Authorization: Bearer ak_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pro",
    "monthlyPriceCents": 1000,
    "annualPriceCents": 10000,
    "grants": [
      { "type": "hypery", "amountUsd": 10, "expiresAfterDays": 31, "issueOn": "each_payment" },
      { "type": "hypery", "amountUsd": 5,  "expiresAfterDays": 7,  "issueOn": "first_payment_only" }
    ]
  }'
FieldTypeNotes
namestring1–120 chars.
descriptionstring?Up to 1000 chars.
monthlyPriceCentsintegerRequired. Minimum 50 ($0.50).
annualPriceCentsinteger?Optional annual price. Minimum 50.
priceCents + intervallegacyStill accepted: a single price billed every interval.
grants[]array0–10 grants.
grants[].typehypery | stripeSee Grant types.
grants[].amountUsdnumberGreater than 0. Set per grant; not tied to the price.
grants[].appleAmountUsdnumberOptional. The amount issued instead when the subscription was bought through App Store In-App Purchase, where Apple keeps a commission. Defaults to amountUsd.
grants[].expiresAfterDaysinteger1–366, counted from when the grant is issued.
grants[].issueOneach_payment | first_payment_onlyDefaults to each_payment (every month). first_payment_only is issued in the subscription's first month only.

plans[].priceCents / stripePriceId / interval mirror the monthly price, for older clients; read prices[] for both.

Update a plan

Endpoint: PATCH /api/apps/{appId}/plans/{planId}

All fields are optional: name, description, monthlyPriceCents, annualPriceCents, grants, status (active / archived).

  • Price changes create a new Stripe price. Existing subscribers on that interval move to it at their next renewal, with no proration.
  • annualPriceCents: null removes the annual option for new subscribers. Existing annual subscribers keep renewing until they cancel or switch.
  • monthlyPriceCents: null (only when an annual price exists) makes the plan annual-only. Existing monthly subscribers keep renewing; clients that don't send interval get 400 INTERVAL_NOT_OFFERED, so update them to send "year".
  • Grant changes apply from the next grant issued (including the next monthly grant of annual subscribers). Grants already issued keep their original amount and expiry.
    • To keep an existing grant, send its id back.
    • A grant without an id is treated as new.
    • A grant you leave out is removed.
  • Archiving hides the plan from new subscribers. Existing subscriptions keep renewing until they're canceled.

Plan details

Endpoint: GET /api/apps/{appId}/plans/{planId}

Returns the plan, its economics, and grant totals grouped by status: grants issued, credits issued, used, and remaining.


Subscribing (from your app)

With @hyperyai/sdk, useAppSubscription(appId) and <SubscribeButton planId interval> handle the whole flow by opening Hypery's hosted subscribe page as a popup (or a new tab / redirect on mobile). The endpoints below are what they call.

Subscriptions belong to a team. On the hosted page the user explicitly picks which Hypery team the subscription is for. Only that team's owner or admin can subscribe it, the team's card is charged (a card can be added right there), and a team has at most one live subscription per plan. Credits go to the team and can be used by its members in your app. Team owners and admins manage the subscription from the team's Billing page in Hypery, or through the endpoints below.

Start a hosted subscribe session

Endpoint: POST /api/marketplace/subscribe-sessions (scope billing:charge, OAuth token for your app)

curl -X POST https://hypery.ai/api/marketplace/subscribe-sessions \
  -H "Authorization: Bearer <user OAuth token>" \
  -H "Content-Type: application/json" \
  -d '{ "planId": "66f…", "interval": "year", "returnOrigin": "https://yourapp.com", "state": "c0ffee-8f3a1" }'
FieldNotes
planIdAn active plan of your app.
intervalPreselected billing interval (month default). The user can change it on the page.
returnOriginPopup mode: the origin the result is posted to. Must be the origin of one of your registered OAuth redirect URIs.
returnUrlNew-tab / redirect mode: where the user is sent back. Must match a registered redirect URI (or its origin). One of returnOrigin / returnUrl is required.
state8–200 chars you generate; echoed back so you can match the result to the request.
teamIdOptional. The Hypery team selected in your app. The page is locked to that team (the user confirms it, adds its card if needed); confirming any other team returns 403 TEAM_LOCKED. Omit it to let the user pick.

Returns 201 { sessionId, url, expiresAt }. Open url (valid for 30 minutes). The session is bound to the first Hypery account that opens it.

On the page the user signs in (and passes 2FA) if needed, picks a team they can bill, adds a card to that team if it has none, picks monthly or yearly, and confirms. 3-D Secure happens in the page.

When it finishes:

  • Popup: the page posts { type: "hypery:subscribe", sessionId, state, status: "completed" | "cancelled", teamId, subscriptionId } to returnOrigin (sender origin is https://hypery.ai) and closes.
  • New tab / redirect: the user is sent to returnUrl?subscribe_session=<id>&state=<state>&subscribe_status=completed|cancelled.

Always confirm the outcome with your token (don't trust the message alone):

Endpoint: GET /api/marketplace/subscribe-sessions/{sessionId}/result (scope read)

{
  "success": true,
  "status": "completed",
  "state": "c0ffee-8f3a1",
  "team": { "id": "66a…", "name": "Acme" },
  "subscription": { "id": "670…", "status": "active", "interval": "year", "nextGrantAt": "…" }
}

status is pending, completed, cancelled, or expired. Only the app that created the session can read it.

Show available plans

Endpoint: GET /api/marketplace/plans (scope read)

With an OAuth token, this returns the plans of the app the token belongs to. Session or API-key callers pass ?appId={appId}.

{
  "plans": [
    {
      "id": "66f…",
      "appId": "66e…",
      "name": "Pro",
      "priceCents": 1000,
      "currency": "usd",
      "interval": "month",
      "prices": [{ "interval": "month", "priceCents": 1000 }, { "interval": "year", "priceCents": 10000 }],
      "grants": [{ "type": "hypery", "amountUsd": 10, "expiresAfterDays": 31, "issueOn": "each_payment" }]
    }
  ]
}

Subscribe directly (server-to-server)

Prefer the hosted session above. This endpoint subscribes the caller's resolved billing team without asking which team, and is kept for existing integrations.

Endpoint: POST /api/marketplace/subscribe (scope billing:charge)

curl -X POST https://hypery.ai/api/marketplace/subscribe \
  -H "Authorization: Bearer <user OAuth token>" \
  -H "Content-Type: application/json" \
  -d '{ "planId": "66f…", "interval": "year", "idempotencyKey": "sub-7f3c9a1e" }'
FieldNotes
planIdAn active plan of the app the token belongs to.
intervalmonth (default) or year. 400 INTERVAL_NOT_OFFERED if the plan has no such price.
paymentMethodIdOptional. Must be a card the user already has on Hypery.
idempotencyKeyOptional but recommended. Use a stable id for each subscribe action.

Responses:

StatusMeaning
201Subscribed. The first month's grants are already issued.
200alreadySubscribed: true. The team already has a live subscription to this plan.
402 PAYMENT_METHOD_REQUIREDNo card on file. Send the user through the add-card flow, then retry.
402 PAYMENT_INCOMPLETEThe card needs authentication (3-D Secure). Confirm error.clientSecret with Stripe.js initialized with stripeAccount: error.stripeAccount.
402 PAYMENT_DECLINEDThe card was declined. message is safe to show the user.
403 PERMISSION_DENIEDThe token belongs to a different app.
403 TEAM_BILLING_ADMIN_REQUIREDThe caller isn't an owner or admin of the billing team.
409 MARKETPLACE_NOT_ONBOARDEDThe app can't accept payments yet.
409 SUBSCRIPTION_IN_PROGRESSAnother subscribe for the same team and plan is still running. Retry shortly.

The grants go to the caller's billing team: their personal team, or the organization selected with x-hypery-active-organization-id.

Status and remaining grants

Endpoint: GET /api/marketplace/subscriptions?appId={appId}[&teamId={teamId}] (scope read)

Returns the subscriptions of every team the user belongs to (filter with teamId).

{
  "subscriptions": [
    {
      "id": "670…",
      "appId": "66e…",
      "team": { "id": "66a…", "name": "Acme", "slug": "acme" },
      "plan": { "id": "66f…", "name": "Pro", "priceCents": 1000, "interval": "month", "grants": [] },
      "status": "active",
      "cancelAtPeriodEnd": false,
      "interval": "year",
      "pendingInterval": null,
      "currentPeriodEnd": "2027-09-15T12:00:00.000Z",
      "nextGrantAt": "2026-10-15T12:00:00.000Z",
      "grants": [
        { "type": "hypery", "amountUsd": 10, "remainingUsd": 6.42, "expiresAt": "2026-10-15T12:00:00.000Z" }
      ]
    }
  ]
}

Gating your app's own features

Hypery doesn't store your app's features; map them from the plan in your app. To decide what a team can use, call the endpoint above with teamId (the team the user is working in, from list_memberships) and treat the team as subscribed when a subscription has:

  • status of active or trialing (optionally allow a short grace period for past_due), and
  • plan.id of the plan that unlocks the feature (cancelAtPeriodEnd: true still has access until currentPeriodEnd).

Check it on your server before doing gated work, and cache it briefly per team.

Cancel and resume

  • POST /api/marketplace/subscriptions/{id}/cancel: stops renewal at the end of the current period. Grants already issued stay usable until they expire.
  • POST /api/marketplace/subscriptions/{id}/resume: undoes a pending cancellation.

Switch between monthly and annual

Endpoint: POST /api/marketplace/subscriptions/{id}/interval with { "interval": "year" | "month" }

  • Monthly → annual happens immediately. The unused part of the current month is credited, the year is charged now, and a new yearly period starts. The current month's grants aren't issued twice. A declined card returns 402 PAYMENT_DECLINED and nothing changes.
  • Annual → monthly happens at the end of the paid year (effective: "period_end", pendingInterval: "month"). Monthly grants continue until then. Calling it again with year keeps annual billing.
  • Switching to annual is refused on an archived plan (409 PLAN_ARCHIVED).

All three require billing:charge. The original subscriber or any owner/admin of the subscribing team may call them, and an OAuth token can only manage subscriptions for its own app.


Lifecycle

EventResult
Monthly invoice paid (first payment or renewal)One grant is issued for each plan grant that applies. This is idempotent per invoice and grant.
Annual invoice paidThe first month's grants are issued immediately; one more set is issued each calendar month of the paid year (checked hourly, catches up if delayed). Each month is funded when it's issued.
Payment failedThe subscription becomes past_due and no grants are issued.
Subscription set to cancel at period endNo further renewals. Annual subscribers keep receiving monthly grants until the paid year ends.
Subscription canceled immediatelyNo further renewals or monthly grants. Issued grants run until they expire.
Full refund or dispute on an invoiceThat invoice's unspent grants are revoked. For an annual invoice, the remaining months are not issued.
Grant expiresIts remaining value is no longer spendable (swept hourly).
User changes their default cardLive subscriptions switch to the new card automatically.

App Store In-App Purchase

An app shipped through the App Store must sell subscriptions and credits with In-App Purchase, not with Hypery's (Stripe) checkout. Hypery applies those purchases for you: an App Store subscription becomes a regular subscription to one of your plans and issues its grants on every renewal, and a consumable credit pack adds credits to the buyer's team. Apple bills, renews, cancels and refunds; Hypery mirrors it.

1. Set it up

In App Store Connect, create your products (an auto-renewable subscription group for your plans, consumables for credit packs) and set App Store Server Notifications (Version 2) to https://hypery.ai/api/marketplace/apple/notifications for both Production and Sandbox. Then tell Hypery what each product is:

PUT /api/apps/{appId}/apple
Authorization: Bearer <owner's API key or session>
 
{
  "bundleIds": ["com.example.app"],
  "commissionBps": 1500,
  "products": [
    { "productId": "pro.monthly", "kind": "subscription", "planId": "<planId>", "interval": "month" },
    { "productId": "credits.849", "kind": "credits", "credits": 849 }
  ],
  "sandboxUserIds": ["<Hypery user id of your App Review demo account>"]
}
FieldMeaning
bundleIdsYour app's bundle ids. One bundle id belongs to one app.
commissionBpsWhat Apple keeps: 1500 (Small Business Program, and subscriptions after a subscriber's first year) or 3000. Default 1500.
products[]Each App Store product id, mapped to a plan and interval (subscription) or to a number of credits (credits). Per-seat plans can't be sold in the App Store.
sandboxUserIdsHypery users whose Sandbox purchases count in production — your App Review demo account and your own testers. Sandbox purchases are free, so anyone else's are refused (403 SANDBOX_NOT_ALLOWED).

GET /api/apps/{appId}/apple returns the setup plus pricing guidance: for each plan price, the App Store price that leaves you at least what the web price does after Apple's commission, and how many credits each common pack price can fund. Credit packs are funded by your app's owner team, like grants.

2. Sell from the app

  1. GET /api/marketplace/apple/config (the user's OAuth token) returns your product ids and the user's appAccountToken.
  2. Buy with StoreKit 2, passing that token: product.purchase(options: [.appAccountToken(token)]).
  3. Send the transaction's JWS to Hypery, then finish the transaction only on 200:
POST /api/marketplace/apple/transactions
Authorization: Bearer <user's OAuth token>
x-hypery-active-organization-id: <team the purchase is for>
 
{ "signedTransaction": "<Transaction.jwsRepresentation>" }

Hypery verifies Apple's signature, checks the purchase belongs to this user and app, and applies it once (replays are no-ops). Send every transaction you see — purchases, Restore Purchases, and Transaction.updates — the same way. 503 means try again later (keep the transaction unfinished; StoreKit re-delivers it); any 4xx will never succeed, so finish it.

A subscription bought in the App Store has "provider": "apple" in GET /api/marketplace/subscriptions. It can only be changed or canceled in the App Store (show AppStore.showManageSubscriptions); Hypery's cancel, resume, plan and interval endpoints answer 409 MANAGED_BY_APP_STORE.

3. What notifications do

App Store notificationResult
SUBSCRIBED, DID_RENEW, OFFER_REDEEMED, RENEWAL_EXTENDEDThe period is updated and its grants are issued (idempotent per transaction).
DID_CHANGE_RENEWAL_PREF (upgrade)The new plan starts now with its grants; the old period's unspent grants are revoked (Apple refunds it).
DID_CHANGE_RENEWAL_STATUSAuto-renew off/on → cancelAtPeriodEnd.
DID_FAIL_TO_RENEWpast_due.
EXPIRED, GRACE_PERIOD_EXPIREDcanceled.
REFUND, REVOKEThe transaction's grants are revoked, and refunding the current period ends the subscription. A refunded credit pack takes back what is left of it.

A notification for a subscription your app hasn't reported yet is answered 500 so Apple retries it after the app reports the purchase (which says which team it is for).

Deleting an account

Deleting a Hypery account can't cancel its App Store subscriptions — only the user can, in Settings → Apple Account → Subscriptions. Hypery stops issuing their grants, and DELETE /api/user/me answers 200 { appStoreSubscriptions: n, message } (instead of 204) so your app can tell the user.

Paying for usage in your App Store build

Usage in an App Store build must also be paid without charging a card the user saved elsewhere: send x-hypery-payment-source: credits on every AI request from that build. See Paying from credits only.