# Errors

> One uniform error format, the catalog of every code and the retry matrix.

Source: https://diko.app/en/api-docs/errors

## Error format

Every 4xx and 5xx error has the same JSON format:

```json
{
  "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](#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 |

> **Important:**
> Branch on the **HTTP status and `error.code`**, never on `message`. The wording of a message can change. By default the message is in English; with the header `Accept-Language: lt` it comes back in Lithuanian and `doc_url` points to the Lithuanian documentation (`https://diko.app/api-docs/klaidos#<code>`). An older link of the form `/api-docs/errors#<code>` redirects to that page and keeps the anchor.

## Classes and HTTP statuses

| `error.type` | HTTP | Codes |
|---|---|---|
| `invalid_request_error` | 400 | [`invalid_request`](#invalid_request), [`invalid_parameter`](#invalid_parameter), [`invalid_cursor`](#invalid_cursor), [`invalid_youtube_url`](#invalid_youtube_url), [`api_key_in_query`](#api_key_in_query), [`idempotency_key_invalid`](#idempotency_key_invalid) |
| `authentication_error` | 401 | [`missing_api_key`](#missing_api_key), [`invalid_api_key`](#invalid_api_key), [`api_key_expired`](#api_key_expired), [`api_key_revoked`](#api_key_revoked), [`api_key_disabled`](#api_key_disabled) |
| `billing_error` | 402 | [`subscription_required`](#subscription_required), [`insufficient_credits`](#insufficient_credits), [`team_member_limit_reached`](#team_member_limit_reached) |
| `permission_error` | 403 | [`insufficient_scope`](#insufficient_scope), [`plan_required`](#plan_required), [`ip_not_allowed`](#ip_not_allowed), [`account_disabled`](#account_disabled), [`email_not_verified`](#email_not_verified), [`api_access_suspended`](#api_access_suspended), [`team_member_disabled`](#team_member_disabled) |
| `not_found_error` | 404 | [`not_found`](#not_found), [`route_not_found`](#route_not_found), [`api_disabled`](#api_disabled) |
| `invalid_request_error` | 405 | [`method_not_allowed`](#method_not_allowed) |
| `conflict_error` | 409 | [`idempotency_in_progress`](#idempotency_in_progress), [`import_not_cancelable`](#import_not_cancelable), [`transcript_changed`](#transcript_changed), [`resource_not_ready`](#resource_not_ready) |
| `invalid_request_error` | 408 | [`request_timeout`](#request_timeout) |
| `request_too_large` | 413 | [`request_too_large`](#request_too_large) |
| `invalid_request_error` | 415 | [`unsupported_media_type`](#unsupported_media_type) |
| `invalid_request_error` | 422 | [`idempotency_key_reused`](#idempotency_key_reused) |
| `rate_limit_error` | 429 | [`rate_limit_exceeded`](#rate_limit_exceeded), [`queue_limit_exceeded`](#queue_limit_exceeded) |
| `api_error` | 500 | [`internal_error`](#internal_error) |
| `service_unavailable_error` | 503 | [`service_unavailable`](#service_unavailable), [`credit_balance_unavailable`](#credit_balance_unavailable), [`writes_disabled`](#writes_disabled) |

## 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.

**Python**

```python
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))
```

**TypeScript**

```typescript
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`

**HTTP 400** · Type: `invalid_request_error` · Retry: no

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`

**HTTP 400** · Type: `invalid_request_error` · Retry: no

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`

**HTTP 400** · Type: `invalid_request_error` · Retry: no

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`

**HTTP 400** · Type: `invalid_request_error` · Retry: no

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`

**HTTP 400** · Type: `invalid_request_error` · Retry: no

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`

**HTTP 400** · Type: `invalid_request_error` · Retry: no

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`

**HTTP 401** · Type: `authentication_error` · Retry: no · `WWW-Authenticate`

The request has no `Authorization` header.

**What to do.** Add the header `Authorization: Bearer dk_live_...`.

### `invalid_api_key`

**HTTP 401** · Type: `authentication_error` · Retry: no · `WWW-Authenticate`

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`

**HTTP 401** · Type: `authentication_error` · Retry: no · `WWW-Authenticate`

The key has expired.

**What to do.** Create a new key in the Diko app and update your systems.

### `api_key_revoked`

**HTTP 401** · Type: `authentication_error` · Retry: no · `WWW-Authenticate`

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`

**HTTP 401** · Type: `authentication_error` · Retry: no · `WWW-Authenticate`

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`

**HTTP 402** · Type: `billing_error` · Retry: no

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`

**HTTP 402** · Type: `billing_error` · Retry: no

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`

**HTTP 402** · Type: `billing_error` · Retry: no

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`

**HTTP 403** · Type: `permission_error` · Retry: no · `WWW-Authenticate`

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`

**HTTP 403** · Type: `permission_error` · Retry: no

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`

**HTTP 403** · Type: `permission_error` · Retry: no

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`

**HTTP 403** · Type: `permission_error` · Retry: no

The account is disabled.

**What to do.** Contact Diko support and include the `request_id`.

### `email_not_verified`

**HTTP 403** · Type: `permission_error` · Retry: no

The account e-mail address is not verified.

**What to do.** Verify the e-mail address in the Diko app.

### `api_access_suspended`

**HTTP 403** · Type: `permission_error` · Retry: no

A Diko administrator suspended API access for this account.

**What to do.** Contact Diko support and include the `request_id`.

### `team_member_disabled`

**HTTP 403** · Type: `permission_error` · Retry: no

Your team membership is disabled.

**What to do.** Contact the team owner or an administrator.

### `not_found`

**HTTP 404** · Type: `not_found_error` · Retry: no

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`

**HTTP 404** · Type: `not_found_error` · Retry: no

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`

**HTTP 404** · Type: `not_found_error` · Retry: conditionally

The public API is currently switched off.

**What to do.** Try again later. If it lasts, contact Diko support.

### `method_not_allowed`

**HTTP 405** · Type: `invalid_request_error` · Retry: no · `Allow`

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`

**HTTP 409** · Type: `conflict_error` · Retry: yes, after Retry-After · `Retry-After`

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`

**HTTP 409** · Type: `conflict_error` · Retry: no

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`

**HTTP 409** · Type: `conflict_error` · Retry: conditionally

The transcript changed while you were paging: its `revision` changed.

**What to do.** Start reading the transcript again without a `cursor`.

### `resource_not_ready`

**HTTP 409** · Type: `conflict_error` · Retry: yes

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`

**HTTP 408** · Type: `invalid_request_error` · Retry: yes

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`

**HTTP 413** · Type: `request_too_large` · Retry: no

The request body is larger than 64 KB.

**What to do.** Reduce the body. These endpoints never need large bodies.

### `unsupported_media_type`

**HTTP 415** · Type: `invalid_request_error` · Retry: no

A POST body must be `application/json`.

**What to do.** Set the header `Content-Type: application/json`.

### `idempotency_key_reused`

**HTTP 422** · Type: `invalid_request_error` · Retry: no

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`

**HTTP 429** · Type: `rate_limit_error` · Retry: yes, after Retry-After · `Retry-After`

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`

**HTTP 429** · Type: `rate_limit_error` · Retry: yes, after Retry-After · `Retry-After`

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`

**HTTP 500** · Type: `api_error` · Retry: conditionally

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`

**HTTP 503** · Type: `service_unavailable_error` · Retry: yes, after Retry-After · `Retry-After`

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`

**HTTP 503** · Type: `service_unavailable_error` · Retry: yes, after Retry-After · `Retry-After`

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`

**HTTP 503** · Type: `service_unavailable_error` · Retry: yes, after Retry-After · `Retry-After`

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`.
