API Reference

Every endpoint, every parameter, every field, every reason a call can be refused.

Basics

All endpoints live under one base URL and speak JSON. There is no SDK to install — every call in this reference is a single HTTP request you can make with curl.

Base URLhttps://buildapi.app/api/v1
  • Method. POST with a form body or JSON body. GET with a query string works too and is convenient for testing; prefer POST in production so keys stay out of access logs.
  • Encoding. application/x-www-form-urlencoded or application/json. Both are read; you do not need to say which.
  • Responses. Always JSON. Every refusal carries a machine-readable reason and, from the enforcement chain, a message for people. Successful answers carry a ts timestamp.
  • Reads and writes. POST /scan runs a fresh scan and POST /assignments changes an assignment. Everything else here only reads, so calling it twice does nothing twice.

Authentication

The key is the authentication. There is no separate token to exchange and no header to sign — you pass key and the gateway answers.

What protects a key depends on which kind it is:

  • Client keys (ak_live_…) are visible in your page source. They are protected by being locked to a verified domain, not by being secret. A client key called from a domain it is not bound to is refused.
  • Server keys are secrets. They have no domain to check, so keep them in an environment variable and out of your repository.

A key is shown in full once, at creation. We store a hash, so we cannot show it to you again — rotate it if you lose it.

POST /api/v1/verify.php

The main endpoint. Answers one question: may this key, from this place, do this thing, right now?

curl -s https://buildapi.app/api/v1/verify.php \
  -d key=ak_live_xxxxxxxxxxxxxxxx \
  -d domain=example.com \
  -d nonce=$(openssl rand -hex 16)

Parameters

NameTypeDescription
keystringrequiredThe API key. Sent in the body or as a query parameter.
domainstringrecommendedThe domain the call is being made for. Compared against the key’s lock. Omit it and a domain-locked key is refused with domain_mismatch.
noncestringrecommendedA value you generate per request. Echoed back inside the signature so a captured response cannot be replayed at you. Any random string; 16 bytes of hex is plenty.
scopestringoptionalThe capability this call needs, e.g. read. Checked against the key’s scopes. Omit it and no scope check is made. A key with no scopes selected has full access.
ipstringoptionalThe visitor’s address, when your server asks on a visitor’s behalf. Checked against the key’s IP allow and deny lists and country rules in place of the connecting address — unless IP binding is switched on, in which case the address the call actually comes from is what counts.
installstringoptionalAn identifier for this installation of your site. A key seen on a second installation waits for your approval (installation_pending), so a copied site does not run on your licence.

Response

Authorised — 200

{
  "authorized": true,
  "reason": "ok",
  "domain": "example.com",
  "key": {"id": 42, "name": "Shop", "prefix": "ak_live_9f2c",
          "environment": "production", "scopes": "full_access",
          "domain": "example.com", "is_trial": false,
          "expires_at": "2026-11-14 09:00:00", …},
  "ts": 1787990400,
  "expires_in": 120,
  "signature": "9f2c…",
  "latency_ms": 7,
  "entitlement": "eyJrIjoi…",
  "entitlement_expires": 1788595200,
  "lifeline": "…",
  "nonce": "3b1f…"
}

Refused — 403

{
  "authorized": false,
  "reason": "domain_not_verified",
  "message": "This domain has not been verified. …",
  "key": {"id": 42, "name": "Shop", …}
}

A refusal names the key only when it is one of ours; an unknown key gets just the reason.

Fields

FieldTypeMeaning
authorizedbooleanalwaysThe answer. Branch on this, then on reason.
reasonstringon refusalMachine-readable cause. See refusal reasons.
messagestringon refusalA sentence for a human. Do not parse it — the wording changes; reason does not.
keyobjectusuallyThe key’s public details: id, name, prefix, environment, industry, scopes (or full_access), domain, is_trial, expires_at. Never the key itself.
domainstringon successThe domain the answer is for. Part of the signature.
tsintegeron successServer time when the answer was produced. Part of the signature.
expires_inintegeron successHow many seconds this answer stays fresh (120). Treat one whose ts is older than that as stale.
signaturestringon successHMAC-SHA256 keyed with your API key, covering ts, the key’s id, the answer, the domain and your nonce. See below.
noncestringif you sent oneYour nonce, echoed back.
latency_msintegeron successHow long we took. Useful when something feels slow and you want to know whose fault it is.
entitlementstringon successA signed token you can cache and re-check offline. See below.
entitlement_expiresintegeron successUnix time the token stops being valid (7 days on).
lifelinestringon successA fallback for the day the service itself is gone, bound to this installation. Keep it with the entitlement.
browser_guardstringclient keysThe page snippet for this key, ready to use. Never sent for a server key.
firewallobjectserver callsThe key’s request rules, when it has any. Only to a server, never to a browser.
noticestringsometimesA message for an outdated client library.

