stubkit docs

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 v2
  • POST /v1/webhooks/google/:app_id — Google Play Real-time Developer Notifications
  • POST /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 — list
  • POST /v1/admin/experiments — create with 2+ variants
  • GET /v1/admin/experiments/:id/results — conversion + revenue per variant
  • PATCH /v1/admin/experiments/:id — pause/resume
  • DELETE /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 delta
  • GET /v1/admin/analytics/mrr-history — time series for chart
  • GET /v1/admin/analytics/mrr-decomposition — new / churned / net new
  • GET /v1/admin/analytics/ltv-by-platform — average LTV per platform
  • GET /v1/admin/analytics/cohort-retention — m1/m2/m3/m6/m12 retention grid
  • GET /v1/admin/analytics/top-products — top N by active subscribers
  • GET /v1/admin/analytics/events-summary — events by type
  • GET /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/status
  • POST /v1/admin/2fa/setup — returns secret + otpauth URL
  • POST /v1/admin/2fa/verify — returns backup codes on success
  • POST /v1/admin/2fa/disable — accepts TOTP or backup code

Team management

  • GET /v1/admin/invitations
  • POST /v1/admin/invitations — invite by email with role
  • DELETE /v1/admin/invitations/:id
  • PATCH /v1/admin/members/:id — update role
  • DELETE /v1/admin/members/:id — remove member

GDPR

  • GET /v1/admin/gdpr/export/:user_id — full JSON export
  • DELETE /v1/admin/gdpr/delete/:user_id — anonymize

Webhook receivers

  • POST /v1/webhooks/apple — Apple App Store Server Notifications v2
  • POST /v1/webhooks/google/:app_id — Google Play Real-time Developer Notifications
  • POST /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.