Errors & limits
Every refusal has the same shape and a code a program can act on. Here is each code, what each plan allows, and the headers that say where you are.
The shape of an error
Every refusal is JSON with an error: a code for your program and a message for a person. Some
add a field that says more, such as the source that is locked or the fields of a 422.
errorErrorRequired2 fields
error.codestringRequiredWhat was refused, for a program:
source_locked,not_found,too_many_calls... A code keeps its meaning.error.messagestringRequiredThe same, for a person.
{
"error": {
"code": "bad_key",
"message": "That API key is not valid."
}
}
Error codes
| Status | code | When |
|---|---|---|
| 400 | bad_request | The request asks for two things that cannot go together, such as after and before. |
| 400 | unknown_event_type | A type that is not one of the ten kinds. |
| 400 | currency_needed | min_price or max_price without currency: prices are never compared across currencies. |
| 400 | no_webhook | A test send for an alert that has no webhook address. |
| 401 | not_signed_in | No key was sent. |
| 401 | bad_key | The key is wrong, revoked, or its account is blocked. |
| 402 | alert_limit | The account has as many alerts as its plan allows. A larger plan allows more; at 20 it is a 400. |
| 402 | source_locked | A source your plan does not open. The error names it in source. |
| 403 | account_blocked | The account was blocked. |
| 404 | not_found | No such product, alert or delivery for this account. |
| 422 | invalid_request | A parameter or field is the wrong type or out of range. The message names it. |
| 429 | too_many_calls | Today's calls are used up. Retry-After says how long until midnight UTC. |
| 429 | too_many_tests | A test send less than 5 seconds after the last one. |
| 503 | unavailable | A part of the service is not running. Try again later. |
What to do
- 4xx: the request needs changing. Trying it again unchanged gives the same answer, except
429, which clears when the day turns. - 5xx: the problem is ours. Wait and try again, a little longer each time.
- 402: a store (source) your plan does not open. Choose it on your Stores page if your plan has room, or see the plans.
import time, requests
def get(url, key, **params):
for attempt in range(5):
r = requests.get(url, params=params, timeout=30,
headers={"Authorization": f"Bearer {key}"})
if r.status_code == 429: # today's calls are used
raise RuntimeError(r.json()["error"]["message"])
if r.status_code >= 500: # ours: try again shortly
time.sleep(2 ** attempt)
continue
if not r.ok: # yours: fix the request
error = r.json()["error"]
raise ValueError(f'{error["code"]}: {error["message"]}')
return r.json()
raise RuntimeError("CarrySpec is not answering; try again later")
Daily limits and headers
Calls made with a key count against your plan's calls a day. The count starts again at midnight UTC. While a limit applies, every answer to a key says where you are:
| Header | What it says |
|---|---|
X-RateLimit-Limit | Calls the plan allows a day. |
X-RateLimit-Remaining | Calls left today. |
X-RateLimit-Reset | When the count starts again, in Unix seconds (midnight UTC). |
Retry-After | Seconds until the count starts again. Only on a 429. |
HTTP/1.1 200 OK
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4987
X-RateLimit-Reset: 1793145600
Past the limit, calls are answered 429 too_many_calls until the day turns. GET /api/v1/me says
your limits and api_calls_today.
What each plan allows
| Plan | Stores | Alerts | API calls a day |
|---|---|---|---|
| Free | 1 (included) | 3 | 200 |
| Starter | 5 you choose + 1 included | 20 | 5,000 |
| Pro | Every source | 20 | 50,000 |
Every feature is on every plan. Only calls made with a key are counted.
Other limits
| What | Limit |
|---|---|
| Working keys per account | 5 |
Products per page (page_size) | 100 (50 by default) |
Events per answer (limit) | 500 (50 by default) |
| Test sends | One every 5 seconds |
| Answering a webhook | 20 seconds |
| Webhook timestamp tolerance (on your side) | 5 minutes |