Run up to 50 searches
const url = 'https://api.serpresult.com/v1/bulk';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"target":"google_search","geo":"us","queries":["serp api",{"query":"bing webmaster guidelines","target":"bing_search","geo":"gb","num":2}]}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://api.serpresult.com/v1/bulk \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "target": "google_search", "geo": "us", "queries": [ "serp api", { "query": "bing webmaster guidelines", "target": "bing_search", "geo": "gb", "num": 2 } ] }'Runs every entry of queries and answers one result per entry, in input
order, inside a single 60-second budget. A row is either a bare query
string or an object of per-row overrides; target, geo and locale
on the body are the defaults each row starts from.
Rows are independent: a row that fails validation or fails upstream is
an error row, never a failed request. The HTTP status is 200 whenever
the body itself was accepted, even when every row failed. Rows still
running when the budget expires are answered upstream_timeout.
Credits: one per row that was actually dispatched to the search
provider, including rows that came back upstream_error or
upstream_timeout. Rejected rows, rows that never started, and rows
that failed with internal_error cost nothing. The rate limiter counts
the whole request as one hit.
Quota: on a hard-limited plan a batch carrying more rows than the quota
has left is refused whole with 402 quota_exceeded, before any row runs.
Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”target, geo, locale and source are defaults every row starts from; a row’s own fields override them. The same rules as GET /v1/query apply to each row.
object
The engine to search.
google_search: pages 1–5,numup to 20, safe search supported.bing_search: page 1 only,numup to 20, safe search supported.duckduckgo_search: page 1 only,numup to 10, no safe search.yahoo_search: page 1 only,numup to 10, safe search supported.brave_search: pages 1–5,numup to 20, safe search supported.yandex_search: pages 1–5,numup to 10, no safe search.
A supported country, as an ISO 3166-1 alpha-2 code. Results are
localized to this country, the same ones a user searching there would
see, and the code also picks the default locale.
A language-region tag, xx-yy.
Optional attribution label stored with the search (for example zapier, make, n8n). Stored truncated to 32 characters. Defaults to api when omitted. Not returned in the response.
object
The engine to search.
google_search: pages 1–5,numup to 20, safe search supported.bing_search: page 1 only,numup to 20, safe search supported.duckduckgo_search: page 1 only,numup to 10, no safe search.yahoo_search: page 1 only,numup to 10, safe search supported.brave_search: pages 1–5,numup to 20, safe search supported.yandex_search: pages 1–5,numup to 10, no safe search.
A supported country, as an ISO 3166-1 alpha-2 code. Results are
localized to this country, the same ones a user searching there would
see, and the code also picks the default locale.
A language-region tag, xx-yy.
Optional attribution label stored with the search (for example zapier, make, n8n). Stored truncated to 32 characters. Defaults to api when omitted. Not returned in the response.
Examples
Batch defaults with one row overriding them
{ "target": "google_search", "geo": "us", "queries": [ "serp api", { "query": "bing webmaster guidelines", "target": "bing_search", "geo": "gb", "num": 2 } ]}Responses
Section titled “Responses”The batch was accepted and every row has an outcome.
object
bat_ plus 32 hex characters. Every row’s log entry carries it.
One entry per input row, in input order.
A served row, in the same shape as a GET /v1/query response plus status.
object
The engine to search.
google_search: pages 1–5,numup to 20, safe search supported.bing_search: page 1 only,numup to 20, safe search supported.duckduckgo_search: page 1 only,numup to 10, no safe search.yahoo_search: page 1 only,numup to 10, safe search supported.brave_search: pages 1–5,numup to 20, safe search supported.yandex_search: pages 1–5,numup to 10, no safe search.
A supported country, as an ISO 3166-1 alpha-2 code. Results are
localized to this country, the same ones a user searching there would
see, and the code also picks the default locale.
A language-region tag, xx-yy.
object
One organic result. x-hub-fields is the flat row every marketplace wrapper exposes: these organic fields plus the request echo (query, target, geo, locale, page), with request_id taken from the response id. It is paired with the fixtures in docs/api/fixtures/.
object
1-based rank within the answered page, after de-duplication.
The registrable domain of url (example.co.uk), or the hostname when it has no public suffix.
The engine’s description text. Empty when the engine gave none.
A row that was refused or failed. Carries no request_id of its own; the batch’s is in the X-Request-Id header.
object
invalid_request, unsupported_geo and unsupported_parameter for a row that did not validate (not charged); upstream_error and upstream_timeout for a row that ran and failed (charged); internal_error for a fault on our side (not charged).
Examples
One google row, one bing row, one rejected row
{ "batch_id": "bat_34218d7162344184bebcd4c1506b0e79", "total": 3, "succeeded": 2, "failed": 1, "results": [ { "status": "ok", "id": "qry_49eaefd2a3d24f4b960a6ea17c4ced18", "target": "google_search", "query": "serp api", "geo": "us", "locale": "en-us", "page": 1, "results": { "organic": [ { "position": 1, "title": "Search engine results page - Wikipedia", "url": "https://en.wikipedia.org/wiki/Search_engine_results_page", "domain": "wikipedia.org", "snippet": "A search engine results page (SERP) is a webpage that is displayed by a search engine in response to a query by a user. Query: serp api." }, { "position": 2, "title": "In-depth guide to how Google Search works", "url": "https://developers.google.com/search/docs/fundamentals/how-search-works", "domain": "google.com", "snippet": "Google Search is a fully-automated search engine that uses software known as web crawlers that explore the web regularly to find pages to add to our index." }, { "position": 3, "title": "SERP Features - What They Are & How to Rank For Them - Moz", "url": "https://moz.com/learn/seo/serp-features", "domain": "moz.com", "snippet": "A SERP feature is any result on a Google Search Engine Results Page (SERP) that is not a traditional organic result." } ] } }, { "status": "ok", "id": "qry_4595f489307f4bceb495c4263f3c5ce0", "target": "bing_search", "query": "bing webmaster guidelines", "geo": "gb", "locale": "en-gb", "page": 1, "results": { "organic": [ { "position": 1, "title": "Bing Webmaster Guidelines", "url": "https://www.bing.com/webmasters/help/webmaster-guidelines-30fba23a", "domain": "bing.com", "snippet": "These guidelines cover a broad range of topics and are intended to help you understand how Bing finds, indexes and ranks websites. Query: bing webmaster guidelines." }, { "position": 2, "title": "Microsoft Bing - Wikipedia", "url": "https://en.wikipedia.org/wiki/Microsoft_Bing", "domain": "wikipedia.org", "snippet": "Microsoft Bing, commonly referred to as Bing, is a search engine owned and operated by Microsoft." } ] } }, { "status": "error", "error": "unsupported_parameter", "message": "page is not supported for bing_search" } ]}Headers
Section titled “Headers”The id of this request, identical to request_id in any error body. Quote it in a support request.
Credits charged by this response.
Requests allowed per rolling 60-second window for this credential.
Requests left in the window, after this one.
Unix time (seconds) at which the current window ends.
Requests the account’s quota allows — this billing cycle on a paid plan, for life on the trial.
Credits spent inside that quota as the request arrived. This response is not counted in it — read X-Credits-Used for what the response itself cost.
Requests left in the period’s quota as the request arrived. Never below 0 — once it reaches 0 the credit balance pays for the next request.
Pack credits left on the account AFTER this response, so whatever this request took out of the credit balance is already deducted. Spent only once X-Quota-Remaining is 0. Never expires, and survives the subscription that bought it.
Unix time (seconds) at which the quota resets: the end of the
subscription’s billing cycle, or 00:00:00Z on the 1st for an account
with no subscription cycle behind its plan. This header is the exact
moment — do not derive it from a calendar. Absent on the trial, whose
100 requests never reset.
The body itself failed validation (invalid_request, with issues). Nothing ran and nothing is charged.
The shape of every error response.
object
Every code the API emits.
invalid_request— validation failed;issueslists the fields.unsupported_geo—geois not supported;detailslists the supported codes.unsupported_parameter— the target cannot servepage,numorsafeas sent.unauthorized— no accepted credential.not_found— unknown route, or an API key id not on this account.rate_limited— per-minute limit spent.quota_exceeded— the period’s quota and the credit balance are both spent. Not charged.upstream_error— the search provider failed. Charged.upstream_timeout— the search provider did not answer in time. Charged.no_subscription— a plan change or a credit-pack purchase was asked for on an account with no active subscription.plan_unchanged— a plan change was asked for on the plan the account is already on.billing_unavailable— purchases are closed on this deployment: either it is not wired to Polar, or direct checkout is switched off while the same plans are sold on RapidAPI.internal_error— a fault on our side. Not charged.
Human-readable, safe to show to an end user.
Same value as the X-Request-Id header.
On unsupported_geo only, the supported country codes.
On invalid_request from a validator, one entry per failed field.
object
Dotted path of the field. Empty for the whole input.
Example
{ "error": "invalid_request", "message": "queries must contain between 1 and 50 entries.", "request_id": "90d30843-bbe6-4e74-a287-8f514e722e24", "issues": [ { "field": "queries", "message": "queries must contain between 1 and 50 entries." } ]}Headers
Section titled “Headers”The id of this request, identical to request_id in any error body. Quote it in a support request.
Requests allowed per rolling 60-second window for this credential.
Requests left in the window, after this one.
Unix time (seconds) at which the current window ends.
Requests the account’s quota allows — this billing cycle on a paid plan, for life on the trial.
Credits spent inside that quota as the request arrived. This response is not counted in it — read X-Credits-Used for what the response itself cost.
Requests left in the period’s quota as the request arrived. Never below 0 — once it reaches 0 the credit balance pays for the next request.
Pack credits left on the account AFTER this response, so whatever this request took out of the credit balance is already deducted. Spent only once X-Quota-Remaining is 0. Never expires, and survives the subscription that bought it.
Unix time (seconds) at which the quota resets: the end of the
subscription’s billing cycle, or 00:00:00Z on the 1st for an account
with no subscription cycle behind its plan. This header is the exact
moment — do not derive it from a calendar. Absent on the trial, whose
100 requests never reset.
No accepted credential (unauthorized). A session-only route answers this to a bearer key; the demo header answers this when a credential rides along with it.
The shape of every error response.
object
Every code the API emits.
invalid_request— validation failed;issueslists the fields.unsupported_geo—geois not supported;detailslists the supported codes.unsupported_parameter— the target cannot servepage,numorsafeas sent.unauthorized— no accepted credential.not_found— unknown route, or an API key id not on this account.rate_limited— per-minute limit spent.quota_exceeded— the period’s quota and the credit balance are both spent. Not charged.upstream_error— the search provider failed. Charged.upstream_timeout— the search provider did not answer in time. Charged.no_subscription— a plan change or a credit-pack purchase was asked for on an account with no active subscription.plan_unchanged— a plan change was asked for on the plan the account is already on.billing_unavailable— purchases are closed on this deployment: either it is not wired to Polar, or direct checkout is switched off while the same plans are sold on RapidAPI.internal_error— a fault on our side. Not charged.
Human-readable, safe to show to an end user.
Same value as the X-Request-Id header.
On unsupported_geo only, the supported country codes.
On invalid_request from a validator, one entry per failed field.
object
Dotted path of the field. Empty for the whole input.
Example
{ "error": "unauthorized", "message": "API key is not valid.", "request_id": "7d1c2f34-1b7e-4d2a-9c33-1f6f0e2b8a90"}Headers
Section titled “Headers”The id of this request, identical to request_id in any error body. Quote it in a support request.
The period’s quota AND the credit balance are both spent
(quota_exceeded). Every plan is hard-limited, so this is the answer on
any of them. Nothing ran, nothing was logged and nothing was charged. On
POST /v1/bulk this is also the answer to a batch carrying more rows
than remaining + balance, refused whole before any row runs. On a
paid plan the way out is a credit pack; on the trial it is a paid plan,
since a pack is priced off a subscription. resets_at is null on the
trial, whose requests never come back.
object
Every code the API emits.
invalid_request— validation failed;issueslists the fields.unsupported_geo—geois not supported;detailslists the supported codes.unsupported_parameter— the target cannot servepage,numorsafeas sent.unauthorized— no accepted credential.not_found— unknown route, or an API key id not on this account.rate_limited— per-minute limit spent.quota_exceeded— the period’s quota and the credit balance are both spent. Not charged.upstream_error— the search provider failed. Charged.upstream_timeout— the search provider did not answer in time. Charged.no_subscription— a plan change or a credit-pack purchase was asked for on an account with no active subscription.plan_unchanged— a plan change was asked for on the plan the account is already on.billing_unavailable— purchases are closed on this deployment: either it is not wired to Polar, or direct checkout is switched off while the same plans are sold on RapidAPI.internal_error— a fault on our side. Not charged.
Human-readable, safe to show to an end user.
Same value as the X-Request-Id header.
On unsupported_geo only, the supported country codes.
On invalid_request from a validator, one entry per failed field.
object
Dotted path of the field. Empty for the whole input.
Requests the quota allows — this billing cycle on a paid plan, for life on the trial.
Credits spent inside that quota.
What is left of the period’s quota. 0 when it is spent; above 0 only when a batch asked for more than remaining + balance.
Pack credits left. 0 alongside remaining 0 is what makes this a 402.
When the quota resets — the end of the subscription’s billing cycle, 00:00:00Z on the 1st for an account with no subscription cycle, or null on the trial, which never resets.
Examples
The trial's 100 requests are spent
{ "error": "quota_exceeded", "message": "Your 100 free trial requests are used up. Upgrade to a paid plan to keep searching.", "request_id": "7d1c2f34-1b7e-4d2a-9c33-1f6f0e2b8a90", "plan": "trial", "quota": 100, "credits_used": 100, "remaining": 0, "balance": 0, "resets_at": null}A paid plan with nothing left to spend
{ "error": "quota_exceeded", "message": "Monthly quota of 75000 requests is spent and your credit balance is empty. The quota resets on 2026-10-16. Buy a credit pack in the dashboard to keep searching.", "request_id": "9b8c0e12-4a7f-4f0b-b3d2-6c1e5a9d7f42", "plan": "growth", "quota": 75000, "credits_used": 75000, "remaining": 0, "balance": 0, "resets_at": "2026-10-16T05:05:09.979Z"}A batch bigger than the quota and balance together
{ "error": "quota_exceeded", "message": "This request needs 20 but only 6 remain (0 of the monthly quota plus 6 pack credits). Buy a credit pack in the dashboard to keep searching.", "request_id": "4f2a9c81-6b3d-4e57-9f12-8c0d5a7e3b64", "plan": "growth", "quota": 75000, "credits_used": 75000, "remaining": 0, "balance": 6, "resets_at": "2026-10-16T05:05:09.979Z"}Headers
Section titled “Headers”The id of this request, identical to request_id in any error body. Quote it in a support request.
Requests the account’s quota allows — this billing cycle on a paid plan, for life on the trial.
Credits spent inside that quota as the request arrived. This response is not counted in it — read X-Credits-Used for what the response itself cost.
Requests left in the period’s quota as the request arrived. Never below 0 — once it reaches 0 the credit balance pays for the next request.
Pack credits left on the account AFTER this response, so whatever this request took out of the credit balance is already deducted. Spent only once X-Quota-Remaining is 0. Never expires, and survives the subscription that bought it.
Unix time (seconds) at which the quota resets: the end of the
subscription’s billing cycle, or 00:00:00Z on the 1st for an account
with no subscription cycle behind its plan. This header is the exact
moment — do not derive it from a calendar. Absent on the trial, whose
100 requests never reset.
The credential’s per-minute limit is spent (rate_limited). Wait Retry-After seconds.
The shape of every error response.
object
Every code the API emits.
invalid_request— validation failed;issueslists the fields.unsupported_geo—geois not supported;detailslists the supported codes.unsupported_parameter— the target cannot servepage,numorsafeas sent.unauthorized— no accepted credential.not_found— unknown route, or an API key id not on this account.rate_limited— per-minute limit spent.quota_exceeded— the period’s quota and the credit balance are both spent. Not charged.upstream_error— the search provider failed. Charged.upstream_timeout— the search provider did not answer in time. Charged.no_subscription— a plan change or a credit-pack purchase was asked for on an account with no active subscription.plan_unchanged— a plan change was asked for on the plan the account is already on.billing_unavailable— purchases are closed on this deployment: either it is not wired to Polar, or direct checkout is switched off while the same plans are sold on RapidAPI.internal_error— a fault on our side. Not charged.
Human-readable, safe to show to an end user.
Same value as the X-Request-Id header.
On unsupported_geo only, the supported country codes.
On invalid_request from a validator, one entry per failed field.
object
Dotted path of the field. Empty for the whole input.
Example
{ "error": "rate_limited", "message": "Rate limit of 20 requests per minute exceeded. Retry in 37 seconds.", "request_id": "7d1c2f34-1b7e-4d2a-9c33-1f6f0e2b8a90"}Headers
Section titled “Headers”The id of this request, identical to request_id in any error body. Quote it in a support request.
Requests allowed per rolling 60-second window for this credential.
Requests left in the window, after this one.
Unix time (seconds) at which the current window ends.
Whole seconds to wait before retrying. Never below 1.
Something failed on our side (internal_error). The detail is in our logs under request_id. Nothing is charged.
The shape of every error response.
object
Every code the API emits.
invalid_request— validation failed;issueslists the fields.unsupported_geo—geois not supported;detailslists the supported codes.unsupported_parameter— the target cannot servepage,numorsafeas sent.unauthorized— no accepted credential.not_found— unknown route, or an API key id not on this account.rate_limited— per-minute limit spent.quota_exceeded— the period’s quota and the credit balance are both spent. Not charged.upstream_error— the search provider failed. Charged.upstream_timeout— the search provider did not answer in time. Charged.no_subscription— a plan change or a credit-pack purchase was asked for on an account with no active subscription.plan_unchanged— a plan change was asked for on the plan the account is already on.billing_unavailable— purchases are closed on this deployment: either it is not wired to Polar, or direct checkout is switched off while the same plans are sold on RapidAPI.internal_error— a fault on our side. Not charged.
Human-readable, safe to show to an end user.
Same value as the X-Request-Id header.
On unsupported_geo only, the supported country codes.
On invalid_request from a validator, one entry per failed field.
object
Dotted path of the field. Empty for the whole input.
Example
{ "error": "internal_error", "message": "The request could not be completed.", "request_id": "7d1c2f34-1b7e-4d2a-9c33-1f6f0e2b8a90"}Headers
Section titled “Headers”The id of this request, identical to request_id in any error body. Quote it in a support request.