Import a YouTube video into the library
Queues a YouTube video for transcription and adds it to your library.
/library/imports- Required scope
library:write- Authentication
Authorization: Bearer
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
urlstringrequiredA YouTube video link (youtube.com, www.youtube.com, m.youtube.com, music.youtube.com or youtu.be).
languagestring | nullHint for the caption language and speech recognition. Omit to use your Diko default language.
Response
201
The import was created (queued), or the transcript already existed (succeeded with reused: true).
Idempotent-Replayedtruewhen the response is a replay of an earlier request with the sameIdempotency-Key.LocationURL of the created import.RateLimitThe most restrictive policy that applied:"name";r=<requests left>;t=<seconds>, wheretis 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-PolicyThe 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:qis the burst quota andwthe 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-IdID of this request (req_prefix), the same asrequest_idin an error body. It is also sent asX-Request-ID. Quote it when you contact support.X-Client-Request-IdYour ownX-Client-Request-Idrequest 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-LimitBurst quota of the most restrictive policy (qinRateLimit-Policy). Absent when no rate limiter ran.X-RateLimit-RemainingRequests left in the most restrictive policy (rinRateLimit). Absent when no rate limiter ran.X-RateLimit-ResetSeconds until one more request is allowed in the most restrictive policy (tinRateLimit; 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
idstringrequiredPublic import ID (
imp_prefix).objectstringrequiredAlways
library_import.statusstringrequiredImport status.
stagestring | nullrequiredCurrent stage; only while
processing.queue_positioninteger | nullrequiredPosition among your queued imports (1 = next); null when the import is not
queued.urlstringrequiredCanonical YouTube URL.
youtube_video_idstringrequiredYouTube video ID.
languagestring | nullrequiredLanguage hint that was sent, or null.
reusedbooleanrequiredTrue when the video was already in your library or in Diko's shared cache, so no credits were used.
transcript_idstring | nullrequiredID (
trn_prefix) of the library transcript once the import hassucceeded; otherwise null.errorobject | nullrequiredError details when the import
failed; otherwise null.2 fields
codestringrequiredStable error code. Unknown internal codes are reported as
internal_error.messagestringrequiredExplanation in the language of
Accept-Language.
created_atstringrequiredWhen the import was created.
updated_atstringrequiredWhen the import last changed.
completed_atstring | nullrequiredWhen the import reached a final status; otherwise null.
200
An import of this video is already queued or running; that import is returned.
Content-LocationURL of the existing import that this answer describes.Idempotent-Replayedtruewhen the response is a replay of an earlier request with the sameIdempotency-Key.RateLimitThe most restrictive policy that applied:"name";r=<requests left>;t=<seconds>, wheretis 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-PolicyThe 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:qis the burst quota andwthe 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-IdID of this request (req_prefix), the same asrequest_idin an error body. It is also sent asX-Request-ID. Quote it when you contact support.X-Client-Request-IdYour ownX-Client-Request-Idrequest 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-LimitBurst quota of the most restrictive policy (qinRateLimit-Policy). Absent when no rate limiter ran.X-RateLimit-RemainingRequests left in the most restrictive policy (rinRateLimit). Absent when no rate limiter ran.X-RateLimit-ResetSeconds until one more request is allowed in the most restrictive policy (tinRateLimit; 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
idstringrequiredPublic import ID (
imp_prefix).objectstringrequiredAlways
library_import.statusstringrequiredImport status.
stagestring | nullrequiredCurrent stage; only while
processing.queue_positioninteger | nullrequiredPosition among your queued imports (1 = next); null when the import is not
queued.urlstringrequiredCanonical YouTube URL.
youtube_video_idstringrequiredYouTube video ID.
languagestring | nullrequiredLanguage hint that was sent, or null.
reusedbooleanrequiredTrue when the video was already in your library or in Diko's shared cache, so no credits were used.
transcript_idstring | nullrequiredID (
trn_prefix) of the library transcript once the import hassucceeded; otherwise null.errorobject | nullrequiredError details when the import
failed; otherwise null.2 fields
codestringrequiredStable error code. Unknown internal codes are reported as
internal_error.messagestringrequiredExplanation in the language of
Accept-Language.
created_atstringrequiredWhen the import was created.
updated_atstringrequiredWhen the import last changed.
completed_atstring | nullrequiredWhen the import reached a final status; otherwise null.
Examples
Request
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"}'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())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())Example response · 201 · application/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
}Example response · 200 · application/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
}Errors
This endpoint can return these errors. Every code is described in the error catalog.
| HTTP | Code | Meaning | Retry? |
|---|---|---|---|
| 400 | 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 | 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 | The URL is not a supported address of a single YouTube video. | no |
| 400 | 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 | The request has no `Authorization` header. | no |
| 401 | 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 | The key has expired. | no |
| 401 | 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 | The key is temporarily disabled. | no |
| 402 | subscription_required | The account has no active paid access. | no |
| 402 | insufficient_credits | There are not enough credits for the import. | no |
| 402 | team_member_limit_reached | Your credit limit in the team has been reached. | no |
| 403 | 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 | The account is disabled. | no |
| 403 | email_not_verified | The account e-mail address is not verified. | no |
| 403 | api_access_suspended | A Diko administrator suspended API access for this account. | no |
| 403 | plan_required | The plan does not include the API. `error.details.required_plans` lists `verslas`. | no |
| 403 | insufficient_scope | The key lacks the scope this endpoint needs. `error.details` contains `required_scopes` and `granted_scopes`. | no |
| 403 | team_member_disabled | Your team membership is disabled. | no |
| 408 | 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 | The same request with the same `Idempotency-Key` is still being processed. | yes, after Retry-After |
| 413 | request_too_large | The request body is larger than 64 KB. | no |
| 415 | unsupported_media_type | A POST body must be `application/json`. | no |
| 422 | idempotency_key_reused | The same `Idempotency-Key` was used with a different method, path or body. | no |
| 429 | 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 | A rate-limit policy was exceeded. `error.details.policy` names it. | yes, after Retry-After |
| 500 | internal_error | An internal server error. | conditionally |
| 503 | 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 | Write operations are temporarily switched off on the platform side. Reading still works. | yes, after Retry-After |
| 503 | service_unavailable | The service is temporarily busy, for example because of database load. | yes, after Retry-After |