The entitlement token

A successful answer includes a signed, dated token good for 7 days. Cache it. Two things follow:

  • Fewer round trips. Re-check the token locally rather than calling us on every request.
  • You survive our outage. If we are unreachable, keep serving on the token you hold until it expires.

A refusal is not an outage. If we answer and say no, close the gate now. If we do not answer at all, fall back to the cached token. Conflating those two means a revoked key keeps working for days — which is the one thing revocation exists to prevent.

Verifying the signature

The signature is an HMAC-SHA256, keyed with your API key, over the answer’s ts, the key’s id, a 1 for “authorized”, the domain and the nonce you sent, joined with dots. Checking it proves the answer you are holding is the one we gave, and that it is not an older answer replayed.

If the response never leaves the request that fetched it, TLS alone is close enough. The moment you cache it — and you should — check the signature, because a cached answer is exactly what is worth tampering with.

// $r is the decoded response, $apiKey your key, $nonce what you sent (or '').
$msg = $r['ts'] . '.' . $r['key']['id'] . '.1.' . $r['domain'] . ($nonce !== '' ? '.' . $nonce : '');
$expected = hash_hmac('sha256', $msg, $apiKey);
if (!hash_equals($expected, $r['signature']) || abs(time() - $r['ts']) > $r['expires_in']) {
    // Not from us, altered in flight, or too old. Refuse.
}

POST /api/v1/botcheck.php

Asks whether a visitor looks like a browser. Enabled per key in the dashboard; when it is off this endpoint answers {"enforced": false, "allow": true} and does nothing else, so you can leave the call in place and toggle the feature without deploying.

The snippet on your page is what calls this. Turning the setting on without installing the snippet means nothing is ever asked about anything.

The two calls

A check is up to two requests. The first describes the visitor and gets a verdict; the second answers the challenge, if one was issued.

1. Ask

POST /api/v1/botcheck.php
{
  "key": "ak_live_xxxx",
  "action": "assess",
  "signals": { … }
}

2. Answer the challenge

POST /api/v1/botcheck.php
{
  "key": "ak_live_xxxx",
  "action": "grant",
  "nonce": "…",
  "held_ms": 2500
}
NameTypeDescription
keystringrequiredThe client key for the site being protected.
actionstringoptionalassess to ask (the default), grant to answer a challenge.
signalsobjectoptionalWhat the page observed about the visitor. Supplied by the snippet.
noncestringon grantThe nonce from the challenge being answered. It lasts two minutes (expired_challenge after that).
held_msintegeron grantHow long the visitor held before answering. Shorter than the key’s hold time is hold_too_short.
tokenstringoptionalA token from a previous pass, so a returning visitor is not challenged again.

Response

FieldTypeMeaning
enforcedbooleanalwaysWhether bot protection is on for this key. false means every other field is advisory.
allowbooleanalwaysLet this visitor through.
challengebooleansometimesPresent when the visitor should be challenged rather than allowed or refused outright.
noncestringwith challengeReturn this with action=grant.
configobjectwith challengeHow to run the challenge: hold_ms, and allow_desktop, allow_tablet, allow_mobile.
reasonstringon refusalWhy, e.g. bot_ua, hold_too_short, domain_mismatch or invalid_key.
tokenstringon grantGive this back on later checks to skip the challenge.
verifiedbooleanwith a good tokentrue when a returning visitor’s token was accepted.
blocked_urlstringoptionalWhere to send a refused visitor, if you configured one.

GET /api/v1/content.php

Fetches a value you manage in the dashboard, gated by the same key rules as verify. Useful for a message, a feature flag or a price you want to change without deploying.

curl -s "https://buildapi.app/api/v1/content.php?key=ak_live_xxxx&domain=example.com&content_key=banner"
NameTypeDescription
keystringrequiredThe API key.
domainstringrecommendedChecked against the key’s lock, exactly as in verify.
content_keystringoptionalWhich value to return. Omit it and you get every value the key may read. One that does not exist is 404 not_found.

