View as Markdownllms.txt

Reference

REST API

Manage apps, links and integrations, read analytics and subscriptions, and send server-side events from your own backend.

All endpoints live under https://apprefer.com/api/v1 (on a self-hosted AppRefer, your own domain). The API is for servers: an organisation API key is a secret, so never put one in an app or a web page. Apps talk to AppRefer through the SDKs with the public pk_ SDK key instead.

Agent workspace and login endpoints

POST /api/agent/workspaces starts a test workspace with a scoped seven-day key and human claim link. GET /api/agent/workspaces/{id} returns its credential-bound status. Existing access-request endpoints remain supported. For exact payloads, limits, MCP OAuth discovery, PKCE, rotating tokens, approvals and revocation, read auth.md and AI agents & MCP.

Authentication

Send an organisation API key as a bearer token on every request:

HTTP
GET /api/v1/apps
Authorization: Bearer ak_live_...
  • Create keys in the dashboard under API keys (account menu at the foot of the sidebar). You need the admin role. The full key is shown once; AppRefer stores only its hash.
  • A key belongs to one company and reaches only that company's apps. Any other app id answers 404, exactly like an app that doesn't exist.
  • Revoking a key in the dashboard stops it everywhere within a minute.

Scopes

Each key carries a list of scopes. A request without the scope its endpoint needs gets 403 FORBIDDEN, and the message names the scope. Give each key only what its job needs, for example users:erase alone for an account-deletion worker. The dashboard offers three presets: Full Access (*), Read Only (the five :read scopes for analytics, apps, links, integrations and SKAN) and Analytics Only.

ScopeAllows
*Everything below.
analytics:readStats, overview, campaigns, events, live events, attributions and the portfolio. Also reads support and billing readiness.
apps:readList apps and read an app's settings and SDK keys, subscription metrics and help articles. Also reads support and billing readiness.
apps:writeCreate, update and delete apps, rotate the live SDK key, send website events and purchases, and save help articles.
links:readList tracking links.
links:writeCreate and delete tracking links.
integrations:readRead ad network, RevenueCat, Superwall and Support delivery settings, and the billing configuration (secrets are always masked). Also reads billing readiness.
integrations:writeChange those integrations and the billing configuration and catalogue.
skan:readRead the SKAN conversion-value table and the postback summary.
skan:writeSave the SKAN conversion-value table.
billing:readLook up a customer's entitlements, read offering experiment results and billing readiness.
billing:writeGrant and revoke promotional entitlements. The standard * scope also includes this permission.
users:eraseErase an end user (account deletion) and read the status of an erase request.
support:agentRun an AI support agent: claim the answer queue, post answers, read and write its knowledge and the app's help articles (new articles default to AI-only). Also reads support readiness.
support:contextSend Support the customer context your backend knows (credits, orders, jobs, failures) and issue identity tokens. The standard * scope also includes this permission.
support:testimonialsRegister photo/video testimonial campaigns, ask a user for one, claim and report rewards, and export the approved library. The standard * scope also includes this permission.
support:actionsRegister the Support actions your backend can perform, claim runs your team or the AI asked for, and report their results. The standard * scope also includes this permission.

Responses and errors

Most endpoints wrap their result in data; lists that page add meta. Errors use one shape:

JSON
{ "data": { "appId": "app_3f9c2a1b7d4e", "name": "Sparkly" } }

{ "error": { "code": "FORBIDDEN", "message": "Missing required scope: links:write" } }
StatusCodeWhen
400BAD_REQUESTA missing or invalid field. The message says which.
401UNAUTHORIZEDNo key, an unknown key or a revoked key.
403FORBIDDENThe key lacks the scope this endpoint needs.
404NOT_FOUNDThe app, link or record doesn't exist, or belongs to another company.
409variesA conflict: the link id is taken, or an idempotent request was retried with different data.
429RATE_LIMITEDToo many requests; see below.
503SERVICE_UNAVAILABLEA backing service (for example the analytics warehouse) is unavailable. Retry later.

A few server-to-server endpoints answer without the data wrapper, because their callers retry on the status alone: users/erase and its status, web-events (errors are { "error": { "code": "invalid_click_id" } }), web-purchases (errors are { "error": "message" }) and the support agent endpoints.

