# Import a YouTube video into the library

> Queues a YouTube video for transcription and adds it to your library.

Source: https://diko.app/en/api-docs/reference/create-library-import

Imports · `POST /library/imports`

- **Base URL:** https://api.diko.app/api/v1
- **Required scope:** `library:write`
- **Rate-limit policy:** `write`, `imports`, `account`
- **Authentication:** `Authorization: Bearer dk_live_...`

The request never waits for the transcription: poll `GET /library/imports/{import_id}` (or subscribe to the `library.import.*` events) until `status` is `succeeded`, `failed` or `canceled`.

* A video already in your library, or already transcribed by Diko, answers `201` with `reused: true` and costs no credits. It is still an import: it counts toward the daily limit.
* An import of the same video that is still running answers `200` with that import.
* Otherwise `201` with a `Location` header. Credits are checked now and again before paid work; when too many imports are queued, or the daily limit is reached, the answer is `429 queue_limit_exceeded` (`error.details.limit` says which limit).
* A video without captions is transcribed by speech recognition in Diko's shared queue. Those jobs use the same per-user transcription slots as your own uploads in the app, but never the last free one (it stays available for your uploads and recordings), and may wait for a free slot; the import stays `queued` or `processing` meanwhile.
* Revoking, disabling or deleting the API key that created an import, rotating it without an overlap period, or a revocation because of a leak cancels that key's imports that are still queued or running, and suspending API access does the same for the whole account: a queued import ends `canceled` at once, a running one becomes `canceling` and ends `canceled` when the work stops (event `library.import.canceled`). A rotation with a grace period leaves them running. Work already done is charged as usual.

Send an `Idempotency-Key` header to make retries safe.

## Parameters

| Name | In | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|---|
| `Idempotency-Key` | header | `string` | no | – | 8–255 characters; pattern: ^[A-Za-z0-9_\-:.]{8,255}$ | Makes a retry safe: the same key with the same request returns the first answer (`Idempotent-Replayed: true`); a different request under the same key is rejected with 422 `idempotency_key_reused`. 8-255 characters from A-Z a-z 0-9 _ - : . Kept for 24 hours. |

## Request body

The video to import. Content type: `application/json`.

- `url` (string, required, 1–2048 characters): A YouTube video link (youtube.com, www.youtube.com, m.youtube.com, music.youtube.com or youtu.be).
- `language` (string | null, optional): Hint for the caption language and speech recognition. Omit to use your Diko default language. Allowed values: `lt`, `en`.

```json
{
  "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "language": "lt"
}
```

## Response

### 201

The import was created (`queued`), or the transcript already existed (`succeeded` with `reused: true`).

- `Idempotent-Replayed`: `true` when the response is a replay of an earlier request with the same `Idempotency-Key`.
- `Location`: URL of the created import.
- `RateLimit`: The most restrictive policy that applied: `"name";r=<requests left>;t=<seconds>`, where `t` is the number of seconds until one more request is allowed (0 when the quota is full). Absent when no rate limiter ran for the request.
- `RateLimit-Policy`: The rate-limit policies that applied to the request, one `"name";q=<burst>;w=<seconds>` item per policy, for example `"read";q=10;w=2, "account";q=20;w=2`: `q` is the burst quota and `w` the seconds in which an empty quota refills. Sent on every authenticated response; absent when no rate limiter ran for the request, for example when it was rejected before the key was checked.
- `Request-Id`: ID of this request (`req_` prefix), the same as `request_id` in an error body. It is also sent as `X-Request-ID`. Quote it when you contact support.
- `X-Client-Request-Id`: Your own `X-Client-Request-Id` request header, echoed back. It is sent only when the value you sent has 1-128 characters from A-Z a-z 0-9 . _ : -.
- `X-RateLimit-Limit`: Burst quota of the most restrictive policy (`q` in `RateLimit-Policy`). Absent when no rate limiter ran.
- `X-RateLimit-Remaining`: Requests left in the most restrictive policy (`r` in `RateLimit`). Absent when no rate limiter ran.
- `X-RateLimit-Reset`: Seconds until one more request is allowed in the most restrictive policy (`t` in `RateLimit`; 0 when the quota is full). It is a number of seconds, not a timestamp. Absent when no rate limiter ran.