A successful response is {"ok": true, "domain", "content", "ts", "signature", "latency_ms"}. content is keyed by content key, each with its type, value (JSON values come back as JSON) and updated_at. The signature is HMAC-SHA256, keyed with your API key, over ts, the domain and the JSON of content, joined with dots. Refusals carry ok: false and the same reasons as verify.

POST /api/v1/secret.php

Hands your server a value it cannot run without — typically the key that decrypts one column of your own database — so a copy of your files and your data is useless without a working licence. You create the secrets under Dashboard → Runtime Secrets.

Server only. The answer is the secret itself, so the endpoint sends no CORS headers and a browser cannot read it. The key must have an IP allowlist (ip_binding_required otherwise), and only POST is accepted.

curl -s https://buildapi.app/api/v1/secret.php \
  -d key=ak_live_SERVER_KEY \
  -d name=db_field_key \
  -d domain=example.com \
  -d nonce=$(openssl rand -hex 16)
NameTypeDescription
keystringrequiredThe key, bound to your server’s address.
namestringrequiredWhich secret. Missing is 400 missing_name; one that does not exist is 404 unknown_secret.
domainstringrecommendedChecked against the key’s lock, exactly as in verify.
noncestringrecommendedEchoed back and covered by the signature, as in verify.

Returns {"ok": true, "name", "value", "ts", "signature"} (plus nonce if you sent one), signed exactly like a verify answer. Never cache it on disk; hold it in memory.

GET/POST /api/v1/scan.php

Runs and fetches the same read-only security scan the dashboard shows — DNS and email deliverability, plus web-security posture — from your own tooling. It only ever scans a domain your organization has verified, and it is non-destructive: active testing is never exposed here.

# the latest stored result (no new traffic)
curl -s "https://buildapi.app/api/v1/scan.php?key=ak_live_xxxx&domain=example.com"

# run a fresh scan
curl -s -X POST https://buildapi.app/api/v1/scan.php -d key=ak_live_xxxx -d domain=example.com

# every verified domain and its latest grades
curl -s "https://buildapi.app/api/v1/scan.php?key=ak_live_xxxx&domain=example.com&list=1"

# a domain's score history (the trend)
curl -s "https://buildapi.app/api/v1/scan.php?key=ak_live_xxxx&domain=example.com&history=1"

list=1 returns a domains array — one entry per domain your organization has verified, each with its latest web, dns and active grade (or null if never scanned). history=1 returns a history object with a web, dns and active series of {score, grade, fail_count, at} points, oldest first — the same trend the dashboard draws. Both are cheap reads.

NameTypeDescription
keystringrequiredA server (secret) key. Its lock must match the domain, exactly as in verify. A client key is refused with server_key_required — client keys sit in your pages, and findings are a map of your weaknesses.
domainstringrequiredThe domain to scan. Must be one your organization has verified; otherwise the call is refused.

GET returns the last stored scan, or 404 no_scan if none has run yet. POST runs a fresh scan, stores it and returns it. A successful response carries ok: true, fresh, and a web and dns object, each with a grade, score, fail_count, warn_count and a findings array (id, group, label, status, summary, detail, evidence). Each finding also carries acknowledged (true when your team has accepted that risk and the acceptance is still active) and, when a decision exists, an acknowledgement object (note, acked_at, expires_at, expired) — so your own tooling can reflect an accepted risk rather than re-flagging it.

Because a fresh scan makes real outbound requests to the target, POST has its own tight limit — at most 10 fresh scans per hour per organization (429 scan_rate_limited). GET is cheap; fetch the stored result as often as you like. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (a Unix timestamp) for that budget, so a client can pace itself.

GET /api/v1/events.php

A feed of your organization’s security events for a SIEM (Splunk, Elastic, …) to poll: every stored scan report, every regression monitoring finds, and every accepted risk accepted, released, nearing or past its review date. Events are kept for 90 days, oldest first.

Rather not run a poller? Under Dashboard → Portfolio → SIEM export we push the same events to a Splunk HTTP Event Collector, an Elasticsearch _bulk endpoint or Datadog Logs as they happen — at-least-once, with retries, and with a fixed Elastic _id per event so a retry never duplicates one.

# newline-delimited JSON, from the start
curl -s "https://buildapi.app/api/v1/events.php?key=ak_live_xxxx&domain=example.com"

# only what is new since your last poll (pass back X-Next-Cursor)
curl -s "https://buildapi.app/api/v1/events.php?key=ak_live_xxxx&domain=example.com&since=1842"

