Endpoint
POST https://api.example.com/sdk-events
Authorization: Bearer apps_99536db9d4f6e2c63336bc6d3ae64a76
Content-Type: application/json
The bearer token is the app's public SDK API key: apps_ for an App Store app or play_ for a Play Store app, followed by 32 hex characters. The key ties the request to its project and app, so requests carry no project or app ID.
Bodies are JSON, up to 1 MiB. Every request is one event, named by event_type.
| Event | Use it to | Requires |
|---|---|---|
CUSTOMER | Create or update the customer, send attributes, and read their benefits. | customer_id |
PURCHASE | Verify a store purchase, record it and read the benefits. The default when event_type is missing. | customer_id, receipt |
PACKAGES | Turn package identifiers into this app's store product IDs. | One or more packages |
TRANSFER | Restore a purchase another customer owns. | customer_id, receipt |
event_type is trimmed, uppercased, and has spaces and hyphens turned into underscores.
Request fields
| Field | Type | Description |
|---|---|---|
event_type | string | CUSTOMER, PURCHASE, PACKAGES or TRANSFER. |
customer_id | string | The app's customer ID. |
store | string | app_store or play_store. Defaults to the key's app, and must match it when sent. |
environment | string | On CUSTOMER: PRODUCTION or SANDBOX to read one environment's benefits. Empty reads both. |
country | string | ISO country code, used when the store gives none. |
attributes | object | String-to-string attributes. Accepted on every event. |
receipt | string | App Store: StoreKit 2's jwsRepresentation, never a transaction ID. Play: the purchase token. |
purchase_token | string | Same as receipt. |
product_id | string | Play one-time products only: Google looks their tokens up together with the product ID. |
presented_package_id | string | On PURCHASE: the package the product was shown in. Kept on the subscription and sent with its webhooks. |
packages | string[] | On PACKAGES. A single package, package_id or package_identifier also works. |
CUSTOMER
Send at launch, on sign-in and when attributes change. RevFox creates or updates the customer, saves the attributes, and answers with their benefits and purchases.
{
"event_type": "CUSTOMER",
"customer_id": "user_123",
"attributes": { "$app_version": "2.4.0", "$anonymous": "false" }
}
Response:
{
"success": true,
"event_type": "CUSTOMER",
"customer_id": "user_123",
"benefits": ["premium"],
"purchases": [
{
"id": "GPA.1234-5678-9012-34567",
"store": "PLAY_STORE",
"environment": "PRODUCTION",
"product_id": "premium:monthly",
"benefits": ["premium"],
"is_active": true,
"will_renew": true,
"in_grace_period": false,
"ownership": "PURCHASED",
"purchased_at_ms": 1767225600000,
"renewed_at_ms": null,
"expires_at_ms": 1769904000000,
"unsubscribed_at_ms": null,
"billing_issue_detected_at_ms": null
}
]
}
benefitslists what the customer can use now. It is empty when they have nothing active.purchaseshas one entry per subscription or one-time purchase, including expired ones withis_active: false. A renewed subscription shows its latest product and expiry, withpurchased_at_msstill the first purchase.ownershipisPURCHASEDorFAMILY_SHARED. Times are Unix milliseconds.expires_at_msisnullfor lifetime purchases; the cancellation and billing times arenulluntil they happen.- When
customer_idis an alias, the response addsoriginal_customer_id.
PURCHASE
Send after the store completes a purchase, a restore or a renewal the app sees. RevFox verifies the receipt with the store and records it.
{
"event_type": "PURCHASE",
"customer_id": "user_123",
"store": "app_store",
"receipt": "eyJhbGciOiJFUzI1NiIsIng1YyI6Wy...",
"presented_package_id": "default"
}
The response has benefits and purchases as for CUSTOMER, in the receipt's environment, plus the recorded transaction:
"transaction": {
"id": "2000000123456789",
"original_transaction_id": "2000000123456789",
"customer_id": "user_123",
"owner_id": "user_123",
"environment": "PRODUCTION",
"store": "APP_STORE",
"product_id": "premium_monthly",
"country": "US",
"purchased_at_ms": 1767225600000,
"expires_at_ms": 1769904000000,
"price": 9.99,
"price_in_purchased_currency": 9.99,
"currency": "USD",
"type": "NEW_SUB",
"renewal_number": 1,
"commission_percentage": 0.3,
"tax_percentage": 0,
"benefits": ["premium"]
}
type is the transaction type. Sending the same receipt again is safe: RevFox records each store transaction once.
PACKAGES
{ "event_type": "PACKAGES", "packages": ["default"] }
{
"success": true,
"event_type": "PACKAGES",
"packages": [
{
"packageIdentifier": "default",
"packageDisplayName": "Default",
"storeIds": ["premium_monthly", "premium_yearly"],
"storeItemIds": ["item_1", "item_2"]
}
],
"storeIdsByPackage": { "default": ["premium_monthly", "premium_yearly"] },
"storeIds": ["premium_monthly", "premium_yearly"],
"storeItemIds": ["item_1", "item_2"]
}
storeIds are the product IDs to load from the store. Top-level storeIds and storeItemIds combine every requested package.
TRANSFER
Send when the app restores purchases. RevFox verifies the receipt, finds who owns the purchase now, and either aliases or transfers depending on whether customer_id is anonymous. See aliases and transfers.
{
"event_type": "TRANSFER",
"customer_id": "user_456",
"store": "app_store",
"receipt": "eyJhbGciOiJFUzI1NiIsIng1YyI6Wy..."
}
The response has benefits and purchases for customer_id, and one of:
"transfer": { "id": "transfer_123", "transfered_from": "user_123", "transfered_to": "user_456", "date": 1767225600000 }
"alias": { "original_id": "user_123", "alias_id": "$anonymous_9f2c", "date": 1767225600000 }
A family-shared App Store receipt is refused: it entitles the family member but does not make them the owner.
Attributes
attributes is a flat map of strings. Values are merged into the customer, so send only what changed; an empty string stores an empty value. Keys starting with $ are reserved and shown in their own groups on the profile.
Sent by the SDK on its own
| Key | iOS | Android |
|---|---|---|
$anonymous | true while the SDK uses its generated ID | |
$platform | ios, ipados, macos | android |
$app_version, $app_build | CFBundleShortVersionString, CFBundleVersion | versionName, longVersionCode |
$sdk_version | The SDK's version, or the wrapper's, such as flutter 0.1.0 | |
$locale, $timezone | Device locale and time zone | |
$storefront | Storefront.current country | Play billing country |
$install_source | app_store, testflight, xcode | Installer package |
$push_token | APNs token, hex | FCM token |
$device_model, $device_manufacturer, $os_version | Only after collectDeviceIdentifiers() | |
Set by the app
| Key | Description |
|---|---|
$email, $display_name, $phone_number | Contact details. The display name is the profile title; phone numbers are E.164. |
$idfv, $idfa, $att_status | iOS identifiers, after collectDeviceIdentifiers(); the IDFA only with tracking consent. |
$gaid | Google advertising ID, when the user has not opted out. |
$media_source, $campaign, $ad_group, $ad, $keyword, $creative | Attribution. |
$adjust_id, $appsflyer_id, $appstack_id, $fb_anon_id, $firebase_app_instance_id, $kochava_device_id, $singular_device_id, $tenjin_id | Attribution tools' install IDs. |
$airship_channel_id, $amplitude_device_id, $amplitude_user_id, $mixpanel_distinct_id, $mparticle_id, $onesignal_id, $onesignal_user_id, $clevertap_id, $posthog_user_id, $customerio_id, $iterable_user_id, $iterable_campaign_id, $iterable_template_id, $braze_alias_name, $braze_alias_label | Analytics and messaging tools' IDs. |
Handled by the server
$ip: send"true". RevFox stores the address the request came from, never a value from the app, and keeps it current.$ad_services_token: Apple's AdServices token. It is not stored; RevFox exchanges it with Apple within its 24 hours and sets$media_sourceApple Search Ads,$campaign,$ad_group,$keywordand the$apple_ads_…IDs.
Errors
| Status | Meaning |
|---|---|
200 | Processed. |
400 | A missing field, a wrong store, an unknown package or transaction, or a receipt not signed by the store. The body is plain text, such as receipt is required. |
401 | No bearer key, or an unknown one. |
501 | The server cannot verify receipts. |
500 | An unexpected server error. Retry later. |
A client should keep an unreported purchase and retry it on network errors and 5xx. The SDKs treat a 400 as final only when it says the receipt itself is bad, because a server without store credentials also answers 400.
