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 (74)#
- 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: Read customer 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, Customer Center support ticket, Virtual currency balances, Redeem a web purchase, Register an Apple external purchase token (iOS), Rewarded ad verification status, Amazon receipt details, Paywall workflows (web SDK), One paywall workflow (web SDK), Restore eligibility (StoreKit 2), Remote config fallback (none), Remote config: paywalls and UI settings, Download a remote-config blob, SDK paywall, Customer Center and ad events, SDK diagnostics (accepted, not stored)
- Web Billing: Web offering products, Start a hosted web checkout, 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
- Subscriber tokens: Get customer info (subscriber token), Get offerings (subscriber token), Intro offer eligibility (StoreKit 1) (subscriber token), Attribution data (deprecated iOS call) (subscriber token), Read customer attributes (subscriber token), Set customer attributes (subscriber token), Apple AdServices token (subscriber token), SDK health report (subscriber token), Customer Center configuration (subscriber token), Customer Center support ticket (subscriber token), Virtual currency balances (subscriber token), Restore eligibility (StoreKit 2) (subscriber token), Rewarded ad verification status (subscriber token), Web offering products (subscriber token), Test Store product details (subscriber token), Spend in-app currency as the subscriber
- Auth sign-in: Sign in with an identity provider's ID token, Refresh the access token, Sign out, Refresh the access token, Sign out, Public key of RevenueDot's ID and access tokens
- Store notifications: App Store Server Notifications v2, Google Play real-time developer notifications (Pub/Sub push), Amazon Appstore Real-time Notifications (SNS), Stripe webhooks, Paddle notifications, Roku Pay push notifications, Galaxy Store server notifications, Stripe Connect webhooks (RevenueDot's platform)
- 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. - Amazon Appstore:
fetch_tokenis the receipt id andstore_user_idthe Amazon user id (X-Platform: amazon). RevenueDot checks both with Amazon's Receipt Verification Service. - Stripe: from your backend, with
X-Platform: stripeand the Stripe app's public key (strp_):fetch_tokenis a subscription id (sub_…) or a Checkout Session id (cs_…). RevenueDot reads it from Stripe with the app's restricted key. An unpaid first invoice or an open session answers 503, so post it again later. - Paddle: from your backend, with
X-Platform: paddleand the Paddle app's public key (pdl_):fetch_tokenis a subscription id (sub_…) or a transaction id (txn_…). RevenueDot reads it from Paddle with the app's API key; a checkout that is not finished answers 503. Products are Paddle price ids (pri_…). - Roku: the Roku SDK posts
fetch_token= the Roku transaction id (X-Platform: roku). RevenueDot validates it with Roku Pay's web services, which also supply the price and currency;X-Is-Sandbox: true(a sideloaded channel) records sandbox. - Galaxy Store: the Android SDK built with
purchases-store-galaxypostsfetch_token= Samsung's purchase id withX-Platform: androidand the Galaxy app'sgalx_key. RevenueDot reads the receipt from Samsung, and a subscription's state with the app's service account;purchased_productstells the SDK which items to consume. - 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. A new app user id makes the customer here, as GET /v1/subscribers/{app_user_id} does (the SDK sends both at once on a first launch), so the first answer already includes an experiment variant. current_offering_id is the customer's override when one is set. Otherwise RevenueDot resolves, in order: the experiment the customer is in (running or paused), a running experiment that enrolls them now (by priority), the first live targeting rule that matches, the project's current offering. A variant's placements overlay the rule's in placements.offering_ids_by_placement. See Experiments.
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.
Read customer attributes#
GET /v1/subscribers/{app_user_id}/attributes · Auth: secret key · RevenueDot extension
The attributes the SDK or your server set, with when each was set. Needs a subscriber access token for this app user id (the app reads its own attributes after an Auth sign-in) or a secret key. An app's public key alone answers 401 with code 7224: attributes can hold an email address or a phone number. An unknown app user id answers an empty object.
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/attributes" -H "Authorization: Bearer $SECRET_KEY"Responses
- 200: The attributes.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"subscriber_attributes": {
"$displayName": {
"value": "Ana",
"updated_at_ms": 1790800914000
},
"plan_goal": {
"value": "sleep",
"updated_at_ms": 1790800914000
}
}
}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, except attribution attributes ($mediaSource, $campaign, $adGroup, $ad, $keyword, $creative, $appleAds*, $claimType, $conversionType): they are write-once, so a stored value is kept and a different value or a null is ignored. Correct them with the REST API v2. 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 on the device while the server answers 5xx (offline entitlements). The SDK fetches it every 25 hours.
A public key gets only its own app's products, keyed the way that SDK looks them up: App Store products by product id (product:monthly for a monthly billing plan), Google Play products by subscription:base_plan and by the bare subscription id, which carries every base plan's entitlements because Android purchases do not name their base plan. Consumables are left out. A secret key gets the whole project. See Offline entitlements.
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#
GET /v1/customercenter/{app_user_id} · Auth: public app key
The project's Customer Center configuration: appearance, the management and no-active screens with their help paths, localized strings and the support email. A built-in default merged with what the project stored through POST /v2/projects/{project_id}/customer_center_config.
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
- 200: The configuration.
Example 200 response:
{
"customer_center": {
"screens": {
"MANAGEMENT": {
"type": "MANAGEMENT",
"title": "Manage subscription",
"paths": []
}
},
"support": {
"email": "support@example.com"
}
}
}Customer Center support ticket#
POST /v1/customercenter/support/create-ticket · Auth: public app key
Stores the ticket, emails the Customer Center support address (Reply-To the customer) and lists it under Lifecycle > Support. sent is false when ticket creation is off (support.support_tickets.allow_creation), a field is missing or invalid, or the customer sent more than 5 tickets in an hour; the SDK then offers its email link.
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | |
customer_email |
string | yes | |
issue_description |
string | yes |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/customercenter/support/create-ticket" -H "Authorization: Bearer $PUBLIC_KEY" \
-H "Content-Type: application/json" -d '{"app_user_id":"user_1234","customer_email":"wren@example.com","issue_description":"My scans are not syncing."}'Responses
- 200: Whether the ticket was taken.
Example 200 response:
{
"sent": true
}Virtual currency balances#
GET /v1/subscribers/{app_user_id}/virtual_currencies · Auth: public app key
The customer's in-app currency balances by code. A customer the server has not seen has 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/virtual_currencies" -H "Authorization: Bearer $PUBLIC_KEY"Responses
- 200: Balances.
Example 200 response:
{
"virtual_currencies": {
"GLD": {
"balance": 700,
"name": "Gold",
"code": "GLD",
"description": null
}
}
}Redeem a web purchase#
POST /v1/subscribers/redeem_purchase · Auth: public app key
What Purchases.redeemWebPurchase() calls with the redemption_token from a redemption link (<scheme>://redeem_web_purchase?redemption_token=rdrt_...). The anonymous customer who paid on RevenueDot's web checkout is merged into this app user id, like logIn: their purchases and attributes move over. The answer is the customer info, and PURCHASE_REDEEMED is sent once. See Redemption links.
- 200: redeemed, or already redeemed by this customer (a retry is safe).
- 400 · 7849: the token is unknown, malformed or from another project. The SDK returns
invalidToken. - 400 · 7852: another customer redeemed it. The SDK returns
purchaseBelongsToOtherUser. - 400 · 7853: the link expired (after the web config's
redemption_link_hours, default 24).purchase_redemption_error_info.obfuscated_emailsays where a new link goes; it is emailed at most once an hour. The SDK returnsexpiredwith that email.
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. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | The app user id that gets the purchase. |
redemption_token |
string | yes | From the redemption link. |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/subscribers/redeem_purchase" -H "Authorization: Bearer $PUBLIC_KEY" \
-H "Content-Type: application/json" -d '{"app_user_id":"user_1","redemption_token":"rdrt_q8Xc2kP0vN5mZ7tL1sW9hB3yR6dF4gJ2aE0uI8oK1nM"}'Responses
- 200: Customer info with the web purchase. Returns CustomerInfo.
- 400: The token cannot be redeemed.
- 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"
}
}
}
}
}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 /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 status#
GET /v1/subscribers/{app_user_id}/ads/reward_verifications/{client_transaction_id} · Auth: public app key
What pollRewardVerification polls after the ad's reward callback fires (up to 10 times, about a second apart). The ad network's server-side callback (GET /v1/ads/admob/ssv) records the reward; the first matching reward rule grants it. See Ads: rewarded ads.
pending: no callback recorded yet for this id, or the grant is still being made. The SDK asks again.verified:rewardis the first granted reward andmore_rewardsthe rest.rewardis null when no rule matched: verified, nothing granted. The SDK then refreshes the customer's balances and customer info.failed:failure_reasonisuser_mismatch(the reward belongs to another customer),missing_user(the callback had no user id) orgrant_failed(the rule names a currency or entitlement that no longer exists).
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: The verification status.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"status": "verified",
"reward": {
"type": "virtual_currency",
"code": "GEMS",
"amount": 10
},
"more_rewards": []
}Amazon receipt details#
GET /v1/receipts/amazon/{store_user_id}/{receipt_id} · Auth: public app key
The Android SDK built for Amazon asks for it on subscription purchases and reads termSku, which it then posts as the product id. RevenueDot asks Amazon's Receipt Verification Service with the app's shared key and answers Amazon's receipt unchanged.
- 400 · 7103: Amazon does not know the receipt or the user. 400 · 7662: the key is not an Amazon app's.
- 500 · 7101: no shared key is saved, or Amazon rejected it. 503 · 7101: Amazon is unavailable; the SDK keeps the purchase unconsumed and retries. See Amazon Appstore setup.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
store_user_id |
string | yes | The Amazon user id. |
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
- 200: Amazon's receipt.
- 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:
{
"autoRenewing": true,
"betaProduct": false,
"cancelDate": null,
"cancelReason": null,
"countryCode": "US",
"freeTrialEndDate": null,
"gracePeriodEndDate": null,
"productId": "pro.subscription",
"productType": "SUBSCRIPTION",
"purchaseDate": 1790800914000,
"receiptId": "q1YqVrJSSs7P1UvMTazKz9PLTCwoTswtyEktM8jLz0kpLQ1JTSlFMsjILCoQ:3:11",
"renewalDate": 1793392914000,
"term": "1 Month",
"termSku": "pro.monthly",
"testTransaction": false
}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 fallback (none)#
GET /v1/config/{domain} · Auth: public app key
The SDK's JSON fallback host path, never used with a proxy URL. Answers 204.
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: paywalls and UI settings#
POST /v1/config/{domain} · Auth: public app key
How current SDKs (iOS 5.83 and later) load paywalls. The body is an RC Container (application/x-rc-format): an 8-byte header ("RC", version 1), then elements of
checksum (24 bytes, the first 24 bytes of SHA-256 of the payload), size (u32, little-endian), encoding (0, none) and 3 reserved bytes, then the payload padded to 8 bytes.
Element 0 is the configuration JSON with the topics sources, ui_config (app, localizations, variable_config, custom_variables) and workflows (one per
published paywall, keyed by workflow id, with offering_identifier). The other elements are the blobs those topics reference, inline. 204 when the manifest the SDK sent is current.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
domain |
string | yes | Config domain the SDK asks for (for example app). |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
manifest |
string | no | The manifest of the configuration the SDK holds. |
prefetched_blobs |
array of string | no | Blob refs the SDK already holds; they are not inlined again. |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/config/app" -H "Authorization: Bearer $PUBLIC_KEY" \
-H "Content-Type: application/json" -d '{"manifest":"v1.Qm9vdHN0cmFw","prefetched_blobs":[]}'Responses
- 200: RC Container.
- 204: The SDK's configuration is current.
Download a remote-config blob#
GET /blobs/{blob_ref} · Auth: none
Public and immutable. The SDK downloads a blob here when it was not inline in the container (the sources topic points here).
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
blob_ref |
string | yes | base64url of the first 24 bytes of SHA-256 of the blob. |
Example request
curl -s "$REVENUEDOT_URL/blobs/$BLOB_REF"Responses
- 200: The blob (JSON).
- 404: Unknown blob.
SDK paywall, Customer Center and ad events#
POST /v1/events · Auth: public app key
Stored for the paywall, ad and Customer Center charts and the Ads Overview (each SDK event id once). Ad events are the rc_ads_* types. A malformed batch is still answered 200 so the SDK does not resend it 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. The iOS SDK's paywall web checkout opens a Stripe Checkout on your own Stripe account (web billing). The purchases-js checkout for RevenueCat Billing (rcb_ keys, Stripe Elements inside the SDK) is not available and 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#
POST /rcbilling/v1/hosted-checkout · Auth: public app key
The iOS SDK's paywall web checkout. RevenueDot finds the package in the offering, picks the project's Stripe app that sells it as a web product, and creates a Stripe Checkout Session for this app user id with that app's key. The SDK opens checkout_url and closes it when the browser reaches success_url or cancel_url; the success page records the purchase, so the customer info has it at once. See Sell on the web with Stripe.
- 400 · 7000: the offering or package is unknown, or the package has no web product.
- 503 · 7101: Stripe is unavailable or refused the key.
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | |
package_id |
string | yes | Package lookup key, such as $rc_monthly. |
presented_offering_identifier |
string | yes | The offering's lookup key. |
Example request
curl -s -X POST "$REVENUEDOT_URL/rcbilling/v1/hosted-checkout" -H "Authorization: Bearer $PUBLIC_KEY" \
-H "Content-Type: application/json" -d '{"app_user_id":"user_1","package_id":"$rc_monthly","presented_offering_identifier":"web"}'Responses
- 200: The checkout.
- 400: No web product for this package. Returns V1Error.
- 401: Unknown API key. Returns V1Error.
- 503: The store could not be reached. Retry later. Returns V1Error.
Example 200 response:
{
"operation_session_id": "wco_8k2m9q4x7a1b3c5d",
"checkout_url": "https://checkout.stripe.com/c/pay/cs_test_a1B2c3D4",
"success_url": "https://api.revenuedot.app/pay/scanner/_/success",
"cancel_url": "https://api.revenuedot.app/pay/scanner/_/cancel"
}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
}Subscriber tokens#
The SDK endpoints for one customer, authorized by a subscriber access token from POST /v2/projects/{project_id}/apps/{app_id}/authenticate instead of the app key. The RevenueCat SDKs call these paths in their internal token mode.
Get customer info (subscriber token)#
GET /v1/customer · Auth: none
The subscriber-token form of GET /v1/subscribers/{app_user_id}: same body and answer, for the app user id of the token. An app key, an expired token or another user's token answers 401 with code 7224.
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/customer"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"
}
}
}
}
}Get offerings (subscriber token)#
GET /v1/customer/offerings · Auth: none
The subscriber-token form of GET /v1/subscribers/{app_user_id}/offerings: same body and answer, for the app user id of the token. An app key, an expired token or another user's token answers 401 with code 7224.
Example request
curl -s "$REVENUEDOT_URL/v1/customer/offerings"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"
}
]
}
]
}Intro offer eligibility (StoreKit 1) (subscriber token)#
POST /v1/customer/intro_eligibility · Auth: none
The subscriber-token form of POST /v1/subscribers/{app_user_id}/intro_eligibility: same body and answer, for the app user id of the token. An app key, an expired token or another user's token answers 401 with code 7224.
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
product_identifiers |
array of string | no |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/customer/intro_eligibility"Responses
- 200: Eligibility per product.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"pro_monthly": null
}Attribution data (deprecated iOS call) (subscriber token)#
POST /v1/customer/attribution · Auth: none
The subscriber-token form of POST /v1/subscribers/{app_user_id}/attribution: same body and answer, for the app user id of the token. An app key, an expired token or another user's token answers 401 with code 7224.
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/customer/attribution" \
-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:
{}Read customer attributes (subscriber token)#
GET /v1/customer/attributes · Auth: none · RevenueDot extension
The subscriber-token form of GET /v1/subscribers/{app_user_id}/attributes: same body and answer, for the app user id of the token. An app key, an expired token or another user's token answers 401 with code 7224.
Example request
curl -s "$REVENUEDOT_URL/v1/customer/attributes"Responses
- 200: The attributes.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"subscriber_attributes": {
"$displayName": {
"value": "Ana",
"updated_at_ms": 1790800914000
},
"plan_goal": {
"value": "sleep",
"updated_at_ms": 1790800914000
}
}
}Set customer attributes (subscriber token)#
POST /v1/customer/attributes · Auth: none
The subscriber-token form of POST /v1/subscribers/{app_user_id}/attributes: same body and answer, for the app user id of the token. An app key, an expired token or another user's token answers 401 with code 7224.
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
attributes |
object | yes |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/customer/attributes" \
-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:
{}Apple AdServices token (subscriber token)#
POST /v1/customer/adservices_attribution · Auth: none
The subscriber-token form of POST /v1/subscribers/{app_user_id}/adservices_attribution: same body and answer, for the app user id of the token. An app key, an expired token or another user's token answers 401 with code 7224.
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/customer/adservices_attribution" \
-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 (subscriber token)#
GET /v1/customer/health_report · Auth: none
The subscriber-token form of GET /v1/subscribers/{app_user_id}/health_report: same body and answer, for the app user id of the token. An app key, an expired token or another user's token answers 401 with code 7224.
Example request
curl -s "$REVENUEDOT_URL/v1/customer/health_report"Responses
- 200: Always passed.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"status": "passed",
"project_id": "proj18pzzkao",
"app_id": "appvnrm0a5h",
"checks": []
}Customer Center configuration (subscriber token)#
GET /v1/customer/customercenter · Auth: none
The subscriber-token form of GET /v1/customercenter/{app_user_id}: same body and answer, for the app user id of the token. An app key, an expired token or another user's token answers 401 with code 7224.
Example request
curl -s "$REVENUEDOT_URL/v1/customer/customercenter"Responses
- 200: The configuration.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"customer_center": {
"screens": {
"MANAGEMENT": {
"type": "MANAGEMENT",
"title": "Manage subscription",
"paths": []
}
},
"support": {
"email": "support@example.com"
}
}
}Customer Center support ticket (subscriber token)#
POST /v1/customer/customercenter/support/create-ticket · Auth: none
The subscriber-token form of POST /v1/customercenter/support/create-ticket: same body and answer, for the app user id of the token. An app key, an expired token or another user's token answers 401 with code 7224.
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
app_user_id |
string | yes | |
customer_email |
string | yes | |
issue_description |
string | yes |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/customer/customercenter/support/create-ticket" \
-H "Content-Type: application/json" -d '{"app_user_id":"user_1234","customer_email":"wren@example.com","issue_description":"My scans are not syncing."}'Responses
- 200: Whether the ticket was taken.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"sent": true
}Virtual currency balances (subscriber token)#
GET /v1/customer/virtual_currencies · Auth: none
The subscriber-token form of GET /v1/subscribers/{app_user_id}/virtual_currencies: same body and answer, for the app user id of the token. An app key, an expired token or another user's token answers 401 with code 7224.
Example request
curl -s "$REVENUEDOT_URL/v1/customer/virtual_currencies"Responses
- 200: Balances.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"virtual_currencies": {
"GLD": {
"balance": 700,
"name": "Gold",
"code": "GLD",
"description": null
}
}
}Restore eligibility (StoreKit 2) (subscriber token)#
POST /v1/customer/restore/eligibility · Auth: none
The subscriber-token form of POST /v1/subscribers/{app_user_id}/restore/eligibility: same body and answer, for the app user id of the token. An app key, an expired token or another user's token answers 401 with code 7224.
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/customer/restore/eligibility"Responses
- 200: Always allowed.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"is_purchase_allowed_by_restore_behavior": true
}Rewarded ad verification status (subscriber token)#
GET /v1/customer/ads/reward_verifications/{client_transaction_id} · Auth: none
The subscriber-token form of GET /v1/subscribers/{app_user_id}/ads/reward_verifications/{client_transaction_id}: same body and answer, for the app user id of the token. An app key, an expired token or another user's token answers 401 with code 7224.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
client_transaction_id |
string | yes | From generateRewardVerificationToken. |
Example request
curl -s "$REVENUEDOT_URL/v1/customer/ads/reward_verifications/$CLIENT_TRANSACTION_ID"Responses
- 200: The verification status.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"status": "verified",
"reward": {
"type": "virtual_currency",
"code": "GEMS",
"amount": 10
},
"more_rewards": []
}Web offering products (subscriber token)#
GET /rcbilling/v1/customer/offering_products · Auth: none
The subscriber-token form of GET /rcbilling/v1/subscribers/{app_user_id}/offering_products: same body and answer, for the app user id of the token. An app key, an expired token or another user's token answers 401 with code 7224.
Example request
curl -s "$REVENUEDOT_URL/rcbilling/v1/customer/offering_products"Responses
- 200: No web offerings.
- 401: Unknown API key. Returns V1Error.
Example 200 response:
{
"offerings": {}
}Test Store product details (subscriber token)#
GET /rcbilling/v1/customer/products · Auth: none
The subscriber-token form of GET /rcbilling/v1/subscribers/{app_user_id}/products: same body and answer, for the app user id of the token. An app key, an expired token or another user's token answers 401 with code 7224.
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/customer/products"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
}
}
}
]
}Spend in-app currency as the subscriber#
POST /v1/customer/virtual_currencies/spend · Auth: none
Takes the amounts off the subscriber's balances, all or nothing. A balance cannot go below zero (422, code 7000); an unknown currency or a malformed body is 400 with code 7226. No webhook is sent, as for balance changes through the API.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
Idempotency-Key |
string | no | A retry with the same key spends nothing again. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
adjustments |
object | yes | Amount to spend per currency code. |
reference |
string or null | no | Your own note, kept in the ledger. |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/customer/virtual_currencies/spend" \
-H "Content-Type: application/json" -d '{"adjustments":{"GLD":5},"reference":"sword"}'Responses
- 200: The balances after the spend.
- 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.
- 422: The request is valid but cannot be done in this state. Returns V1Error.
Example 200 response:
{
"virtual_currencies": {
"GLD": {
"balance": 95,
"name": "Gold",
"code": "GLD",
"description": null
}
}
}Auth sign-in#
Sign app users in with a Firebase or OpenID Connect ID token and get a subscriber access token for the /v1/customer/* paths, in the wire format of the RevenueCat SDKs' token login. See Auth.
Sign in with an identity provider's ID token#
POST /v1/auth/login · Auth: public app key · RevenueDot extension
For apps without their own backend (Auth): the app sends the ID token its identity provider gave it, with its public SDK key. RevenueDot verifies the token with the provider's published keys, maps it to an app user id (the first sign-in creates the link; later ones reuse it), merges an anonymous link_to_id like logIn, and returns tokens. The RevenueCat SDKs' token login sends the same body to POST /auth/login. CORS is open, for web apps.
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
method |
firebase, oidc, google, apple, facebook, anonymous |
yes | firebase uses the project's Firebase provider; the others its OpenID Connect providers (the token's iss picks one). anonymous needs allow_anonymous. |
scope |
string | no | The SDK sends openid offline_access. |
id_token |
string | no | The provider's ID token. Not for anonymous. |
link_to_id |
string | no | The app user id the app used before signing in. An anonymous id ($RCAnonymousID:…) merges into the signed-in user like logIn; any other id is ignored. |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/auth/login" -H "Authorization: Bearer $PUBLIC_KEY" \
-H "Content-Type: application/json" -d '{"method":"firebase","scope":"openid offline_access","id_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6…","link_to_id":"$RCAnonymousID:9f3c1a0e2b7d4c5a8e6f1b2d3c4a5e6f"}'Responses
- 200: Signed in.
- 400: Malformed body (7226). Returns V1Error.
- 401: The SDK key is unknown (7225) or the ID token fails verification (7224). Returns V1Error.
- 403: Auth is off, or no enabled provider answers this method (7224). Returns V1Error.
- 503: The provider's keys could not be loaded, or the server has no key to sign tokens with. Retry. Returns V1Error.
Example 200 response:
{
"access_token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImF0K2p3dCIsImtpZCI6IjNmMmMxYTlkMGU4YjRjNzEifQ.eyJpc3MiOiJodHRwczovL2FwaS5yZXZlbnVlZG90LmFwcCIsInN1YiI6IlhrMmY5UXBMMGFaIiwicmMuYXBwX3VzZXJfaWQiOiJYazJmOVFwTDBhWiIsImFtciI6WyJmaXJlYmFzZSJdfQ.…",
"refresh_token": "rdrf_9b0c…",
"id_token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.…",
"scope": "openid offline_access",
"expires_in": 3600,
"token_type": "Bearer"
}Refresh the access token#
POST /v1/auth/token · Auth: public app key · RevenueDot extension
The refresh token is replaced: the old one stops working, and using it again answers 401 with 7224.
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
grant_type |
refresh_token |
yes | |
refresh_token |
string | yes | |
scope |
string | no |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/auth/token" -H "Authorization: Bearer $PUBLIC_KEY" \
-H "Content-Type: application/json" -d '{"grant_type":"refresh_token","refresh_token":"rdrf_9b0c…"}'Responses
- 200: New tokens.
- 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:
{
"access_token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImF0K2p3dCIsImtpZCI6IjNmMmMxYTlkMGU4YjRjNzEifQ.eyJpc3MiOiJodHRwczovL2FwaS5yZXZlbnVlZG90LmFwcCIsInN1YiI6IlhrMmY5UXBMMGFaIiwicmMuYXBwX3VzZXJfaWQiOiJYazJmOVFwTDBhWiIsImFtciI6WyJmaXJlYmFzZSJdfQ.…",
"refresh_token": "rdrf_9b0c…",
"id_token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.…",
"scope": "openid offline_access",
"expires_in": 3600,
"token_type": "Bearer"
}Sign out#
POST /v1/auth/revoke · Auth: public app key · RevenueDot extension
A refresh token ends its session and every access token it issued; an access token (token_type_hint: access_token) stops working alone. An unknown token is not an error (RFC 7009).
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
token |
string | yes | |
token_type_hint |
refresh_token, access_token |
no |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/auth/revoke" -H "Authorization: Bearer $PUBLIC_KEY" \
-H "Content-Type: application/json" -d '{"token":"rdrf_9b0c…","token_type_hint":"refresh_token"}'Responses
- 200: Done.
- 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.
Refresh the access token#
POST /auth/token · Auth: public app key · RevenueDot extension
The refresh token is replaced: the old one stops working, and using it again answers 401 with 7224.
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
grant_type |
refresh_token |
yes | |
refresh_token |
string | yes | |
scope |
string | no |
Example request
curl -s -X POST "$REVENUEDOT_URL/auth/token" -H "Authorization: Bearer $PUBLIC_KEY" \
-H "Content-Type: application/json" -d '{"grant_type":"refresh_token","refresh_token":"rdrf_9b0c…"}'Responses
- 200: New tokens.
- 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:
{
"access_token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImF0K2p3dCIsImtpZCI6IjNmMmMxYTlkMGU4YjRjNzEifQ.eyJpc3MiOiJodHRwczovL2FwaS5yZXZlbnVlZG90LmFwcCIsInN1YiI6IlhrMmY5UXBMMGFaIiwicmMuYXBwX3VzZXJfaWQiOiJYazJmOVFwTDBhWiIsImFtciI6WyJmaXJlYmFzZSJdfQ.…",
"refresh_token": "rdrf_9b0c…",
"id_token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.…",
"scope": "openid offline_access",
"expires_in": 3600,
"token_type": "Bearer"
}Sign out#
POST /auth/revoke · Auth: public app key · RevenueDot extension
A refresh token ends its session and every access token it issued; an access token (token_type_hint: access_token) stops working alone. An unknown token is not an error (RFC 7009).
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
token |
string | yes | |
token_type_hint |
refresh_token, access_token |
no |
Example request
curl -s -X POST "$REVENUEDOT_URL/auth/revoke" -H "Authorization: Bearer $PUBLIC_KEY" \
-H "Content-Type: application/json" -d '{"token":"rdrf_9b0c…","token_type_hint":"refresh_token"}'Responses
- 200: Done.
- 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.
Public key of RevenueDot's ID and access tokens#
GET /.well-known/jwks.json · Auth: none · RevenueDot extension
One Ed25519 key (OKP), derived from the server's signing key. Backends verify an Auth id_token with it (match kid, check iss and aud). Empty keys when the server has no signing or encryption key.
Example request
curl -s "$REVENUEDOT_URL/.well-known/jwks.json"Responses
- 200: The key set.
Example 200 response:
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"x": "gXdn2hmqR_TbdtQwK02laE0YgFz0Rtf918LICLrgZhg",
"kid": "3f2c1a9d0e8b4c71",
"alg": "EdDSA",
"use": "sig"
}
]
}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"
}Amazon Appstore Real-time Notifications (SNS)#
POST /v1/notifications/amazon/{app_id} · Auth: none
Add this URL as an endpoint under App Services → Real-time Notifications in the Amazon Appstore Console. Amazon delivers through Amazon SNS.
Every message must carry a valid SNS signature (SignatureVersion 1 or 2, certificate from an sns.<region>.amazonaws.com URL). The subscription confirmation is accepted by fetching its SubscribeURL, which is what makes Amazon show "Verified". Each message is stored once (by SNS message id), forwarded when notification_forward_url is set, and applied by reading the receipt from Amazon's Receipt Verification Service.
- 200: handled, confirmed, a duplicate, ignored (another package) or a receipt that can never be checked.
- 400: not an SNS message, a bad signature, or a topic other than the app's
sns_topic_arn. 404: no Amazon app with this id. - 500 or 503: a temporary failure; SNS retries. See Amazon Appstore setup.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_id |
string | yes | App id (app...). |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
Type |
Notification, SubscriptionConfirmation, UnsubscribeConfirmation |
yes | |
MessageId |
string | yes | |
TopicArn |
string | no | |
Message |
string | no | For notifications: Amazon's JSON (appPackageName, notificationType, appUserId, receiptId, relatedReceipts, timestamp, betaProductTransaction). |
SubscribeURL |
string | no | Subscription confirmations only. |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/notifications/amazon/$APP_ID"Responses
- 200: Handled.
- 400: Not accepted. Returns V1Error.
- 404: Unknown app. Returns V1Error.
- 500: Temporary failure; SNS retries. Returns V1Error.
- 503: The SNS certificate or confirmation failed; SNS retries. Returns V1Error.
Example 200 response:
{
"status": "processed"
}Stripe webhooks#
POST /v1/notifications/stripe/{app_id} · Auth: none
Add this URL as a webhook endpoint in your Stripe account with the events customer.subscription.created, .updated, .deleted, .paused, .resumed, invoice.paid, invoice.payment_failed, invoice.updated, charge.refunded and checkout.session.completed, and save its signing secret on the app.
Each event is stored once (by event id), forwarded when notification_forward_url is set, and applied by reading the subscription from Stripe, so event order does not matter. Other event types are accepted and ignored.
- 200: handled, a duplicate, ignored, an unknown purchase (applied only with
track_new_purchases) or an object Stripe no longer has. - 400: no signing secret saved, a missing or wrong
Stripe-Signature, or not a Stripe event. 404: no Stripe app with this id. - 500: a temporary failure; Stripe retries for three days. See Stripe setup.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_id |
string | yes | App id (app...). |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
Stripe-Signature |
string | yes | t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<body>">, checked with the app's stripe_webhook_secret within 5 minutes. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | evt_… |
object |
"event" |
no | |
type |
string | yes | |
created |
integer | no | |
livemode |
boolean | no | |
data |
object | yes | |
data.object |
object | no |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/notifications/stripe/$APP_ID"Responses
- 200: Handled.
- 400: Not accepted. Returns V1Error.
- 404: Unknown app. Returns V1Error.
- 500: Temporary failure; Stripe retries. Returns V1Error.
Example 200 response:
{
"status": "processed"
}Paddle notifications#
POST /v1/notifications/paddle/{app_id} · Auth: none
A notification destination in your Paddle account (Apply in Paddle creates it: POST /v2/projects/{project_id}/apps/{app_id}/actions/apply_notification_settings) with the subscription, transaction and adjustment events.
Each event is stored once (by event_id), forwarded when notification_forward_url is set, and applied by reading the subscription or transaction from Paddle, so delivery order does not matter. Approved full refunds and chargebacks (adjustment.created, adjustment.updated) mark the paid period refunded. Simulated events (ntfsimevt_…) count as received and change nothing. Paddle counts only 200 as delivered.
- 200: handled, a duplicate, simulated, ignored, an unknown purchase (applied only with
track_new_purchases) or an object Paddle no longer has. - 400: no secret key saved, a missing or wrong
Paddle-Signature, or not a Paddle event. 404: no Paddle app with this id. - 500: a temporary failure; Paddle retries. See Paddle setup.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_id |
string | yes | App id (app...). |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
Paddle-Signature |
string | yes | ts=<unix seconds>;h1=<hex HMAC-SHA256 of "<ts>:<body>">, checked with the destination's secret key (paddle_webhook_secret) within 5 minutes. Several h1 values are accepted while a key rotates. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
event_id |
string | yes | evt_… |
event_type |
string | yes | |
occurred_at |
string | no | |
notification_id |
string | no | ntf_… |
data |
object | yes |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/notifications/paddle/$APP_ID"Responses
- 200: Handled.
- 400: Not accepted. Returns V1Error.
- 404: Unknown app. Returns V1Error.
- 500: Temporary failure; Paddle retries. Returns V1Error.
Example 200 response:
{
"status": "processed"
}Roku Pay push notifications#
POST /v1/notifications/roku/{app_id} · Auth: none
Set this URL as the push notification URL under Roku Pay web services in the Roku developer dashboard. Roku has one URL per developer account: a push for another channel goes to the project's Roku app with that roku_channel_id.
The body is a JWS (RS256) signed with a key from Roku's published key set (the test key set for Roku's test endpoint), with issuer Roku, Inc. urn:roku:apps:partner-service.roku.com and x-Roku-message-type: roku.rpay.push; the message is base64 JSON in x-Roku-message. Each push is stored once (by x-Roku-message-key), forwarded when notification_forward_url is set, and applied by validating its transaction with Roku again.
- 200: handled; the body is the push's
responseKey. - 400: not a signed Roku push (bad signature, unknown key, another issuer or message type). 404: no Roku app with this id.
- 500: a temporary failure; Roku retries for 36 hours. See Roku setup.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_id |
string | yes | App id (app...). |
Request body (text/plain)
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/notifications/roku/$APP_ID"Responses
- 200: Handled: the push's responseKey.
- 400: Not accepted. Returns V1Error.
- 404: Unknown app. Returns V1Error.
- 500: Temporary failure; Roku retries. Returns V1Error.
Galaxy Store server notifications#
POST /v1/notifications/galaxy/{app_id} · Auth: none
Set this URL as the Instant Server Notification URL in Samsung Seller Portal. The body is a JWT (RS256) from iap.samsungapps.com whose aud names the app's package. With the app's galaxy_iap_public_key saved its signature must match; without it nothing in the body is trusted. Either way the purchase it names is read again from Samsung (receipt and subscription APIs) and applied. TEST counts as received.
- 200: handled, a duplicate, ignored or an unknown purchase (applied only with
track_new_purchases).verifiedsays whether the signature was checked. - 400: not a Samsung notification, a signature that does not match, or another package. 404: no Galaxy Store app with this id.
- 500: a temporary failure. See Galaxy Store setup.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
app_id |
string | yes | App id (app...). |
Request body (text/plain)
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/notifications/galaxy/$APP_ID"Responses
- 200: Handled.
- 400: Not accepted. Returns V1Error.
- 404: Unknown app. Returns V1Error.
- 500: Temporary failure. Returns V1Error.
Example 200 response:
{
"status": "processed",
"verified": true
}Stripe Connect webhooks (RevenueDot's platform)#
POST /v1/notifications/stripe-connect · Auth: none · RevenueDot extension
The platform's endpoint for "Events on Connected accounts". Each event carries account and is handled for every app connected to that account exactly like its own Stripe endpoint (stored once per app, forwarded, applied), when its livemode matches the connection's mode. account.updated refreshes onboarding status; account.application.deauthorized disconnects the apps.
- 200: handled, a duplicate,
unknown_account(no app is connected to it),other_mode, or a platform event withoutaccount. 400: a bad signature or not a Stripe event. 404: Connect is not set up on this server. 500: a temporary failure; Stripe retries.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
Stripe-Signature |
string | yes | Checked against each of the platform's webhook secrets (REVENUEDOT_STRIPE_CONNECT_WEBHOOK_SECRET). |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | evt_… |
type |
string | yes | |
account |
string | no | acct_… |
livemode |
boolean | no | |
data |
object | yes | |
data.object |
object | no |
Example request
curl -s -X POST "$REVENUEDOT_URL/v1/notifications/stripe-connect"Responses
- 200: Handled.
- 400: Not accepted. Returns V1Error.
- 404: Connect is not set up. Returns V1Error.
- 500: Temporary failure; Stripe retries. 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 customer's current offering: their override, else their experiment variant's offering, else the first live targeting rule's, else the project's current offering. |
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. |
placements |
object | no | What currentOffering(forPlacement:) reads. |
placements.fallback_offering_id |
string or null | no | Lookup key of the offering for placements not listed below: the current offering. |
placements.offering_ids_by_placement |
object | no | Placement id → offering lookup key, or null for no paywall there: the matching targeting rule's placements, overlaid with the customer's experiment variant's. |
targeting |
object | no | Present when a targeting rule matched the customer. |
targeting.revision |
integer | yes | The rule's revision. |
targeting.rule_id |
string | yes | The targeting rule that matched. |
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 |