Quickstart
From creating a key to reading your first transcript in a few minutes.
In this guide you create a key, make a first call that reads your own data, import a public YouTube video and read its transcript. Every example uses the base URL https://api.diko.app/api/v1.
1. Create an API key
- Sign in to Diko and open Nustatymai → API (Settings → API), the API keys page (
/settings/api). The old/kurejamslink redirects there. You need the Verslas plan. - Choose Sukurti raktą (Create key), enter a name and pick the Skaitymas ir importas (Read and import) scope template (
meetings:read,library:write). The default expiry is 90 days. - Confirm the action by signing in again if Diko asks for it.
- Copy the key. It is shown only once; afterwards you only see a hint such as
dk_live_Ab12…9xYz.
Keep the key in an environment variable, not in code:
export DIKO_API_KEY="dk_live_..."2. Make your first call
Start with a call that returns your own data. The Skaitymas ir importas (Read and import) template covers both lists below: library:write includes library:read, and meetings:read is listed separately. Set the base URL and headers once; the later steps reuse them.
Library transcripts
GET /library/transcripts?limit=1 returns the most recently added transcript in your library. It needs library:read (or library:write, which includes it).
curl "https://api.diko.app/api/v1/library/transcripts?limit=1" \
-H "Authorization: Bearer $DIKO_API_KEY"import os
import requests
BASE = "https://api.diko.app/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['DIKO_API_KEY']}"}
first = requests.get(f"{BASE}/library/transcripts", headers=HEADERS, params={"limit": 1}, timeout=30)
first.raise_for_status()
print(first.json()["data"])const BASE = "https://api.diko.app/api/v1"
const headers = { Authorization: `Bearer ${process.env.DIKO_API_KEY}` }
const first = await fetch(`${BASE}/library/transcripts?limit=1`, { headers })
if (!first.ok) throw new Error(`${first.status} ${await first.text()}`)
console.log((await first.json()).data)The response is a list. data holds the items (an empty array if the library has no transcripts yet), has_more tells you whether another page exists and next_cursor is how you fetch it (Pagination):
{
"object": "list",
"data": [
{
"id": "trn_ZFF3NHc5V2dYY1E",
"object": "library_transcript",
"title": "Kaip veikia transkribavimas",
"source_type": "youtube",
"youtube_video_id": "dQw4w9WgXcQ",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"channel": "Diko",
"language": "lt",
"duration_ms": 212000,
"transcription_method": "captions",
"summary": { "status": "ready", "text": null },
"categories": ["Technologijos"],
"added_at": "2026-10-11T09:30:00.000Z",
"archived": false,
"segment_count": 120,
"revision": "r_6f6f4bb9762915b561878fd1e1fa7f56",
"web_url": "https://studio.diko.app/library/dQw4w9WgXcQ"
}
],
"has_more": false,
"next_cursor": null
}Meetings
If the key has meetings:read, GET /meetings?limit=1 returns your latest meeting in the same list format:
curl "https://api.diko.app/api/v1/meetings?limit=1" \
-H "Authorization: Bearer $DIKO_API_KEY"meetings = requests.get(f"{BASE}/meetings", headers=HEADERS, params={"limit": 1}, timeout=30)
meetings.raise_for_status()
print(meetings.json()["data"])const meetings = await fetch(`${BASE}/meetings?limit=1`, { headers })
if (!meetings.ok) throw new Error(`${meetings.status} ${await meetings.text()}`)
console.log((await meetings.json()).data)A meeting item has an id (mtg_...), title, status, processing, transcript and minutes. Participants, speakers and counts come only from GET /meetings/{meeting_id}.
If the first call fails
- 401 means the key itself was not accepted:
missing_api_key,invalid_api_key,api_key_expired,api_key_revokedorapi_key_disabled. Check thatDIKO_API_KEYis set and that the key is still active. - 403
insufficient_scopemeans the key is valid but lacks the scope this endpoint needs. For example, a key with onlylibrary:readcallingGET /meetings:
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer realm="diko", error="insufficient_scope", scope="meetings:read"{
"type": "error",
"error": {
"type": "permission_error",
"code": "insufficient_scope",
"message": "The API key lacks the permission this endpoint needs.",
"doc_url": "https://diko.app/en/api-docs/errors#insufficient_scope",
"details": {
"required_scopes": ["meetings:read"],
"granted_scopes": ["library:read"]
}
},
"request_id": "req_QBPIQ5QTIWAUYTMDNCLD7CMKBE"
}details.required_scopes is what the endpoint needs and details.granted_scopes is what your key has. Scopes cannot be changed after a key is created, so create a new key with the right scope (Scopes). Other 403 codes (plan_required, ip_not_allowed and so on) are in the error catalog.
3. Import a YouTube video
POST /library/imports needs library:write (the Skaitymas ir importas template has it). It starts an asynchronous import and never waits for a download. Always send an Idempotency-Key: if the request is interrupted, you can repeat the same request with the same key and the import is not created twice.
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"}'import uuid
# BASE and HEADERS come from step 2. One key per import; reuse it when you repeat the request.
idempotency_key = str(uuid.uuid4())
response = requests.post(
f"{BASE}/library/imports",
headers={**HEADERS, "Idempotency-Key": idempotency_key},
json={"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"},
timeout=30,
)
response.raise_for_status()
job = response.json()
print(job["id"], job["status"])// BASE and headers come from step 2. One key per import; reuse it when you repeat the request.
const idempotencyKey = crypto.randomUUID()
const response = await fetch(`${BASE}/library/imports`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json", "Idempotency-Key": idempotencyKey },
body: JSON.stringify({ url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ" }),
})
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`)
let job = await response.json()
console.log(job.id, job.status)The response is 201 Created with a Location header. A video imported for the first time starts at queued:
{
"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": null,
"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
}If the transcript is already in your library or in the shared cache, the import becomes succeeded at once with reused: true and no credits are used.
4. Wait for the result
You can poll the import status or get a webhook.
Polling
While the import has not reached a final status, GET /library/imports/{import_id} returns the header Retry-After: 3. Poll no more often than that and set an overall deadline:
curl "https://api.diko.app/api/v1/library/imports/imp_6Mfj7Oxm4MfxGGmZRhiUQf" \
-H "Authorization: Bearer $DIKO_API_KEY"import time
deadline = time.monotonic() + 15 * 60
while job["status"] not in ("succeeded", "failed", "canceled"):
if time.monotonic() > deadline:
raise TimeoutError("The import is still running: use webhooks or check again later")
time.sleep(3)
response = requests.get(f"{BASE}/library/imports/{job['id']}", headers=HEADERS, timeout=30)
response.raise_for_status()
job = response.json()
if job["status"] != "succeeded":
raise RuntimeError(job["error"] or job["status"])
print(job["transcript_id"])const deadline = Date.now() + 15 * 60_000
while (!["succeeded", "failed", "canceled"].includes(job.status)) {
if (Date.now() > deadline) throw new Error("The import is still running: use webhooks or check again later")
await new Promise(resolve => setTimeout(resolve, 3000))
const poll = await fetch(`${BASE}/library/imports/${job.id}`, { headers })
if (!poll.ok) throw new Error(`${poll.status} ${await poll.text()}`)
job = await poll.json()
}
if (job.status !== "succeeded") throw new Error(JSON.stringify(job.error ?? job.status))
console.log(job.transcript_id)A long video can take longer than your deadline. Then leave the loop and check again later, or use webhooks.
Import statuses and errors
An import has one of these status values. succeeded, failed and canceled are final.
status |
Meaning |
|---|---|
queued |
waits in the queue; queue_position shows your place |
processing |
running; stage names the step |
canceling |
you asked to cancel a running import; it ends as canceled when the work stops |
succeeded |
done; transcript_id is set |
failed |
stopped by an error; error.code says why |
canceled |
canceled by you or by the system. The system cancels an import when its key is revoked, disabled, deleted or rotated without an overlap period, when the key is revoked after a leak, and when API access is suspended. A queued import ends canceled at once, a running one goes through canceling. Work already done is charged as usual. A rotation with a grace period does not cancel it |
While the import is processing, stage is one of: captions (Diko looks for YouTube captions), waiting (waits for a free speech-recognition slot), downloading, transcribing or summarizing. In every other status stage is null.
A failed import carries error.code:
error.code |
Meaning |
|---|---|
video_unavailable, video_private, age_restricted |
the video cannot be fetched; a retry does not help |
video_too_long |
the video is longer than the limit for speech recognition (currently 4 hours) |
video_duration_unknown |
Diko could not read the duration of the video |
captions_unavailable |
captions could not be retrieved |
download_failed, transcription_failed |
the download or the transcription failed; try again later |
transcription_timeout |
the import stayed queued for more than 24 hours or processing for more than 6 hours |
provider_limit |
a caption provider limit was reached; try again later |
insufficient_credits |
there were not enough credits to finish the import |
internal_error |
an error on Diko’s side; try again later |
To try again, create a new import with a new Idempotency-Key.
Credits. A transcript that is already in your library or in the shared cache costs nothing (reused: true). Otherwise Diko checks your credit balance when it creates the import and again before paid work. GET /me shows the balance for a key with library:write. Credit rates and plan allowances are on the pricing page (in Lithuanian).
Webhook
Create an endpoint under Nustatymai → API → Webhooks (/settings/api/webhooks) and choose the events library.import.succeeded, library.import.failed and library.import.canceled. Diko sends a POST request to your address when the import ends. Signature verification and retries are described on Events and webhooks.
5. Read the transcript
When the import is succeeded, transcript_id is the trn_... ID of a transcript that is now in your library (it also appears in the GET /library/transcripts list from step 2). Fetch its text:
# The whole text with timestamps (use the transcript_id from the import)
curl "https://api.diko.app/api/v1/library/transcripts/trn_ZFF3NHc5V2dYY1E/transcript?format=text" \
-H "Authorization: Bearer $DIKO_API_KEY"transcript_id = job["transcript_id"]
text = requests.get(
f"{BASE}/library/transcripts/{transcript_id}/transcript",
headers=HEADERS,
params={"format": "text"},
timeout=30,
)
text.raise_for_status()
print(text.text)const transcriptId = job.transcript_id
const text = await fetch(`${BASE}/library/transcripts/${transcriptId}/transcript?format=text`, { headers })
if (!text.ok) throw new Error(`${text.status} ${await text.text()}`)
console.log(await text.text())The text comes as lines [HH:MM:SS] Name: text. Want segments as JSON, SRT or VTT? See getLibraryTranscriptTranscript.
Check what your key can do (GET /me)
GET /me is account metadata, not your data. It works with any valid key, so it is a quick way to see which key you are using, whether the account may use the API and, for a key with the library:write scope, what the credit balance is. Use it when something does not work as expected, not as the first call.
curl "https://api.diko.app/api/v1/me" \
-H "Authorization: Bearer $DIKO_API_KEY"me = requests.get(f"{BASE}/me", headers=HEADERS, timeout=30)
me.raise_for_status()
print(me.json()["access"])const me = await fetch(`${BASE}/me`, { headers })
if (!me.ok) throw new Error(`${me.status} ${await me.text()}`)
console.log((await me.json()).access)The access part of a successful response looks like this:
{ "allowed": true, "reason": "subscription", "plan": "verslas" }When allowed is false, reason says why (for example plan_required). The meaning of every code is in the error catalog.
GET /me follows least privilege, so a narrowly scoped key never reveals more than it needs. user.email is always masked, and credits is filled in only for a key with library:write, the only scope that spends credits:
{
"user": { "id": "usr_4iAD1dYhdAEJIkMN5GPczR", "object": "user", "email": "j***@example.com", "display_name": "Jonas" },
"credits": { "scope": "user", "available": "123.5000", "as_of": "2026-10-11T09:30:00.000Z" }
}For a read-only key (meetings:read, library:read) the same response has "credits": null. GET /me has its own, stricter rate limit (Rate limits), so do not call it in a loop.
What next
- Errors: how to handle every code and when to retry.
- Rate limits: request and import limits.
- Meetings: read meeting transcripts, minutes and action items with
meetings:read.