Which endpoints do the RevenueCat SDKs call on RevenueDot?
The RevenueCat SDKs call these endpoints when their proxy URL points at RevenueDot. They take a public app key (appl_, goog_, test_ ...), which is safe to ship in an app.
Several also accept a secret key for server-side use. Errors use the SDK's error format { "code": 7103, "message": "..." }; see Errors.
Responses under /v1 and /rcbilling are signed when the server has a signing key; see Trusted Entitlements.
Base URL: your server, for example http://localhost:8787 or https://revenuedot.example.com. The examples read REVENUEDOT_URL, PUBLIC_KEY, SECRET_KEY and PROJECT_ID from your shell.
Operations on this page (44)#
- Server: Server name and docs link, Health check, Connectivity probe
- Customer info: Get customer info
- Receipts: Post a purchase or restore
- Offerings (SDK): Get offerings, Get offerings without a user, Test Store product details
- Identity: Log in (identify), Alias two app user ids
- Attributes: Set customer attributes
- SDK support: Intro offer eligibility (StoreKit 1), Sign a promotional offer (iOS), Attribution data (deprecated iOS call), Apple AdServices token, SDK health report availability, SDK health report, Product to entitlement mapping (offline entitlements), Customer Center configuration (not built), Customer Center support ticket (not built), Virtual currency balances (not built), Redeem a web purchase (not available), Register an Apple external purchase token (iOS), Rewarded ad verification (not available), Amazon receipt details (not supported), Paywall workflows (web SDK), One paywall workflow (web SDK), Restore eligibility (StoreKit 2), Remote config (none yet), Remote config (none yet), SDK paywall and feature events (accepted, not stored), SDK diagnostics (accepted, not stored)
- Web Billing: Web offering products, Start a hosted web checkout (not available), Web Billing purchase (not available), Prepare a Web Billing checkout (not available), Start a Web Billing checkout (not available), Web Billing checkout status, Refresh Web Billing checkout pricing, Complete a Web Billing checkout, Web checkout branding
- Store notifications: App Store Server Notifications v2, Google Play real-time developer notifications (Pub/Sub push)
- Response signing: Public key for response signatures
Server#
Health and server info. No API key.
Server name and docs link#
GET / · Auth: none
Answers a small JSON document. The Docker health check calls it.
Example request
curl -s "$REVENUEDOT_URL/"Responses
- 200: Server info.
Example 200 response:
{
"name": "RevenueDot",
"docs": "https://revenuedot.app/docs"
}Health check#
GET /v1/health · Auth: none
Needs no API key. Use it for load balancer and uptime checks.
Example request
curl -s "$REVENUEDOT_URL/v1/health"Responses
- 200: The server is up.
Example 200 response:
{
"status": "ok"
}Connectivity probe#
GET /v1/health/connectivity · Auth: none
Needs no API key. The iOS SDK probes it only with its internal API failover setting, which is off by default.
Example request
curl -s "$REVENUEDOT_URL/v1/health/connectivity"Responses
- 200: The server is up.
Example 200 response:
{
"status": "ok"
}Customer info#
The customer's entitlements, subscriptions and one-time purchases, as the SDK decodes them into CustomerInfo.
Get customer info#
GET /v1/subscribers/{app_user_id} · Auth: public app key or secret key
What Purchases.getCustomerInfo() calls. Creates the customer when the app user id is new (answer 201). With a secret key the answer also has subscriber_attributes.
Entitlements are listed even after they expire; an entitlement is active while expires_date is null or in the future.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | App user id, URL-encoded (anonymous ids look like $RCAnonymousID:...). |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
X-Nonce |
string | no | Base64 nonce the SDK sends when entitlement verification is on; it is part of the signed message. |
Example request
curl -s "$REVENUEDOT_URL/v1/subscribers/user_1" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 200: Customer info. Returns CustomerInfo.
- 201: Customer info of a customer created by this call. Returns CustomerInfo.
- 400: Bad request. For receipts, a 4xx tells the SDK the purchase can never be accepted, so it finishes the transaction. Returns V1Error.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"request_date": "2026-09-30T20:41:54Z",
"request_date_ms": 1790800914034,
"subscriber": {
"entitlements": {
"pro": {
"expires_date": "2026-10-30T20:41:54Z",
"grace_period_expires_date": null,
"product_identifier": "pro_monthly",
"purchase_date": "2026-09-30T20:41:54Z"
}
},
"first_seen": "2026-09-30T20:41:54Z",
"last_seen": "2026-09-30T20:41:54Z",
"management_url": null,
"non_subscriptions": {},
"original_app_user_id": "user_1",
"original_application_version": null,
"original_purchase_date": "2026-09-30T20:41:54Z",
"other_purchases": {},
"subscriptions": {
"pro_monthly": {
"auto_resume_date": null,
"billing_issues_detected_at": null,
"display_name": null,
"expires_date": "2026-10-30T20:41:54Z",
"grace_period_expires_date": null,
"is_sandbox": true,
"management_url": null,
"original_purchase_date": "2026-09-30T20:41:54Z",
"ownership_type": "PURCHASED",
"period_type": "normal",
"purchase_date": "2026-09-30T20:41:54Z",
"refunded_at": null,
"store": "test_store",
"store_transaction_id": "test_1790800914000_quickstart",
"unsubscribe_detected_at": null,
"price": {
"amount": 9.99,
"currency": "USD"
}
}
}
}
}Receipts#
Purchases, restores and syncs. RevenueDot verifies them with the store.
Post a purchase or restore#
POST /v1/receipts · Auth: public app key or secret key
Every purchase, restore and syncPurchases() ends here. RevenueDot verifies the purchase with the store, saves it, records events and answers the updated customer info.
- App Store:
fetch_tokenis a StoreKit 2 signed transaction (JWS), a StoreKit 1 app receipt (base64) or an Xcode StoreKit test receipt. With the app's in-app purchase key, Apple's App Store Server API supplies the full history and renewal state. - Google Play:
fetch_tokenis the purchase token. RevenueDot checks it with the Play Developer API and acknowledges it. - Test Store:
fetch_tokenistest_<purchase time in ms>_<id>. Any such token is accepted.
4xx or 5xx matters. A 4xx tells the SDK the purchase can never be accepted, so it finishes the transaction. RevenueDot answers 5xx for its own and the store's temporary failures so the SDK keeps the purchase and retries.
With a secret key, send X-Platform so RevenueDot knows which app the receipt belongs to.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
X-Platform |
string | no | SDK platform (ios, android, macos, web ...). With a secret key it picks the project's app for that platform. |
X-Nonce |
string | no | Base64 nonce the SDK sends when entitlement verification is on; it is part of the signed message. |
X-Is-Sandbox |
string | no | true when the SDK knows the purchase is sandbox (used for StoreKit 1 receipts without an environment). |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | The customer posting the receipt. |
fetch_token |
string | no | Receipt, signed transaction, purchase token or Test Store token. |
app_transaction |
string | no | StoreKit 2 app transaction JWS (accepted; not required). |
transaction_id |
string | no | Store transaction id. |
product_id |
string | no | Product being bought. |
product_ids |
array of string | no | |
platform_product_ids |
array of object | no | |
platform_product_ids[].product_id |
string | no | |
platform_product_ids[].base_plan_id |
string | no | |
platform_product_ids[].offer_id |
string | no | |
price |
number | no | |
currency |
string | no | |
store_country |
string | no | |
normal_duration |
string | no | ISO 8601 period of the product. |
is_restore |
boolean | no | |
store_user_id |
string | no | |
presented_offering_identifier |
string | no | Offering the purchase was made from; it appears in webhooks. |
attributes |
object | no | Customer attributes to save with the purchase. |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/receipts" -H "Authorization: Bearer $PUBLIC_KEY" \
-H "Content-Type: application/json" -d '{"app_user_id":"user_1","fetch_token":"test_1790800914000_quickstart","product_id":"pro_monthly","price":9.99,"currency":"USD","presented_offering_identifier":"default"}'Responses
- 200: Updated customer info, plus
purchased_products. Returns ReceiptResponse. - 400: Bad request. For receipts, a 4xx tells the SDK the purchase can never be accepted, so it finishes the transaction. Returns V1Error.
- 401: Unknown API key. Returns V1Error.
- 500: Server error. The SDK keeps the purchase and retries. Returns V1Error.
- 503: The store could not be reached. Retry later. Returns V1Error.
Example 200 response:
{
"request_date": "2026-09-30T20:41:54Z",
"request_date_ms": 1790800914034,
"subscriber": {
"entitlements": {
"pro": {
"expires_date": "2026-10-30T20:41:54Z",
"grace_period_expires_date": null,
"product_identifier": "pro_monthly",
"purchase_date": "2026-09-30T20:41:54Z"
}
},
"first_seen": "2026-09-30T20:41:54Z",
"last_seen": "2026-09-30T20:41:54Z",
"management_url": null,
"non_subscriptions": {},
"original_app_user_id": "user_1",
"original_application_version": null,
"original_purchase_date": "2026-09-30T20:41:54Z",
"other_purchases": {},
"subscriptions": {
"pro_monthly": {
"auto_resume_date": null,
"billing_issues_detected_at": null,
"display_name": null,
"expires_date": "2026-10-30T20:41:54Z",
"grace_period_expires_date": null,
"is_sandbox": true,
"management_url": null,
"original_purchase_date": "2026-09-30T20:41:54Z",
"ownership_type": "PURCHASED",
"period_type": "normal",
"purchase_date": "2026-09-30T20:41:54Z",
"refunded_at": null,
"store": "test_store",
"store_transaction_id": "test_1790800914000_quickstart",
"unsubscribe_detected_at": null,
"price": {
"amount": 9.99,
"currency": "USD"
}
}
}
},
"purchased_products": {
"pro_monthly": {
"should_consume": false
}
}
}Offerings (SDK)#
What the paywall shows.
Get offerings#
GET /v1/subscribers/{app_user_id}/offerings · Auth: public app key or secret key
What Purchases.getOfferings() calls. Lists active offerings with the packages whose product belongs to the calling app. current_offering_id is the customer's override when one is set.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | App user id, URL-encoded (anonymous ids look like $RCAnonymousID:...). |
Example request
curl -s "$REVENUEDOT_URL/v1/subscribers/user_1/offerings" -H "Authorization: Bearer $PUBLIC_KEY"Responses
Example 200 response:
{
"current_offering_id": "default",
"offerings": [
{
"description": "Standard plans",
"identifier": "default",
"metadata": null,
"packages": [
{
"identifier": "$rc_monthly",
"platform_product_identifier": "pro_monthly"
},
{
"identifier": "$rc_annual",
"platform_product_identifier": "pro_annual"
},
{
"identifier": "$rc_lifetime",
"platform_product_identifier": "pro_lifetime"
}
]
}
]
}Get offerings without a user#
GET /v1/offerings · Auth: public app key or secret key
Same answer as the per-user call, without a customer override.
Example request
curl -s "$REVENUEDOT_URL/v1/offerings" -H "Authorization: Bearer $PUBLIC_KEY"Responses
Test Store product details#
GET /rcbilling/v1/subscribers/{app_user_id}/products · Auth: public app key
Product details the SDK needs for Test Store (and web) products, in the web billing products shape. Prices are 0 until the catalog stores Test Store prices.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | App user id, URL-encoded (anonymous ids look like $RCAnonymousID:...). |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
array of string | no | Product ids; repeat the parameter. None lists every product of the app. |
Example request
curl -s "$REVENUEDOT_URL/rcbilling/v1/subscribers/user_1/products" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 200: Product details.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"product_details": [
{
"identifier": "pro_monthly",
"product_type": "subscription",
"title": "Pro monthly",
"description": null,
"current_price": {
"amount": 0,
"amount_micros": 0,
"currency": "USD"
},
"normal_period_duration": "P1M",
"default_purchase_option_id": "base",
"default_subscription_option_id": "base",
"purchase_options": {
"base": {
"id": "base",
"price_id": "base",
"base": {
"period_duration": "P1M",
"cycle_count": 1,
"price": {
"amount": 0,
"amount_micros": 0,
"currency": "USD"
}
},
"base_price": null,
"trial": null,
"intro_price": null
}
},
"subscription_options": {
"base": {
"id": "base",
"price_id": "base",
"base": {
"period_duration": "P1M",
"cycle_count": 1,
"price": {
"amount": 0,
"amount_micros": 0,
"currency": "USD"
}
},
"base_price": null,
"trial": null,
"intro_price": null
}
}
}
]
}Identity#
logIn and aliases.
Log in (identify)#
POST /v1/subscribers/identify · Auth: public app key or secret key
What Purchases.logIn() calls. When new_app_user_id is new and the current id is anonymous with no other ids, the anonymous customer takes the new id (201).
When new_app_user_id exists, an anonymous-only current customer is merged into it (200). See Customers and app user IDs.
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | The current app user id. |
new_app_user_id |
string | yes | Your user id. |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/subscribers/identify" -H "Authorization: Bearer $PUBLIC_KEY" \
-H "Content-Type: application/json" -d '{"app_user_id":"$RCAnonymousID:abc123","new_app_user_id":"user_2"}'Responses
- 200: The user existed. Returns CustomerInfo.
- 201: The user is new. Returns CustomerInfo.
- 400: Bad request. For receipts, a 4xx tells the SDK the purchase can never be accepted, so it finishes the transaction. Returns V1Error.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"request_date": "2026-09-30T20:41:54Z",
"request_date_ms": 1790800914034,
"subscriber": {
"entitlements": {
"pro": {
"expires_date": "2026-10-30T20:41:54Z",
"grace_period_expires_date": null,
"product_identifier": "pro_monthly",
"purchase_date": "2026-09-30T20:41:54Z"
}
},
"first_seen": "2026-09-30T20:41:54Z",
"last_seen": "2026-09-30T20:41:54Z",
"management_url": null,
"non_subscriptions": {},
"original_app_user_id": "user_1",
"original_application_version": null,
"original_purchase_date": "2026-09-30T20:41:54Z",
"other_purchases": {},
"subscriptions": {
"pro_monthly": {
"auto_resume_date": null,
"billing_issues_detected_at": null,
"display_name": null,
"expires_date": "2026-10-30T20:41:54Z",
"grace_period_expires_date": null,
"is_sandbox": true,
"management_url": null,
"original_purchase_date": "2026-09-30T20:41:54Z",
"ownership_type": "PURCHASED",
"period_type": "normal",
"purchase_date": "2026-09-30T20:41:54Z",
"refunded_at": null,
"store": "test_store",
"store_transaction_id": "test_1790800914000_quickstart",
"unsubscribe_detected_at": null,
"price": {
"amount": 9.99,
"currency": "USD"
}
}
}
}
}Alias two app user ids#
POST /v1/subscribers/{app_user_id}/alias · Auth: public app key or secret key
Links new_app_user_id to the customer with the same merge rules as log in. The Android SDK uses it for Block Store recovery.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | App user id, URL-encoded (anonymous ids look like $RCAnonymousID:...). |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
new_app_user_id |
string | yes |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/subscribers/user_1/alias" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 200: Accepted.
- 400: Bad request. For receipts, a 4xx tells the SDK the purchase can never be accepted, so it finishes the transaction. Returns V1Error.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{}Attributes#
Customer attributes such as $email.
Set customer attributes#
POST /v1/subscribers/{app_user_id}/attributes · Auth: public app key or secret key
Saves attributes such as $email, $displayName or your own keys. A null value deletes the attribute. An invalid $email is refused with 7263; the other attributes are saved. collectDeviceIdentifiers() sends $ip and $deviceVersion as "true": RevenueDot stores the request's IP address and the device and OS from the SDK's headers instead.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | App user id, URL-encoded (anonymous ids look like $RCAnonymousID:...). |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
attributes |
object | yes |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/subscribers/user_1/attributes" -H "Authorization: Bearer $PUBLIC_KEY" \
-H "Content-Type: application/json" -d '{"attributes":{"$email":{"value":"ana@example.com","updated_at_ms":1790800914000}}}'Responses
- 200: Saved.
- 400: Some attributes were not saved. Returns V1Error.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{}SDK support#
Endpoints the SDK calls for features RevenueDot answers minimally, so the SDK keeps working.
Intro offer eligibility (StoreKit 1)#
POST /v1/subscribers/{app_user_id}/intro_eligibility · Auth: public app key or secret key
Answers null (unknown) for every product, so the SDK decides eligibility on the device.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | App user id, URL-encoded (anonymous ids look like $RCAnonymousID:...). |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
product_identifiers |
array of string | no |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/subscribers/user_1/intro_eligibility" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 200: Eligibility per product.
Example 200 response:
{
"pro_monthly": null
}Sign a promotional offer (iOS)#
POST /v1/offers · Auth: public app key or secret key
What Purchases.promotionalOffer(forProductDiscount:product:) calls. RevenueDot signs each offer with the App Store app's In-App Purchase key (key_id, issuer_id and private_key in the app's credentials) the way Apple verifies it: ECDSA P-256 with SHA-256 over the bundle id, key id, product id, offer id, app account token, nonce and timestamp, DER-encoded and base64 (Apple's format).
The app account token matches what the SDK puts on the payment: with StoreKit 2 the lowercase app user id when it is a UUID and empty otherwise; with StoreKit 1 the app user id.
Without an In-App Purchase key the answer is 400 with code 7234, which the SDK reports as invalidAppleSubscriptionKeyError for that offer only.
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | |
fetch_token |
string | no | The receipt or signed transaction; not needed for signing. |
generate_offers |
array of object | yes | |
generate_offers[].offer_id |
string | yes | Promotional offer id from App Store Connect. |
generate_offers[].product_id |
string | yes |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/offers" -H "Authorization: Bearer $PUBLIC_KEY" \
-H "Content-Type: application/json" -d '{"app_user_id":"user_1","fetch_token":"…","generate_offers":[{"offer_id":"winback_50","product_id":"pro_monthly"}]}'Responses
- 200: One signature per offer.
- 400: No In-App Purchase key, or no offers. Returns V1Error.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"offers": [
{
"key_id": "2X9R4HXF34",
"offer_id": "winback_50",
"product_id": "pro_monthly",
"signature_data": {
"nonce": "0f3c2a8e-5d7b-4d7e-9a53-3b8f2c1e6d40",
"signature": "MEUCIQDD…",
"timestamp": 1790800914034
}
}
]
}Attribution data (deprecated iOS call)#
POST /v1/subscribers/{app_user_id}/attribution · Auth: public app key or secret key
What the deprecated Purchases.addAttributionData calls. The advertising identifiers in data (rc_idfa, rc_idfv, rc_gps_adid, rc_ip_address) become $idfa, $idfv, $gpsAdId and $ip.
For Apple Search Ads (network 0) with iad-attribution true, the iAd fields become $mediaSource ("Apple Search Ads"), $campaign, $adGroup, $keyword, $creative and the $appleAds* ids.
Attribution is write-once: a campaign attribute the customer already has is kept.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | App user id, URL-encoded (anonymous ids look like $RCAnonymousID:...). |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
network |
integer | yes | The SDK's AttributionNetwork: 0 Apple Search Ads. |
data |
object | yes |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/subscribers/user_1/attribution" -H "Authorization: Bearer $PUBLIC_KEY" \
-H "Content-Type: application/json" -d '{"network":0,"data":{"rc_idfv":"4CEE1BEE-3C19-4591-9E34-1AD968D7B609","Version3.1":{"iad-attribution":"true","iad-campaign-name":"Spring","iad-keyword":"scanner"}}}'Responses
- 200: Stored.
- 400: Bad request. For receipts, a 4xx tells the SDK the purchase can never be accepted, so it finishes the transaction. Returns V1Error.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{}Apple AdServices token#
POST /v1/subscribers/{app_user_id}/adservices_attribution · Auth: public app key or secret key
What enableAdServicesAttributionTokenCollection() sends once per install (the same token can also arrive as aad_attribution_token on a receipt).
After answering, RevenueDot looks the token up with Apple's attribution API, retrying a 404 or 5xx 3 times 5 seconds apart, and stores an attributed install as $mediaSource ("Apple Search Ads"), $campaign, $adGroup, $keyword, $ad, $appleAdsCampaignId, $appleAdsAdGroupId, $appleAdsKeywordId, $appleAdsAdId, $appleAdsOrgId, $appleAdsCountryOrRegion, $claimType and $conversionType. Apple returns ids, not names. They show on the customer page and in every webhook's subscriber_attributes.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | App user id, URL-encoded (anonymous ids look like $RCAnonymousID:...). |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
aad_attribution_token |
string | yes | The token from AAAttribution.attributionToken(). |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/subscribers/user_1/adservices_attribution" -H "Authorization: Bearer $PUBLIC_KEY" \
-H "Content-Type: application/json" -d '{"aad_attribution_token":"wD3Ma…"}'Responses
- 200: Accepted; the lookup runs after the answer.
- 400: No token. Returns V1Error.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{}SDK health report availability#
GET /v1/subscribers/{app_user_id}/health_report_availability · Auth: none
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | App user id, URL-encoded (anonymous ids look like $RCAnonymousID:...). |
Example request
curl -s "$REVENUEDOT_URL/v1/subscribers/user_1/health_report_availability"Responses
- 200: No report logs.
Example 200 response:
{
"report_logs": false
}SDK health report#
GET /v1/subscribers/{app_user_id}/health_report · Auth: public app key
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | App user id, URL-encoded (anonymous ids look like $RCAnonymousID:...). |
Example request
curl -s "$REVENUEDOT_URL/v1/subscribers/user_1/health_report" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 200: Always passed.
Example 200 response:
{
"status": "passed",
"project_id": "proj18pzzkao",
"app_id": "appvnrm0a5h",
"checks": []
}Product to entitlement mapping (offline entitlements)#
GET /v1/product_entitlement_mapping · Auth: public app key or secret key
Lets the SDK grant entitlements while the server cannot be reached.
Example request
curl -s "$REVENUEDOT_URL/v1/product_entitlement_mapping" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 200: The mapping.
Example 200 response:
{
"product_entitlement_mapping": {
"pro_monthly": {
"product_identifier": "pro_monthly",
"entitlements": [
"pro"
]
}
}
}Customer Center configuration (not built)#
GET /v1/customercenter/{app_user_id} · Auth: public app key
Always 404 with code 7259: the SDK returns an error and the Customer Center screen shows its error state. Customer Center configuration is planned for Tier 2.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | App user id, URL-encoded (anonymous ids look like $RCAnonymousID:...). |
Example request
curl -s "$REVENUEDOT_URL/v1/customercenter/user_1" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 404: Not configured. Returns V1Error.
Customer Center support ticket (not built)#
POST /v1/customercenter/support/create-ticket · Auth: public app key
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/customercenter/support/create-ticket" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 200: Not sent.
Example 200 response:
{
"sent": false
}Virtual currency balances (not built)#
GET /v1/subscribers/{app_user_id}/virtual_currencies · Auth: public app key
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | App user id, URL-encoded (anonymous ids look like $RCAnonymousID:...). |
Example request
curl -s "$REVENUEDOT_URL/v1/subscribers/user_1/virtual_currencies" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 200: Empty balances.
Example 200 response:
{
"virtual_currencies": {}
}Redeem a web purchase (not available)#
POST /v1/subscribers/redeem_purchase · Auth: public app key
What Purchases.redeemWebPurchase() calls with the redemption_token from a redemption deep link. RevenueDot takes no web payments, so no token is valid: 400 with code 7849, which the SDKs return as the invalidToken result.
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/subscribers/redeem_purchase" -H "Authorization: Bearer $PUBLIC_KEY"Responses
Register an Apple external purchase token (iOS)#
POST /v1/external_purchase_tokens · Auth: public app key
Part of Apple's external purchase and link-out flows, before a web checkout. The token is acknowledged with an id, which is all the SDK reads; the web checkout that follows is not available (see /rcbilling/v1/hosted-checkout).
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | |
purchase_type |
IN_APP, LINK_OUT |
yes | |
token |
string | no | Apple's external purchase token, when there is one. |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/external_purchase_tokens" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 200: Registered.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"id": "ept3b1f0c9e2d8a4f6b9c7e5d3a1b2c4d6e",
"purchase_type": "LINK_OUT",
"is_sandbox": true,
"token_source": "APPLE_SDK"
}Rewarded ad verification (not available)#
GET /v1/subscribers/{app_user_id}/ads/reward_verifications/{client_transaction_id} · Auth: public app key
What pollRewardVerification polls. There is no server-side ad verification, so the answer is always the final failed, and the SDK stops after one request.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | App user id, URL-encoded (anonymous ids look like $RCAnonymousID:...). |
client_transaction_id |
string | yes | From generateRewardVerificationToken. |
Example request
curl -s "$REVENUEDOT_URL/v1/subscribers/user_1/ads/reward_verifications/$CLIENT_TRANSACTION_ID" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 200: Failed.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"status": "failed",
"reward": null,
"failure_reason": "not_supported",
"message": "Server-side reward verification is not available on RevenueDot."
}Amazon receipt details (not supported)#
GET /v1/receipts/amazon/{store_user_id}/{receipt_id} · Auth: public app key
The Android SDK asks for it on Amazon subscription purchases. Amazon Appstore purchases are not supported: 400 with code 7662, the same answer as a receipt post for an Amazon app, which leaves the purchase unconsumed.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
store_user_id |
string | yes | |
receipt_id |
string | yes | Not encoded by the SDK; may contain /. |
Example request
curl -s "$REVENUEDOT_URL/v1/receipts/amazon/$STORE_USER_ID/$RECEIPT_ID" -H "Authorization: Bearer $PUBLIC_KEY"Responses
Paywall workflows (web SDK)#
GET /v1/subscribers/{app_user_id}/workflows · Auth: public app key
What purchases-js presentPaywall asks first. There are no workflows, so the SDK uses the offering's own paywall.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | App user id, URL-encoded (anonymous ids look like $RCAnonymousID:...). |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
type |
string | no | paywall. |
Example request
curl -s "$REVENUEDOT_URL/v1/subscribers/user_1/workflows" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 200: No workflows.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"workflows": [],
"ui_config": {}
}One paywall workflow (web SDK)#
GET /v1/subscribers/{app_user_id}/workflows/{workflow_id} · Auth: public app key
Never called, because the workflow list is empty.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | App user id, URL-encoded (anonymous ids look like $RCAnonymousID:...). |
workflow_id |
string | yes |
Example request
curl -s "$REVENUEDOT_URL/v1/subscribers/user_1/workflows/$WORKFLOW_ID" -H "Authorization: Bearer $PUBLIC_KEY"Responses
Restore eligibility (StoreKit 2)#
POST /v1/subscribers/{app_user_id}/restore/eligibility · Auth: public app key
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | App user id, URL-encoded (anonymous ids look like $RCAnonymousID:...). |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/subscribers/user_1/restore/eligibility" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 200: Always allowed.
Example 200 response:
{
"is_purchase_allowed_by_restore_behavior": true
}Remote config (none yet)#
GET /v1/config/{domain} · Auth: public app key
Answers 204 (no config). getOfferings waits on this call.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
domain |
string | yes | Config domain the SDK asks for (for example app). |
Example request
curl -s "$REVENUEDOT_URL/v1/config/app" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 204: No config.
Remote config (none yet)#
POST /v1/config/{domain} · Auth: public app key
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
domain |
string | yes | Config domain the SDK asks for (for example app). |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/config/app" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 204: No config.
SDK paywall and feature events (accepted, not stored)#
POST /v1/events · Auth: public app key
Accepted so the SDK does not resend them forever.
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/events" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 200: Accepted.
Example 200 response:
{}SDK diagnostics (accepted, not stored)#
POST /v1/diagnostics · Auth: public app key
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/diagnostics" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 200: Accepted.
Example 200 response:
{}Web Billing#
Web checkout calls from the iOS SDK and purchases-js. RevenueDot takes no web payments, so a checkout answers an error the SDK shows as a failed purchase.
Web offering products#
GET /rcbilling/v1/subscribers/{app_user_id}/offering_products · Auth: public app key
Defined in the iOS SDK with no caller. There are no web offerings.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | App user id, URL-encoded (anonymous ids look like $RCAnonymousID:...). |
Example request
curl -s "$REVENUEDOT_URL/rcbilling/v1/subscribers/user_1/offering_products" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 200: No web offerings.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"offerings": {}
}Start a hosted web checkout (not available)#
POST /rcbilling/v1/hosted-checkout · Auth: public app key
The iOS SDK's paywall web checkout. RevenueDot takes no payments: 400 with code 7000, and the SDK returns failed for the checkout without retrying.
Example request
curl -s -X POST "$REVENUEDOT_URL/rcbilling/v1/hosted-checkout" -H "Authorization: Bearer $PUBLIC_KEY"Responses
Web Billing purchase (not available)#
POST /rcbilling/v1/purchase · Auth: public app key
Defined in purchases-js with no caller. 400 with code 7000.
Example request
curl -s -X POST "$REVENUEDOT_URL/rcbilling/v1/purchase" -H "Authorization: Bearer $PUBLIC_KEY"Responses
Prepare a Web Billing checkout (not available)#
POST /rcbilling/v1/checkout/prepare · Auth: public app key
purchases-js with an rcb_ key. 400 with code 7000: the purchase fails with an error in the SDK's purchase screen.
Example request
curl -s -X POST "$REVENUEDOT_URL/rcbilling/v1/checkout/prepare" -H "Authorization: Bearer $PUBLIC_KEY"Responses
Start a Web Billing checkout (not available)#
POST /rcbilling/v1/checkout/start · Auth: public app key
Example request
curl -s -X POST "$REVENUEDOT_URL/rcbilling/v1/checkout/start" -H "Authorization: Bearer $PUBLIC_KEY"Responses
Web Billing checkout status#
GET /rcbilling/v1/checkout/{operation_session_id} · Auth: public app key
No checkout session exists: 400 with code 7877.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
operation_session_id |
string | yes |
Example request
curl -s "$REVENUEDOT_URL/rcbilling/v1/checkout/$OPERATION_SESSION_ID" -H "Authorization: Bearer $PUBLIC_KEY"Responses
Refresh Web Billing checkout pricing#
PATCH /rcbilling/v1/checkout/{operation_session_id} · Auth: public app key
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
operation_session_id |
string | yes |
Example request
curl -s -X PATCH "$REVENUEDOT_URL/rcbilling/v1/checkout/$OPERATION_SESSION_ID" -H "Authorization: Bearer $PUBLIC_KEY"Responses
Complete a Web Billing checkout#
POST /rcbilling/v1/checkout/{operation_session_id}/complete · Auth: public app key
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
operation_session_id |
string | yes |
Example request
curl -s -X POST "$REVENUEDOT_URL/rcbilling/v1/checkout/$OPERATION_SESSION_ID/complete" -H "Authorization: Bearer $PUBLIC_KEY"Responses
Web checkout branding#
GET /rcbilling/v1/branding · Auth: public app key
purchases-js with an rcb_ key loads it before a checkout: the app's name and the SDK's default look.
Example request
curl -s "$REVENUEDOT_URL/rcbilling/v1/branding" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 200: Branding.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"id": "appvnrm0a5h",
"app_name": "Scanner Web",
"app_icon": null,
"app_icon_webp": null,
"app_wordmark": null,
"app_wordmark_webp": null,
"appearance": null,
"support_email": null,
"gateway_tax_collection_enabled": false,
"brand_font_config": null
}Store notifications#
Where App Store Connect and Google Pub/Sub send server notifications.
App Store Server Notifications v2#
POST /v1/notifications/apple/{app_id} · Auth: none
Set this URL (shown on the app's page in the dashboard) as the Production and Sandbox Server URL in App Store Connect, with version 2 notifications.
RevenueDot verifies Apple's signature and the bundle id, stores the raw body, copies it to notification_forward_url when set, and applies it.
- 200: handled, including notifications about purchases this server has not seen (stored; applied only with
track_new_purchases). - 400: the payload cannot be verified or belongs to another app. App Store Connect shows it as failed.
- 404: no App Store app with this id.
- 500: RevenueDot failed; Apple retries.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_id |
string | yes | App id (app...). |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
signedPayload |
string | yes | Apple's signed JWS. |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/notifications/apple/$APP_ID"Responses
- 200: Handled.
- 400: Unverifiable.
- 404: Unknown app.
- 500: Failed; Apple retries.
Example 200 response:
{
"ok": true
}Google Play real-time developer notifications (Pub/Sub push)#
POST /v1/notifications/google/{app_id} · Auth: Pub/Sub push token (optional)
Set this URL as the endpoint of a Pub/Sub push subscription on the topic Google Play publishes to.
When the app's pubsub_audience credential is set, the push must carry a Google-signed OIDC token for that audience (and for pubsub_service_account when set).
Each message is stored once (by message id), forwarded when notification_forward_url is set, and applied by reading the purchase from the Play Developer API.
- 200: handled, a duplicate, ignored (another package, not a developer notification) or an invalid token that can never succeed.
- 400: not a Pub/Sub push body. 401: bad push token. 404: no Google Play app with this id.
- 500 or 503: a temporary failure; Pub/Sub redelivers.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_id |
string | yes | App id (app...). |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
message |
object | yes | |
message.data |
string | no | Base64 JSON developer notification. |
message.messageId |
string | no | |
message.publishTime |
string | no | |
subscription |
string | no |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/notifications/google/$APP_ID"Responses
- 200: Handled.
- 400: Not a push body. Returns V1Error.
- 401: Bad push token. Returns V1Error.
- 404: Unknown app. Returns V1Error.
- 500: Temporary failure; Pub/Sub retries. Returns V1Error.
- 503: Google's signing keys could not be loaded. Returns V1Error.
Example 200 response:
{
"status": "processed"
}Response signing#
Trusted Entitlements: the public key responses are signed with.
Public key for response signatures#
GET /.well-known/revenuedot-signing-key · Auth: none
The Ed25519 root public key this server signs SDK responses with (Trusted Entitlements). Pin this key in SDK builds that verify responses.
The server signs only when REVENUEDOT_SIGNING_KEY is set; otherwise this answers 404. See Trusted Entitlements.
Example request
curl -s "$REVENUEDOT_URL/.well-known/revenuedot-signing-key"Responses
- 200: The key.
- 404: Signing is off. Returns V1Error.
Example 200 response:
{
"algorithm": "Ed25519",
"public_key": "ZzwPxGlon0E8ErpDh9QAH0Jh6+E6D6qufvTSetXZY9Y=",
"encoding": "base64",
"header": "X-Signature",
"docs": "https://revenuedot.app/docs"
}Objects#
The shapes the operations above send and return.
CustomerInfo#
| Field | Type | Required | Description |
|---|---|---|---|
request_date |
string | yes | Server time of the response. ISO 8601 in UTC, whole seconds. |
request_date_ms |
integer | yes | Server time of the response. Epoch milliseconds. |
subscriber |
object | yes | |
subscriber.entitlements |
object | yes | Entitlements the customer has now or had, keyed by lookup key. Check expires_date (or use the SDK's isActive). |
subscriber.first_seen |
string | yes | When the customer was first seen. ISO 8601 in UTC, whole seconds. |
subscriber.last_seen |
string | yes | When the customer was last seen. ISO 8601 in UTC, whole seconds. |
subscriber.management_url |
string or null | no | Always null today. |
subscriber.non_subscriptions |
object | yes | One-time purchases by product id, oldest first. |
subscriber.original_app_user_id |
string | yes | The customer's first app user id. |
subscriber.original_application_version |
string or null | no | Always null today. |
subscriber.original_purchase_date |
string or null | no | Earliest purchase. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
subscriber.other_purchases |
object | no | Always empty. |
subscriber.subscriber_attributes |
object | no | Only in answers to secret-key requests. |
subscriber.subscriptions |
object | yes | Latest subscription per product id. |
EntitlementInfo#
| Field | Type | Required | Description |
|---|---|---|---|
expires_date |
string or null | yes | When access ends. Null for lifetime access. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
grace_period_expires_date |
string or null | yes | End of the billing grace period, when the store grants one. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
product_identifier |
string | yes | Store product id that gives this access. |
product_plan_identifier |
string | no | Google Play base plan id, only when there is one. |
purchase_date |
string | yes | Start of the current period. ISO 8601 in UTC, whole seconds. |
NonSubscriptionInfo#
| Field | Type | Required | Description |
|---|---|---|---|
display_name |
string or null | no | |
id |
string | yes | RevenueDot purchase id. |
is_sandbox |
boolean | yes | |
original_purchase_date |
string or null | no | Purchase time. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
purchase_date |
string or null | yes | Purchase time. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
store |
string | yes | |
store_transaction_id |
string | yes | Store transaction id. |
price |
object | no | |
price.amount |
number | yes | Price in the purchase currency. |
price.currency |
string | yes | ISO 4217 currency code. |
Offerings#
| Field | Type | Required | Description |
|---|---|---|---|
current_offering_id |
string or null | yes | Lookup key of the current offering, or the customer's override. |
offerings |
array of object | yes | |
offerings[].description |
string | yes | Offering display name. |
offerings[].identifier |
string | yes | Offering lookup key. |
offerings[].metadata |
object or null | yes | Your JSON metadata. |
offerings[].packages |
array of object | yes | |
offerings[].packages[].identifier |
string | yes | Package lookup key, for example $rc_monthly. |
offerings[].packages[].platform_product_identifier |
string | yes | Store product id for the calling app. |
offerings[].packages[].platform_product_plan_identifier |
string | no | Google Play base plan id, when the product is subscription:base-plan. |
ReceiptResponse#
| Field | Type | Required | Description |
|---|---|---|---|
request_date |
string | yes | Server time of the response. ISO 8601 in UTC, whole seconds. |
request_date_ms |
integer | yes | Server time of the response. Epoch milliseconds. |
subscriber |
object | yes | |
subscriber.entitlements |
object | yes | Entitlements the customer has now or had, keyed by lookup key. Check expires_date (or use the SDK's isActive). |
subscriber.first_seen |
string | yes | When the customer was first seen. ISO 8601 in UTC, whole seconds. |
subscriber.last_seen |
string | yes | When the customer was last seen. ISO 8601 in UTC, whole seconds. |
subscriber.management_url |
string or null | no | Always null today. |
subscriber.non_subscriptions |
object | yes | One-time purchases by product id, oldest first. |
subscriber.original_app_user_id |
string | yes | The customer's first app user id. |
subscriber.original_application_version |
string or null | no | Always null today. |
subscriber.original_purchase_date |
string or null | no | Earliest purchase. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
subscriber.other_purchases |
object | no | Always empty. |
subscriber.subscriber_attributes |
object | no | Only in answers to secret-key requests. |
subscriber.subscriptions |
object | yes | Latest subscription per product id. |
purchased_products |
object | no | One entry per purchase the receipt contained. |
SubscriptionInfo#
| Field | Type | Required | Description |
|---|---|---|---|
auto_resume_date |
string or null | no | When a paused Google Play subscription resumes. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
billing_issues_detected_at |
string or null | no | When the latest renewal failed. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
display_name |
string or null | no | Product display name. |
expires_date |
string or null | yes | End of the current period. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
grace_period_expires_date |
string or null | no | End of the billing grace period. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
is_sandbox |
boolean | yes | True for sandbox and Test Store purchases. |
management_url |
string or null | no | Always null today. |
original_purchase_date |
string or null | no | Start of the subscription. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
ownership_type |
PURCHASED, FAMILY_SHARED |
no | |
period_type |
normal, trial, intro, promotional, prepaid |
yes | |
purchase_date |
string or null | yes | Start of the current period. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
refunded_at |
string or null | no | When the store refunded it. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
store |
app_store, mac_app_store, play_store, amazon, stripe, rc_billing, promotional, test_store, paddle, roku, external |
yes | |
store_transaction_id |
string or null | no | Latest store transaction id (Apple), order id (Google) or Test Store token. |
unsubscribe_detected_at |
string or null | no | When auto-renew was turned off. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
product_plan_identifier |
string | no | Google Play base plan id, only when there is one. |
price |
object | no | |
price.amount |
number | yes | Price in the purchase currency. |
price.currency |
string | yes | ISO 4217 currency code. |
V1Error#
| Field | Type | Required | Description |
|---|---|---|---|
code |
integer | yes | RevenueCat-compatible backend error code. See the error table. |
message |
string | yes | What went wrong. |
attribute_errors |
array of object | no | Only for 7263. |
attribute_errors[].key_name |
string | no | |
attribute_errors[].message |
string | no |