Rate limits

Requests from all of a company's keys share one one-minute window. A request is refused with 429 once the window already holds more requests than that endpoint allows. The response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds). Back off and retry after the reset.

EndpointsPer minute
Reads, website events, support agent queue and answers120
Writes (apps, links, integrations, billing configuration, SKAN, help articles, agent knowledge PUT)30
Billing customer lookup, support context, identity tokens and support actions600
Experiment results, new erase requests, agent knowledge GET60

Pagination

/attributions and /events page with a cursor. Pass limit (1 to 500, default 50) and, for the next page, the cursor from the previous response. When meta.hasMore is false there is nothing more. Other lists return everything in one response.

JSON
{
  "data": [ ... ],
  "meta": { "cursor": "eyJpZCI6ImFwcF8zZjljX2Rldl8xMjMifQ==", "hasMore": true }
}

Analytics endpoints take days (1 to 365, default 14) and report live (production) data only. Revenue is net USD: paid revenue minus refunds.

Endpoints

Paths below are relative to /api/v1.

Apps

EndpointScopeWhat it does
GET /appsapps:readYour company's apps: appId, name, sdkEnabled, storeUrl, createdAt.
POST /appsapps:writeCreate an app from { name, iosUrl?, androidUrl? }. Returns appId and both SDK keys (sdkKeyLive, sdkKeyTest).
GET /apps/{appId}apps:readOne app: name, SDK keys, store URLs, the settings below and its integrations with secrets masked.
PATCH /apps/{appId}apps:writeChange name, storeUrl ({ ios, android }) or any setting below. Other fields are ignored. Returns the fields updated.
DELETE /apps/{appId}apps:writeDelete the app (its SDK keys stop resolving).
POST /apps/{appId}/sdk-key/rotateapps:writeReplace the live SDK key (pk_live_) and return the new one. The old key stops working at once, so every app build that still uses it loses attribution, events, Billing and Support until users update. Plan a release with the new key before rotating, and only rotate a key that has leaked. The test key is unchanged.

App settings you can change with PATCH /apps/{appId}:

FieldDefaultEffect
sdkEnabledtrueKill switch. false tells every SDK install to stop sending.
probabilisticMatchingtruefalse turns off multi-signal probabilistic matching. Installs then match only through Apple AdServices, Play Install Referrer click ids and an exact single-candidate IP match.
trackingConsentRequiredfalsetrue treats installs with no tracking consent recorded (SDKs before 0.6.0, purchases not tied to an install) as not granted: no click matching or ad-network forwarding for them. Turn it on once every live app version sends consent.
webEvents{ enabled: false }Accept verified website receipts on /web-events. The object takes only enabled.
supportAi{ mode: "off" }The support AI policy and persona. Replaced whole, so always send mode. See Support: AI answers.
Turn off probabilistic matching and require consentTerminal
curl -X PATCH "https://apprefer.com/api/v1/apps/app_3f9c2a1b7d4e" \
  -H "Authorization: Bearer ak_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "probabilisticMatching": false, "trackingConsentRequired": true }'

{ "data": { "appId": "app_3f9c2a1b7d4e", "updated": ["trackingConsentRequired", "probabilisticMatching"] } }
EndpointScopeWhat it does
GET /apps/{appId}/linkslinks:readEvery link, newest first: id, name, network, url, destinationMode, store destinations, external ids, clicks, installs.
POST /apps/{appId}/linkslinks:writeCreate a link. Body: linkId (3 to 50 letters, digits and dashes), name, network? (default organic), storeUrl?, iosStoreUrl?, androidStoreUrl?, storeVariantKey?, landingPage? (Google and ChatGPT Ads links), externalCampaignId?, externalAdGroupId?, externalAdId?. A taken id is 409.
DELETE /apps/{appId}/links/{linkId}links:writeDelete a link.

The returned url depends on the network: /api/c/ for most, /g/ for Google Ads, /o/ for ChatGPT Ads and /x/ for X Ads (which also needs a clean HTTPS storeUrl). See Tracking links.

