RESTful API
Version 1.1

Base URL

https://appdrift-backend-1fabfc95f592.herokuapp.com

A machine-readable OpenAPI 3.1 spec of the /v1 surface lives at /.well-known/openapi.json.

Authentication

All /v1 requests authenticate with an AppDrift API access key (ad_live_…) as a Bearer token. Create keys on the Developers page — the full key is shown exactly once; we store only a hash. API access is included on every paid plan (7-day free trial).

Request Header

Authorization: Bearer ad_live_your_key_here

API Endpoints

GET/v1/keyword-difficultyScore any keyword (free)
GET/v1/appsList connected apps (free)
GET/v1/apps/{id}/keywordsTracked keywords + ranks (free)
GET/v1/apps/{id}/opportunitiesPage-one candidates (free)
GET/v1/action-planWeekly ASO action plan (free)
GET/v1/mePlan + token balance (free)
POST/v1/apps/{id}/optimizeAI keyword optimize (3 tokens)
POST/v1/apps/{id}/optimize/applyApply proposal to draft (free)
POST/v1/generate-metadataGenerate ASO metadata (1-2 tokens/field)
POST/v1/translateTranslate a field (1-5 tokens/lang)
GET/webhooksList webhook endpoints (JWT)
POST/webhooksCreate webhook endpoint (JWT)
PATCH/webhooks/{id}Update webhook endpoint (JWT)
DELETE/webhooks/{id}Delete webhook endpoint (JWT)
POST/webhooks/{id}/testSend test delivery (JWT)
GET/webhooks/{id}/deliveriesLast 25 deliveries (JWT)

Reading your ASO data

GET/v1/keyword-difficultyScore any keyword — difficulty + popularity

Query Parameters

keywordRequired. The keyword or phrase to score.
platformios (default) or android
countryTwo-letter storefront code, default us

Response

{
  "error": null,
  "message": "Success",
  "data": {
    "keyword": "meditation",
    "platform": "ios",
    "country": "us",
    "difficulty": 63,
    "popularity": 41,
    "source": "live"
  }
}
GET/v1/appsList the apps connected to your account

Response

{
  "error": null,
  "message": "Success",
  "data": [
    {
      "id": 123,
      "name": "MyApp",
      "platform": "ios",
      "bundle_id": "com.example.myapp",
      "status": "active"
    }
  ]
}
GET/v1/apps/{id}/keywordsTracked keywords with their latest store rank
GET/v1/apps/{id}/opportunitiesPage-one push candidates (rank 11-30, easiest first)
GET/v1/action-planThis week's prioritized ASO actions across all apps
GET/v1/meKey introspection — plan name + remaining token balance
{
  "error": null,
  "message": "Success",
  "data": { "plan": "Pro", "tokens_remaining": 2841 }
}

AI actions

These consume plan tokens at the same prices as the dashboard and only charge on success — a failed call never charges. They return proposals or write to your app's draft version inside AppDrift; nothing reaches the App Store or Play Store without your normal review → publish flow. Check affordability first with GET /v1/me.

POST/v1/apps/{id}/optimizeAI-rewrite metadata toward a target keyword — 3 tokens

Request Body

{
  "keyword": "habit tracker",
  "country": "us"
}

Response

{
  "error": null,
  "message": "Success",
  "data": {
    "platform": "ios",
    "keyword": "habit tracker",
    "current":  { "title": "Drift", "subtitle": "Simple routines", "keywords": "..." },
    "proposed": { "title": "Drift: Habit Tracker", "subtitle": "Daily streaks & reminders", "keywords": "..." },
    "rationale": "…why these changes target the keyword…",
    "tokens_charged": 3
  }
}
POST/v1/apps/{id}/optimize/applyWrite accepted fields into the app's draft version — free

Request Body

{
  "fields": { "subtitle": "Daily streaks & reminders" }
}

Responds 409 NO_DRAFT_VERSION if the app has no editable draft version yet — create one in the dashboard first.

POST/v1/generate-metadataGenerate ASO metadata from a brief — 1 token/short field, 2/long

Request Body

{
  "platform": "ios",
  "app_name": "Drift",
  "app_description": "A minimalist habit tracker with streaks and smart reminders.",
  "fields": ["subtitle", "keywords"],
  "language": "en-US",
  "keywords": ["habit tracker", "daily routine"]
}

iOS fields: name, subtitle, promotional_text, description, keywords. Android: title, short_description, full_description. Omit fields to generate all.

Response

{
  "error": null,
  "message": "Success",
  "data": {
    "platform": "ios",
    "language": "en-US",
    "fields": {
      "subtitle": "Build habits that stick",
      "keywords": "habit,tracker,routine,streak,daily,goals,productivity"
    },
    "tokens_charged": 2
  }
}
POST/v1/translateTranslate one field into up to 15 languages — 1 token/lang (3-5 long)

Request Body

{
  "field": "subtitle",
  "text": "Build habits that stick",
  "target_languages": ["de-DE", "ja", "es-MX"],
  "source_language": "en-US"
}

Fields: title, subtitle, description (3 tokens/lang), whats_new, promotional_text, android_title, short_description, full_description (5 tokens/lang), recent_changes.

Response