Content type: `application/json`

- `id` (string, required, 26–26 characters, pattern: ^imp_[0-9A-Za-z]{22}$): Public import ID (`imp_` prefix).
- `object` (string, required): Always `library_import`.
- `status` (string, required): Import status. Allowed values: `queued`, `processing`, `canceling`, `succeeded`, `failed`, `canceled`.
- `stage` (string | null, required): Current stage; only while `processing`. Allowed values: `captions`, `waiting`, `downloading`, `transcribing`, `summarizing`.
- `queue_position` (integer | null, required, 1–2147483647, format: int32): Position among your queued imports (1 = next); null when the import is not `queued`.
- `url` (string, required, format: uri): Canonical YouTube URL.
- `youtube_video_id` (string, required): YouTube video ID.
- `language` (string | null, required): Language hint that was sent, or null. Allowed values: `lt`, `en`.
- `reused` (boolean, required): True when the video was already in your library or in Diko's shared cache, so no credits were used.
- `transcript_id` (string | null, required, 6–64 characters, pattern: ^trn_[A-Za-z0-9_-]+$): ID (`trn_` prefix) of the library transcript once the import has `succeeded`; otherwise null.
- `error` (object | null, required): Error details when the import `failed`; otherwise null.
  - `code` (string, required): Stable error code. Unknown internal codes are reported as `internal_error`. Allowed values: `video_unavailable`, `video_private`, `age_restricted`, `video_too_long`, `video_duration_unknown`, `captions_unavailable`, `download_failed`, `transcription_failed`, `transcription_timeout`, `provider_limit`, `insufficient_credits`, `internal_error`.
  - `message` (string, required): Explanation in the language of `Accept-Language`.
- `created_at` (string, required, ≤ 24 characters, format: date-time): When the import was created.
- `updated_at` (string, required, ≤ 24 characters, format: date-time): When the import last changed.
- `completed_at` (string | null, required, ≤ 24 characters, format: date-time): When the import reached a final status; otherwise null.

```json
{
  "id": "imp_6Mfj7Oxm4MfxGGmZRhiUQf",
  "object": "library_import",
  "status": "queued",
  "stage": null,
  "queue_position": 1,
  "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "youtube_video_id": "dQw4w9WgXcQ",
  "language": "lt",
  "reused": false,
  "transcript_id": null,
  "error": null,
  "created_at": "2026-10-11T09:30:00.000Z",
  "updated_at": "2026-10-11T09:30:00.100Z",
  "completed_at": null
}
```

### 200

An import of this video is already queued or running; that import is returned.

- `Content-Location`: URL of the existing import that this answer describes.
- `Idempotent-Replayed`: `true` when the response is a replay of an earlier request with the same `Idempotency-Key`.
- `RateLimit`: The most restrictive policy that applied: `"name";r=<requests left>;t=<seconds>`, where `t` is the number of seconds until one more request is allowed (0 when the quota is full). Absent when no rate limiter ran for the request.
- `RateLimit-Policy`: The rate-limit policies that applied to the request, one `"name";q=<burst>;w=<seconds>` item per policy, for example `"read";q=10;w=2, "account";q=20;w=2`: `q` is the burst quota and `w` the seconds in which an empty quota refills. Sent on every authenticated response; absent when no rate limiter ran for the request, for example when it was rejected before the key was checked.
- `Request-Id`: ID of this request (`req_` prefix), the same as `request_id` in an error body. It is also sent as `X-Request-ID`. Quote it when you contact support.
- `X-Client-Request-Id`: Your own `X-Client-Request-Id` request header, echoed back. It is sent only when the value you sent has 1-128 characters from A-Z a-z 0-9 . _ : -.
- `X-RateLimit-Limit`: Burst quota of the most restrictive policy (`q` in `RateLimit-Policy`). Absent when no rate limiter ran.
- `X-RateLimit-Remaining`: Requests left in the most restrictive policy (`r` in `RateLimit`). Absent when no rate limiter ran.
- `X-RateLimit-Reset`: Seconds until one more request is allowed in the most restrictive policy (`t` in `RateLimit`; 0 when the quota is full). It is a number of seconds, not a timestamp. Absent when no rate limiter ran.

