SDK / PROTOCOL

SDK HTTP protocol.

The one endpoint every RevFox SDK talks to. Read this to write a client for another platform, or to see exactly what the SDKs send.

01

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.

EventUse it toRequires
CUSTOMERCreate or update the customer, send attributes, and read their benefits.customer_id
PURCHASEVerify a store purchase, record it and read the benefits. The default when event_type is missing.customer_id, receipt
PACKAGESTurn package identifiers into this app's store product IDs.One or more packages
TRANSFERRestore a purchase another customer owns.customer_id, receipt

event_type is trimmed, uppercased, and has spaces and hyphens turned into underscores.

02

Request fields

FieldTypeDescription
event_typestringCUSTOMER, PURCHASE, PACKAGES or TRANSFER.
customer_idstringThe app's customer ID.
storestringapp_store or play_store. Defaults to the key's app, and must match it when sent.
environmentstringOn CUSTOMER: PRODUCTION or SANDBOX to read one environment's benefits. Empty reads both.
countrystringISO country code, used when the store gives none.
attributesobjectString-to-string attributes. Accepted on every event.
receiptstringApp Store: StoreKit 2's jwsRepresentation, never a transaction ID. Play: the purchase token.
purchase_tokenstringSame as receipt.
product_idstringPlay one-time products only: Google looks their tokens up together with the product ID.
presented_package_idstringOn PURCHASE: the package the product was shown in. Kept on the subscription and sent with its webhooks.
packagesstring[]On PACKAGES. A single package, package_id or package_identifier also works.
03

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
    }
  ]
}
  • benefits lists what the customer can use now. It is empty when they have nothing active.
  • purchases has one entry per subscription or one-time purchase, including expired ones with is_active: false. A renewed subscription shows its latest product and expiry, with purchased_at_ms still the first purchase.
  • ownership is PURCHASED or FAMILY_SHARED. Times are Unix milliseconds. expires_at_ms is null for lifetime purchases; the cancellation and billing times are null until they happen.
  • When customer_id is an alias, the response adds original_customer_id.
04

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.

05

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.

06

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.

07

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

KeyiOSAndroid
$anonymoustrue while the SDK uses its generated ID
$platformios, ipados, macosandroid
$app_version, $app_buildCFBundleShortVersionString, CFBundleVersionversionName, longVersionCode
$sdk_versionThe SDK's version, or the wrapper's, such as flutter 0.1.0
$locale, $timezoneDevice locale and time zone
$storefrontStorefront.current countryPlay billing country
$install_sourceapp_store, testflight, xcodeInstaller package
$push_tokenAPNs token, hexFCM token
$device_model, $device_manufacturer, $os_versionOnly after collectDeviceIdentifiers()

Set by the app

KeyDescription
$email, $display_name, $phone_numberContact details. The display name is the profile title; phone numbers are E.164.
$idfv, $idfa, $att_statusiOS identifiers, after collectDeviceIdentifiers(); the IDFA only with tracking consent.
$gaidGoogle advertising ID, when the user has not opted out.
$media_source, $campaign, $ad_group, $ad, $keyword, $creativeAttribution.
$adjust_id, $appsflyer_id, $appstack_id, $fb_anon_id, $firebase_app_instance_id, $kochava_device_id, $singular_device_id, $tenjin_idAttribution 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_labelAnalytics 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_source Apple Search Ads, $campaign, $ad_group, $keyword and the $apple_ads_… IDs.
08

Errors

StatusMeaning
200Processed.
400A 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.
401No bearer key, or an unknown one.
501The server cannot verify receipts.
500An 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.

On your server

Call the server API with a secret key.

Server API