Create a Meta linkTerminal
curl -X POST "https://apprefer.com/api/v1/apps/app_3f9c2a1b7d4e/links" \
  -H "Authorization: Bearer ak_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "linkId": "spring-sale", "name": "Spring sale", "network": "meta" }'

{
  "data": {
    "id": "spring-sale",
    "name": "Spring sale",
    "network": "meta",
    "url": "https://apprefer.com/api/c/spring-sale",
    "destinationMode": "legacy_redirect",
    "storeUrl": "",
    "clicks": 0,
    "installs": 0
  }
}

Integrations

EndpointScopeWhat it does
GET /apps/{appId}/integrationsintegrations:readEvery configured integration with its connection status. Secrets come back masked (starting with •).
PATCH /apps/{appId}/integrations/{network}integrations:writeUpdate one integration. network: meta, google, tiktok, openai, x_ads, apple_ads, revenuecat, superwall or support. Send only the fields you change; a masked value sent back keeps the stored secret. Returns connected (plus reportingConnected for ChatGPT Ads and enabled for X Ads, which stays off until you set enabled: true).

Fields per network are on each integration page: Meta, Google, TikTok, ChatGPT Ads, X Ads, RevenueCat. The support network holds push and email delivery (see Support).

Analytics

EndpointScopeWhat it does
GET /apps/{appId}/stats?days=analytics:readKPIs (installs, revenue, grossRevenue, refunds, trials, purchases) and a daily chart.
GET /apps/{appId}/overview?days=analytics:readKPIs, chart data, a breakdown by network and the top campaigns.
GET /apps/{appId}/campaigns?days=&network=&groupBy=analytics:readClicks, installs, revenue and synced spend per row, plus totals. network: meta, google, tiktok, apple_ads, openai or x_ads (default all). groupBy: campaign (default), adgroup, ad, keyword, searchterm or country.
GET /apps/{appId}/events?limit=&cursor=&deviceId=analytics:readThe event log, newest first, paged. deviceId filters to one install.
GET /apps/{appId}/events/live?limit=analytics:readThe most recent events (limit up to 100), for a live view.
GET /apps/{appId}/attributions?limit=&cursor=analytics:readInstall attributions, newest first, paged: id, deviceId, network, campaignName, matchType, createdAt. Test-key installs are left out.
GET /portfolio?days=analytics:readTotals and a per-app comparison across every app of your company.

Event names and properties are described in Events.

Billing and subscriptions

EndpointScopeWhat it does
GET /apps/{appId}/billingintegrations:readThe AppRefer Billing configuration (mode, stores, products, catalogue, outbound webhook) with secrets masked.
PUT /apps/{appId}/billingintegrations:writeMerge an update: mode (off, shadow or primary), apple, google, stripe, webhook, products, defaultEntitlement, and the catalogue sections entitlements, offerings, currentOfferingId, placements, targeting, experiments (each catalogue section sent is replaced whole). Validated before saving; returns the new configuration, masked.
GET /apps/{appId}/billing/customers/{appUserId}billing:readServer-side entitlement check for your own backend: the same customer info the SDK sees (entitlements, activeSubscriptions). URL-encode the user id. 404 when Billing is off for the app.
PUT /apps/{appId}/billing/customers/{appUserId}/entitlements/{entitlementId}billing:writeGrant an entitlement with no purchase (App Review accounts, migrated customers, goodwill). Body { expiresAt, reason }: an ISO 8601 time with a zone, at least a minute ahead and no later than 2100-12-31T23:59:59Z, and why (up to 500 characters). The entitlement must be defined for the app. Repeating the same grant changes nothing; a new expiresAt replaces the old one. Returns grant, changed and the resulting customerInfo. See promotional grants.
DELETE /apps/{appId}/billing/customers/{appUserId}/entitlements/{entitlementId}billing:writeRevoke that grant at once (optional body { reason }). Purchases are untouched. 404 when there is no grant; repeating it changes nothing.
GET /apps/{appId}/billing/experiments/{experimentId}/resultsbilling:readPer-variant results of an offering experiment.
GET /apps/{appId}/billing/readinessanalytics, apps, billing or integrations readFor release checks: mode, connected stores and their public ids, which secrets are set (never their values), when each store last sent a notification, catalogue counts.
GET /apps/{appId}/subscriptions/metrics?start=&end=&bucket=&store=&sandbox=apps:readRevenue, trials, conversions, churn, active subscriptions and MRR. Dates YYYY-MM-DD (UTC, inclusive, default the last 30 days, up to 731 days); bucket day, week or month; store all, app_store, play_store or stripe; sandbox=true for test purchases.
Is this user premium?Terminal
curl "https://apprefer.com/api/v1/apps/app_3f9c2a1b7d4e/billing/customers/user_8421" \
  -H "Authorization: Bearer ak_live_..."

