Reference
REST API
Base URL: https://api.stubkit.com. Every response is a JSON envelope: { success: true, data: ... } on 2xx and { success: false, error: { code, message } } on 4xx/5xx.
Client API
GET /v1/entitlement/:app_id/:user_id
Read current entitlement list. Requires a tenant JWT via Authorization: Bearer. The sub claim must equal :user_id.
{
"success": true,
"data": {
"app_id": "your-app",
"user_id": "user-123",
"entitlements": [
{
"id": "pro",
"status": "active",
"expires_at": "2026-05-11T00:00:00.000Z",
"source": "iap",
"platform": "ios",
"product_id": "com.example.pro.monthly"
}
]
}
}POST /v1/entitlement/:app_id/:user_id/refresh
Force a fresh read and bust the 30-second edge cache. Same auth as the read. Returns the same envelope.
POST /v1/purchases
Sync a freshly completed in-app purchase receipt from the client. Required body:
{
"app_id": "your-app",
"platform": "ios",
"product_id": "com.example.pro.monthly",
"transaction_id": "1000000123456789",
"user_id": "user-123"
}Returns 201 with the updated entitlement list for the user.
Admin API
Requires an API key with the appropriate scope in Authorization: Bearer sk_live_....
POST /v1/admin/apps
Create a tenant. Scope: admin:apps. Credentials in the body are encrypted at rest before storage.
GET /v1/admin/apps/:app_id
Read tenant config. Encrypted columns are not returned — only has_* flags.
PATCH /v1/admin/apps/:app_id
Partial update. Same encryption rules apply to secret fields.
POST /v1/admin/grants
Single-user grant. Scope: admin:grants. Body:
{
"app_id": "your-app",
"user_id": "user-123",
"entitlement": "pro",
"duration_days": 30,
"reason": "Goodwill gift",
"send_email": true
}POST /v1/admin/grants/bulk
Campaign grant. Target selector: all, non-premium, active-30d, since:<iso-date>, or user-ids:csv. Requires confirm: true to prevent accidents.
GET /v1/admin/users
Query params: app_id, q, limit, offset. Scope: admin:users.
GET /v1/admin/events
Query params: app_id, type, user_id, from, to, limit, offset. Scope: admin:events.
GET /v1/admin/analytics/mrr
Snapshot MRR, active subs, and active users. Optional app_id. Scope: admin:analytics.
GET /v1/admin/cron/runs
Scheduled-job run history. Optional job_name filter. Scope: admin:cron.
POST /v1/admin/api-keys
Generate a new key. The raw key is returned exactly once — copy it immediately. Scope: admin:api-keys.
DELETE /v1/admin/api-keys/:id
Revoke. Idempotent.
Webhook receivers
POST /v1/webhooks/apple— Apple App Store Server Notifications v2POST /v1/webhooks/google/:app_id— Google Play Real-time Developer NotificationsPOST /v1/webhooks/stripe/:app_id— Stripe webhook
Paywall A/B testing
GET /v1/experiments/:app_id/:slug?user_id=…
Public (pk_live_). Returns the variant assigned to this user plus the variant's offering. Bucketing is deterministic (SHA-256 ofuser_id + experiment_id).
{
"success": true,
"data": {
"experiment_id": "...",
"variant_id": "...",
"variant_key": "a",
"offering": { /* same shape as /v1/offerings */ }
}
}POST /v1/paywall-events
Record a view, purchase, or dismiss event. Use the experiment_id and variant_id returned from the GET endpoint.
Admin endpoints
GET /v1/admin/experiments— listPOST /v1/admin/experiments— create with 2+ variantsGET /v1/admin/experiments/:id/results— conversion + revenue per variantPATCH /v1/admin/experiments/:id— pause/resumeDELETE /v1/admin/experiments/:id— delete (with events)
Attribution
POST /v1/attribution
Record a campaign touch for a user. All fields except app_id and user_id optional. First-touch wins in LTV aggregation.
{
"app_id": "notesam",
"user_id": "c6e42d...",
"campaign_id": "c_12345",
"source": "appsflyer",
"medium": "cpc",
"campaign_name": "Spring upgrade",
"ad_id": "creative_789",
"metadata": { "placement": "story" }
}GET /v1/admin/analytics/ltv-by-campaign
First-touch attribution aggregated with subscription revenue. Users without a recorded touch fall into organic.
Analytics
GET /v1/admin/analytics/summary— stat cards, period-over-period deltaGET /v1/admin/analytics/mrr-history— time series for chartGET /v1/admin/analytics/mrr-decomposition— new / churned / net newGET /v1/admin/analytics/ltv-by-platform— average LTV per platformGET /v1/admin/analytics/cohort-retention— m1/m2/m3/m6/m12 retention gridGET /v1/admin/analytics/top-products— top N by active subscribersGET /v1/admin/analytics/events-summary— events by typeGET /v1/admin/analytics/platform-breakdown— subs by platform
Two-factor authentication
TOTP (RFC 6238), 6 digits, 30s period, SHA-1. Setup flow returns an otpauth:// URL for QR rendering.
GET /v1/admin/2fa/statusPOST /v1/admin/2fa/setup— returns secret + otpauth URLPOST /v1/admin/2fa/verify— returns backup codes on successPOST /v1/admin/2fa/disable— accepts TOTP or backup code
Team management
GET /v1/admin/invitationsPOST /v1/admin/invitations— invite by email with roleDELETE /v1/admin/invitations/:idPATCH /v1/admin/members/:id— update roleDELETE /v1/admin/members/:id— remove member
GDPR
GET /v1/admin/gdpr/export/:user_id— full JSON exportDELETE /v1/admin/gdpr/delete/:user_id— anonymize
Webhook receivers
POST /v1/webhooks/apple— Apple App Store Server Notifications v2POST /v1/webhooks/google/:app_id— Google Play Real-time Developer NotificationsPOST /v1/webhooks/stripe/:app_id— Stripe webhook
Rate limiting
Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. Default is 600 requests/minute per key. Burst traffic triggers HTTP 429 with Retry-After.
Pagination
List endpoints (users, events, webhook deliveries) return a next_cursor alongside the page. Pass it back as ?cursor=… to fetch the next page. Cursors are opaque base64url tokens encoding a (timestamp, id) tuple and are stable across inserts.
Machine-readable spec
The full OpenAPI 3.1 spec lives at stubkithq/stubkit/openapi.yaml. Generate clients with openapi-typescript, oapi-codegen, etc.
Misc
GET /v1/health
Unauthenticated health check. Returns service name, version, and timestamp.