Content type: `application/json`

- `id` (string, required, 26–26 characters, pattern: ^imp_[0-9A-Za-z]{22}$): Public import ID (`imp_` prefix).
- `object` (string, required): Always `library_import`.
- `status` (string, required): Import status. Allowed values: `queued`, `processing`, `canceling`, `succeeded`, `failed`, `canceled`.
- `stage` (string | null, required): Current stage; only while `processing`. Allowed values: `captions`, `waiting`, `downloading`, `transcribing`, `summarizing`.
- `queue_position` (integer | null, required, 1–2147483647, format: int32): Position among your queued imports (1 = next); null when the import is not `queued`.
- `url` (string, required, format: uri): Canonical YouTube URL.
- `youtube_video_id` (string, required): YouTube video ID.
- `language` (string | null, required): Language hint that was sent, or null. Allowed values: `lt`, `en`.
- `reused` (boolean, required): True when the video was already in your library or in Diko's shared cache, so no credits were used.
- `transcript_id` (string | null, required, 6–64 characters, pattern: ^trn_[A-Za-z0-9_-]+$): ID (`trn_` prefix) of the library transcript once the import has `succeeded`; otherwise null.
- `error` (object | null, required): Error details when the import `failed`; otherwise null.
  - `code` (string, required): Stable error code. Unknown internal codes are reported as `internal_error`. Allowed values: `video_unavailable`, `video_private`, `age_restricted`, `video_too_long`, `video_duration_unknown`, `captions_unavailable`, `download_failed`, `transcription_failed`, `transcription_timeout`, `provider_limit`, `insufficient_credits`, `internal_error`.
  - `message` (string, required): Explanation in the language of `Accept-Language`.
- `created_at` (string, required, ≤ 24 characters, format: date-time): When the import was created.
- `updated_at` (string, required, ≤ 24 characters, format: date-time): When the import last changed.
- `completed_at` (string | null, required, ≤ 24 characters, format: date-time): When the import reached a final status; otherwise null.

```json
{
  "id": "imp_6Mfj7Oxm4MfxGGmZRhiUQf",
  "object": "library_import",
  "status": "queued",
  "stage": null,
  "queue_position": 1,
  "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "youtube_video_id": "dQw4w9WgXcQ",
  "language": "lt",
  "reused": false,
  "transcript_id": null,
  "error": null,
  "created_at": "2026-10-11T09:30:00.000Z",
  "updated_at": "2026-10-11T09:30:00.100Z",
  "completed_at": null
}
```

## Examples

**cURL**

```bash
curl -X POST "https://api.diko.app/api/v1/library/imports" \
  -H "Authorization: Bearer $DIKO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","language":"lt"}'
```

**Python**

```python
import os
import uuid

import requests

response = requests.post(
    "https://api.diko.app/api/v1/library/imports",
    headers={
        "Authorization": f"Bearer {os.environ['DIKO_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),  # create once per operation, reuse on retries
    },
    json={
        "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
        "language": "lt",
    },
    timeout=30,
)
response.raise_for_status()
print(response.json())
```

**TypeScript**

