How to Handle CoinMarketCap API Errors and Validation Responses
A failed CoinMarketCap API call tells you what went wrong twice: once in the HTTP status, and more precisely in the status object inside the body. Reading only the first is why integrations retry errors that will never clear.
Find Your Code
| Code | HTTP | Meaning | Retry? |
|---|---|---|---|
1001 |
401 | Key is invalid | No, regenerate it |
1002 |
401 | No key on the request | No, add the header |
1003 |
402 | Key not activated | No, activate the plan |
1004 |
402 | Subscription expired | No, renew |
1005 |
403 | Endpoint requires a key | No, add the key |
1006 |
403 | Plan excludes this endpoint | No, upgrade |
1007 |
403 | Key disabled | No, contact support |
1008 |
429 | Per-minute rate limit | Yes, after ~60 seconds |
1009 |
429 | Daily allowance spent | Only after the daily reset |
1010 |
429 | Monthly allowance spent | No, not this period |
1011 |
429 | Per-IP rate limit | Yes, with lower concurrency |
400 |
400 | Bad parameter | No, fix the request |
500 |
500 | Server-side issue | Yes, with backoff |
4001 |
400 | Unresolvable identifier on /v5/cmc-ai/coins/latest |
No, fix the identifier or set skip_invalid |
4002 |
400 | Missing a required parameter, observed on /v1/dex/holders/* |
No, add the parameter |
Key Takeaways
- Check
error_codebefore consuming data even on HTTP 200. A 200 with a non-zero code carries no valid data. - Four different limits return 429, and only the code separates them:
1008per minute,1009per day,1010per month,1011per IP. error_codeis a number on some endpoints and a JSON string on others, split by endpoint family rather than by whether you sent a key. Normalise before comparing.1006and1007return 403 and no retry fixes either: the plan excludes the endpoint, or the key is disabled.- A nonexistent symbol returns HTTP 200 with
error_code"0", so check that the response contains each identifier you asked for. - Two codes exist that no CoinMarketCap documentation lists:
4001for an unresolvable identifier, and4002for a missing required parameter.
The Response Envelope
Every response carries a status object, on success and on failure. It always includes the server time as timestamp, the credits consumed as credit_count, the processing time in milliseconds as elapsed, and on failure error_code and error_message.
{
"status": {
"timestamp": "2026-09-10T07:52:27.273Z",
"error_code": 1002,
"error_message": "API key missing.",
"elapsed": 0,
"credit_count": 0
}
}
Two headers are expected on every request: Accept: application/json, and Accept-Encoding: deflate, gzip to keep responses compact.
The Type of error_code Depends on Which Endpoint You Call
This is the detail most likely to cost an afternoon. error_code arrives as a JSON number on some endpoints and a quoted string on others, and the boundary does not follow any rule you can apply from the outside.
Measured 10 September 2026 by reading the raw response text of each call and checking whether the value was quoted:
error_code as |
Endpoints observed |
|---|---|
Number, e.g. 1002 |
/v1/cryptocurrency/listings/latest, /v2/cryptocurrency/info, /v2/cryptocurrency/ohlcv/historical, /v1/exchange/map, /v1/global-metrics/quotes/latest, /v1/content/latest, /v1/key/info |
String, e.g. "1002" |
/v3/cryptocurrency/listings/latest, /v3/cryptocurrency/quotes/latest, /v3/fear-and-greed/latest, /v1/simple/price, /v1/dex/search, /v4/cmc-ai/latest, /v5/cmc-ai/latest, /v5/cmc-ai/coins/map, and any unrecognised path |
Twenty live observations against pro-api.coinmarketcap.com, 10 September 2026.
Three things make this harder to reason about than it first appears, each measured rather than assumed.
You can reproduce this in ten seconds without a key. Both calls below are unauthenticated, cost nothing, and return 1002:
# /v1 prints "error_code":1002 , a number
curl -s "https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest?slug=bitcoin" | grep -o '"error_code":[^,]*'
# /v3 prints "error_code":"1002" , a string
curl -s "https://pro-api.coinmarketcap.com/v3/cryptocurrency/quotes/latest?slug=bitcoin" | grep -o '"error_code":[^,]*'
The first prints "error_code":1002 and the second prints "error_code":"1002". Same code, same second, same parameter, different JSON type. The /v1 envelope also carries a notice field that the /v3 envelope omits. If you take nothing else from this section, run those two commands against whichever endpoints your own code touches before you write a comparison against an integer.
The keyless prefix does not decide it. Calling an endpoint through /public-api returns the same type as calling it directly. /public-api/v2/cryptocurrency/ohlcv/historical returns a number; /public-api/v3/cryptocurrency/quotes/latest returns a string. The type belongs to the endpoint, not to the surface you reach it through.
Sending a key does not decide it either. A keyed request to /v4/cmc-ai/coins/latest on an expired subscription returns "1004" as a string, with a valid key present on the request.
The version number is not a reliable proxy. Most /v1 endpoints return a number, but /v1/simple/price and the whole /v1/dex/* family return strings. The split tracks which service handles the endpoint, which is not something a client can see.
/v1/cryptocurrency/listings/latest returns a number and /v3/cryptocurrency/listings/latest returns a string. Same logical endpoint, and the v1 version sits on the deprecated reference, so following CoinMarketCap’s own deprecation advice silently changes the type in your error handler.
The practical conclusion is not to memorise the table. It is that no strict equality check against a numeric literal is safe anywhere, so normalise once at the point of parsing and stop thinking about it.
def error_code(payload):
raw = payload.get("status", {}).get("error_code", 0)
try:
return int(raw)
except (TypeError, ValueError):
return -1 # unparseable: treat as an unknown failure
Parse defensively more generally. Read only the fields you need, so new properties added later are ignored rather than breaking a parser, and wrap type-sensitive parsing such as dates in error handling. Keyed error responses, for instance, carry a notice field alongside the five documented keys.
HTTP Status Codes
| Status | Meaning | What to do |
|---|---|---|
| 200 | Success, or a failure carrying a non-zero error_code |
Check error_code before using data |
| 400 | The request could not be processed, usually an invalid argument | Check parameters against the endpoint reference |
| 401 | Missing or invalid credentials | Verify the key and the X-CMC_PRO_API_KEY header |
| 402 | Overdue balance or an unactivated plan | Settle it in the Developer Portal billing tab |
| 403 | The plan does not include this endpoint, or the key is disabled | Check the plan-to-endpoint map, or contact support |
| 429 | A limit was exceeded | Read the code: slow down, or wait for the period to reset |
| 500 | Unexpected server-side issue | Retry with exponential backoff, then check the status dashboard |
Full Error Code Reference
| Code | Constant | HTTP | Meaning | Fix | Source |
|---|---|---|---|---|---|
1001 |
API_KEY_INVALID |
401 | The key is not valid | Regenerate it in the Developer Portal | Observed |
1002 |
API_KEY_MISSING |
401 | No key on the request | Add the X-CMC_PRO_API_KEY header |
Observed |
1003 |
API_KEY_PLAN_REQUIRES_PAYEMENT |
402 | The key is not activated | Activate the plan | Documented |
1004 |
API_KEY_PLAN_PAYMENT_EXPIRED |
402 | The subscription has expired | Renew the subscription | Observed |
1005 |
API_KEY_REQUIRED |
403 | This endpoint requires a key | Include the key; not available keyless | Observed |
1006 |
API_KEY_PLAN_NOT_AUTHORIZED |
403 | The plan does not include this endpoint | Upgrade, or use an endpoint on the current tier | Observed |
1007 |
API_KEY_DISABLED |
403 | The key has been disabled | Contact support. Retries will not help | Documented |
1008 |
API_KEY_PLAN_MINUTE_RATE_LIMIT_REACHED |
429 | Per-minute request rate exceeded | Wait 60 seconds, then retry | Documented |
1009 |
API_KEY_PLAN_DAILY_RATE_LIMIT_REACHED |
429 | Daily limit exhausted | Wait for the daily reset, or upgrade | Documented |
1010 |
API_KEY_PLAN_MONTHLY_RATE_LIMIT_REACHED |
429 | Monthly limit exhausted | Wait for the monthly reset, or upgrade | Documented |
1011 |
IP_RATE_LIMIT_REACHED |
429 | Too many concurrent requests from one IP | Reduce concurrency from that address | Documented |
Observed records a code confirmed by a live request on 10 or 14 September 2026. Documented records a code taken from the errors and rate limits guide: the four rate-limit codes are untested by design, since exercising them means deliberately exhausting a plan.
The Four 429s Are Not Interchangeable
One status code covers limits whose reset windows range from a minute to a month.
- 1008Clears in 60s
The per-minute request rate. A short backoff recovers automatically.
- 1009Clears at daily reset
Credit exhaustion for the day. No amount of backoff clears it.
- 1010Clears next period
Credit exhaustion for the month. The billing cycle has to roll over, or the plan has to change.
- 1011Clears on lower concurrency
An IP-level limit rather than a key-level one, which usually means several workers sharing one address.
A retry loop that treats all four alike will hammer a monthly cap every few seconds until the billing cycle turns over.
import time
import requests
TRANSIENT = {1008, 1011} # backoff clears these
EXHAUSTED = {1009, 1010} # only a reset or upgrade clears these
FATAL = {1001, 1002, 1003, 1004, 1005, 1006, 1007, 400, 4001, 4002}
def call(url, headers, params, retries=4, base_delay=1):
for attempt in range(retries):
r = requests.get(url, headers=headers, params=params)
code = error_code(r.json()) # normalised, see above
if code in FATAL:
raise RuntimeError(f"Not retryable: {code}")
if code in EXHAUSTED:
raise RuntimeError(f"Allowance exhausted: {code}")
if code in TRANSIENT or r.status_code >= 500:
time.sleep(base_delay * (2 ** attempt))
continue
if code:
raise RuntimeError(f"API error {code}")
return r.json()
raise RuntimeError("Max retries exceeded")
The separation matters more than the delays. Backoff belongs to 1008 and 1011; everything else needs a decision rather than a wait.
Errors a Retry Cannot Fix
Three codes look transient and are not. 1006 means the plan does not cover the endpoint, so the request will fail identically until the plan changes. 1007 means the key has been disabled, which support has to resolve. 1005 means the endpoint requires authentication and the request arrived without a key, which is configuration rather than permission.
A call made from browser JavaScript against a keyed endpoint fails with a CORS error rather than an API error code, because client-side calls are not supported. Proxy through a backend, which also keeps the key out of the browser.
Code 4001 on the CMC AI Endpoints
The /v5/cmc-ai family introduces one additional code. On /v5/cmc-ai/coins/latest, which accepts comma-separated lists of crypto_id, slug or symbol, an identifier that cannot be resolved fails the whole request with 4001 by default.
That default is worth changing for most batch work. Passing skip_invalid=true drops the unresolvable identifiers and returns the valid ones, so one bad symbol in a list of fifty does not cost the other forty-nine. Coins skipped this way are not charged.
# One bad identifier fails everything (skip_invalid defaults to false)
GET /v5/cmc-ai/coins/latest?symbol=BTC,ETH,ZZZNOTREAL
# Same request, partial results instead of a failure
GET /v5/cmc-ai/coins/latest?symbol=BTC,ETH,ZZZNOTREAL&skip_invalid=true
The same endpoint enforces mutual exclusivity: exactly one of crypto_id, slug or symbol is required, and supplying two is a request error rather than a silent preference for one.
These endpoints return error_code as a string live, measured 10 September 2026, even though the shipped reference declares the field as type: integer. Their status object includes the documented notice field. Normalise the value rather than trusting either the declared type or the observed one.
Behaviour Worth Knowing Before You Debug
Validation already exists on the current endpoints, and three of its behaviours surprise people.
| Request | Result |
|---|---|
ids=abc |
400, with a message naming the field and its expected format |
ids omitted |
400, 'ids' is a required parameter |
limit=999999 |
400, 'limit' should be a positive number in range [1, 5000] |
ids=1,1 |
200, duplicates accepted rather than rejected |
symbol=ZZZNOTREAL |
200, error_code "0", no indication the asset is unknown |
| A path that does not exist | 200, error_code "500", The system is busy, please try again later! |
Live requests to the keyless endpoints, executed 10 September 2026.
The last two are the ones to code around.
A nonexistent symbol returns success. Nothing in the envelope reports that the asset you asked for is missing, so check that the response actually contains each identifier you requested. This is the strongest argument for using CoinMarketCap IDs from /cryptocurrency/map: a symbol matching several cryptocurrencies resolves silently to the highest market cap, a symbol matching none returns silently empty, and neither surfaces as an error.
A mistyped path returns HTTP 200 with a “system is busy” message. That reads like a transient outage and is not one, so a client that retries on it will retry forever against a URL that will never exist. Check the path against the endpoint reference before assuming the service is degraded.
Pass credentials in the X-CMC_PRO_API_KEY header. A key in a query string is recorded in server logs, proxy logs and browser history, and it leaks through the referrer header, so a URL-borne key should be treated as compromised and rotated.
Common Mistakes
- Retrying every 429.
Four limits share that status.
1008clears in a minute;1010does not clear this month. - Ignoring
error_codeon a 200.A 200 with a non-zero code carries no data. Check the code first, always.
- Comparing
error_codeto a numeric literal.It is a number on the older
/v1and/v2endpoints and a string on/v3, DEX, CMC AI and keyless. Normalise once at parse time. - Backing off on a 403.
1006and1007are plan and key states, and will return the same answer indefinitely. - Retrying “the system is busy”.
On an unknown path that message arrives with HTTP 200 and will never clear. Verify the path first.
- Assuming a missing asset produces an error.
A nonexistent symbol returns 200 with
error_code"0".
A 500 Is Not Always Transient
The status table says to retry a 500 with exponential backoff, and that is right for a genuine server blip. It is not always what a 500 means here. Two behaviours observed on 11 September 2026 break the assumption.
An unrecognised path returns HTTP 200 with error_code "500" and “The system is busy, please try again later!”. A typo in a path segment therefore reads to a client as a successful call reporting a transient fault. /v9/cmc-ai/latest returns a clean 404, so version routing is correct and sub-path routing is not.
At least one real endpoint returns a persistent 500. /v1/dex/holders/trend/list returned HTTP 500 with the same “system is busy” message both without parameters and with valid ones, while its sibling endpoints answered normally in the same pass.
In both cases a correct retry-with-backoff implementation retries forever. So cap your retries on 500 rather than looping indefinitely, and treat a 500 that survives a few attempts as a permanent failure to surface, not a transient one to keep hammering. Log the full path when you do: the most likely cause is that the URL is wrong rather than the server being busy.
Two Codes the Documentation Does Not List
The published error table runs 1001 to 1011. Two more codes are returned live and appear in no error reference.
4001 is documented, but only in passing: it appears in the skip_invalid parameter description on /v5/cmc-ai/coins/latest, not in the error table. It means an identifier could not be resolved.
4002 appears nowhere at all. Its message is “Missing required parameter.”, and it was confirmed by controlled test on 11 September 2026: /v1/dex/holders/count and /v1/dex/holders/tag_count both declare platform and tokenAddress as required. Omitting either returned HTTP 400 with error_code 4002. Supplying both returned HTTP 200. Every other endpoint tested in the same pass declares no required parameter and returned a plain 400 instead, with a different message.
So 4002 means a required parameter is missing, and it is raised by a different validation layer from the generic 400. Both are permanent failures: no retry fixes either, and neither is charged.
That distinction is useful in a handler. A 400 may mean your value was wrong; a 4002 means a parameter was absent entirely, which is a different fix and usually a coding error rather than a data error.
If you are writing a classifier, add both 4001 and 4002 to the fatal set. The published range demonstrably is not the whole set.
Where to Go Next
For what a call costs and which endpoints publish no cost at all, read the credits guide. For 4001 in the context of the endpoints that raise it, see the CMC AI section of the Pro API reference.
FAQ
What does error code 1008 mean?
The per-minute request rate limit for your key has been exceeded, returned with HTTP 429. It resets every 60 seconds, so a short exponential backoff recovers from it.
What is the difference between 1008, 1009 and 1010?
All three are 429s. 1008 is the per-minute request rate and clears in 60 seconds. 1009 is the daily allowance and clears at the daily reset. 1010 is the monthly allowance and clears when the billing period resets. Only 1008 responds to backoff.
Why am I getting a 403 with a valid API key?
Two codes produce that. 1006 means your plan does not include the endpoint you called. 1007 means the key has been disabled and needs support to resolve.
Why does a successful HTTP 200 contain no data?
Because error_code is non-zero. The API returns 200 with an error in the status object in some failure cases, so error_code has to be checked before reading data.
Is error_code a number or a string?
Both, depending on the endpoint. The older /v1 and /v2 market-data endpoints return a number such as 1002. The /v3 endpoints, the DEX family, the CMC AI family and the keyless surface return a string such as "1002". Sending a key does not change it. Normalise the value at parse time.
What does error code 4001 mean?
On /v5/cmc-ai/coins/latest it means one of the identifiers you passed could not be resolved. By default that fails the whole request. Pass skip_invalid=true to drop the bad identifiers and return the valid ones instead.
Why did my request for an unknown symbol return 200 with no error?
A symbol that matches nothing returns a success envelope. The response simply will not contain that asset, so check for each identifier you asked for rather than relying on error_code.
I get "The system is busy, please try again later!" on every attempt. Is the API down?
Check the path first. An unknown endpoint path returns that message with HTTP 200, and no amount of retrying will change it. If the path is correct, check the API status dashboard.
Should I pass my API key as a query parameter?
No. Use the X-CMC_PRO_API_KEY header. Keys in URLs are captured by logs, history and referrer headers.
Why does my browser request fail when the same call works from my server?
Client-side calls to keyed endpoints are not supported and fail with a CORS error, by design, so that keys are not exposed in the browser. Proxy the request through your own backend.


