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: *, methodsPOST, OPTIONS, headersauthorization, content-type, x-revenuehog-unattested, max age 86400.OPTIONSreturns204. - Caching: every response sends
cache-control: no-store. - Idempotency: identify and attribute merge on repeat, so retries are safe.
| Status | When |
|---|---|
200 | Accepted (stored or dropped) |
400 | Malformed JSON or a missing required field. Do not retry as-is. |
401 | Unknown device token. Clear it and re-enroll once. |
429 | Rate limited. Wait retry-after seconds. |
Enrollment
POST /api/sdk/v1/attest/challenge
/api/sdk/v1/attest/challengeNoneNo 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
/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" }| Status | Body | Meaning |
|---|---|---|
400 | missing keyId, attestation, challenge or bundleId | A field is absent |
400 | attestation too large | Over 64 KB of base64 |
400 | invalid JSON | |
401 | invalid or expired challenge | Unknown, expired or reused challenge |
401 | attestation verification failed | Apple's statement did not verify |
404 | app not connected | No connected App Store app matches the attested team and bundle ID. The Swift SDK backs off 24 hours. |
429 | rate_limited | 10 per minute per IP |
POST /api/sdk/v1/identify
/api/sdk/v1/identifyDevice token or noneUpserts 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
/api/sdk/v1/attributeDevice token or noneLinks 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
/api/sdk/v1/eventsDevice token or noneBatch-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
/api/sdk/v1/feedbackDevice token or noneStores in-app feedback. Body and validation are on In-app feedback. Response: { "ok": true, "id": "…" } (200).
POST /api/sdk/v1/adservices
/api/sdk/v1/adservicesDevice token or noneStores 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
/api/sdk/v1/paywallDevice token or noneWhich 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
/api/sdk/v1/paywall/impressionDevice token or noneFire 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
/api/settings/sdk-devicesSessionBody { "deviceId": "…" }. Deletes the enrollment so its token stops working. Response { "ok": true }. Errors: 400 invalid JSON, missing deviceId; 401 unauthorized; 404 not found.
Related
Last updated on
API keys
Create, list and revoke RevenueHog API keys for server-to-server feedback ingest, including live and test modes, app binding, scopes and the one-time secret.
Feedback API
Reference for POST /api/v1/feedback, the API key endpoint that forwards feedback from your servers and forms into RevenueHog, with every error and limit.