Errors
One uniform error format, the catalog of every code and the retry matrix.
Error format
Every 4xx and 5xx error has the same JSON format:
{
"type": "error",
"error": {
"type": "invalid_request_error",
"code": "invalid_parameter",
"message": "limit must be between 1 and 100.",
"param": "limit",
"doc_url": "https://diko.app/en/api-docs/errors#invalid_parameter"
},
"request_id": "req_QBPIQ5QTIWAUYTMDNCLD7CMKBE"
}| Field | Meaning |
|---|---|
type |
always error |
error.type |
error class, for example invalid_request_error |
error.code |
stable machine-readable code. Every code is described in the code catalog |
error.message |
human-readable explanation |
error.param |
name of the offending parameter, when one applies |
error.doc_url |
link to the description of this code on this page |
error.details |
extra context only when relevant: required_scopes, granted_scopes, required_plans, policy (rate limits), limit (which import limit queue_limit_exceeded hit), retry_after |
request_id |
request ID (req_...). Quote it when you contact support |
Classes and HTTP statuses
Headers worth reading
| Header | When | What to do |
|---|---|---|
Request-Id (and X-Request-ID) |
always | keep it in your log; it equals the request_id field |
X-Client-Request-Id |
when you sent one | Diko echoes back the value you sent in the request header of the same name (1 to 128 characters from A-Za-z0-9._:-; any other value is ignored). Use it to match a response to your own log entry |
Retry-After |
429, 503 and idempotency_in_progress |
wait that many whole seconds (at least 1) |
WWW-Authenticate |
401 and insufficient_scope |
names Bearer realm="diko"; for insufficient_scope also the required scope |
Allow |
405 | lists the permitted methods |
Retry matrix
Retry only when it makes sense. Errors caused by the request itself do not go away on retry.
| Situation | Retry? | How |
|---|---|---|
| Network error or timeout (no response received) | yes | backoff with jitter; always the same Idempotency-Key for POST |
408 request_timeout |
yes | the body did not arrive in time (10 s by default); the request created nothing, so send the same request again with the same Idempotency-Key. If it keeps happening, check your connection and send the body in one piece |
| 400, 404, 405, 413, 415, 422 | no | fix the request (parameters, ID, body, Content-Type, Idempotency-Key) |
| 401, 403 | no | check the key, scopes, plan, IP and account state |
| 402 | no, until resolved | pay for a plan or top up credits; then the request can be repeated |
409 idempotency_in_progress |
yes, after Retry-After |
the same request with the same key |
409 resource_not_ready |
yes, later | wait for meeting.ready or check transcript.status |
409 transcript_changed |
yes, from the start | read the transcript again without a cursor |
409 import_not_cancelable |
no | the import has already finished |
| 429 | yes, after Retry-After |
never sooner; use backoff |
| 500 | yes, 2 or 3 times | with backoff; if it persists, report it with the request_id |
| 503 | yes, after Retry-After |
for writes_disabled usually after 300 s |
404 api_disabled |
later | the API is temporarily switched off |
Retry recipe
This helper retries 408, 429, 500 and 503, honours Retry-After and otherwise uses exponential backoff with jitter. For POST pass the same Idempotency-Key to every attempt.
import random
import time
import requests
RETRYABLE = {408, 429, 500, 503}
def request_with_retry(method, url, *, max_attempts=5, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
response = requests.request(method, url, timeout=30, **kwargs)
except (requests.ConnectionError, requests.Timeout):
if attempt == max_attempts:
raise
delay = min(2**attempt, 30)
else:
if response.status_code not in RETRYABLE or attempt == max_attempts:
return response
retry_after = response.headers.get("Retry-After", "")
delay = int(retry_after) if retry_after.isdigit() else min(2**attempt, 30)
time.sleep(delay + random.uniform(0, delay * 0.2))const RETRYABLE = new Set([408, 429, 500, 503])
async function requestWithRetry(url: string, init: RequestInit, maxAttempts = 5): Promise<Response> {
for (let attempt = 1; ; attempt++) {
let delay: number
try {
const response = await fetch(url, { ...init, signal: AbortSignal.timeout(30_000) })
if (!RETRYABLE.has(response.status) || attempt >= maxAttempts) return response
const retryAfter = Number(response.headers.get("Retry-After"))
delay = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : Math.min(2 ** attempt, 30)
} catch (error) {
if (attempt >= maxAttempts) throw error
delay = Math.min(2 ** attempt, 30)
}
await new Promise(resolve => setTimeout(resolve, (delay + Math.random() * delay * 0.2) * 1000))
}
}Code catalog
Every code has its own anchor, for example #invalid_parameter, so the doc_url link leads straight to its description.
| Code | HTTP | Type | Retry |
|---|---|---|---|
invalid_request | 400 | invalid_request_error | no |
invalid_parameter | 400 | invalid_request_error | no |
invalid_cursor | 400 | invalid_request_error | no |
invalid_youtube_url | 400 | invalid_request_error | no |
api_key_in_query | 400 | invalid_request_error | no |
idempotency_key_invalid | 400 | invalid_request_error | no |
missing_api_key | 401 | authentication_error | no |
invalid_api_key | 401 | authentication_error | no |
api_key_expired | 401 | authentication_error | no |
api_key_revoked | 401 | authentication_error | no |
api_key_disabled | 401 | authentication_error | no |
subscription_required | 402 | billing_error | no |
insufficient_credits | 402 | billing_error | no |
team_member_limit_reached | 402 | billing_error | no |
insufficient_scope | 403 | permission_error | no |
plan_required | 403 | permission_error | no |
ip_not_allowed | 403 | permission_error | no |
account_disabled | 403 | permission_error | no |
email_not_verified | 403 | permission_error | no |
api_access_suspended | 403 | permission_error | no |
team_member_disabled | 403 | permission_error | no |
not_found | 404 | not_found_error | no |
route_not_found | 404 | not_found_error | no |
api_disabled | 404 | not_found_error | conditionally |
method_not_allowed | 405 | invalid_request_error | no |
idempotency_in_progress | 409 | conflict_error | yes, after Retry-After |
import_not_cancelable | 409 | conflict_error | no |
transcript_changed | 409 | conflict_error | conditionally |
resource_not_ready | 409 | conflict_error | yes |
request_timeout | 408 | invalid_request_error | yes |
request_too_large | 413 | request_too_large | no |
unsupported_media_type | 415 | invalid_request_error | no |
idempotency_key_reused | 422 | invalid_request_error | no |
rate_limit_exceeded | 429 | rate_limit_error | yes, after Retry-After |
queue_limit_exceeded | 429 | rate_limit_error | yes, after Retry-After |
internal_error | 500 | api_error | conditionally |
service_unavailable | 503 | service_unavailable_error | yes, after Retry-After |
credit_balance_unavailable | 503 | service_unavailable_error | yes, after Retry-After |
writes_disabled | 503 | service_unavailable_error | yes, after Retry-After |
invalid_request
The request is malformed: the body is not valid JSON, is missing, is not a JSON object, or Content-Length is not a number. A missing required field or an unknown field is invalid_parameter instead.
What to do. Send a JSON object and compare it with the endpoint schema.
invalid_parameter
A path, query or body parameter has an invalid value, a required field is missing or a field is unknown (unknown fields are rejected). error.param names the parameter.
What to do. Fix the parameter according to its constraints, for example limit must be between 1 and 100.
invalid_cursor
The cursor is corrupted or was issued for another user, route, sort order or set of filters.
What to do. Start paging again without a cursor and keep the filters unchanged between pages.
invalid_youtube_url
The URL is not a supported address of a single YouTube video.
What to do. Use a link from youtube.com, www.youtube.com, m.youtube.com, music.youtube.com or youtu.be.
api_key_in_query
The key was sent in the query string (api_key, key, token or access_token).
What to do. Send the key only in the Authorization: Bearer header. The key may have reached logs, so rotate it.
idempotency_key_invalid
The Idempotency-Key is invalid: it needs 8 to 255 characters from A-Z a-z 0-9 _ - : ..
What to do. Generate a UUID and send it without extra characters.
missing_api_key
The request has no Authorization header.
What to do. Add the header Authorization: Bearer dk_live_....
invalid_api_key
The header is unusable (a scheme other than Bearer, an empty key or more than one Authorization header) or the key is unknown or malformed (wrong structure or checksum). The response is identical in all these cases.
What to do. Send exactly one header Authorization: Bearer <key> and check that you copied the whole key without spaces. If the key is lost, create a new one.
api_key_expired
The key has expired.
What to do. Create a new key in the Diko app and update your systems.
api_key_revoked
The key was revoked: manually, after a rotation grace period ended, because of a leak or because the account was disabled.
What to do. Create a new key and update your systems. A revoked key cannot be restored.
api_key_disabled
The key is temporarily disabled.
What to do. Enable the key in the Diko app (this needs re-authentication) or use another key.
subscription_required
The account has no active paid access.
What to do. Choose a plan in the Diko app. The API is available on the Verslas plan.
insufficient_credits
There are not enough credits for the import.
What to do. Top up credits and repeat the request. A 402 response is not stored, so you can reuse the same Idempotency-Key.
team_member_limit_reached
Your credit limit in the team has been reached.
What to do. Ask the team owner or an administrator to raise the limit.
insufficient_scope
The key lacks the scope this endpoint needs. error.details contains required_scopes and granted_scopes.
What to do. Create a key with the required scope. Scopes cannot be changed after creation.
plan_required
The plan does not include the API. error.details.required_plans lists verslas.
What to do. Move to the Verslas plan. Keys are not deleted and work again once the plan allows it.
ip_not_allowed
The key has an IP allowlist and the request IP is not on it, or it cannot be determined reliably.
What to do. Send from an allowed address or change the list in the Diko app. Check the IP with another key that has no restriction: GET /me → request.
account_disabled
The account is disabled.
What to do. Contact Diko support and include the request_id.
email_not_verified
The account e-mail address is not verified.
What to do. Verify the e-mail address in the Diko app.
api_access_suspended
A Diko administrator suspended API access for this account.
What to do. Contact Diko support and include the request_id.
team_member_disabled
Your team membership is disabled.
What to do. Contact the team owner or an administrator.
not_found
The resource does not exist, does not belong to your account or its ID is encoded incorrectly.
What to do. Check the ID. The API deliberately returns 404 rather than 403 so it never reveals whether another account's resource exists.
route_not_found
No such route exists. A path with a trailing / also counts as unknown.
What to do. Check the path and the base URL https://api.diko.app/api/v1.
api_disabled
The public API is currently switched off.
What to do. Try again later. If it lasts, contact Diko support.
method_not_allowed
The method is not supported for this path. The Allow header lists the permitted methods. OPTIONS also returns 405 because browser requests are not supported.
What to do. Use one of the methods listed in the Allow header.
idempotency_in_progress
The same request with the same Idempotency-Key is still being processed.
What to do. Wait Retry-After seconds and send the identical request with the same key.
import_not_cancelable
The import has already finished (succeeded or failed) and can no longer be canceled.
What to do. Read the import status. An import that is already canceling or canceled returns 200.
transcript_changed
The transcript changed while you were paging: its revision changed.
What to do. Start reading the transcript again without a cursor.
resource_not_ready
The transcript does not exist yet: the meeting is still being processed.
What to do. Check transcript.status on the meeting or wait for the meeting.ready event, then try again.
request_timeout
The request body was not received in time (10 s by default). This happens when a client sends the body too slowly or stalls while sending. Nothing was created.
What to do. Send the whole body at once and check your connection. It is safe to repeat the request with the same Idempotency-Key.
request_too_large
The request body is larger than 64 KB.
What to do. Reduce the body. These endpoints never need large bodies.
unsupported_media_type
A POST body must be application/json.
What to do. Set the header Content-Type: application/json.
idempotency_key_reused
The same Idempotency-Key was used with a different method, path or body.
What to do. Generate a new key for every new operation. Repeat only an identical request.
rate_limit_exceeded
A rate-limit policy was exceeded. error.details.policy names it.
What to do. Wait Retry-After seconds. Retry with backoff and jitter, not immediately.
queue_limit_exceeded
The import capacity is reached: up to 10 active per user, 200 overall or 500 per day.
What to do. Wait for Retry-After (60 s) or until some imports finish. Do not send more requests while the queue is full.
internal_error
An internal server error.
What to do. Retry up to 2 or 3 times with backoff (POST with the same Idempotency-Key). If it persists, contact Diko support and include the request_id.
service_unavailable
The service is temporarily busy, for example because of database load.
What to do. Wait Retry-After seconds (usually 2) and try again.
credit_balance_unavailable
The credit balance could not be determined right now, so the import was not started.
What to do. Wait Retry-After seconds and repeat the request with the same Idempotency-Key.
writes_disabled
Write operations are temporarily switched off on the platform side. Reading still works.
What to do. Wait Retry-After seconds (300) and repeat the request with the same Idempotency-Key.