Errors and limits
Error format
Every error is a JSON object in the Problem Details format (RFC 9457 ), sent with Content-Type: application/problem+json. The one exception is a 429, which comes from the load balancer and has no such body (see Rate limit).
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"code": "INVALID_REQUEST",
"detail": "1 query parameter is invalid.",
"errors": [
{
"in": "QUERY",
"name": "from",
"code": "IS_ALIGNED_TO_GRANULARITY",
"message": "With granularity=QUARTER, from must be the first month of a quarter: 2025-01, 2025-04, 2025-07 or 2025-10."
}
]
}| Field | Meaning |
|---|---|
status | The HTTP status code. |
code | A stable machine code for the problem. |
title | The HTTP status phrase, for humans. |
detail | What went wrong, for humans. |
type | Always about:blank today: status and code identify the problem. |
errors | What was wrong with each parameter or field, or each record that conflicted. Empty when nothing is listed. |
Branch on status and code only. title, detail and every message are for people and can change at any time. New codes can be added; accept codes you don’t know.
errors
Each entry names one problem with your request:
in: where the value was sent,QUERY,PATHorBODY.name: the parameter or field. A body field is a path into the body:data[3].dateis thedateof the fourth record.code: why it was rejected, as a stableUPPER_SNAKE_CASEcode.UNKNOWN_PARAMETERis a parameter or field the operation doesn’t accept; otherwise it is the rule the value broke, such asIS_DATE_STRINGorARRAY_MAX_SIZE.message: for people.
Status codes
| Status | code | Means | Retry? |
|---|---|---|---|
400 | INVALID_REQUEST | The request is invalid; errors says how. | No: fix the request. |
401 | UNAUTHORIZED | The API key is missing, invalid, expired or revoked. | No: see Authentication. |
403 | FORBIDDEN | The key isn’t valid for this company, or lacks the scope. | No: see Authentication. |
404 | NOT_FOUND | The resource doesn’t exist, or isn’t visible to this key. | No. |
409 | CONFLICT | The request conflicts with the stored data, such as an upload that would change locked records. | No: see Upload data. |
429 | (no body) | Rate limited. | Yes, with exponential backoff. |
500 | INTERNAL_ERROR | Something went wrong on our side. | Contact support if it persists. |
503 | SERVICE_UNAVAILABLE | Temporarily unavailable. | Yes, with exponential backoff. |
An operation that finds nothing to return, such as an empty list or a range without data, answers 200 with empty or null values, not an error. Each operation’s possible errors are listed in the API reference.
Retries
Retry 429 and 503 with exponential backoff: wait about 1 second, then 2, 4, 8 and so on, with some random jitter, up to a maximum. The API sends no Retry-After header, so don’t wait for one. The samples below also retry 502 and 504, which a gateway can return while the service is briefly unreachable, and network errors.
Retrying a read is always safe. Retrying an upload is safe when every record has a correlationId; a record without one is stored again on every attempt. See Upload data.
Python
import random
import time
import requests
RETRY_STATUSES = {429, 502, 503, 504}
def request_with_retry(method, url, *, attempts=6, **kwargs):
"""Sends a request, retrying temporary failures with exponential backoff."""
for attempt in range(attempts):
try:
response = requests.request(method, url, timeout=60, **kwargs)
if response.status_code not in RETRY_STATUSES:
return response
except (requests.ConnectionError, requests.Timeout):
if attempt == attempts - 1:
raise
else:
if attempt == attempts - 1:
return response
time.sleep(min(2**attempt, 60) + random.uniform(0, 1))Python
import random
import time
import requests
RETRY_STATUSES = {429, 502, 503, 504}
def request_with_retry(method, url, *, attempts=6, **kwargs):
"""Sends a request, retrying temporary failures with exponential backoff."""
for attempt in range(attempts):
try:
response = requests.request(method, url, timeout=60, **kwargs)
if response.status_code not in RETRY_STATUSES:
return response
except (requests.ConnectionError, requests.Timeout):
if attempt == attempts - 1:
raise
else:
if attempt == attempts - 1:
return response
time.sleep(min(2**attempt, 60) + random.uniform(0, 1))Rate limit
The API accepts 100 requests per minute per IP address. The limit is enforced at the load balancer, so it counts every request from your address, whichever API key it uses: systems that share one public IP address, such as behind a corporate network, share one budget.
Requests over the limit get a 429 with no Problem body and no Retry-After header. An address that keeps sending, at more than twice the limit in a minute, is blocked for 5 minutes. Back off exponentially on the first 429 rather than retrying at once.
To stay under the limit, spread work out instead of sending it in bursts, and use the batch upload: one request can carry up to 500 records.
Timeouts
Allow at least 30 seconds for every request before your client gives up. The samples on this site use 60 seconds.
Other limits
| Limit | Value |
|---|---|
| Records per upload | 1 to 500 |
| Dimensions per record | 10 |
| Range of one emissions summary | 36 months, 40 quarters or 20 years |
Requests over a limit are rejected with a 400; nothing is cut short silently.