RevenueHogdocs
API

SDK API reference

Wire-level reference for every /api/sdk/v1 endpoint, with request bodies, responses, status codes, error messages, CORS rules and the keyless auth model.

The SDK API is the keyless ingestion API behind the optional SDKs. Every endpoint is a POST with a JSON body on https://revenuehog.dev. There is no API key: iOS devices prove which app they are with App Attest, and everything else goes through the quarantine path. See Authentication.

Common behavior

  • Auth header: Authorization: Bearer dt_… when enrolled; omit it otherwise.
  • Never reject: a write whose bundle ID resolves to no app, or to more than one workspace, returns 200 { "ok": true } and is not stored. Clients must never retry these forever.
  • Strings are trimmed; a string field that is empty or longer than its limit (191 characters unless noted) counts as missing.
  • CORS: access-control-allow-origin: *, methods POST, OPTIONS, headers authorization, content-type, x-revenuehog-unattested, max age 86400. OPTIONS returns 204.
  • Caching: every response sends cache-control: no-store.
  • Idempotency: identify and attribute merge on repeat, so retries are safe.
StatusWhen
200Accepted (stored or dropped)
400Malformed JSON or a missing required field. Do not retry as-is.
401Unknown device token. Clear it and re-enroll once.
429Rate limited. Wait retry-after seconds.

Enrollment

POST /api/sdk/v1/attest/challenge

POST/api/sdk/v1/attest/challengeNone

No body. Returns a single-use challenge valid for 5 minutes.

{ "challenge": "<base64url>" }

Limit: 10 requests per minute per IP, then 429 { "error": "rate_limited" }.

POST /api/sdk/v1/attest

POST/api/sdk/v1/attestNone
{
  "keyId": "<App Attest key ID, base64>",
  "attestation": "<attestation object, base64>",
  "challenge": "<the challenge, echoed back>",
  "bundleId": "com.example.app"
}
{ "deviceToken": "dt_0123456789abcdef0123456789abcdef" }
StatusBodyMeaning
400missing keyId, attestation, challenge or bundleIdA field is absent
400attestation too largeOver 64 KB of base64
400invalid JSON
401invalid or expired challengeUnknown, expired or reused challenge
401attestation verification failedApple's statement did not verify
404app not connectedNo connected App Store app matches the attested team and bundle ID. The Swift SDK backs off 24 hours.
429rate_limited10 per minute per IP

POST /api/sdk/v1/identify

POST/api/sdk/v1/identifyDevice token or none

Upserts an app user. Attributes shallow-merge into what is stored, device fields update, and lastSeenAt bumps.

Prop

Type

Response: { "ok": true, "id": "<app user row ID>" }.

Errors: 400 missing appUserId, missing bundleId, attributes must be a small JSON object, invalid JSON.

POST /api/sdk/v1/attribute

POST/api/sdk/v1/attributeDevice token or none

Links a store transaction to an app user. One transaction belongs to one user per workspace; re-posting it under another appUserId moves it.

Prop

Type

Response: { "ok": true }.

Errors: 400 missing appUserId, missing bundleId, missing originalTransactionId, invalid JSON.

When the write resolves to a Google Play app, the Play token is converted to the subscription's lineage key so it joins the Play customer.

POST /api/sdk/v1/events

POST/api/sdk/v1/eventsDevice token or none

Batch-ingests signpost events. Full rules on Signpost events.

{
  "bundleId": "com.example.app",
  "events": [
    { "name": "paywall_viewed", "appUserId": "user_42", "at": 1755798000000, "properties": { "placement": "onboarding" } }
  ]
}

bundleId may be on the envelope or the first event. Response:

{ "ok": true, "stored": 1, "dropped": [{ "reason": "invalid_name", "name": "Bad" }], "rejectedNames": ["name_33"] }

Errors: 400 missing bundleId, missing events, too many events (max 50), invalid JSON.

POST /api/sdk/v1/feedback

POST/api/sdk/v1/feedbackDevice token or none

Stores in-app feedback. Body and validation are on In-app feedback. Response: { "ok": true, "id": "…" } (200).

POST /api/sdk/v1/adservices

POST/api/sdk/v1/adservicesDevice token or none

Stores an Apple Ads AdServices token for later resolution. Body: bundleId, installId, token (required), appVersion, storefront, environment (production, debug or sandbox; default production). First touch per (bundleId, installId) wins; duplicates are no-ops. Response: { "ok": true }. Errors: 400 missing bundleId, missing installId, missing token. See Attribution.

POST /api/sdk/v1/paywall

POST/api/sdk/v1/paywallDevice token or none

Which SKUs should this install's paywall show for an entitlement?

Prop

Type

{
  "entitlement": "pro",
  "skus": [
    { "productId": "com.example.pro.annual", "kind": "subscription" },
    { "productId": "com.example.pro.monthly", "kind": "subscription" },
    { "productId": "com.example.credits.500", "kind": "iap" }
  ],
  "experimentId": "…",
  "variantKey": "b",
  "ttlSeconds": 900
}

experimentId and variantKey appear only while an experiment serves this install; forced: true marks a forced preview. ttlSeconds is 3600 for defaults and 900 while an experiment exists (running or paused). An unknown entitlement, an unresolvable bundle ID or any other miss returns 200 with skus: []: render your compiled-in fallback.

Errors: 400 missing bundleId, missing installId, missing appUserId, missing entitlement, invalid JSON.

POST /api/sdk/v1/paywall/impression

POST/api/sdk/v1/paywall/impressionDevice token or none

Fire when the paywall actually appears, after product loading.

{
  "bundleId": "com.example.app",
  "installId": "1B7A2C…",
  "experimentId": "…",
  "renderedSkus": ["com.example.pro.annual", "com.example.pro.monthly"]
}

The first beacon sets the assignment's exposure time; every beacon increments its impression count; a beacon missing any SKU the variant specified increments its render-issue count (extra SKUs are fine). At most 50 rendered SKUs are read. A beacon with no matching assignment (concluded experiment, stale cache) is a no-op. Response: { "ok": true }.

Errors: 400 missing bundleId, missing installId, missing experimentId, invalid JSON.

Revoke an SDK device

DELETE/api/settings/sdk-devicesSession

Body { "deviceId": "…" }. Deletes the enrollment so its token stops working. Response { "ok": true }. Errors: 400 invalid JSON, missing deviceId; 401 unauthorized; 404 not found.

Last updated on

On this page