Quickstart
Create a key, confirm it works, and read your first store profile.
Create an API key
In the dashboard, open Settings → API keys and create a key. Pick a label that names the consumer ("Clay production", "Local testing") and the scopes it needs. For this walkthrough, read:stores is enough.
The full key is shown once, at creation. Store it in your secret manager: afterwards the dashboard shows only its prefix, so a key you lose has to be revoked and replaced.
export ONGOINGAI_API_KEY="oai_live_…"Confirm the key
GET/v1/account is free and tells you what the key can do.
curl https://api.ongoing.ai/v1/account \
-H "Authorization: Bearer $ONGOINGAI_API_KEY"Read two things from the response: key.scopes, which decides what you may call, and plan.limits, which decides how much.
A 401 here means the key is wrong, revoked, or expired. A 200 confirms the key is valid. Later calls can still fail for their own reasons, so check the scopes and limits before a bulk run.
Example account response
The plan and limits below are one example, not what every account receives. Yours are authoritative on your own response.
{
"workspace": { "id": "ws_01J9X4K6ZP3M8Q2R5T7V9W1Y3A", "name": "Acme Agency" },
"key": {
"id": "key_01J9X4K6ZP3M8Q2R5T7V9W1Y3A",
"label": "Local testing",
"prefix": "oai_live_a1b2",
"environment": "live",
"scopes": ["read:stores"],
"expires_at": null
},
"plan": {
"slug": "pro",
"limits": {
"rpm": 120,
"concurrency": 5,
"enrich_batch_max": 100,
"freshness_days": 30,
"history_days": 365,
"live_refreshes_per_period": 100
}
},
"period": {
"starts_at": "2026-09-01T00:00:00Z",
"ends_at": "2026-10-01T00:00:00Z",
"included": {
"lookup_cached": 10000,
"lookup_live": 100,
"history_query": 10000,
"refresh_requested": 0
},
"used": {
"lookup_cached": 0,
"lookup_live": 0,
"history_query": 0,
"refresh_requested": 0
}
}
}Read a store
GET/v1/stores/{domain} takes a hostname and canonicalizes it. A full URL works too, but must be percent-encoded, because the domain is a single path segment; the parameter reference shows that form.
curl https://api.ongoing.ai/v1/stores/gymshark.com \
-H "Authorization: Bearer $ONGOINGAI_API_KEY"An excerpt of a real response, trimmed to one platform, two technologies, and the observation:
{
"match": "matched",
"store": {
"domain": "gymshark.com",
"platform": { "slug": "shopify", "name": "Shopify", "confidence": "high" },
"status": { "state": "active_storefront", "as_of": "2026-09-07T04:06:05.068Z" },
"technologies": [
{ "slug": "attentive", "name": "Attentive", "category": "sms-marketing", "install_state": "active", "confidence": "high", "last_confirmed_at": "2026-09-01T04:39:18.315Z" },
{ "slug": "cloudflare", "name": "Cloudflare", "category": "cdn", "install_state": "active", "confidence": "high", "last_confirmed_at": "2026-09-01T04:39:18.315Z" }
],
"observation": {
"last_completed_scan_at": "2026-09-07T04:06:05.068Z",
"scope": "homepage_only",
"coverage_note": "Observed on the storefront homepage only. A technology that loads on product, cart, or checkout pages may not appear here."
},
"recent_signals": []
}
}That is the shape of every answer: a store, the technologies we saw, and when we saw them. Three fields deserve a look before you use the rest:
observation.last_completed_scan_atandobservation.scope: when we last saw the site and how much of it.technologies[].install_stateandlast_confirmed_at: whether each technology was loading, and when that was last confirmed.recent_signals: the last five confirmed changes, each witheffective_atandconfirmed_at.
This call costs one lookup_cached unit. A 404 not_found costs nothing and means the domain is not in the corpus.
Enrich a batch
POST/v1/stores/enrich answers up to your plan's enrich_batch_max domains in one request and preserves input order. 100 is the contract maximum; the free plan allows 25.
curl https://api.ongoing.ai/v1/stores/enrich \
-H "Authorization: Bearer $ONGOINGAI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-batch-1" \
-d '{"domains": ["gymshark.com", "allbirds.com", "notastore.example"]}'Each result has a match outcome. matched and stale results include the store and are billed. no_match results carry a reason and are free, and inputs that collapse to the same domain are answered individually but billed once.
Generate a new Idempotency-Key for each new batch, then reuse that key only when retrying that same batch. A retry within 24 hours replays the original response with Idempotent-Replayed: true and bills nothing. Reusing a key with a different set of domains returns 409 conflict, which is the API catching a moved batch boundary rather than answering with the wrong rows.
Pick a client
The clients page shows the same calls through the TypeScript SDK, plain fetch, and Python, with the retry and pacing rules already applied.
Next
- Authentication: scopes, test keys, rotation.
- Errors and limits: the error envelope, rate limits, idempotency.
- Observations and evidence: how to read absence and freshness.