{
  "error": null,
  "message": "Success",
  "data": {
    "field": "subtitle",
    "translations": {
      "de-DE": "Gewohnheiten, die bleiben",
      "ja": "続く習慣づくり",
      "es-MX": "Hábitos que perduran"
    },
    "tokens_charged": 3
  }
}

Webhooks

Outbound webhooks push events to your own endpoints as they happen. Available on every paid plan (Free plans receive a 403). Unlike the /v1 endpoints above, these management routes authenticate with your dashboard session token (the JWT issued at login) — not an ad_live_… API key. The easiest way to manage endpoints is the Developers page in your dashboard. Up to 5 endpoints per account. Each endpoint gets an HMAC signing secret shown exactly once at creation — every delivery carries an X-AppDrift-Signature header (t=<unix>,v1=<hex>, HMAC-SHA256 of `${t}.${rawBody}`).

Events

keyword.rank_changeKeyword rank threshold crossed
monitoring.alertStore monitoring alert
action_plan.readyWeekly Autopilot plan ready

Delivery: 4 attempts per event — immediate, then +1m, +10m, +60m. After 20 consecutive failures the endpoint is auto-disabled; re-enable it from the dashboard or via PATCH.

GET/webhooksList webhook endpoints

Response

{
  "error": null,
  "message": "Success",
  "data": [
    {
      "id": 1,
      "url": "https://example.com/webhooks/appdrift",
      "events": ["keyword.rank_change", "monitoring.alert"],
      "secret_suffix": "9d0e1f2a",
      "enabled": true,
      "disabled_reason": null,
      "consecutive_failures": 0,
      "last_success_at": "2026-08-03T09:00:00Z",
      "last_failure_at": null,
      "created_at": "2026-08-01T10:00:00Z"
    }
  ]
}
POST/webhooksCreate webhook endpoint

Request Body

{
  "url": "https://example.com/webhooks/appdrift",
  "events": ["keyword.rank_change", "monitoring.alert", "action_plan.ready"]
}

Response (201)

Includes the full row plus the plaintext signing secret — returned exactly once, never shown again.

{
  "error": null,
  "message": "Store this signing secret now — it won't be shown again.",
  "data": {
    "id": 1,
    "url": "https://example.com/webhooks/appdrift",
    "events": ["keyword.rank_change", "monitoring.alert", "action_plan.ready"],
    "secret": "whsec_9f2c1a7d3b4e5f6a7b8c9d0e1f2a3b4c",
    "secret_suffix": "1f2a3b4c",
    "enabled": true,
    "disabled_reason": null,
    "consecutive_failures": 0,
    "last_success_at": null,
    "last_failure_at": null,
    "created_at": "2026-08-01T10:00:00Z"
  }
}
PATCH/webhooks/{id}Update webhook endpoint

Request Body (all fields optional)

{
  "url": "https://example.com/hooks/new-path",
  "events": ["action_plan.ready"],
  "enabled": true
}

Setting enabled: true re-enables an auto-disabled endpoint. Delete an endpoint with DELETE /webhooks/{id}.

POST/webhooks/{id}/testSend test delivery

Response

{
  "error": null,
  "message": "Success",
  "data": { "ok": true, "status": 200 }
}

// The test delivery is a signed event of type "test.ping" — if your
// receiver validates event types, accept it alongside the three real events.
// On failure, data carries a coarse error instead of a status:
// "data": { "ok": false, "error": "timed_out" }        // or "connection_failed",
//                                                      // "endpoint_disabled", "HTTP 4xx/5xx"
GET/webhooks/{id}/deliveriesLast 25 deliveries

Response

{
  "error": null,
  "message": "Success",
  "data": [
    {
      "event_type": "keyword.rank_change",
      "status": "delivered",
      "attempts": 1,
      "response_status": 200,
      "last_error": null,
      "created_at": "2026-08-03T09:00:00Z",
      "delivered_at": "2026-08-03T09:00:01Z"
    },
    {
      "event_type": "monitoring.alert",
      "status": "failed",
      "attempts": 4,
      "response_status": 500,
      "last_error": "HTTP 500",
      "created_at": "2026-08-02T18:00:00Z",
      "delivered_at": null
    }
  ]
}

status is one of pending, delivered, or failed.

Error Responses

Every response — success or failure — uses the same envelope: { "error", "message", "data" }. On failure, error carries a short machine-readable string.

400 Bad Request

Invalid parameters or body — the error string says which

401 Unauthorized

missing_api_key / invalid_api_key — bad or revoked key

402 Payment Required

insufficient_tokens / free_tokens_exhausted — top up or upgrade

404 Not Found

Resource doesn't exist or belongs to another account

409 Conflict

NO_DRAFT_VERSION on optimize/apply — create a draft version first

429 Too Many Requests

rate_limited — see Rate Limiting below

500 Internal Server Error

Something failed on our side — AI endpoints never charge on failure

Error Response Format

{
  "error": "insufficient_tokens",
  "message": "This call costs 3 token(s); you have 1 remaining.",
  "data": { "required": 3, "remaining": 1 }
}

Rate Limiting

API Rate Limits

Per API key

300 requests / 15 min

AI endpoints, per key

30 requests / 15 min

Standard RateLimit-Limit,RateLimit-Remaining, andRateLimit-Reset headers are returned on every response. A coarse per-IP backstop (500 / 15 min) also applies.