# RFC 5424 syslog lines instead
curl -s "https://buildapi.app/api/v1/events.php?key=ak_live_xxxx&domain=example.com&format=syslog"
NameTypeDescription
keystringrequiredA server (secret) key; the feed is scoped to its organization. A client key is refused with server_key_required.
domainstringrecommendedThe key’s domain, checked against its lock exactly as in verify (a poller sends no Origin). The feed covers every domain of the organization either way.
sinceintegeroptionalReturn events after this id. Pass back the last X-Next-Cursor and you never miss or repeat one. Default 0.
limitintegeroptionalEvents per call, 1–1000. Default 500. X-Has-More: 1 means call again.
typestringoptionalOnly one type: scan.completed, scan.regression, risk.accepted, risk.unaccepted, risk.expiring, risk.expired, finding.assigned, finding.reopened, finding.overdue, finding.near_target, finding.escalated or finding.status_changed.
formatstringoptionalndjson (default, application/x-ndjson) or syslog (RFC 5424, severity by type: regressions, expired risks, and reopened, overdue or escalated findings are warning).

Each NDJSON line is an object with id, type, time (UTC) and domain, plus the event’s own fields — e.g. kind, grade, score, fail_count on scan.completed, or new_issues and resolved on scan.regression. The finding.* events come one per finding, with finding_key, finding (its name), owner ({name, email} or null), status and due_at — plus by and automatic on finding.assigned, target_date on finding.near_target, and days_overdue on finding.escalated, and from (the previous status) with by on finding.status_changed.

GET /api/v1/assignments.php

Who owns which security finding — the same list as the dashboard’s Assignments page — for your own tracker or reports. Overdue first, then by due date. Server key only.

# open and in-progress findings, overdue first
curl -s "https://buildapi.app/api/v1/assignments.php?key=ak_live_SERVER_KEY&domain=example.com"

# one person's overdue work on one domain
curl -s "https://buildapi.app/api/v1/assignments.php?key=ak_live_SERVER_KEY&domain=example.com&owner=ana@example.com&overdue=1&for_domain=shop.example.com"
NameTypeDescription
keystringrequiredA server (secret) key. A client key is refused with server_key_required.
domainstringrecommendedThe key’s domain, checked against its lock (a backend sends no Origin).
statusstringoptionalopen, in_progress, done or all. Default: open and in progress.
for_domainstringoptionalOnly findings on this scanned domain.
ownerstringoptionalOnly this member’s findings, by email. Not a member → 400 unknown_owner.
overdue1optionalOnly findings past their due date.

Returns {"ok":true,"count":N,"assignments":[…]}; each has domain, finding_key, finding, status, owner ({name, email} or null), due_at, overdue, past_target (open longer than your fix-time target since a scan found it) and updated_at. At most 500.

GET …&history=1 — one finding’s story

curl -s "https://buildapi.app/api/v1/assignments.php?key=ak_live_SERVER_KEY&domain=example.com&history=1&for_domain=shop.example.com&finding_key=dns:spf"

Returns the assignment, its history (time, who, what, oldest first — what people did and what happened on its own: reopened, overdue, escalated…) and its notes (time, who, body). 404 not_found unless that finding is assigned in your organization.

POST /api/v1/assignments.php — update one from your own tracker

# give a finding an owner and a due date (or change its status)
curl -s -X POST "https://buildapi.app/api/v1/assignments.php" \
  -H "Content-Type: application/json" \
  -d '{"key":"ak_live_SERVER_KEY","domain":"example.com","for_domain":"shop.example.com",
       "finding_key":"dns:spf","owner":"ana@example.com","status":"in_progress","due":"2026-11-30"}'

for_domain (a domain your organization has verified) and finding_key are required. owner is a member’s email (empty: nobody), status is open, in_progress or done, and due is a date from today on (empty: clear). Anything you leave out stays as it is. Returns {"ok":true,"assignment":{…}} in the shape above. The usual emails, webhooks and events follow, and the change is on your team activity record, marked as made through the API. Errors: unknown_domain, bad_finding_key, unknown_owner, unknown_status, bad_due, due_in_past; at most 120 changes an hour. Send "recheck":"1" (with for_domain and finding_key) instead to re-scan that finding now: you get passed and check_status, and a passing finding is marked done (30 re-checks an hour). Server key only.

GET /api/v1/activity.php

