Errors and rate limiting¶
The error envelope¶
Every failed request returns a JSON body with a single error object. The HTTP status is
400 for all application errors, including authentication failures and the rate
limit, so always look at error.code, not just the status.
{
"error": {
"message": "(#100) Tried accessing nonexisting field (budget) on node type (Campaign)",
"type": "OAuthException",
"code": 100,
"fbtrace_id": "AgPFmAMRIgRiW8BDjh1EI"
}
}
| Key | Type | Description |
|---|---|---|
message |
string | Human-readable explanation. Often prefixed with the code in parentheses, e.g. (#100). |
type |
string | OAuthException for almost everything; GraphMethodException when an object id does not exist. |
code |
int | Machine-readable error code. Branch on this. |
error_subcode |
int | Present on some errors (e.g. 33 for unknown object, 2446079 for the rate limit). |
fbtrace_id |
string | Request trace id. Include it when asking for help. |
Successful responses never contain an error key, and error responses never contain
data. A robust client checks "error" in body first.
Error codes¶
| HTTP | code |
type |
Meaning | What to do |
|---|---|---|---|---|
| 400 | 104 |
OAuthException |
Missing access token. No access_token parameter and no Authorization: Bearer header. |
Send the token. See Authentication. |
| 400 | 190 |
OAuthException |
Invalid access token. The token is not recognised. | Check for typos / trailing whitespace; ask the instructor if the token is really yours. Do not retry blindly. |
| 400 | 100 |
OAuthException |
Invalid parameter or field. Unknown field name, bad level, bad time_range, bad limit, bad cursor, unsupported breakdown, and so on. The message says which. |
Fix the request. Retrying unchanged will fail again. |
| 400 | 100 + error_subcode 33 |
GraphMethodException |
Object does not exist (or you cannot access it): an unknown act_..., campaign, ad set or ad id. |
Check the id. Ids come from the listing endpoints; do not invent them. |
| 400 | 17 + error_subcode 2446079 |
OAuthException |
Rate limit reached (more than 60 requests in the last 60 seconds). | Wait 60 seconds, then retry the same request. See below. |
| 400 | 100 |
GraphMethodException |
Unsupported HTTP method. The API is read-only; a POST, PUT, DELETE or other non-GET request to any edge returns (POST) Unsupported method on this edge; use GET. |
Use GET. |
| 400 | 2500 |
OAuthException |
Unknown path. The URL does not match any endpoint: wrong version (/v20.0/...), misspelled edge, extra path segments. |
Check the URL against this reference. Only /v21.0/... exists. |
Examples¶
104 — missing token
{
"error": {
"message": "An access token is required to request this resource.",
"type": "OAuthException",
"code": 104,
"fbtrace_id": "A4J_EQTPMXEW7jShSsHi7"
}
}
190 — invalid token
{
"error": {
"message": "Invalid OAuth access token - Cannot parse access token",
"type": "OAuthException",
"code": 190,
"fbtrace_id": "AokyI0dkKgRSzsQdx1qqT"
}
}
100 — invalid field
{
"error": {
"message": "(#100) installs is not valid for fields param. please check https://ads-docs.huydata.com/meta/insights/ for all valid values",
"type": "OAuthException",
"code": 100,
"fbtrace_id": "AoU73Oo7DoYUfoWTsJcNe"
}
}
100 / 33 — object does not exist
{
"error": {
"message": "Unsupported get request. Object with ID 'act_123' does not exist, cannot be loaded due to missing permissions, or does not support this operation.",
"type": "GraphMethodException",
"code": 100,
"error_subcode": 33,
"fbtrace_id": "AoeFaty9DVAYm_-nQjVGt"
}
}
17 — rate limit
{
"error": {
"message": "(#17) User request limit reached",
"type": "OAuthException",
"code": 17,
"error_subcode": 2446079,
"fbtrace_id": "AmAy2EokYB-wiFkYgTJmO"
}
}
100 — unsupported method
{
"error": {
"message": "(POST) Unsupported method on this edge; use GET",
"type": "GraphMethodException",
"code": 100,
"fbtrace_id": "ARZsCflsQXSq0AI85bY2X"
}
}
2500 — unknown path
{
"error": {
"message": "Unknown path components: /v20.0/me/adaccounts",
"type": "OAuthException",
"code": 2500,
"fbtrace_id": "AxMLIvBknpLU7_-d5ItCE"
}
}
Other code 100 messages you may meet¶
| Message | Fix |
|---|---|
(#100) Tried accessing nonexisting field (xyz) on node type (Campaign) |
Use a field from the object's field table. |
(#100) Param limit must be a positive integer |
limit must be 1..500. |
(#100) The after cursor provided is invalid |
Pass paging.cursors.after unchanged. |
(#100) Invalid parameter: level must be one of account, campaign, adset, ad |
Bad level. |
(#100) ad_name is not available at this level; raise the level parameter |
Dimension finer than level. |
(#100) time_range must be a JSON object with 'since' and 'until' in YYYY-MM-DD |
URL-encode the JSON; check the date format. |
(#100) Invalid parameter: breakdowns value 'age' is not supported (only country) |
Only country. |
(#100) Tried accessing nonexisting field (adsets) on node type (AdSet) |
/adsets only exists under campaigns, /ads under campaigns and ad sets. |
Rate limiting¶
Each access token may make 60 requests per minute, measured over a sliding window of
the last 60 seconds. Every request counts, including /me, errors and empty pages. The
limit is per token, so your pulls do not interfere with other people's.
Monitoring usage: the x-app-usage header¶
Every successful response carries an x-app-usage header with a JSON value. call_count
is the percentage of the per-minute quota you have used in the current window:
x-app-usage: {"call_count": 50, "total_cputime": 25, "total_time": 25}
When call_count approaches 100, slow down. (total_cputime and total_time are
reported for compatibility and track call_count.)
What happens at the limit¶
The 61st request within 60 seconds is rejected with HTTP 400 and error code 17:
HTTP/1.1 400 Bad Request
x-app-usage: {"call_count": 100, "total_cputime": 50, "total_time": 50}
x-fb-ads-insights-throttle: {"app_id_util_pct": 100, "acc_id_util_pct": 100, "ads_api_access_tier": "development_access"}
{
"error": {
"message": "(#17) User request limit reached",
"type": "OAuthException",
"code": 17,
"error_subcode": 2446079,
"fbtrace_id": "AmAy2EokYB-wiFkYgTJmO"
}
}
Rejected requests are not counted against the quota, but the window does not reset
either: you must let the oldest requests age out. There is no Retry-After header.
How to handle it¶
- On
code == 17, wait 60 seconds, then retry the same request (or the samepaging.nextURL). Do not skip the page. - Keep a steady pace: at most 1 request per second is always safe.
- Use
limit=500on every list and insights request to cut the number of pages. - Pull at the coarsest granularity that answers your question. One
level=ad,time_increment=1,breakdowns=countryrequest per account per 3-day window (see Data freshness) is a handful of pages per account, well within the limit. - Never retry
100,104,190or2500without changing the request; they are not transient.
Health check¶
GET https://graph.huydata.com/health (no version segment, no token) returns the service
status and the latest report date currently readable:
{ "status": "ok", "platform": "meta", "latest_report_date": "2026-10-29" }
It does not count against your rate limit.