REST API reference

Base URL https://entitlehub.com/v1. JSON in, JSON out. Authenticate every request with Authorization: Bearer <key>. See API keys. Reads accept a pk_ key; writes require an sk_ key.

POST /v1/check

Authoritative single-entitlement check. Any key.

http
POST /v1/check
Authorization: Bearer pk_live_…
{ "app_user_id": "user_123", "entitlement": "pro", "sandbox": false }

200 OK
{
  "app_user_id": "user_123", "entitlement": "pro", "active": true,
  "status": "active", "store": "play", "product": "app_pro_monthly",
  "expires_at": "2026-08-01T00:00:00Z", "will_renew": true, "since": "2026-07-01T00:00:00Z"
}
// not entitled → { "active": false, "reason": "no_active_grant" }

GET /v1/subscribers/{app_user_id}

A subscriber's active entitlements (the "customer info"). Any key.

http
GET /v1/subscribers/user_123
Authorization: Bearer pk_live_…

200 OK
{
  "app_user_id": "user_123",
  "active_entitlements": [
    { "entitlement": "premium", "status": "active", "store": "play",
      "product": "app_premium_lifetime", "expires_at": null, "will_renew": false, "since": "…" },
    { "entitlement": "pro", "status": "active", "store": "play",
      "product": "app_pro_monthly", "expires_at": "2026-08-01T00:00:00Z", "will_renew": true, "since": "…" }
  ]
}

Only currently-active entitlements are returned; an unknown user returns an empty list (not an error).

POST /v1/subscribers/{app_user_id}/purchases

Record a store purchase → maps product to entitlement(s) and writes a grant. Secret key required. Three input modes:

Google Play (validated)

http
POST /v1/subscribers/user_123/purchases
Authorization: Bearer sk_live_…
{ "store": "play", "store_product_id": "app_pro_monthly",
  "purchase_token": "…", "is_subscription": true }

Apple StoreKit 2 (validated)

http
POST /v1/subscribers/user_123/purchases
Authorization: Bearer sk_live_…
{ "signed_transaction": "<StoreKit2 JWS>" }
// store + product are read from the verified JWS

Stripe (validated)

http
POST /v1/subscribers/user_123/purchases
Authorization: Bearer sk_live_…
{ "store": "stripe", "stripe_subscription_id": "sub_1P…" }
// validated via the project's Stripe secret key; store_product_id ← the Stripe Price id

Trusted server-report (no store validation)

http
POST /v1/subscribers/user_123/purchases
Authorization: Bearer sk_live_…
{ "store": "play", "store_product_id": "app_premium_lifetime" }
// only when you've already validated the receipt elsewhere, call server-side only

All three return the updated customer-info object (same shape as the subscribers endpoint).

POST /v1/subscribers/{app_user_id}/grant

Grant an entitlement directly: promos, comps, redemption codes. Secret key required.

http
POST /v1/subscribers/user_123/grant
Authorization: Bearer sk_live_…
{ "entitlement": "premium", "duration_days": 0 }
// duration_days: 0 = lifetime, N = expires in N days. is_sandbox optional.

GET /v1/offerings

Your catalog: entitlements and products, for building a paywall. Any key.

http
GET /v1/offerings
Authorization: Bearer pk_live_…

200 OK
{ "entitlements": [ { "key": "pro", "name": "Pro", ... } ],
  "products": [ { "store_product_id": "app_pro_monthly", "type": "subscription",
                  "price_micros": 2990000, "currency": "USD", "entitlements": ["pro"] } ] }

Errors

StatusMeaning
401Missing / invalid / revoked key.
403Used a pk_ key on a write endpoint (needs sk_).
400Invalid token, unknown/unmapped product, or no Play credential configured.
Reads never 404 on unknown users

A check or subscriber read for a user with no grants returns an inactive/empty result, not an error.

Revoke access

POST /v1/subscribers/{app_user_id}/revoke ends access that was already granted: a support case, a chargeback settled outside the store, abuse, or a manual grant that should not have happened. Omit entitlement to revoke everything they hold. Secret key only, since it takes access away.

bash
curl -X POST https://entitlehub.com/v1/subscribers/user_123/revoke \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"entitlement":"pro"}'
Revoke keeps the books, delete rewrites them

Revoking flips the grant to revoked and pulls its expiry to now, so check() stops returning it while the grant and the event ledger stay intact. Revenue already recognized is still real, and "had access, then lost it" is the history you want in a billing dispute. Your webhooks receive an expiration event, not override, because consumers read override as a grant.

DELETE /v1/subscribers/{app_user_id} is the other tool: it erases the subscriber, their grants, their events, and the revenue those events contributed. That is for test residue, like a webhook test that left a phantom sale in your reports. On a real customer, revoke instead.