Errors
Every SerpKite error uses the same JSON body with a stable code, a human-readable message and a request ID. Errors are never billed. Here is every code and what to do about it.
Error shape
Whenever a call fails, the HTTP status is 4xx or 5xx and the body looks like this:
{
"error": {
"code": "insufficient_credits",
"message": "Your balance is 0 credits. Buy a pack at https://app.serpkite.com/billing.",
"request_id": "req_01J8ZK7T2RX4B"
}
}| Field | Meaning |
|---|---|
error.code |
Stable, machine-readable code. Branch on this, not on the message. |
error.message |
Human-readable explanation. It may change wording over time. |
error.request_id |
The same value as the X-Request-Id header. Quote it when you contact support. |
The shape is the same on every endpoint, including the Custom Search–compatible endpoint (which does not imitate Google’s error format) and the dashboard API.
Errors are never billed. A failed call has X-Credits-Used: 0, and any credits reserved for it are refunded. See Credits and billing.
All error codes
| Status | Code | Retry? | Meaning |
|---|---|---|---|
| 400 | invalid_request | No | A parameter is missing or invalid. |
| 401 | unauthorized | No | The API key is missing, invalid or revoked. |
| 402 | insufficient_credits | No | Your balance is too low. Buy a pack or wait for the monthly free grant. |
| 403 | key_limit_reached | No | This key hit its monthly credit limit. |
| 403 | spend_cap_reached | No | The account's monthly spend cap is reached. Raise it in the dashboard or wait for the next UTC month. |
| 404 | not_found | No | The batch job does not exist, belongs to another account, or its result has expired. |
| 429 | rate_limited | Yes, with backoff | Too many requests per second for your plan. Retry after the Retry-After header. |
| 429 | daily_limit_reached | No | Free-tier daily limit reached (when one is configured). Buying any pack removes it. |
| 500 | internal | Yes, with backoff | Unexpected server error. Not billed; retry with backoff and quote the request_id if it persists. |
| 503 | upstream_error | Yes, with backoff | Google could not be fetched or parsed. Not billed; retry after Retry-After. |
| 503 | upstream_blocked | Yes, with backoff | Google is rate limiting the upstream right now. Not billed; retry after Retry-After. |
| 503 | upstream_timeout | Yes, with backoff | The search took too long. Not billed; retry after Retry-After. |
| 503 | unavailable | Yes, with backoff | The endpoint is temporarily unavailable. Not billed; retry with backoff. |
What to do
Fix the request (400)
invalid_request means a parameter is missing, unknown, out of range or of the wrong type. The message names the parameter, for example page must be between 1 and 10, num > 10 (depth) requires page=1 or include_content is only supported on /v1/search. Retrying the same request will fail the same way. Check the parameter reference and the endpoint’s page.
Unknown parameters are rejected rather than ignored, and when the name is one other SERP APIs use, the message tells you the replacement:
{
"error": {
"code": "invalid_request",
"message": "unknown parameter \"gl\": use country",
"request_id": "req_01J8ZK9W3HC2D"
}
}A JSON array body is also a 400: send one object per call, or queue many with POST /v1/batches. See Strict validation.
Fix the key (401)
unauthorized means the key is missing, malformed or revoked. The message tells you which. Check that you send Authorization: Bearer skt_live_… (or ?api_key= on a GET), and that the key still exists under API keys in the dashboard. See Authentication.
Add credits or raise a limit (402, 403)
insufficient_credits(402): the balance can’t cover the call. Buy a pack or wait for the monthly free grant. Turn on low-balance alerts or auto-recharge so this doesn’t surprise you in production.key_limit_reached(403): this key used its monthly credit limit. Raise or clear the limit on the key, or use another key. Limits reset at the start of the UTC calendar month. See API keys.spend_cap_reached(403): the whole account hit its monthly spend cap. Raise it under Settings or wait for the next UTC month. See Spend controls.
Slow down (429)
rate_limited: too many requests per second for this key, or too many batch jobs queued. Wait forRetry-Afterseconds and retry with backoff. See Rate limits.daily_limit_reached: a free account used its daily allowance. Retrying today won’t help; buying any pack removes the cap.- The no-login playground has its own per-IP limits (5 searches per minute, 20 per day) and answers
429 rate_limitedwhen you pass them. API keys are not affected.
Retry later (5xx)
These are on our side or Google’s, are never billed, and are safe to retry with exponential backoff. Upstream failures come with a Retry-After header (a few seconds); wait at least that long:
upstream_error(503): Google’s page couldn’t be fetched or parsed.upstream_blocked(503): Google is rate limiting our upstream at the moment.upstream_timeout(503): the search took too long.include_contentandnum: 100calls take longer, so give them a generous client timeout.unavailable(503): the endpoint is temporarily unavailable.internal(500): unexpected error. If it persists, send us therequest_id.
404 not_found is only returned by GET /v1/batches/{id}: the job ID is wrong, belongs to another account, or its result expired.
Errors in batch requests
POST /v1/batches returns 202 with one entry per request, in order. An entry that couldn’t be queued (for example an invalid parameter) is an error object instead of a batch job:
{
"batches": [
{ "id": "0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90", "status": "queued", "endpoint": "/v1/search", "poll_url": "https://api.serpkite.com/v1/batches/0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90" },
{ "error": { "code": "invalid_request", "message": "q is required", "request_id": "req_01J8ZKB7M2:1" } }
]
}
Only queued jobs are billed. Check each entry for an error key rather than relying on the HTTP status. Errors that apply to the whole call (a bad key, a rate limit, more than 100 requests) come back as a single error object with the matching status. A job that fails later has status: "failed" and an error object when you poll it, and is refunded.
Handling errors in code
The SDKs turn every error body into a typed exception with the same fields, and retry 429 and 5xx for you (maxRetries / max_retries / WithMaxRetries):
import { SerpKite, SerpKiteError } from "serpkite";
const sk = new SerpKite();
try {
const res = await sk.search({ q: "best espresso machine 2026" });
console.log(res.results.length);
} catch (err) {
if (err instanceof SerpKiteError) {
if (["insufficient_credits", "key_limit_reached", "spend_cap_reached"].includes(err.code)) {
// alert a human: money or limits
}
throw new Error(`${err.status} ${err.code}: ${err.message} (${err.requestId})`);
}
throw err;
}With a raw HTTP client, check the status code and branch on error.code: retry rate_limited, upstream_error, upstream_blocked, upstream_timeout and unavailable with backoff, and surface everything else.
A retry helper with backoff is in Rate limits.
Empty results are not errors
A query that Google answers with no results returns 200 with an empty results array and meta.parse_quality: "empty", and it is not billed. parse_quality: "partial" means some parts of the page could not be parsed; the parts that were are returned. Neither is an error. See Response headers.
Getting help
Email [email protected] with the request_id and the time of the request. We can find the request from its ID alone: we don’t log query text, so the ID is how we trace a call. Service status is at status.serpkite.com.
Last updated: 2026-09-29