SubKit exposes JSON POST endpoints under /api/runtime/** and /api/server/**.
Use the SDKs when possible: @piparotech/subkit-expo wraps Runtime endpoints and
@piparotech/subkit-node wraps Server endpoints. The Zod request/response schemas
live in @piparotech/subkit-core and are the normative contract.
Common headers
Authorization: Bearer <key>
Content-Type: application/jsonServer mutations also require:
Idempotency-Key: stable-operation-keyMutation bodies carry a non-empty reason. SubKit records the acting key,
reason, and before/after evidence. Do not place keys or raw provider evidence in
the reason or idempotency key.
Runtime API
Runtime requests use a public app-bound sk_sdk_… key. The caller cannot select
an app or Store environment; the key resolves the app and verified provider
evidence resolves the environment.
| Endpoint | Purpose |
|---|---|
/api/runtime/offerings |
Runtime packages and current offering |
/api/runtime/customer-info |
Effective entitlements and access context |
/api/runtime/entitlements/check |
One fail-closed entitlement decision |
/api/runtime/iap/reconcile |
Provider-verified purchase reconciliation (202 + job poll) |
/api/runtime/iap/reconcile/$reconcileId |
Poll a durable reconcile job (pending/result, stable id) |
/api/runtime/devices/list |
Installation/device activation state |
/api/runtime/devices/claim |
Claim an eligible activation |
/api/runtime/devices/renew |
Renew the current activation lease |
/api/runtime/devices/replace |
Replace an activation within policy |
/api/runtime/devices/revoke |
Revoke a selected activation |
Only provider verification followed by a granted Effective Access decision
unlocks a mobile feature. A successful purchase transport response or one raw
CustomerInfo field is not authority by itself.
The Expo decision surface is:
const detailed = await client.getAccess('pro')
const allowed = await client.hasAccess('pro')detailed is a discriminated union; device_blocked requires a typed recovery
reason, while granted cannot carry one. Raw /customer-info remains available
for diagnostics and advanced recovery UI.
Server API reads
Server requests use an app-scoped sk_srv_… key with the listed capability.
| Endpoint | Capability |
|---|---|
/api/server/offerings |
catalog:read |
/api/server/products |
catalog:read |
/api/server/contract-plan-versions |
catalog:read |
/api/server/customer-info |
access:read |
/api/server/entitlements/check |
access:read |
/api/server/licenses |
access:read |
/api/server/licenses/:sourceId |
access:read |
/api/server/access-pools/:poolId |
access:read |
/api/server/devices |
access:read |
/api/server/direct-billing/summary |
direct_billing:read |
allowed: false is a normal domain response, not an HTTP error.
POST /api/server/access-reservations/claim requires access:write, the reviewed
reservationId, poolId, accessSourceId, app, authenticated Subject, token hash
and audit reason. It returns an identity-bound claimed allocation or durable typed
rejection, not capacity usage. New result/audit/claim commit together. Retry the
same payload/key after uncertainty; do not infer effective access from completion.
POST /api/server/access-reservations/claim/status requires access:read and
the exact original claim body plus idempotencyKey. It performs no mutation and
returns the original claimed/rejected result or pending. Tenant, app, source
environment, reviewed identity and full request hash are checked. Missing or
historical processing/failed journals remain uncertain. Token hash and key stay
in the POST body, are not returned, and all responses use Cache-Control: no-store.
POST /api/server/access-reservations/preview requires access:read and accepts
{ appId, claimTokenHash, subjectId } from an authenticated application backend.
It returns a non-cacheable recipient-scoped reservation snapshot with product,
pinned plan, pool and entitlement labels. It rejects foreign assignment/claimant
without disclosing the reservation. The token hash stays in the body and is never
returned. No write, effective entitlement or application membership is implied;
unassigned full tokens remain bearer invitations. See
subkit.access.previewReservation and the preview guide.
GET /api/server/access-reservations/:reservationId?appId=... additionally
provides an access:read-protected, non-cacheable reservation snapshot. It
checks tenant, app and source environment and returns canonical state plus an
exact claim/allocation reference when claimed. It does not expose token or
invitee-reference hashes, authorize an invitee or grant entitlement. See
serverReservationReadRequestSchema, serverReservationReadResponseSchema
and the reservation recovery guide.
The route requires a matching service deployment; it is not a Runtime API.
Server API mutations
| Endpoint | Capability |
|---|---|
/api/server/subjects/upsert |
subjects:write |
/api/server/subjects/:subjectId/aliases |
subjects:write |
/api/server/organizations/:organizationSubjectId/memberships |
organizations:write |
/api/server/billing-accounts |
billing_accounts:write |
/api/server/contracts |
contracts:write |
/api/server/contracts/:sourceId/licensee |
contracts:write |
/api/server/contracts/:sourceId/lifecycle |
access:write |
/api/server/payments |
payments:write |
/api/server/free-enrollments |
access:write |
/api/server/manual-provisions |
manual_provisions:write |
/api/server/promotion-codes/redeem |
promotions:redeem |
/api/server/access-pools/:poolId |
access:write |
/api/server/access-pools/:poolId/reservations |
access:write |
/api/server/access-pools/:poolId/allocations |
access:write |
/api/server/access-reservations/claim |
access:write |
/api/server/access-reservations/:reservationId |
access:write |
/api/server/access-allocations/:allocationId |
access:write |
/api/server/devices/:activationId |
access:write |
/api/server/devices/budget-reset |
access:write |
/api/server/plan-versions/:planVersionId |
catalog:write |
/api/server/sdk-keys |
sdk_keys:write |
/api/server/direct-checkout/sessions |
direct_billing:write |
/api/server/billing-portal/sessions |
direct_billing:write |
There is no direct grant-write endpoint. Mutations create or change verified sources, pools, reservations, allocations, or device activations; grants remain derived.
Hosted direct billing
The direct billing surface is for trusted server code. It accepts an app-bound Subject identity and published catalog selections, then returns only service-owned opaque intents and short-lived redirects. For the first Individual slice, the service owns Billing Account selection: it resolves or creates the Individual Billing Account from the authenticated active app-user Subject. The public client contract does not accept an account ID, email, or display name for this selection.
| Endpoint | Purpose |
|---|---|
/api/server/direct-checkout/sessions |
Create a hosted checkout session from an Offering/package |
/api/server/billing-portal/sessions |
Create a hosted billing portal session |
/api/server/direct-billing/summary |
Read the canonical direct billing summary |
POST /api/server/direct-checkout/sessions accepts
offeringIdentifier and packageIdentifier, plus subjectId and an optional
allowlisted returnTarget. It does not accept a Billing Account ID, amount,
currency, Stripe Product/Price IDs, payment-method data, or caller-controlled
success/cancel URLs. The service resolves the Individual Billing Account from
the authenticated active app-user Subject, then resolves the remaining values
from its app configuration and published catalog.
{
"appId": "app_123",
"offeringIdentifier": "default",
"packageIdentifier": "monthly",
"reason": "start selected direct billing checkout",
"returnTarget": "billing_settings",
"subjectId": "subject_123"
}Checkout and portal responses contain a SubKit-owned intent ID with the
checkout-intent: or billing-portal: prefix, a short-lived HTTPS
redirectUrl, and an ISO datetime redirectUrlExpiresAt. They never contain
provider IDs, client secrets, or payment-method details. The summary response
contains only canonical catalog, amount/currency, period, cancellation, and
provider-state status fields. Status values are normalized at the service’s
provider integration boundary; they are not provider commands. The normative
Zod contracts are exported by @piparotech/subkit-core as
serverDirectCheckoutSessionRequestSchema,
serverDirectCheckoutSessionResponseSchema,
serverBillingPortalSessionRequestSchema,
serverBillingPortalSessionResponseSchema,
serverDirectBillingSummaryRequestSchema, and
serverDirectBillingSummaryResponseSchema.
Link a previous Subject identity
POST /api/server/subjects/:subjectId/aliases links one previous opaque runtime
identifier to the existing Subject. It requires an app-scoped subjects:write
key, a reason, and Idempotency-Key.
{
"alias": "previous-opaque-app-user-id",
"appId": "app_123",
"reason": "link identity after account migration"
}Aliases are unique per app and cannot be moved between Subjects. The raw alias
remains in the identity table so Runtime resolution works, but audit and
lifecycle evidence contain only a short hash suffix. The mutation writes the
Alias, operator audit, and subject_alias_added event atomically. The Node
client exposes it as subkit.customers.addSubjectAlias(...).
Manage Organization Memberships and roles
POST /api/server/organizations/:organizationSubjectId/memberships starts one
app-scoped membership for an existing App User. PATCH assigns or ends an
admin/trainer role, or ends the membership. Both methods require
organizations:write, a non-empty reason, an authoritative effectiveAt, and
Idempotency-Key.
{
"appId": "app_123",
"effectiveAt": "2026-08-01T00:00:00.000Z",
"memberSubjectId": "subject_trainer_456",
"reason": "trainer joined the club roster",
"roles": ["trainer"]
}Membership, operator Audit, and lifecycle evidence commit atomically. Membership
and roles are historical identity/governance evidence only: they allocate no
seat and grant no entitlement. Seats remain authoritative through
Reservation/Allocation; the Organization detail view displays both authorities
separately. The Node client exposes
subkit.customers.startOrganizationMembership(...) and
subkit.customers.mutateOrganizationMembership(...).
Change a Contract licensee
PATCH /api/server/contracts/:sourceId/licensee changes the current historical
organization-licensee relationship for one Contract Source. It requires
contracts:write, an app-scoped Server key, and Idempotency-Key.
{
"appId": "app_123",
"effectiveAt": "2026-08-01T00:00:00.000Z",
"licenseeSubjectId": "subject_club_456",
"reason": "club legal entity changed"
}The target must be an organization Access Subject in the same app. The
effective time must follow the current licensee period. SubKit closes the old
period, creates the new one, writes operator audit evidence, and records one
licensee_changed lifecycle event in the same transaction. This relationship
does not allocate a seat or grant an entitlement. The Node client exposes the
same contract as subkit.contracts.changeLicensee(input, { idempotencyKey }).
Change Contract lifecycle or renewal intent
Use POST /api/server/contracts/:sourceId/lifecycle for a read-only preview and
PATCH with Idempotency-Key for Apply. Supported actions are suspend,
resume, revoke, renew, schedule_non_renewal, and
revert_non_renewal. Apply always repeats the preview guards
expectedState, optional expectedAutoRenews, and optional expectedTermEnd.
renew additionally requires a strictly later newTermEnd.
{
"action": "schedule_non_renewal",
"appId": "app_123",
"expectedAutoRenews": true,
"expectedState": "active",
"expectedTermEnd": "2027-08-01T00:00:00.000Z",
"reason": "customer requested cancellation at term end"
}Scheduling non-renewal changes only renewal intent. It does not suspend,
revoke, or shorten current access. Reverting the schedule remains a separate
lifecycle event. Renewal extends Contract and source-bound validity windows in
one transaction and records operator audit plus contract_renewed evidence.
The Node client exposes preview/apply through subkit.licenses.
Example
curl -X POST https://subkit.piparo.tech/api/server/entitlements/check \
-H "Authorization: Bearer $SUBKIT_SERVER_KEY" \
-H 'Content-Type: application/json' \
-d '{"appUserId":"user_123","entitlement":"pro"}'For mutation examples, use the Node.js backend guide.