Used by thousands of students worldwide - help us reach 150 members
Mobile Subscriptions
/iapUnified subscription entitlement across the website (Stripe) and the mobile apps (App Store / Google Play), synced through RevenueCat. The RevenueCat App User ID is the Sharh user id, so a subscription bought in any store grants the same `premium` entitlement everywhere. Lets any client read the account's current entitlement, lets the mobile app ask the backend to re-read RevenueCat after a purchase or restore, and keeps the older direct App Store validation flow working for app builds that predate RevenueCat.
/iap/subscriptionGet the current user's subscription entitlementPOST/iap/syncRe-read the entitlement from RevenueCatGET/iap/credit-packsList the consumable OCR page packsPOST/iap/ios/validateValidate an iOS App Store subscription directly with Apple (legacy)Get the current user's subscription entitlement
Returns the account's effective premium entitlement. Resolution order: the entitlement cached from RevenueCat (re-read from RevenueCat when older than the configured TTL, when it has lapsed, or when `refresh=1` is sent), then the legacy App Store record, then the user's Stripe subscription (active, trialing, or cancelled but still inside its paid period). RevenueCat or Stripe being unreachable never fails the request; the cached value is used. The subscription object is the unified premium entitlement, the same whichever store it was bought in. `isActive` is true when access is currently granted; admins receive active access automatically even without a paid subscription. `status` is one of `none`, `active`, `trial`, `cancelled` (will not renew but still active until `expiresAt`), `billing_issue`, `expired`, `refunded`. `store` is `app_store`, `play_store`, `stripe`, `promotional` or null. `source` says where the answer came from: `revenuecat` (synced from RevenueCat), `legacy_mobile` (App Store record validated through `POST /iap/ios/validate` before RevenueCat), `legacy_stripe` (looked up directly in Stripe), or `admin` (role-based access without a paid entitlement). `appUserId` is the RevenueCat App User ID for this account (the user id) and `entitlementId` the entitlement the mobile SDK should check. `managementUrl` is the store's subscription management page when known.
Query parameters
refreshstring | Send `1` or `true` to force a fresh read from RevenueCat instead of the cached entitlement.Default: 0 |
Errors
401Plain text `Unauthorized: No token provided`.403Plain text `Forbidden: Invalid token`, or `Forbidden: You do not have the required permissions`.404Plain text `User not found` (the token's user no longer exists).500JSON `{ "error": "entitlement_lookup_failed", "message": "..." }` for unexpected failures.
curl -X GET "https://app.ummahspot.com/iap/subscription" \
-H "Authorization: Bearer $SHARH_TOKEN"{
"subscription": {
"isActive": true,
"status": "active",
"store": "app_store",
"productId": "sharh.monthly.subscription",
"periodType": "normal",
"purchasedAt": "2026-09-18T14:05:00.000Z",
"expiresAt": "2026-10-18T14:05:00.000Z",
"gracePeriodExpiresAt": null,
"willRenew": true,
"billingIssue": false,
"managementUrl": "https://apps.apple.com/account/subscriptions",
"environment": "production",
"source": "revenuecat",
"lastSyncedAt": "2026-10-18T12:00:00.000Z",
"appUserId": "64f1c2e5a1b2c3d4e5f60718",
"entitlementId": "premium",
"revenueCatEnabled": true
}
}Re-read the entitlement from RevenueCat
Called by the mobile app right after a purchase, restore or sign-in so the backend (and therefore the website and OCR credits) reflects the new state immediately instead of when RevenueCat's webhook arrives. Reads the subscriber from RevenueCat, stores the normalised entitlement on the user, opens or closes the OCR usage record for store-backed subscriptions, and returns the effective entitlement in the same shape as `GET /iap/subscription`. Idempotent; safe to call repeatedly. When RevenueCat is not configured on the server the current entitlement is returned with `synced: false` and nothing is changed. The subscription object is the unified premium entitlement, the same whichever store it was bought in. `isActive` is true when access is currently granted; admins receive active access automatically even without a paid subscription. `status` is one of `none`, `active`, `trial`, `cancelled` (will not renew but still active until `expiresAt`), `billing_issue`, `expired`, `refunded`. `store` is `app_store`, `play_store`, `stripe`, `promotional` or null. `source` says where the answer came from: `revenuecat` (synced from RevenueCat), `legacy_mobile` (App Store record validated through `POST /iap/ios/validate` before RevenueCat), `legacy_stripe` (looked up directly in Stripe), or `admin` (role-based access without a paid entitlement). `appUserId` is the RevenueCat App User ID for this account (the user id) and `entitlementId` the entitlement the mobile SDK should check. `managementUrl` is the store's subscription management page when known.
Errors
401Plain text `Unauthorized: No token provided`.403Plain text `Forbidden: Invalid token`, or `Forbidden: You do not have the required permissions`.404Plain text `User not found` (the token's user no longer exists).502JSON `{ "error": "entitlement_sync_failed" | "REVENUECAT_ERROR", "message": "..." }` when RevenueCat could not be reached or rejected the request.503JSON with `error: "REVENUECAT_NOT_CONFIGURED"` when the server has no RevenueCat secret key.
curl -X POST "https://app.ummahspot.com/iap/sync" \
-H "Authorization: Bearer $SHARH_TOKEN"{
"subscription": {
"isActive": true,
"status": "active",
"store": "stripe",
"productId": "prod_QgR2k9LmN0pQrS",
"periodType": "normal",
"purchasedAt": "2026-10-01T09:00:00.000Z",
"expiresAt": "2026-11-01T09:00:00.000Z",
"gracePeriodExpiresAt": null,
"willRenew": true,
"billingIssue": false,
"managementUrl": null,
"environment": "production",
"source": "revenuecat",
"lastSyncedAt": "2026-10-18T12:00:00.000Z",
"appUserId": "64f1c2e5a1b2c3d4e5f60718",
"entitlementId": "premium",
"revenueCatEnabled": true
},
"synced": true
}List the consumable OCR page packs
Returns the one-time OCR page packs sold as consumable in-app purchases in the mobile app (App Store / Google Play, tracked through RevenueCat). Each entry gives the store product id to fetch, the pages it grants and the list price in USD; the store's localized price should be shown to the user. Purchased pages are added to the account's balance (never expire) and are spent on uploads only after the plan's monthly credits are used up. Public.
curl -X GET "https://app.ummahspot.com/iap/credit-packs"{
"packs": [
{ "productId": "sharh.ocr.pages.500", "pages": 500, "price": 9.99, "title": "500 OCR pages" },
{ "productId": "sharh.ocr.pages.2000", "pages": 2000, "price": 39.99, "title": "2,000 OCR pages" }
]
}Validate an iOS App Store subscription directly with Apple (legacy)
Pre-RevenueCat flow, kept for older app builds. Validates proof of an App Store subscription purchase with Apple and saves the result as the account's legacy App Store record. At least one of `transactionReceipt`, `transactionId` or `originalTransactionId` must be sent. When the server is configured for the App Store Server API and a transaction id is supplied, the transaction and its subscription status are fetched from Apple (Production first, then Sandbox); otherwise the `transactionReceipt` is verified through Apple's receipt verification endpoint. The purchase must belong to the Sharh app bundle and to a recognised subscription product (by default `sharh.monthly.subscription` or `sharh.yearly.subscription`). The latest transaction for a recognised product determines the result. An App Store subscription can be linked to only one Sharh account: if another user already holds the same `originalTransactionId`, the request fails with 409. On success the response is the account's effective entitlement in the same shape as `GET /iap/subscription` (a RevenueCat-synced entitlement, when present, takes precedence over the record just validated). A validation that finds an inactive or expired subscription still returns 200 with `isActive: false`. The subscription object is the unified premium entitlement, the same whichever store it was bought in. `isActive` is true when access is currently granted; admins receive active access automatically even without a paid subscription. `status` is one of `none`, `active`, `trial`, `cancelled` (will not renew but still active until `expiresAt`), `billing_issue`, `expired`, `refunded`. `store` is `app_store`, `play_store`, `stripe`, `promotional` or null. `source` says where the answer came from: `revenuecat` (synced from RevenueCat), `legacy_mobile` (App Store record validated through `POST /iap/ios/validate` before RevenueCat), `legacy_stripe` (looked up directly in Stripe), or `admin` (role-based access without a paid entitlement). `appUserId` is the RevenueCat App User ID for this account (the user id) and `entitlementId` the entitlement the mobile SDK should check. `managementUrl` is the store's subscription management page when known.
Body parameters
transactionReceiptstring | Base64 App Store receipt. Required when no transaction id is sent, or when the server is not configured for the App Store Server API. |
transactionIdstring | StoreKit transaction id of the purchase. |
originalTransactionIdstring | StoreKit original transaction id identifying the subscription. |
productIdstring | Product id of the purchase. When sent, it must be one of the recognised subscription product ids. |
Errors
400JSON `{ "error": "<code>", "message": "..." }`. Codes: `missing_purchase_proof` (none of transactionReceipt, transactionId, originalTransactionId sent), `IAP_PRODUCT_NOT_ALLOWED` (unrecognised product id), `IAP_BUNDLE_MISMATCH` (purchase belongs to a different app), `IAP_TRANSACTION_NOT_FOUND` (Apple does not know the transaction), `IAP_RECEIPT_INVALID` (Apple rejected the receipt).401Plain text `Unauthorized: No token provided`.403Plain text `Forbidden: Invalid token`, or `Forbidden: You do not have the required permissions`.409JSON with `error: "subscription_already_linked"`: this App Store subscription is already linked to another Sharh account.500JSON `{ "error": "...", "message": "..." }` for unexpected failures (for example Apple being unreachable). `error` defaults to `iap_validation_failed` when the underlying error carries no code.503JSON with `error: "IAP_SHARED_SECRET_MISSING"` or `"IAP_VALIDATION_NOT_CONFIGURED"` when the server lacks the Apple credentials needed for the supplied proof.
curl -X POST "https://app.ummahspot.com/iap/ios/validate" \
-H "Authorization: Bearer $SHARH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"transactionReceipt":"string"}'{
"subscription": {
"isActive": true,
"status": "active",
"store": "app_store",
"productId": "sharh.monthly.subscription",
"periodType": "normal",
"purchasedAt": "2026-09-18T14:05:00.000Z",
"expiresAt": "2026-10-18T14:05:00.000Z",
"gracePeriodExpiresAt": null,
"willRenew": true,
"billingIssue": false,
"managementUrl": "https://apps.apple.com/account/subscriptions",
"environment": "production",
"source": "legacy_mobile",
"lastSyncedAt": "2026-10-18T12:00:00.000Z",
"appUserId": "64f1c2e5a1b2c3d4e5f60718",
"entitlementId": "premium",
"revenueCatEnabled": true
}
}