```typescript
const response = await fetch("https://api.diko.app/api/v1/library/imports", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.DIKO_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(), // create once per operation, reuse on retries
  },
  body: JSON.stringify({
    url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    language: "lt",
  }),
})
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`)
console.log(await response.json())
```

## Errors

This endpoint can return these errors. Every code is described in the error catalog.

| HTTP | Code | Meaning | Retry? |
|---|---|---|---|
| 400 | [`invalid_request`](https://diko.app/en/api-docs/errors.md#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. | no |
| 400 | [`invalid_parameter`](https://diko.app/en/api-docs/errors.md#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. | no |
| 400 | [`invalid_youtube_url`](https://diko.app/en/api-docs/errors.md#invalid_youtube_url) | The URL is not a supported address of a single YouTube video. | no |
| 400 | [`idempotency_key_invalid`](https://diko.app/en/api-docs/errors.md#idempotency_key_invalid) | The `Idempotency-Key` is invalid: it needs 8 to 255 characters from `A-Z a-z 0-9 _ - : .`. | no |
| 401 | [`missing_api_key`](https://diko.app/en/api-docs/errors.md#missing_api_key) | The request has no `Authorization` header. | no |
| 401 | [`invalid_api_key`](https://diko.app/en/api-docs/errors.md#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. | no |
| 401 | [`api_key_expired`](https://diko.app/en/api-docs/errors.md#api_key_expired) | The key has expired. | no |
| 401 | [`api_key_revoked`](https://diko.app/en/api-docs/errors.md#api_key_revoked) | The key was revoked: manually, after a rotation grace period ended, because of a leak or because the account was disabled. | no |
| 401 | [`api_key_disabled`](https://diko.app/en/api-docs/errors.md#api_key_disabled) | The key is temporarily disabled. | no |
| 402 | [`subscription_required`](https://diko.app/en/api-docs/errors.md#subscription_required) | The account has no active paid access. | no |
| 402 | [`insufficient_credits`](https://diko.app/en/api-docs/errors.md#insufficient_credits) | There are not enough credits for the import. | no |
| 402 | [`team_member_limit_reached`](https://diko.app/en/api-docs/errors.md#team_member_limit_reached) | Your credit limit in the team has been reached. | no |
| 403 | [`ip_not_allowed`](https://diko.app/en/api-docs/errors.md#ip_not_allowed) | The key has an IP allowlist and the request IP is not on it, or it cannot be determined reliably. | no |
| 403 | [`account_disabled`](https://diko.app/en/api-docs/errors.md#account_disabled) | The account is disabled. | no |
| 403 | [`email_not_verified`](https://diko.app/en/api-docs/errors.md#email_not_verified) | The account e-mail address is not verified. | no |
| 403 | [`api_access_suspended`](https://diko.app/en/api-docs/errors.md#api_access_suspended) | A Diko administrator suspended API access for this account. | no |
| 403 | [`plan_required`](https://diko.app/en/api-docs/errors.md#plan_required) | The plan does not include the API. `error.details.required_plans` lists `verslas`. | no |
| 403 | [`insufficient_scope`](https://diko.app/en/api-docs/errors.md#insufficient_scope) | The key lacks the scope this endpoint needs. `error.details` contains `required_scopes` and `granted_scopes`. | no |
| 403 | [`team_member_disabled`](https://diko.app/en/api-docs/errors.md#team_member_disabled) | Your team membership is disabled. | no |
| 408 | [`request_timeout`](https://diko.app/en/api-docs/errors.md#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. | yes |
| 409 | [`idempotency_in_progress`](https://diko.app/en/api-docs/errors.md#idempotency_in_progress) | The same request with the same `Idempotency-Key` is still being processed. | yes, after Retry-After |
| 413 | [`request_too_large`](https://diko.app/en/api-docs/errors.md#request_too_large) | The request body is larger than 64 KB. | no |
| 415 | [`unsupported_media_type`](https://diko.app/en/api-docs/errors.md#unsupported_media_type) | A POST body must be `application/json`. | no |
| 422 | [`idempotency_key_reused`](https://diko.app/en/api-docs/errors.md#idempotency_key_reused) | The same `Idempotency-Key` was used with a different method, path or body. | no |
| 429 | [`queue_limit_exceeded`](https://diko.app/en/api-docs/errors.md#queue_limit_exceeded) | The import capacity is reached: up to 10 active per user, 200 overall or 500 per day. | yes, after Retry-After |
| 429 | [`rate_limit_exceeded`](https://diko.app/en/api-docs/errors.md#rate_limit_exceeded) | A rate-limit policy was exceeded. `error.details.policy` names it. | yes, after Retry-After |
| 500 | [`internal_error`](https://diko.app/en/api-docs/errors.md#internal_error) | An internal server error. | conditionally |
| 503 | [`credit_balance_unavailable`](https://diko.app/en/api-docs/errors.md#credit_balance_unavailable) | The credit balance could not be determined right now, so the import was not started. | yes, after Retry-After |
| 503 | [`writes_disabled`](https://diko.app/en/api-docs/errors.md#writes_disabled) | Write operations are temporarily switched off on the platform side. Reading still works. | yes, after Retry-After |
| 503 | [`service_unavailable`](https://diko.app/en/api-docs/errors.md#service_unavailable) | The service is temporarily busy, for example because of database load. | yes, after Retry-After |