{
  "data": {
    "appUserId": "user_8421",
    "entitlements": {
      "premium": {
        "id": "premium",
        "isActive": true,
        "productId": "com.example.app.annual",
        "store": "app_store",
        "expiresAtMs": 1792000000000,
        "willRenew": true,
        "periodType": "normal",
        "state": "active",
        "subscriptionKey": "2000000123456789"
      }
    },
    "activeSubscriptions": ["com.example.app.annual"],
    "requestDateMs": 1760870400000
  }
}

Set-up, store notifications and the outbound webhook are covered in Billing; the catalogue and experiments in Offerings; metric definitions in Subscription analytics.

Website conversions

EndpointScopeWhat it does
POST /apps/{appId}/web-eventsapps:writeVerified website receipts from your backend: purchase, renewal, refund, and the funnel steps sign_up, checkout_started and start_trial. Needs webEvents.enabled on the app (otherwise 503 web_events_disabled). Unknown fields are refused. 202 means durably accepted; a retry with the same eventId is idempotent, different data is 409.
POST /apps/{appId}/web-purchasesapps:writeThe original ChatGPT Ads web-funnel purchase receipt. Same idempotency rules; new integrations should use /web-events.

Payloads and network mapping: Events, ChatGPT Ads and X Ads.

End users

EndpointScopeWhat it does
POST /apps/{appId}/users/eraseusers:eraseErase one of your app's users when they delete their account. Body: requestId (8 to 64 of A-Z, a-z, 0-9, _ and -), appUserId, deviceIds? (up to 20), environment? (all by default, live or sandbox). Answers 202 for the first call and for every retry with the same requestId; reusing a requestId with different input is 409.
GET /apps/{appId}/users/erase/{requestId}users:eraseStatus of an erase request: status, warehouse step, counts erased, ignored device ids.

What is erased and what is kept for accounting: Privacy.

Support

