Errors, limits, and usage
Handle SearchChamp API errors, throttling, plan gates, and usage metering safely.
Response envelope
Successes and handler-generated errors share one JSON shape:
{
"data": null,
"meta": {},
"errors": [
{
"code": "BAD_REQUEST",
"message": "A \"site\" query parameter (domain or slug) is required."
}
]
}errors is empty on success. Do not branch on error-message text; use the HTTP status and errors[].code. Authentication failures rejected at the API Gateway authorizer can be shorter gateway responses rather than this handler envelope.
Status reference
| Status | Common cause | What to do |
|---|---|---|
200 | Successful read, including setup_required states | Read data; treat setup_required as an actionable empty state. |
400 | Missing/ambiguous site, unknown route, unrecognized status filter value, malformed PATCH body, or an unsupported target status | Correct the request. Do not retry unchanged. |
401 | Missing, expired, revoked, or unknown API key | Replace or rotate the key. |
403 | Below-Pro plan, wrong credential type, missing permission, or organization usage ceiling | Check plan, key permissions, and billing/usage. |
404 | Site not found inside the key's organization, or an article/session id that is unknown, malformed, archived, or outside the organization | Call with the exact domain, or verify the id came from the matching feed. The same 404 covers all of these deliberately — a distinguishable response would confirm which ids exist. |
405 | A verb the route does not implement — for example PATCH against a feed path, or POST against anything | Read the Allow header. /v1/api/* implements GET and PATCH only. |
409 | The article or session cannot move to published from its current state | Read meta.current_status and meta.reason. Do not retry unchanged. |
429 | The API key exceeded its per-minute plan allowance | Honor Retry-After when present and retry with backoff. |
503 | Rate limiting or another required guard could not be evaluated | Retry later with bounded exponential backoff. SearchChamp denies the request rather than serving it without protection. |
Rate limits
Public /v1/api/* traffic is counted per API key, per minute. One noisy integration therefore does not consume another key's bucket.
| Plan | Requests per minute per key |
|---|---|
| Pro | 300 |
| Agency | 600 |
| Scale | 900 |
| Enterprise | 1,200 |
The public API is unavailable below Pro even though lower plans have limits for internal application traffic.
On 429, pause new work, honor Retry-After if returned, and retry with jitter. Do not create extra API keys to distribute one integration's traffic; use caching, batching in your own system, and separate keys for operational isolation.
Credits and usage ceilings
Each successful REST call is metered at the same non-zero credit cost as its equivalent MCP read tool:
| REST route | Equivalent MCP tool | Credits per successful call |
|---|---|---|
GET /v1/api/rankings | get_rankings | 1 |
GET /v1/api/ai-visibility/summary | get_ai_visibility | 1 |
GET /v1/api/keywords | get_tracked_keywords | 1 |
GET /v1/api/audit-issues | get_audit_issues | 1 |
GET /v1/api/content/articles | list_content_articles | 1 |
GET /v1/api/content/articles/{id} | get_article_content | 1 |
GET /v1/api/research/sessions | list_research_sessions | 1 |
GET /v1/api/research/sessions/{id} | get_research_session | 1 |
PATCH /v1/api/content/articles/{id} | set_content_article_status | 5 |
PATCH /v1/api/research/sessions/{id} | set_research_session_status | 5 |
The two status updates cost more than a read because they are state changes, not because they do more work — they contact no third party and no CMS. A repeat call that reports alreadyPublished: true is a successful call and is metered like any other.
Denied and invalid calls do not run the underlying read or write and are not debited as successful tool work. The organization-wide monthly usage ceiling also applies; once reached, requests receive 403 until usage becomes available again. The ceiling is checked before both verbs — a status update is never treated as a cheaper request than a read.
Retry pattern
async function searchChampGet(path, params, apiKey) {
const url = new URL(path, 'https://api.searchchamp.com');
for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.ok) return response.json();
if (![429, 503].includes(response.status)) throw new Error(`SearchChamp API ${response.status}`);
const retryAfter = Number(response.headers.get('retry-after'));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1000
: (2 ** attempt) * 500 + Math.random() * 250;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
throw new Error('SearchChamp API retry budget exhausted');
}Version and compatibility boundary
The route prefix is /v1. SearchChamp has not published a broader deprecation schedule yet. Build only against fields documented in the endpoint reference, tolerate additive fields, and do not infer public support from the web app's internal network calls.