SERP API returns Google search results as raw JSON. It runs on the same crawling infrastructure and the same unit price as SERP Checker. This guide covers authentication, every endpoint, the response format, the webhook and the full error code table.
The basics
- Base URL:
https://app.serpupdate.com/api/v1 - Authentication: the
X-API-KEYheader - Search engine: Google, in this phase
- Format: raw JSON with real HTTP status codes
1. Integration workflow
- Get your API key in the app, under SERP API, on the API Keys tab.
- Set your Pingback URL and Pingback Token on the same tab.
- Call
POST /serp-api/triggerwith a keyword and receive asnapshot_id. - Wait for SERPUPDATE to call your webhook when the snapshot finishes.
- Call
GET /serp-api/{snapshot_id}to read the data.
Prefer the webhook over polling
Polling burns requests and adds delay. The pingback webhook calls your server the moment a result is ready.
2. Authentication
Send the X-API-KEY header on every request. The key is a 32 character lowercase hex string with no hyphens.
curl -X GET https://app.serpupdate.com/api/v1/serp-api/languages
-H "X-API-KEY: 3f9a1c7e4b2d48a6915e0c83d7f6b214"Generating a new key disables the old one immediately
Pressing “Get new key” invalidates the current key at once and cannot be undone. Any integration still using the old key will start failing until you update it.
3. Endpoints
3.1. Create a snapshot
POST /serp-api/trigger
This is the billable call. It accepts exactly one keyword per request and returns a snapshot_id straight away, with the data fetched later.
| Parameter | Type | Required | Description |
|---|---|---|---|
keyword |
string | Yes | The keyword to crawl, 1 to 500 characters. |
language |
string | No | Language code such as en or vi. Take it from the languages endpoint. |
country |
string | No | Two letter ISO country code such as US or VN. |
device |
string | No | desktop or mobile. Defaults to desktop. |
start_page |
number | No | First SERP page, starting at 1. Defaults to 1. |
end_page |
number | No | Last SERP page, 10 at most. Defaults to 10, which is the full Top 100. |
location |
string | No | A location name from the locations endpoint. Leave empty if you send uule. |
uule |
string | No | A pre-encoded UULE string. Leave empty to have it derived from location. |
include_aio |
boolean | No | true also captures the AI Overview and its reference list. |
custom_id |
string | No | Your own identifier, useful for reconciliation. |
Pricing is flat per call
One page or all ten costs the same. There is nothing to save by lowering end_page. Only include_aio changes the unit price, and it replaces that price rather than adding a second charge.
Example request
curl -X POST https://app.serpupdate.com/api/v1/serp-api/trigger
-H "X-API-KEY: 3f9a1c7e4b2d48a6915e0c83d7f6b214"
-H "Content-Type: application/json"
-H "Idempotency-Key: order-8841"
-d '{
"input": [
{
"keyword": "best protein powder for muscle gain",
"language": "en",
"country": "US",
"device": "desktop",
"start_page": 1,
"end_page": 10,
"include_aio": true
}
]
}'Response
{ "snapshot_id": "sd_abc123xyz" }One keyword per call
The input field is an array, but only a single element is accepted in this phase. Sending more returns the TOO_MANY_INPUTS error code.
3.2. Avoiding duplicate charges
Send the optional Idempotency-Key header, up to 64 characters, with your trigger call. If the network fails and you retry with the same key, the system recognises it and does not run a second billable crawl. Use one unique value per business event, such as an order reference or a UUID.
3.3. Fetch the result
GET /serp-api/{snapshot_id}
| HTTP status | Meaning |
|---|---|
200 |
Finished. The body is the JSON result array. |
202 |
Still running, no data yet. Wait for the webhook or ask again later. |
404 |
Snapshot not found. |
410 |
The snapshot is past its retention window and the data has been deleted. |
422 |
The crawl failed. Read error_code in the body. |
curl https://app.serpupdate.com/api/v1/serp-api/sd_abc123xyz
-H "X-API-KEY: 3f9a1c7e4b2d48a6915e0c83d7f6b214"3.4. Look up locations and languages
GET /serp-api/locations takes these query parameters: name (partial match), country (ISO-2), page (zero based) and size.
GET /serp-api/languages takes no parameters and returns every language code Google supports.
Use both endpoints to get valid values for location and language before you call trigger.
4. Result format
The body is an array with one entry per keyword. In this phase it always holds exactly one entry.
[
{
"keyword": "best protein powder for muscle gain",
"organic": [
{
"rank": 1,
"title": "Protein Powders For Muscle Growth Support",
"url": "https://www.gnc.com/buy/protein-powders-for-muscle-growth",
"domain": "gnc.com",
"description": "Whey protein is often chosen for its rapid absorption..."
}
],
"aio_text": "AI Overview content in markdown, with citation markers [[1]](https://...)",
"aio_reference": [
{
"rank": 1,
"url": "https://www.healthline.com/nutrition/best-protein-powder-to-build-muscle",
"domain": "healthline.com",
"title": "The 10 Best Protein Powders to Build Muscle",
"description": "Protein powders, especially when combined with resistance training..."
}
]
}
]| Field | Type | Meaning |
|---|---|---|
keyword |
string | The keyword that was crawled, echoed back exactly as you sent it. |
organic |
array | Organic results in rank order. |
organic[].rank |
number | Position on the SERP, starting at 1 and running across all pages. |
organic[].title |
string | The title as shown on Google. |
organic[].url |
string | The real destination URL, not a Google redirect link. |
organic[].domain |
string | The bare domain of that URL. |
organic[].description |
string | The snippet shown under the title. |
aio_text |
string | AI Overview content in markdown, with [[n]] citation markers. |
aio_reference |
array | The sources the AI Overview cites, same shape as organic. |
The two AI Overview fields can be missing
aio_text and aio_reference only appear when you send include_aio: true and Google actually rendered an AI Overview for that query. Otherwise both keys are absent entirely rather than empty. Your parser must handle the missing keys.
5. Pingback webhook
When a snapshot finishes or fails, SERPUPDATE sends a POST request to the URL you configured. This is SERPUPDATE calling your server, not an endpoint you call.
POST https://your-server.com/webhook/serpupdate
User-Agent: SerpUpdate-Webhook/1.0
X-SerpUpdate-Delivery: dlv_a1b2c3d4e5f6
X-SerpUpdate-Token: the token you set on the API Keys tab
Content-Type: application/json
{
"snapshot_id": "sd_abc123xyz",
"status": "ready",
"keyword": "best protein powder for muscle gain",
"collect_time_ms": 18450
}status is ready on success and failed on failure. collect_time_ms is the collection time in milliseconds.
Always verify the token before trusting the payload
Compare the X-SerpUpdate-Token header against the token you set. If it does not match, return 401 and ignore the request. Without this check, anyone who learns your URL can push fake data into your system.
Your server must answer with a 2xx status. If it does not, SERPUPDATE retries up to 3 times, after 30 seconds, 5 minutes and 30 minutes.
Handling the webhook in Node.js
import express from "express";
const app = express();
app.use(express.json());
app.post("/webhook/serpupdate", (req, res) => {
if (req.get("X-SerpUpdate-Token") !== process.env.SERPUPDATE_PINGBACK_TOKEN) {
return res.sendStatus(401);
}
const { snapshot_id: id, status } = req.body;
if (status === "ready") {
console.log("snapshot ready:", id);
}
res.sendStatus(200);
});Configuration requirements
- The Pingback URL must use
https://. Internal addresses are blocked, includinglocalhost,127.0.0.1and the10.x,192.168.xand172.16.xto172.31.xranges. - The Pingback Token must be 16 to 128 characters long.
- The app has a Test pingback button that sends a trial request and shows the status your server returned.
- If a delivery fails, the Request logs table has a button to send it again manually.
6. Error codes
| Code | Meaning |
|---|---|
INVALID_REQUEST |
Invalid request, missing data or malformed JSON. |
INVALID_INPUT |
One of the fields is invalid. |
TOO_MANY_INPUTS |
Only one keyword per run is supported in this phase. |
INVALID_IDEMPOTENCY_KEY |
The Idempotency-Key is too long, 64 characters at most. |
UNAUTHORIZED |
Authentication failed, check your API key. |
INSUFFICIENT_BALANCE |
Not enough balance for this request. Please top up. |
FEATURE_DISABLED |
SERP API is currently disabled for this account. |
SNAPSHOT_EXPIRED |
The snapshot has expired and its data has been deleted. |
SNAPSHOT_FAILED |
The snapshot failed. |
COLLECTION_ERROR |
The request was not accepted. |
COLLECTION_TIMEOUT |
The request timed out. |
COLLECTION_UNAVAILABLE |
The crawling backend is unavailable, try again later. |
TRIGGER_TIMEOUT |
The request could not be started. |
INTERNAL_ERROR |
A system error occurred, try again or contact support. |
Rate limiting
When you exceed the rate limit the API returns HTTP 429 with a Retry-After header in seconds. Wait for that interval before calling again instead of retrying in a tight loop.
Failed runs are not refunded automatically
Most of the codes above come with no automatic refund. If you need a run reviewed, contact support with the snapshot_id.
7. Lifecycle and limits
- Retention: 30 days from the moment a snapshot completes. After that the result endpoint returns
410. - Expiry warning: the Request logs table flags a snapshot once fewer than 3 days remain.
- Snapshot states:
running,ready,failed,expired. - Snapshots cannot be deleted manually. A background job clears them according to the retention policy.
- Page range:
start_pageandend_pageboth sit between 1 and 10.
Keep the original JSON string if you need to reconcile
If you store the response for auditing or signature hashing, keep the exact raw string you received. Parsing it and writing it back out as new JSON changes key order and whitespace, which breaks any byte comparison.

