# Quickstart

> From creating a key to reading your first transcript in a few minutes.

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

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

> **Note:**
> The Diko app is in Lithuanian. Where a step names a button or a setting, the label you will see in the app comes first, with its English meaning in brackets, for example **Sukurti raktą** (Create key).

## 1. Create an API key

1. Sign in to Diko and open **Nustatymai → API** (Settings → API), the [API keys page](https://studio.diko.app/settings/api) (`/settings/api`). The old `/kurejams` link redirects there. You need the Verslas plan.
2. 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.
3. Confirm the action by signing in again if Diko asks for it.
4. 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:

```bash
export DIKO_API_KEY="dk_live_..."
```

> **Warning:**
> The key is a password for your data. Do not put it in a repository, in browser code or in logs. More: [Security guidelines](https://diko.app/en/api-docs/security.md).

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

```bash
curl "https://api.diko.app/api/v1/library/transcripts?limit=1" \
  -H "Authorization: Bearer $DIKO_API_KEY"
```

**Python**

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

**TypeScript**

```typescript
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](https://diko.app/en/api-docs/pagination.md)):

```json
{
  "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**

```bash
curl "https://api.diko.app/api/v1/meetings?limit=1" \
  -H "Authorization: Bearer $DIKO_API_KEY"
```

**Python**

```python
meetings = requests.get(f"{BASE}/meetings", headers=HEADERS, params={"limit": 1}, timeout=30)
meetings.raise_for_status()
print(meetings.json()["data"])
```

**TypeScript**

```typescript
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_revoked` or `api_key_disabled`. Check that `DIKO_API_KEY` is set and that the key is still active.
- **403 `insufficient_scope`** means the key is valid but lacks the scope this endpoint needs. For example, a key with only `library:read` calling `GET /meetings`:

```http
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer realm="diko", error="insufficient_scope", scope="meetings:read"
```

```json
{
  "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](https://diko.app/en/api-docs/scopes.md)). Other 403 codes (`plan_required`, `ip_not_allowed` and so on) are in the [error catalog](https://diko.app/en/api-docs/errors.md).

> **Note:**
> The TypeScript samples need Node 20+ with ES modules (`.mjs` files or `"type": "module"`), because they use top-level `await` and `fetch`.

> **Tip:**
> Using `httpx` instead of `requests`? The examples work unchanged: `httpx.get(...)` and `httpx.post(...)` accept the same `headers`, `params`, `json` and `timeout` arguments.

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

```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"}'
```

**Python**

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

**TypeScript**

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

```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": 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**

```bash
curl "https://api.diko.app/api/v1/library/imports/imp_6Mfj7Oxm4MfxGGmZRhiUQf" \
  -H "Authorization: Bearer $DIKO_API_KEY"
```

**Python**

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

**TypeScript**

```typescript
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](https://diko.app/kainos) (in Lithuanian).

### Webhook

Create an endpoint under **Nustatymai → API → Webhooks** ([`/settings/api/webhooks`](https://studio.diko.app/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](https://diko.app/en/api-docs/webhooks.md).

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

**cURL**

```bash
# 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"
```

**Python**

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

**TypeScript**

```typescript
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`](https://diko.app/en/api-docs/reference/get-library-transcript-transcript.md).

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

```bash
curl "https://api.diko.app/api/v1/me" \
  -H "Authorization: Bearer $DIKO_API_KEY"
```

**Python**

```python
me = requests.get(f"{BASE}/me", headers=HEADERS, timeout=30)
me.raise_for_status()
print(me.json()["access"])
```

**TypeScript**

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

```json
{ "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](https://diko.app/en/api-docs/errors.md).

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

```json
{
  "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](https://diko.app/en/api-docs/rate-limits.md)), so do not call it in a loop.

## What next

- [Errors](https://diko.app/en/api-docs/errors.md): how to handle every code and when to retry.
- [Rate limits](https://diko.app/en/api-docs/rate-limits.md): request and import limits.
- [Meetings](https://diko.app/en/api-docs/reference/list-meetings.md): read meeting transcripts, minutes and action items with `meetings:read`.
