Skip to Content
Errors and limits

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." } ] }
FieldMeaning
statusThe HTTP status code.
codeA stable machine code for the problem.
titleThe HTTP status phrase, for humans.
detailWhat went wrong, for humans.
typeAlways about:blank today: status and code identify the problem.
errorsWhat 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, PATH or BODY.
  • name: the parameter or field. A body field is a path into the body: data[3].date is the date of the fourth record.
  • code: why it was rejected, as a stable UPPER_SNAKE_CASE code. UNKNOWN_PARAMETER is a parameter or field the operation doesn’t accept; otherwise it is the rule the value broke, such as IS_DATE_STRING or ARRAY_MAX_SIZE.
  • message: for people.

Status codes

StatuscodeMeansRetry?
400INVALID_REQUESTThe request is invalid; errors says how.No: fix the request.
401UNAUTHORIZEDThe API key is missing, invalid, expired or revoked.No: see Authentication.
403FORBIDDENThe key isn’t valid for this company, or lacks the scope.No: see Authentication.
404NOT_FOUNDThe resource doesn’t exist, or isn’t visible to this key.No.
409CONFLICTThe 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.
500INTERNAL_ERRORSomething went wrong on our side.Contact support if it persists.
503SERVICE_UNAVAILABLETemporarily 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.

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))
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

LimitValue
Records per upload1 to 500
Dimensions per record10
Range of one emissions summary36 months, 40 quarters or 20 years

Requests over a limit are rejected with a 400; nothing is cut short silently.

Last updated on