Requests
Errors & limits
Every error has one shape: a code you can switch on, a message you can read, the field at fault, and what to do about it.
Find a code (A–Z)
BACKEND_FAILURE502BILLING_UNAVAILABLE503BUDGET_EXCEEDED402CLASSIFIER_FIT_FAILED422CLASSIFIER_NOT_FOUND404CONCURRENCY_LIMITED429CONFLICT409IDEMPOTENCY_KEY_REUSED422INSUFFICIENT_CREDIT402INTERNAL500INVALID_REQUEST422IP_NOT_ALLOWED403KEY_EXPIRED401KEY_REVOKED401NO_REFERENCE_DATA422NOT_FOUND404PAYLOAD_TOO_LARGE413POOL_SATURATED503POOL_UNAVAILABLE503RATE_LIMITED429SCOPE_MISSING403SERVING_DISABLED503TASK_UNSUPPORTED422TENANT_SUSPENDED403TOO_FEW_TRIALS422UNAUTHENTICATED401UNITS_REQUIRED422USAGE_CAP_EXCEEDED429
Error format
Example error
HTTP/1.1 401 Unauthorized
X-Request-Id: req_dc3ff85b0bd44d62b2c4606f89ec3021
{
"type": "authentication_error",
"code": "UNAUTHENTICATED",
"message": "API key is not recognised",
"field": "authorization",
"alternatives": ["check that the whole key was copied", "create a new key in your dashboard"],
"details": { "request_id": "req_dc3ff85b0bd44d62b2c4606f89ec3021" }
}| Field | Description |
|---|---|
type | The error class: authentication_error, permission_error, invalid_request_error, not_found_error, conflict_error, rate_limit_error, billing_error, service_unavailable_error or api_error. |
code | A stable identifier to handle in code (tables below). |
message | What went wrong, in words, with the numbers involved. |
field | The part of the request the error is about. |
alternatives | How to fix it, most useful first. |
details | Structured extras. Always includes request_id; quote it when you contact us. |
Surface the whole error
Python
import os
import requests
def check(response: requests.Response) -> dict:
"""Return the JSON body, or raise with everything the API said about the error."""
if response.ok:
return response.json()
error = response.json()
raise RuntimeError(
f"{response.status_code} {error['code']} ({error['field']}): {error['message']}. "
f"Try: {'; '.join(map(str, error['alternatives']))}. Request id: {error['details'].get('request_id')}"
)
# A task model id that is not on your account, to see what a refusal says:
response = requests.get(
'https://dydema--eegapi-gateway.modal.run/v1/task-models/clf_00000000000000000000000000000000',
headers={"Authorization": f"Bearer {os.environ['DYDEMA_API_KEY']}"},
timeout=30,
)
try:
check(response)
except RuntimeError as error:
print(error) # 404 CLASSIFIER_NOT_FOUND (classifier_id): This task model is unavailable or has been deleted. ...JavaScript
// Node 22 or newer. Save as example.mjs and run: node example.mjs
const key = process.env.DYDEMA_API_KEY;
if (!key) throw new Error("set DYDEMA_API_KEY first");
/** Return the JSON body, or throw with everything the API said about the error. */
async function check(response) {
if (response.ok) return response.json();
const error = await response.json();
throw new Error(
`${response.status} ${error.code} (${error.field}): ${error.message}. ` +
`Try: ${error.alternatives.map(String).join("; ")}. Request id: ${error.details.request_id}`,
);
}
// A task model id that is not on your account, to see what a refusal says:
const response = await fetch('https://dydema--eegapi-gateway.modal.run/v1/task-models/clf_00000000000000000000000000000000', {
headers: { Authorization: `Bearer ${key}` },
signal: AbortSignal.timeout(30_000),
});
try {
await check(response);
} catch (error) {
console.log(error.message); // 404 CLASSIFIER_NOT_FOUND (classifier_id): This task model is unavailable or has been deleted. ...
}Terminal
# A task model id that is not on your account, to see what a refusal says; -w prints the HTTP status.
curl -sS https://dydema--eegapi-gateway.modal.run/v1/task-models/clf_00000000000000000000000000000000 \
-H "Authorization: Bearer $DYDEMA_API_KEY" \
-w '\nHTTP %{http_code}\n'Authentication and permissions
| Code | Status | Meaning |
|---|---|---|
UNAUTHENTICATED | 401 | No key, or a key the API does not recognize. Check the Authorization: Bearer header. |
KEY_REVOKED | 401 | The key was revoked in the dashboard. Use another key or create a new one. |
KEY_EXPIRED | 401 | The key passed its expiry date. Create a new one. |
SCOPE_MISSING | 403 | The key is not allowed to use this endpoint (see Authentication). |
TENANT_SUSPENDED | 403 | The account is paused, usually after a failed payment. Update your card under Billing. |
IP_NOT_ALLOWED | 403 | The key is restricted to certain IP addresses and this is not one of them. |
Request errors
| Code | Status | Meaning |
|---|---|---|
CLASSIFIER_NOT_FOUND | 404 | No task model with that id on your account, including one that expired or was deleted: details.status is expired or deleted then, and the message says when and why (see Wizard). |
NOT_FOUND | 404 | No such endpoint (check the path), or an id that does not exist or is not yours. |
CONFLICT | 409 | A request with this Idempotency-Key is still running. Retry after Retry-After. |
PAYLOAD_TOO_LARGE | 413 | More than 4,000,000 numbers (trials × channels × samples) as sent or after resampling, or a body over the size cap. Send the dataset in batches with defer_fit and /trials (how). |
INVALID_REQUEST | 422 | The body failed validation. field names the problem; details.errors lists every field. |
TOO_FEW_TRIALS | 422 | A task model needs at least two classes and at least five trials of each. |
CLASSIFIER_FIT_FAILED | 422 | The task model could not be fitted on these trials. Not billed: no create fee. |
UNITS_REQUIRED | 422 | units is unitless, normalized or unknown. The encoders need physical units: send uV, mV or V. |
IDEMPOTENCY_KEY_REUSED | 422 | This Idempotency-Key was already used with a different body. Use a new key. |
TASK_UNSUPPORTED | 422 | A task whose description or labels show emotion or affect recognition, a clinical or diagnostic use, identifying or looking up a specific person (biometrics), or profiling age, sex or gender, at the dry run or at create. These are refused; see what is refused and why. |
NO_REFERENCE_DATA | 422 | A zero-shot task model for classes with no matching reference data, or for a task where zero-shot does not measurably work on unseen datasets. The zero-shot tasks on offer are listed on the Wizard page; otherwise send labeled trials. |
Rate limits and spending caps
Every response to an authenticated request says where you stand. Read these headers to slow down before you are refused:
| Header | Description |
|---|---|
x-ratelimit-limit-requests | Requests allowed per 10-second window. |
x-ratelimit-remaining-requests | Requests left in the current window. |
x-ratelimit-reset-requests | Time until the window resets, e.g. 3.2s. |
| Code | Status | Meaning | Retry-After |
|---|---|---|---|
RATE_LIMITED | 429 | Too many requests in a short period: one second (burst) or ten seconds (sustained). | yes |
CONCURRENCY_LIMITED | 429 | Too many requests in flight at once. | yes |
USAGE_CAP_EXCEEDED | 429 | A daily or monthly usage cap on your account was reached, or the daily allowance of estimates (dry runs). | yes: seconds until the next day or month; do not retry in a loop |
INSUFFICIENT_CREDIT | 402 | Insufficient credits. Add credit under Billing to resume. An estimate is refused this way too when the balance cannot cover the create fee and the first month of hosting. | no |
BUDGET_EXCEEDED | 402 | Your own daily or monthly spending cap was reached. | no — raise it under Limits |
Your limits are on the dashboard and in GET /v1/usage under limits. Need more? Email us.
Server errors
| Code | Status | Meaning |
|---|---|---|
INTERNAL | 500 | Our fault. Not charged. Retry, and send us the request_id if it keeps happening. |
BACKEND_FAILURE | 502 | The model failed on our side. Not charged. Retry, and send us the request_id if it keeps happening. |
POOL_UNAVAILABLE | 503 | The model is starting up or briefly unreachable. Retry after Retry-After. |
POOL_SATURATED | 503 | The model is at capacity. Back off and retry. |
SERVING_DISABLED | 503 | Serving is paused for maintenance. Retry after Retry-After. |
BILLING_UNAVAILABLE | 503 | Billing is briefly unavailable (dashboard billing actions). Try again later. |
Retrying
- Retry
409,5xx, and429with codeRATE_LIMITEDorCONCURRENCY_LIMITED, after theRetry-Afterseconds, backing off exponentially if it is absent. - Do not retry
USAGE_CAP_EXCEEDEDin a loop: itsRetry-Afteris the time until the next day or month. - Do not retry other
4xxerrors unchanged: fix whatalternativessays first. - Send an
Idempotency-Keyheader (any unique string, for example a UUID) onPOST /v1/task-models,/trials,/fitand/predictions, and reuse it, with the same body, on every retry of that request: the API answers it once and bills it once. A retry while the first is still running is409 CONFLICT; the same key with a different body is422 IDEMPOTENCY_KEY_REUSED. - Use a generous timeout. A prediction normally answers in seconds; a create tests several methods on your trials and takes longer, and any request can take up to about 30 seconds more right after we deploy.
Refused requests are recorded on your usage page with their code, and they cost nothing.