Your team’s security activity record — risks accepted and released, findings assigned and moved, monitoring and scan settings, trust pages, the SIEM export, evidence downloads, membership changes — newest first. The same record as the Team activity page and every evidence pack. Never includes IP addresses. Server key only.

# the latest 100 actions
curl -s "https://buildapi.app/api/v1/activity.php?key=ak_live_SERVER_KEY&domain=example.com"

# risk decisions in Q3, 500 a page
curl -s "https://buildapi.app/api/v1/activity.php?key=ak_live_SERVER_KEY&domain=example.com&group=risks&from=2026-07-01&to=2026-09-30&limit=500"
NameTypeDescription
keystringrequiredA server (secret) key. A client key is refused with server_key_required.
domainstringrecommendedThe key’s domain, checked against its lock.
from / toYYYY-MM-DDoptionalInclusive date range. A malformed date is 400 bad_from / bad_to.
groupstringoptionalrisks, ownership, monitoring, settings, trust, siem, evidence, members, team_security. Anything else is 400 unknown_group.
for_domainstringoptionalOnly actions about this scanned domain.
limit / pageintegeroptionalPage size (1–500, default 100) and page number. has_more says whether to ask for the next page.

Returns {"ok":true,"total":N,"page":P,"has_more":bool,"activity":[…]}; each has time (UTC), who, action (in words), action_code (e.g. risk_accepted), domain and detail.

GET /api/status.php

The status page as JSON, for your own dashboards and monitors: the overall indicator, each component, 30-day uptime per service, open and recent incidents, and maintenance in progress or ahead. No key and nothing personal in it; readable from any origin (CORS *), cached for 60 seconds, read-only (anything but GET is 405). It reports exactly what the page shows.

curl -s "https://buildapi.app/api/status.php"

// in a browser
const s = await (await fetch("https://buildapi.app/api/status.php")).json();
if (s.status.indicator !== "ok") showBanner(s.status.description);
FieldTypeDescription
status.indicatorstringalwaysok, degraded, down or maintenance (planned work running, nothing worse). status.description says it in words.
components[]arrayalwaysEach part of the platform: name and status — operational, degraded, outage or not_configured.
uptime_30dobject|nullalwaysFrom the outside checks: overall_pct, checks, and services — website, api, dashboard, each with uptime_pct, checks and typical_ms. null if it cannot be read.
incidents.open[] / recent[]arrayalwaysOpen ones, and those resolved in the last 14 days: id, title, impact (minor/major), status, affects (service keys; empty means everything), created_at, resolved_at, postmortem (the write-up of what happened and what changed, or null), and updates newest first.
maintenance.in_progress[] / upcoming[]arrayalwaysid, title, body, affects, starts_at, ends_at.
pageobjectalwaysname, url of the status page, and generated_at.

Times are ISO 8601 in UTC. A trimmed example, during an incident:

{
  "page": {"name": "BuildAPI", "url": "https://buildapi.app/status.php", "generated_at": "2026-10-09T08:12:40Z"},
  "status": {"indicator": "degraded", "description": "Partial service degradation"},
  "components": [{"name": "API Gateway", "status": "degraded"}, {"name": "Database", "status": "operational"}],
  "uptime_30d": {"overall_pct": 99.93, "checks": 1440,
    "services": {"api": {"name": "API", "uptime_pct": 99.86, "checks": 1440, "typical_ms": 131}}},
  "incidents": {"open": [{"id": 12, "title": "Slow key verification", "impact": "minor", "status": "identified",
    "affects": ["api"], "created_at": "2026-10-09T07:40:00Z", "resolved_at": null, "postmortem": null,
    "updates": [{"status": "identified", "body": "A database index was missing; adding it now.", "at": "2026-10-09T07:58:00Z"},
                {"status": "investigating", "body": "Some verify calls take several seconds.", "at": "2026-10-09T07:40:00Z"}]}],
    "recent": []},
  "maintenance": {"in_progress": [], "upcoming": []}
}

Also available: the same history as an Atom feed, a printable quarterly report, and a badge for your own site — /status-badge.php (SVG; ?lang=fr for French) shows operational, degraded, outage or maintenance, coloured to match:

<a href="https://buildapi.app/status.php"><img src="https://buildapi.app/status-badge.php" alt="BuildAPI status" height="20"></a>

Refusal reasons

