Store credentials

To validate real purchases server-side, EntitleHub needs to talk to the stores. Apple works out of the box; Google Play needs a service account.

Two setups, not one

A credential lets EntitleHub verify purchases. Notifications let the store tell EntitleHub what happened afterwards, renewals, cancellations, and above all refunds. They are configured separately, in different consoles, and skipping the second is invisible: purchases keep working, and a refunded customer simply keeps their entitlement.

Every URL on this page is shown pre-filled, with your project id already substituted, in Dashboard → Settings → Store credentials. Each store there also shows whether it has ever sent EntitleHub a notification, and says so loudly when an app has purchases on a store that has never once talked back.

Apple (StoreKit 2): nothing to configure

Apple StoreKit 2 transactions are signed JWS blobs. Send the signed_transaction to /v1/subscribers/{id}/purchases and EntitleHub verifies it against Apple's root CA, checking the certificate chain and signature, with no per-project secret. The authenticated product id and environment from the receipt override anything the client claims, so a purchase can't be forged.

Apple server-to-server notifications (renewals, cancels, refunds)

So a subscription's renewals / cancellations / refunds stay current server-side without the app re-reporting, point Apple's notifications at EntitleHub. In App Store Connect → your app → App Information → App Store Server Notifications, set the Production (and Sandbox) Version 2 URL to:

text
https://entitlehub.com/v1/notifications/apple/{YOUR_PROJECT_ID}

EntitleHub verifies each notification's signature against Apple's root CA and updates the matching grant. Your Project ID is in Settings → General.

Apple has no scheduled backstop. Wire this up.

Google Play gets a nightly sweep of the store's own voided-purchase list, so a refund that a notification never delivered is still caught within a day. Apple has no equivalent: there is no refund list we can poll, because EntitleHub holds no App Store Server API credential (StoreKit 2 receipts self-verify, so we deliberately collect none). If these notifications are not registered, an Apple refund reaches us only when the customer next opens your app.

That fallback is real but passive. Apple stamps a refunded transaction with revocationDate permanently and StoreKit keeps handing it back, so when your app re-reports that receipt on restore or launch, EntitleHub revokes the grant and answers 409 receipt_revoked. It fires when the user opens the app, not on a schedule, so a refunded customer who never returns keeps their entitlement indefinitely.

Google Play: add a service account

Google purchase tokens are validated by calling the Play Developer API, which needs a service account with access to your Play account.

  1. In Google Cloud Console, create a service account and a JSON key for it (IAM & Admin → Service Accounts → Keys → Add key → JSON).
  2. Enable the Google Play Android Developer API for that Cloud project.
  3. In Play Console → Users & permissions, invite the service account's email and grant it access to view financial data and manage orders / subscriptions for your app.
  4. In EntitleHub → Dashboard → Settings → Store credentials, paste the JSON key and your app's package name (e.g. com.yourco.app).
The JSON key is a secret

It's stored encrypted and never returned to the browser. Treat it like a password; rotate it in Cloud Console if it's ever exposed.

Reporting a validated Google purchase

Once the credential is in place, pass the purchase token; EntitleHub validates it and uses the store's authenticated expiry:

bash
curl https://entitlehub.com/v1/subscribers/USER_ID/purchases \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "store": "play",
    "store_product_id": "app_pro_monthly",
    "purchase_token": "PLAY_PURCHASE_TOKEN",
    "is_subscription": true
  }'

Google Real-Time Developer Notifications (RTDN)

For automatic renewal / expiry / refund tracking, wire up RTDN:

The topic does not have to live with your service account

The Pub/Sub topic can be in anyGoogle Cloud project. All that matters is that Google Play's publisher account has permission to publish to it, and that the push subscription points here. Don't infer whether RTDN is set up from the project your Play service-account key came from - they are routinely different, and looking in the wrong project is how you end up creating a second, redundant topic that Play never publishes to.

  1. In Google Cloud → Pub/Sub, create a topic (e.g. play-rtdn).
  2. Add a push subscription on that topic with the endpoint:
    text
    https://entitlehub.com/v1/notifications/google/{YOUR_PROJECT_ID}
  3. Grant Google Play's publisher service account permission to publish to the topic (add [email protected] as a Pub/Sub Publisher).
  4. In Play Console → Monetization setup → Real-time developer notifications, set the topic name to your play-rtdn topic.

On each notification EntitleHub re-validates the purchase token against your Play credential (the source of truth for expiry) and updates the grant, then fires your webhooks. Requires the Google Play credential above.

Notification content: what Play will actually send

Play Console offers two settings under Notification content, and the difference is easy to miss:

  • Subscriptions and voided purchases only - subscription lifecycle events, plus voided purchases (refunds) for both subscriptions and one-time products. You do notget a notification when someone buys a one-time product; that purchase reaches EntitleHub only from your app's own report.
  • Subscriptions, voided purchases, and all one-time products - the above, plus a notification on every one-time purchase.

Either is fine for refunds, which is the half that silently breaks. Pick the second if you want the store to independently confirm one-time purchases rather than trusting the client report alone.

The nightly refund sweep (Google Play only)

A notification is a delivery, and deliveries fail: a topic created late, a Pub/Sub outage, an endpoint that was down. So EntitleHub also asks Play directly, once a night, for everything it has voided (purchases.voidedpurchases.list), and revokes anything still showing as granted. That repairs refunds RTDN dropped and refunds that happened before you wired RTDN up at all.

It needs nothing extra beyond the service account already above, as long as that account has view financial datapermission. One limit is Google's, not ours: the voided-purchase list only reaches back 30 days, so refunds older than that can never be recovered. Wire the notifications up on day one.

This sweep is Google Play only. Apple and Stripe have no scheduled equivalent here, so for those two the notification wiring is the whole refund path (plus, for Apple, the passive restore repair described above).

Stripe: add your secret key

For web subscriptions billed through Stripe, add your Stripe secret key in Dashboard → Settings → Store credentials → Stripe (from Stripe → Developers → API keys). EntitleHub verifies the key against Stripe when you save it, and stores it encrypted.

Map your Stripe Price as an EntitleHub product: create a product with store stripe and store_product_id set to the Stripe Price id (e.g. price_1P…), mapped to an entitlement. Then report a subscription by id, EntitleHub confirms it's active/trialing and uses the current period end as the entitlement expiry:

bash
curl https://entitlehub.com/v1/subscribers/USER_ID/purchases \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "store": "stripe", "stripe_subscription_id": "sub_1P…" }'

Stripe webhooks (renewals, cancels, refunds)

So EntitleHub keeps subscriptions in sync after the first purchase, add a Stripe webhook endpoint. In Stripe → Developers → Webhooks, point a new endpoint at:

text
https://entitlehub.com/v1/notifications/stripe/YOUR_PROJECT_ID

Subscribe to customer.subscription.updated, customer.subscription.deleted, invoice.payment_succeeded, invoice.payment_failed, and charge.refunded. Copy the endpoint's signing secret (whsec_…) into Settings → Store credentials → Stripe. EntitleHub verifies every delivery's signature, re-validates the subscription against Stripe for the authoritative period end, updates the grant, and fans the change out to your own webhooks.

Without the webhook secret

EntitleHub only learns about a Stripe subscription when it's reported at purchase time. A later renewal, cancellation, or refund won't update the entitlement until you add the signing secret above. The dashboard flags this as webhooks off.

Credentials set. Time to integrate: SDK or REST API.