Errors and quotas¶
Every failed request returns a JSON body in the google.rpc.Status format together
with a matching HTTP status code. This page describes the envelope, lists every error
code the API can return, and explains the request quota.
Error envelope¶
{
"error": {
"code": 400,
"message": "Request contains an invalid argument.",
"status": "INVALID_ARGUMENT",
"details": [
{
"@type": "type.googleapis.com/google.ads.googleads.v18.errors.GoogleAdsFailure",
"errors": [
{
"errorCode": { "queryError": "UNRECOGNIZED_FIELD" },
"message": "Error in query: unrecognized field 'campaign.foo'."
}
],
"requestId": "01a11460b9a44b0d8ae5"
}
]
}
}
| Path | Type | Description |
|---|---|---|
error.code |
number | Same as the HTTP status code |
error.message |
string | Generic description of the failure class |
error.status |
string | gRPC status name: INVALID_ARGUMENT, UNAUTHENTICATED, PERMISSION_DENIED, RESOURCE_EXHAUSTED, NOT_FOUND, METHOD_NOT_ALLOWED |
error.details[0].@type |
string | Always type.googleapis.com/google.ads.googleads.v18.errors.GoogleAdsFailure |
error.details[0].errors[0].errorCode |
object | A single-key object. The key is the error category (queryError, requestError, authenticationError, authorizationError, quotaError); the value is the specific code |
error.details[0].errors[0].message |
string | Human-readable detail — names the bad field, header, token or value |
error.details[0].requestId |
string | Request id; also sent as the x-request-id response header on every request, successful or not |
To handle errors programmatically read error.details[0].errors[0].errorCode and
branch on its key and value. 404 NOT_FOUND (unknown URL) and 405 METHOD_NOT_ALLOWED
(wrong HTTP method) are the only errors without a details array.
HTTP status to category¶
| HTTP | status |
Category key | Meaning | Retry? |
|---|---|---|---|---|
| 400 | INVALID_ARGUMENT |
queryError |
The GAQL query is invalid | No — fix the query |
| 400 | INVALID_ARGUMENT |
requestError |
The request body or paging parameters are invalid | No — fix the request |
| 401 | UNAUTHENTICATED |
authenticationError |
Missing or invalid access token, or missing developer token | No — fix the headers |
| 403 | PERMISSION_DENIED |
authorizationError |
Developer token not allowed, wrong login-customer-id, or customer not accessible |
No — fix the headers / customer id |
| 404 | NOT_FOUND |
— | Unknown URL (typo in the path, wrong version) | No |
| 405 | METHOD_NOT_ALLOWED |
— | GET used on googleAds:search or googleAds:searchStream |
No — use POST |
| 429 | RESOURCE_EXHAUSTED |
quotaError |
Rate limit exceeded | Yes — wait Retry-After seconds |
All error codes¶
queryError (HTTP 400)¶
Raised while parsing or validating the GAQL query. See GAQL › Query errors for the trigger of each code.
EXPECTED_SELECT, EXPECTED_FROM, EXPECTED_BY, BAD_FIELD_NAME,
BAD_RESOURCE_TYPE_IN_FROM_CLAUSE, UNRECOGNIZED_FIELD,
PROHIBITED_RESOURCE_TYPE_IN_SELECT_CLAUSE, PROHIBITED_METRIC_IN_SELECT_OR_WHERE_CLAUSE,
PROHIBITED_SEGMENT_IN_SELECT_OR_WHERE_CLAUSE, EXPECTED_FILTERS_ON_DATE_RANGE,
PROHIBITED_FIELD_IN_ORDER_BY_CLAUSE, PROHIBITED_FIELD_IN_WHERE_CLAUSE,
INVALID_VALUE_WITH_DURING_OPERATOR, INVALID_DATE_FORMAT,
INVALID_VALUE_WITH_BETWEEN_OPERATOR, OPERATOR_FIELD_MISMATCH, BAD_OPERATOR,
BAD_VALUE, EXPECTED_LIST, EXPECTED_AND, BAD_LIMIT_VALUE, BAD_SYMBOL,
UNEXPECTED_INPUT, UNEXPECTED_END_OF_QUERY.
Example — resource name misspelled:
{
"error": {
"code": 400,
"message": "Request contains an invalid argument.",
"status": "INVALID_ARGUMENT",
"details": [
{
"@type": "type.googleapis.com/google.ads.googleads.v18.errors.GoogleAdsFailure",
"errors": [
{
"errorCode": { "queryError": "BAD_RESOURCE_TYPE_IN_FROM_CLAUSE" },
"message": "Error in query: resource 'campaigns' is not available. Use one of: ad_group, ad_group_ad, campaign, customer, geo_target_constant."
}
],
"requestId": "d41a8b4992d840f4933c"
}
]
}
}
Example — ORDER BY on a field that is not selected:
{
"error": {
"code": 400,
"message": "Request contains an invalid argument.",
"status": "INVALID_ARGUMENT",
"details": [
{
"@type": "type.googleapis.com/google.ads.googleads.v18.errors.GoogleAdsFailure",
"errors": [
{
"errorCode": { "queryError": "PROHIBITED_FIELD_IN_ORDER_BY_CLAUSE" },
"message": "Error in query: ORDER BY field 'campaign.name' must appear in SELECT."
}
],
"requestId": "52420009c4134e479d77"
}
]
}
}
Example — OR is not supported:
{
"error": {
"code": 400,
"message": "Request contains an invalid argument.",
"status": "INVALID_ARGUMENT",
"details": [
{
"@type": "type.googleapis.com/google.ads.googleads.v18.errors.GoogleAdsFailure",
"errors": [
{
"errorCode": { "queryError": "UNEXPECTED_INPUT" },
"message": "Error in query: unexpected input 'OR'."
}
],
"requestId": "b569c9aefc6047c1b7d0"
}
]
}
}
requestError (HTTP 400)¶
Raised when the request body or paging parameters are wrong, before the query is parsed.
| Code | Trigger |
|---|---|
INVALID_INPUT |
Body is not valid JSON, or pageSize is not an integer |
MISSING_REQUIRED_FIELD |
Body has no string field named query |
INVALID_PAGE_SIZE |
pageSize outside 1 … 10000 (search only) |
INVALID_PAGE_TOKEN |
pageToken is malformed — not a value returned by this API as nextPageToken (search only) |
{
"error": {
"code": 400,
"message": "Request body must be a JSON object with a 'query' string.",
"status": "INVALID_ARGUMENT",
"details": [
{
"@type": "type.googleapis.com/google.ads.googleads.v18.errors.GoogleAdsFailure",
"errors": [
{
"errorCode": { "requestError": "MISSING_REQUIRED_FIELD" },
"message": "Required field 'query' is missing."
}
],
"requestId": "fbe03c8b06294974bb91"
}
]
}
}
{
"error": {
"code": 400,
"message": "pageSize must be between 1 and 10000.",
"status": "INVALID_ARGUMENT",
"details": [
{
"@type": "type.googleapis.com/google.ads.googleads.v18.errors.GoogleAdsFailure",
"errors": [
{
"errorCode": { "requestError": "INVALID_PAGE_SIZE" },
"message": "pageSize must be between 1 and 10000."
}
],
"requestId": "50d8c43734564bb0b353"
}
]
}
}
{
"error": {
"code": 400,
"message": "Request contains an invalid argument.",
"status": "INVALID_ARGUMENT",
"details": [
{
"@type": "type.googleapis.com/google.ads.googleads.v18.errors.GoogleAdsFailure",
"errors": [
{
"errorCode": { "requestError": "INVALID_PAGE_TOKEN" },
"message": "The pageToken is not valid for this query."
}
],
"requestId": "35b7d8209c184e199ae3"
}
]
}
}
authenticationError (HTTP 401)¶
| Code | Trigger |
|---|---|
AUTHENTICATION_ERROR |
Authorization: Bearer … header missing (or not a Bearer scheme) |
OAUTH_TOKEN_INVALID |
Access token unknown or expired |
DEVELOPER_TOKEN_PARAMETER_MISSING |
developer-token header missing |
{
"error": {
"code": 401,
"message": "Request had invalid authentication credentials.",
"status": "UNAUTHENTICATED",
"details": [
{
"@type": "type.googleapis.com/google.ads.googleads.v18.errors.GoogleAdsFailure",
"errors": [
{
"errorCode": { "authenticationError": "OAUTH_TOKEN_INVALID" },
"message": "The access token is invalid or expired."
}
],
"requestId": "75170ed3ac3a4452b0a7"
}
]
}
}
Full examples for every variant are in Authentication.
authorizationError (HTTP 403)¶
| Code | Trigger |
|---|---|
DEVELOPER_TOKEN_NOT_ON_ALLOWLIST |
developer-token value is not recognised |
INVALID_LOGIN_CUSTOMER_ID_SERVING_CUSTOMER_ID_COMBINATION |
login-customer-id is present and is not 9990001111 |
USER_PERMISSION_DENIED |
The {customerId} in the URL is not in customers:listAccessibleCustomers |
{
"error": {
"code": 403,
"message": "The caller does not have permission",
"status": "PERMISSION_DENIED",
"details": [
{
"@type": "type.googleapis.com/google.ads.googleads.v18.errors.GoogleAdsFailure",
"errors": [
{
"errorCode": { "authorizationError": "USER_PERMISSION_DENIED" },
"message": "User doesn't have permission to access customer 1234567890. Call customers:listAccessibleCustomers to see the customers you can query."
}
],
"requestId": "aa7e765e6f7c42f68923"
}
]
}
}
quotaError (HTTP 429)¶
| Code | Trigger |
|---|---|
RESOURCE_EXHAUSTED |
More than 60 requests in the trailing 60 seconds for your access token |
The response carries a Retry-After header with the number of seconds to wait; the
same number appears in the detail message.
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60
x-request-id: 0ad62385e17a4af6af96
{
"error": {
"code": 429,
"message": "Resource has been exhausted (e.g. check quota).",
"status": "RESOURCE_EXHAUSTED",
"details": [
{
"@type": "type.googleapis.com/google.ads.googleads.v18.errors.GoogleAdsFailure",
"errors": [
{
"errorCode": { "quotaError": "RESOURCE_EXHAUSTED" },
"message": "Too many requests. Retry in 60 seconds."
}
],
"requestId": "0ad62385e17a4af6af96"
}
]
}
}
NOT_FOUND (HTTP 404)¶
An unknown path (for example /v18/customers/123/googleAds:foo, or /v17/...) returns
a google.rpc.Status body without details:
{
"error": {
"code": 404,
"message": "The requested URL /v18/customers/123/googleAds:foo was not found on this server.",
"status": "NOT_FOUND"
}
}
METHOD_NOT_ALLOWED (HTTP 405)¶
Both search endpoints accept POST only; customers:listAccessibleCustomers is GET.
Sending GET to a search endpoint returns a google.rpc.Status body without details.
The check happens before authentication, so the response is the same with or without
headers:
{
"error": {
"code": 405,
"message": "GET is not allowed on /v18/customers/5423891542/googleAds:searchStream; googleAds:search and googleAds:searchStream take POST.",
"status": "METHOD_NOT_ALLOWED"
}
}
Quotas and rate limiting¶
| Limit | Value |
|---|---|
| Requests per minute | 60 per access token, measured over a sliding 60-second window |
| Scope | All endpoints and all customers together — ten customers queried in a loop share the same 60 |
| Counted | Every request whose three headers are valid, successful or not — including requests that then fail with USER_PERMISSION_DENIED, a queryError or a requestError. Requests rejected for a missing/invalid access token, developer token or login-customer-id are not counted |
| On breach | HTTP 429, quotaError: RESOURCE_EXHAUSTED, Retry-After: <seconds> |
| Result size | Not limited: searchStream returns the whole result set in batches, search up to 10,000 rows per page |
Staying within quota¶
- Prefer fewer, larger queries. One
searchStreamcall for 30 days of ad-level rows costs one request; thirty daily calls cost thirty. A full month at the finest grain (ad × country × day) for one customer is under 2,000 rows and comes back in two batches of a single request. - Use
searchStreamfor pulls,searchfor exploration. Paging throughsearchcosts one request per page. - Cache static data.
customers:listAccessibleCustomers,geo_target_constantand entity attributes change rarely; refresh them once per run, not per query. - Back off on 429. Sleep for
Retry-Afterseconds (never less than 1) and retry the same request. Do not retry 400/401/403 — they will fail again until the request is fixed.
import time
import requests
def post_with_retry(url, headers, body, max_attempts=5):
for attempt in range(max_attempts):
resp = requests.post(url, headers=headers, json=body, timeout=60)
if resp.status_code != 429:
return resp
wait = int(resp.headers.get("Retry-After", "60"))
time.sleep(max(wait, 1))
return resp