# Keeley Keys API

Give your agent an app link, an idea, or a competitor list. Get keyword research and ready-to-use App Store listing options.

**Base URL:** `https://aso.hugmediary.com`

**Service setup:** choose the App Store country for each research request. The default is the United States. Check `/health` for `research_available` and `supported_countries`. An API key is provided when access is activated.

[Connect your agent](https://t.me/tkorchagin) · [Agent instructions](/agents.md)

## Quick start

1. Get your API key. Use `https://aso.hugmediary.com` as the base URL.
2. Submit your app or idea to `POST /v1/research`.
3. Check the returned job with `GET /v1/jobs/{job_id}`.
4. Retrieve the report and download the formats you need.

Supply your credentials through your agent's secret configuration. Use HTTPS and the `Authorization: Bearer` header. Never put the key into a prompt, URL, generated report or public repository.

The following is an illustrative request for an unpublished app. Set `ASO_KIT_BASE_URL`, `ASO_KIT_API_KEY` and a unique `ASO_KIT_REQUEST_ID` through your own configuration before running it. Set `ASO_KIT_BASE_URL=https://aso.hugmediary.com`; keep credentials in your secret configuration.

```sh
curl --request POST "$ASO_KIT_BASE_URL/v1/research" \
  --header "Authorization: Bearer $ASO_KIT_API_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: $ASO_KIT_REQUEST_ID" \
  --data '{
    "description": "Relaxing puzzle games for short breaks",
    "app_name": "Pause",
    "include_app_name_in_title": true,
    "country": "us",
    "variants_limit": 10,
    "exports": ["markdown", "xlsx", "csv"]
  }'
```

The response is `202 Accepted` with the job ID, status, queue information and `replayed`. Save the job ID before polling. A job is not a finished report; successful completion supplies `report_id`. Repeating the same operation with the same idempotency key returns its existing job without consuming another submission.

## What you can send

Provide at least one of `description`, `app_url` or a nonempty `competitor_urls` list.

| Field | Type | Purpose |
| --- | --- | --- |
| `description` | string | Describe the app, audience or idea. The app does not need to be published. |
| `app_url` | string | An App Store link for the target app. |
| `competitor_urls` | string array | An optional explicit competitor list. Supplying it disables automatic competitor selection. Can be used without a target app. |
| `app_name` | string | The name you want to use; overrides the name read from an existing app. |
| `include_app_name_in_title` | boolean | Keep the supplied app name in title options. Defaults to false; requires `app_name` when true. |
| `country` | string | Lowercase two-letter App Store storefront code, such as `us`, `gb`, `de`, `ru` or `jp`; defaults to `us`. See `/health` for accepted countries. |
| `competitors_limit` | integer | Requested upper bound for automatic selection, 1–10; defaults to 5. Up to 10 explicit competitors. The operator may configure a lower limit. |
| `variants_limit` | integer | Maximum listing options requested, 1–100; defaults to 10. Fewer may be returned if fewer valid options exist. The operator may configure a lower limit. |
| `exports` | string array | Unique requested formats: `markdown`, `xlsx`, `csv`. Defaults to all three. |

App links must use HTTPS on `apps.apple.com` and include an App Store ID. A target app cannot also be a competitor; duplicate competitors are rejected. Unknown input fields are rejected. Names preserve spelling and punctuation and may contain up to 30 characters, without control characters or surrounding whitespace. Request bodies are limited to 64 KiB.

Without explicit competitors, the service finds relevant apps and prioritizes candidates by rating count. The report includes the selected competitors so you can inspect the research basis.

Each research job covers one country. Request separate jobs for other markets; keep their keywords and popularity data separate. A country and a listing language are different concepts. Country-specific research does not imply that every language is indexed in every storefront.

Set `"country": "gb"` for the United Kingdom, `"de"` for Germany or another accepted storefront code. The country inside an input App Store link does not override this field. Cards, competitor discovery, recommendations and keyword metrics use the requested market. An app unavailable there or missing provider data produces a clear failure or a partial report; the service never silently falls back to US.

Countries follow the [Apple storefront list](https://developer.apple.com/help/app-store-connect/reference/app-information/app-store-localizations/) and [AppFollow country parameters](https://support.appfollow.io/hc/en-us/articles/360020979578-API-Country-and-Language-Parameters). An accepted country code means a request can be submitted, not that every app or keyword has data there.

The generator uses the actual keywords returned for that country. It does not translate descriptions, keywords or listing fields automatically. For discovery, describe the niche using terms relevant to the chosen market, or supply competitors explicitly. The current vocabulary cleaner has English plural normalization and English/Russian stop words; other scripts are preserved without language-specific translation or morphology.

## What you get

### Keyword research

The full list of search phrases where at least one selected competitor ranks in positions 1–10, using the current popularity-above-5 filter. Rows retain available popularity, competitor positions and provenance.

**Popularity helps compare search terms.** It is a source metric, not a count of searches or downloads, and it does not guarantee a ranking. Keep the country and collection date with the values. Missing values remain missing; they are not converted to zero.

### Listing options

Ready-to-use combinations of:

- **Title**, with your app name when requested.
- **Subtitle**.
- **Keywords**, as a comma-separated string for the App Store field.
- Character counts, covered-query counts and available coverage scores.

Options fit the selected generator's field limits: title up to 30 characters, subtitle up to 30 and keywords up to 100. A name that cannot fit produces a validation problem instead of being silently shortened.

The service distributes useful words across the three fields so you can compare complete options instead of fitting every word by hand. Coverage describes the collected phrases represented by an option; it is not a forecast of traffic or ranking.

The **full research list** and an option's **keywords field** are different outputs. The first contains search phrases and evidence. The second fills one limited listing field.

### Reports and files

Markdown for agent reading, XLSX for reviewing the workbook, and CSV for tables. A CSV export may include separate tables for keywords, variants and vocabulary; do not assume the complete report fits into one CSV file. Reports also include available competitor app details from Apple.

The XLSX workbook's first sheet puts the full keyword list and popularity on the left, vocabulary in the middle and up to five best-scoring title/subtitle/keywords options on the right, with character counts. The other sheets retain the complete variants, Top 10 keywords, vocabulary and app details.

## API reference

All routes below are relative to the base URL provided with access. Send the bearer key with API requests, including reports, export discovery and file downloads. `/health`, this documentation, `/docs.md`, `/agents.md` and `/openapi.json` are public. `/reference/` provides an interactive schema reference.

| Method | Route | Result |
| --- | --- | --- |
| `POST` | `/v1/research` | Start a research job from an app, description or competitors. |
| `GET` | `/v1/jobs/{job_id}` | Read progress, terminal state, report ID or error details. |
| `GET` | `/v1/reports/{report_id}` | Read the research summary, selected competitors, keyword data and listing options. Keyword rows are paginated; use `offset` and `limit`. |
| `POST` | `/v1/reports/{report_id}/variants` | Request new listing options from an existing report without recollecting source data. Returns another job to check. |
| `GET` | `/v1/reports/{report_id}/exports` | Discover the available export files and download locations. |
| `GET` | `/v1/reports/{report_id}/files/{file_index}` | Download an export with the same bearer key. Use URLs from export discovery, not guessed indexes. |
| `GET` | `/v1/usage` | Read the assigned allowance, used and remaining task submissions, UTC month and subscription expiry. |

Keys are scoped to their own jobs, reports and exports. A different key receives `404` for those identifiers, even if it belongs to the same person. Reusing a report across keys is not supported in this version.

### Report response

The report includes `report_id`, `job_id`, `country`, `device`, `collected_at`, `source`, `competitors`, `keywords`, `variants`, `vocabulary`, `warnings`, `partial` and the research input. Fresh app-based research also includes available `target_app` and automatic selection `seed_queries`.

Keyword rows expose `keyword`, `popularity`, `rank`, `app`, `app_url`, `ext_id` and provenance. Each variant includes `variant_id`, the three listing fields, `title_chars`, `subtitle_chars`, `keywords_chars`, `balanced_score`, `covered_phrase_count`, coverage percentages and example covered phrases.

`keywords` contains the requested page. `keyword_pagination` supplies `total`, `offset`, `limit` and `next_offset`; defaults are offset 0 and limit 200, with a maximum page of 1000 rows. Use the exports for complete files.

Export discovery returns `report_id` and `files`. Each file has `filename`, `format`, `media_type` and a relative `download_url`. Resolve it against your trusted API base URL and send the bearer key to that host only. No provider logs or checkpoints are exposed as exports.

For new variants, submit `app_name`, `include_app_name_in_title` and `variants_limit` as needed. The existing report keeps its original country and source date. Regenerating options does not refresh the keyword metrics.

### Jobs

| State | Client action |
| --- | --- |
| `queued` | Save the ID and wait before checking again. |
| `running` | Continue checking the same job at a bounded interval. |
| `completed` | Retrieve the report identified by `report_id`. |
| `partial` | Retrieve the available report and disclose missing data and warnings. Do not present it as complete. |
| `failed` | Surface the failure. Do not silently create a replacement job. |

Job responses include `created_at`, `updated_at`, `started_at`, `finished_at`, `queue_wait_seconds` and `run_seconds`. Timing is measured from the service's actual timestamps; an unfinished task has no finish timestamp. Runtime is wall-clock time from first worker start and includes a restart or cleanup delay. Older jobs created before timing was introduced may have missing start/finish measurements.

`queue_position` is 1-based while waiting, 0 while running and null after completion. `jobs_ahead` counts work ahead in the shared queue. `estimated_queue_wait_seconds` and `estimated_run_seconds` give indicative minimum/maximum seconds. Runtime estimates use up to 20 recently completed jobs of the same type when available, with a margin around the observed durations; sparse history retains the broader initial range. `samples` and `basis` identify their source. With no measured history, initial ranges are 60–300 seconds for fresh research and 5–60 seconds for regeneration. A small fresh collection has taken roughly one to three minutes in actual runs; market, competitors, tracking capacity and queue length can make it longer. These ranges are guidance, not deadlines or guarantees. If a running task has exceeded its observed range, or cleanup holds the queue, the wait estimate is null rather than an invented completion time.

Use `poll_after_seconds` and the `Retry-After` response header. The default is five seconds, ten seconds when work is ahead and fifteen seconds while cleanup pauses the queue. Stop waiting after your own deadline and preserve the job ID so the task can be resumed. Do not submit duplicates while waiting.

During provider cleanup, a running job may show `stage: "cleaning_up"` and an actionable `error`. It retains exclusive access; later jobs stay queued until cleanup is confirmed. Temporary keyword batches are backed up, their metrics are saved locally, and only service-added keywords are removed. Existing keywords and apps are preserved. Do not create a new job to work around a pending cleanup.

## Retries and errors

Use one unique `Idempotency-Key` per intended POST operation. The header is required and must contain 8–128 printable ASCII characters without whitespace. Reuse that key and the same body if the transport fails and the outcome is uncertain. A changed operation returns `409`. Keys are scoped to the API key and retained with their job records; there is currently no automatic job deletion.

| HTTP status | Meaning and action |
| --- | --- |
| `400` / `422` | Invalid request. Read the error and correct the input. |
| `401` | Missing or invalid key. Stop and check access configuration. |
| `403` | Access is unavailable for this operation. Surface the reason. |
| `404` | Job or report not found or unavailable to this key. Check the identifier. |
| `409` | Operation conflict, including an idempotency key used with a different body. |
| `429` | Rate or capacity limit. Honor `Retry-After` when present; distinguish temporary throttling from exhausted subscription allowance. |
| `503` | Fresh research is disabled or the dedicated account configuration is missing. Do not retry in a loop; ask the operator to enable collection. |
| `5xx` | Service failure. Retry reads with a bounded backoff; retry writes only with the same idempotency key. |

Operational errors use `{"error":{"code":"...","message":"..."}}`. Input validation errors use FastAPI's `detail` array. A failed job exposes its failure as the job's `error` string. `/health` lists accepted storefront countries. The default deployment accepts all supported storefront codes; an operator can temporarily restrict the list. Invalid or disabled countries return `422 unsupported_country` before a job is created or a task submission is counted.

The default per-key admission limit is ten queued/running jobs and may be changed by the operator. Jobs execute sequentially through one persistent FIFO queue because they share the provider account. Waiting jobs do not modify that workspace. A restart resumes interrupted jobs with their original IDs and saved collection checkpoints.

## Monthly allowance

Each key has an operator-assigned allowance of accepted task submissions per UTC calendar month. For example, a key assigned 100 submissions can enqueue 100 new tasks that month. Read `/v1/usage` for your actual allowance. Both `POST /v1/research` and `POST /v1/reports/{report_id}/variants` consume one submission when a new job is accepted. Completion, partial results or later failure do not change that count. A replay with the same idempotency key and identical body consumes no additional submission. Rejected input, unavailable collection, exhausted quota or admission limits do not create a job and do not consume a submission. Status checks, report reads, export discovery and downloads are free.

`GET /v1/usage` returns `monthly_task_submissions`, `used`, `remaining`, `period`, `subscription_valid_until` and `billing_unit: "task_submission"`. The older `monthly_research_jobs` field is retained as an alias for the assigned allowance; it now means task submissions, including regeneration. Operator-assigned key limits may differ. Historic uncharged jobs are not retroactively charged when this accounting rule is introduced. Revoked or expired keys return `403`.

## Request records

The operator keeps durable request records for API calls, static pages, errors and downloads: timestamp, request ID, key identifier/fingerprint, method, path, HTTP status and response duration. Response duration includes file delivery through the server. Records may include bounded research inputs such as the description, app link, competitor links, name, country and requested formats. Authorization and cookie headers, URL queries and raw request bodies are not stored. Do not include secrets in research descriptions or app names. An `X-Request-ID` response header helps the operator locate a request.

## For agents

Start with the user's app link or description. Keep an explicit competitor list and requested name intact. Use US unless another market was requested. Submit one job per country, keep its ID, wait for completion, then fetch the report and files.

Present recommended title, subtitle and keywords together, with their character counts. Attach the full keyword research and popularity data. Explain partial results and missing metrics; do not invent them. Never change an App Store listing without the user's authorization.

[Read or download the focused agent instructions](/agents.md). Raw documentation is served at `/docs.md`, and the focused guide at `/agents.md`; neither requires JavaScript or an API key. A service URL is still required to access these paths outside the local preview.

## Access

[Connect your agent](https://t.me/tkorchagin) to arrange access, an API key and connection instructions. Allowances are agreed directly. Key issuance and payment are not automated yet.