EndpointScopeWhat it does
GET /apps/{appId}/support/articles?body=apps:read or support:agentEvery help article, drafts and AI-only ones included. body=false leaves the bodies out.
PUT /apps/{appId}/support/articlesapps:write or support:agentSave, publish, unpublish or remove articles: { articles?, publish?, unpublish?, remove? }, up to 200 changes.
GET /apps/{appId}/support/readiness?refresh=any read scope or support:agentPush credentials checked with Apple and Google, email set-up, help-centre counts and the AI mode. refresh=true checks again (at most once a minute).
PUT /apps/{appId}/support/customers/{appUserId}/contextsupport:contextReplace one source's context snapshot for a customer: { environment?, source?, observedAt, staleAfterMinutes?, records }. Typed records only, up to 50. An older observedAt is 409 STALE_SNAPSHOT. See live customer context.
GET /apps/{appId}/support/customers/{appUserId}/context?environment=support:contextWhat is stored for the customer, per source, with stale flags.
DELETE /apps/{appId}/support/customers/{appUserId}/context?environment=&source=support:contextRemove one source, or every source.
POST /apps/{appId}/support/identity-tokenssupport:contextA single-use identity token for a signed-in user: { appUserId, environment?, ttlSeconds? } (30-600, default 300). The app passes it to support.verifyIdentity(token) (SDK 0.12.0+), which verifies its current support session.
PUT /apps/{appId}/support/actionssupport:actionsReplace the app's action catalogue: { actions: [{ id, label, effect, params?, staff?, ai?, refundUsdParam?, environments?, timeoutSeconds?, identityMaxAgeMinutes?, enabled? }] }, up to 20. See resolution actions.
GET /apps/{appId}/support/actionssupport:actionsThe registered actions (with their version) and when your backend last claimed work.
POST /apps/{appId}/support/actions/claimsupport:actionsClaim waiting runs: { environment?, limit? 1-10 }, each for 2 minutes. Re-delivered with the same executionId if no result arrives: apply each at most once.
POST /apps/{appId}/support/actions/runs/{executionId}/completesupport:actionsReport a result: { leaseId, status: succeeded|failed, code?, message?, data? }. The same result again is a no-op; a different one is 409 ALREADY_COMPLETED.
PUT /apps/{appId}/support/testimonials/campaignssupport:testimonialsReplace the app's photo/video campaigns (at most 10): what to ask, media, reward, public-use permission text, frequency, AI and automation. Each stays off until a company admin switches it on in Support.
GET /apps/{appId}/support/testimonials/campaignssupport:testimonialsThe campaigns with their version and on/off state, and when your backend last claimed rewards.
POST /apps/{appId}/support/testimonials/requestssupport:testimonialsAsk one user (from your screens or your coach tool): { campaignId, appUserId, environment?, source?: host|coach, idempotencyKey }. Returns requestId for the SDK's testimonials.present. The reward comes from the campaign.
GET /apps/{appId}/support/testimonials/requests/{requestId}support:testimonialsA request's status, media summary, publication permission and reward state.
POST /apps/{appId}/support/testimonials/rewards/claimsupport:testimonialsClaim earned rewards ({ environment?, limit? 1-10 }, 2-minute leases). Apply each fulfillmentId at most once.
POST /apps/{appId}/support/testimonials/rewards/{fulfillmentId}/completesupport:testimonialsReport a reward: { leaseId, status: fulfilled|failed, receiptId?, code?, message? }; fulfilled needs your receiptId.
GET /apps/{appId}/support/testimonials/library?environment=&limit=support:testimonialsApproved media with public-use permission as a manifest: scopes, whether rewarded, 10-minute download links. No customer identifiers.
GET /apps/{appId}/support/actions/runs/{executionId}support:actionsA run's status, attempts and result.
GET /support/agent/queue?limit=support:agentClaim conversations waiting for an AI answer (limit 1 to 20, default 5), each for 5 minutes.
POST /support/agent/conversations/{id}support:agentPost the agent's answer. Returns what AppRefer did with it: send, schedule, draft or none.
GET /support/agent/knowledge?appId=&sandbox=support:agentPublished help articles and your team's recent answers, personal details masked.
PUT /support/agent/knowledgesupport:agentAdd or replace articles for one app: { appId, articles, publish?, unpublish?, remove? }. New articles are AI-only unless they say audience: "public".

Field-by-field details: Support.

SKAdNetwork and AdAttributionKit

EndpointScopeWhat it does
GET /apps/{appId}/skanskan:readThe conversion-value table saved in the dashboard ({ rows }). It is for reference: the SDKs use AppRefer's built-in conversion schema.
PUT /apps/{appId}/skanskan:writeSave that table: { rows: [...] }.
GET /apps/{appId}/skan/postbacks?days=skan:readSummary of Apple's verified postbacks for the app (days 1 to 400, default 30).

See SKAdNetwork.

Calling the API

curl "https://apprefer.com/api/v1/apps/app_3f9c2a1b7d4e/campaigns?days=30&groupBy=adgroup" \
  -H "Authorization: Bearer $APPREFER_API_KEY"

OpenAPI and agents

An OpenAPI file for the core endpoints (apps, links, integrations, analytics and SKAN) is at /openapi.yaml, with an interactive viewer at /api/v1/docs; this page is the complete list. To let a coding agent work with AppRefer, see AI agents and MCP.

Self-hosted operator endpoints

/api/v1/admin/* (organisations, their apps and stats, one app and its links) accept only the deployment's platform admin key (ak_admin_, the APPREFER_ADMIN_KEY set on the server), never an organisation key. The same admin key is also accepted by the billing configuration, readiness and help article endpoints above. They exist for whoever runs an AppRefer deployment.