Every refusal from the enforcement chain carries one of these in reason. Branch on this, not on the status code — a 403 covers 14 different situations with different fixes. Endpoints add a few of their own, named in their sections above (not_found, no_scan, unknown_secret, scan_rate_limited…).

StatusReasonWhat happenedWhat to do
400 missing_key No key was sent at all. Send the key as a key parameter in the POST body or query string.
401 invalid_key The key does not exist, or it has been revoked. Check for a copy-paste truncation. A revoked key never comes back — issue a new one.
403 key_suspended The key is paused — by you on the API Keys page, or by us. If you paused it, resume it from the key's page; if we did, contact support. It comes back as it was, and calls refused meanwhile are not charged.
403 expired The key is past its expiry date. Keys run to the end of the current 30-day window. If the subscription is live the window rolls automatically; if it lapsed, renew and the key reactivates.
403 ip_blocked The caller IP is on the key's deny list. Remove the address from the deny list if it should be allowed. A deny always wins over the allowlist.
403 ip_not_allowed The caller IP is not in the key's allowlist. Add the IP in the dashboard, or clear the allowlist to accept any address. Remember your server's egress IP is not your office IP.
403 geo_blocked The call came from a country the key blocks. Change the key's country rule if that country should be allowed.
403 geo_not_allowed The call came from a country outside the key's allowed list. Add the country to the key's allowed list. An address we cannot place in a country is let through, not refused.
403 insufficient_scope The key lacks the scope the endpoint asked for. Tick the scope on the key, or stop sending scope if you do not need the check. A key with no scopes selected has full access.
403 domain_mismatch The request came from a domain the key is not bound to. Every key is locked to one domain. Either call from that domain, or bind the key to the right one.
403 domain_not_verified The domain is bound but ownership has never been proved. Add the TXT record or the file shown on the key's page, then press Check. Only needs doing once per domain.
429 usage_limit_exceeded The account has spent its call allowance for the current cycle. The allowance refills on your subscription anniversary, not the 1st. It is pooled across all your keys, so adding keys does not add allowance — upgrade for a bigger pool.
429 rate_limited Too many calls from this key in one minute. Set or raise the per-minute limit on the key. This is a burst guard, separate from the monthly allowance.
403 installation_pending The key is running on a second installation that is waiting for your approval. Approve or block it on the key's page. The first installation a key is seen on keeps working.
403 installation_blocked The key is running on an installation you blocked. Approve it on the key's page if it is yours.
403 server_key_required The endpoint returns security data and was called with a client key. Create a server (secret) key and call it from your backend. Client keys are published in your pages by design.
403 ip_binding_required Secrets are only released to a key bound to your server's address, and this key has no IP allowlist. Add your server's IP to the key's allowlist.
403 origin_mismatch Protected content was asked for from a page other than the one it is published on. Request it from the page the content belongs to.

Status codes

  • 200 — the call was answered. Read authorized; it may still be false.
  • 400 — the request was malformed. Usually a missing key.
  • 401 — the key is not one of ours, or no longer is.
  • 403 — the key is real and this call is not permitted. Read reason.
  • 404 — the thing asked for does not exist: a content key, a secret, a stored scan.
  • 429 — a limit was hit, either the burst rate or the cycle allowance.
  • 5xx — our problem. Treat as unreachable, not as a refusal: fall back to your cached entitlement token.

Rate limits and quotas

Two separate limits, often confused:

  • Per-minute rate limit — set per key, a burst guard. Exceeding it returns rate_limited and clears within the minute.
  • Cycle allowance — set by your plan, pooled across every key on the account, reset on your subscription anniversary. Exceeding it returns usage_limit_exceeded until the reset or an upgrade.

A refused call with one of your keys counts against your allowance — a broken integration retrying is traffic you want to notice — except these, which are not your doing or would count twice: key_suspended, usage_limit_exceeded, rate_limited, server_key_required, ip_binding_required, origin_mismatch. A key we do not recognise belongs to no account, so it counts against nobody.

PlanCalls per cycleKeys
Free 1,000 1
Enterprise Unlimited Unlimited
Starter 300,000 3
Professional 2,500,000 15

Versioning

The version is in the path: /api/v1/. Within a version we will add response fields and add optional parameters, and we will not remove a field, change what one means, or make an optional parameter required.

Write your client so an unrecognised field is ignored rather than fatal. If we ever need a breaking change it will be /api/v2/, and v1 will keep working while anyone is using it.

Changes are listed in the changelog.

Try it now

The playground runs these calls against your own key, in your browser.

Get a key