Developers

Transcribe from
your own code.

The API is the same code the app runs on: /v1 mounts the very routers the web client uses, behind key auth instead of a session. There is no second implementation, so what you read here cannot drift from what the product does.

API access is included from Starter up. Webhooks are a Business feature. Prefer no code? Use Zapier.

Sixty seconds

Your first call

Create a key in Developer → API keys. It is shown once — only a SHA-256 of it is stored, so it cannot be recovered afterwards. Then ask the API what it accepts:

curl -H "Authorization: Bearer dvk_your_key_here" \
    https://api.dovev.io/v1

That returns the endpoint index, so it doubles as a connection test: a wrong or revoked key gets 401 instead.

Authentication

One key, one workspace

A key acts for its whole workspace, with admin rights inside it, and spends that workspace's balance. If the plan lapses below Starter the key stops working — it cannot outlive the subscription that justified it.

Treat it like a password: server-side only. Anything you ship to a browser is public, and this key can read every transcript in the workspace.

Authorization: Bearer dvk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
StatusMeaning
401Missing, malformed, unknown or revoked key.
403Valid key, but the workspace's plan no longer includes API access. The body carries code: plan_upgrade_required.
429Upload rate limit — 120 new files per hour, per API key.
404No such endpoint. GET /v1 lists everything this API accepts.

Reference

Endpoints

Base URL https://api.dovev.io/v1. Every response is JSON, including errors.

GET/v1/me

Confirms the key and names the workspace. Use it as a connection test.

Response

{
    "workspace": { "id": "de64e595-…", "name": "Acme" },
    "plan": "business"
  }
GET/v1/transcripts

Completed transcripts, newest first.

Query parameters

sinceISO 8601. Only transcripts finished after it — this is what makes the endpoint a poller rather than a full re-read.
limit1–100. Defaults to 25.

Response

{
    "transcripts": [
      {
        "id": "d6bf9b7f-…",
        "filename": "board-call.mp4",
        "duration_seconds": 297,
        "language": "en",
        "completed_at": "2026-08-15T09:12:04.000Z"
      }
    ]
  }
GET/v1/transcripts/:id

One transcript with its text. Add ?language=es for a translation; omit it for the original.

Response

{
    "id": "d6bf9b7f-…",
    "filename": "board-call.mp4",
    "language": "en",
    "text": "Speaker A: Good afternoon.\nSpeaker B: Thanks for joining."
  }
POST/v1/transcripts

Submit a media URL. Returns 202 as soon as the job is queued — transcription is not instant.

Request body

{
    "url": "https://example.com/interview.mp4",
    "languages": ["es"],
    "medical": false
  }

Response

{ "id": "3f914eaa-…", "status": "queued" }

The URL must be reachable without a login. YouTube links are extracted server-side. Poll GET /v1/transcripts/:id, or register a webhook and be told.

GET/v1/languages

Every translation target, as { code, name }.

Response

{ "languages": [{ "code": "es", "name": "Spanish" }, …] }

Webhooks

Be told, instead of asking

Register an endpoint under Developer → Webhooks and we POST to it when a job settles. Endpoints must be public https: private and link-local addresses are refused, because otherwise a webhook would be a way to make our servers fetch your internal network on your behalf.

Events

  • job.completed

    A transcription finished and its text is ready to fetch.

  • job.failed

    A job stopped for good. The payload carries the reason.

  • transcript.updated

    Someone edited the transcript in the workspace.

  • translation.completed

    A translation finished for one target language.

New endpoints receive all four.

Retries

Five attempts over about eight hours — long enough to ride out your deploy, short enough that a permanently dead endpoint stops being retried the same day.

  1. 1.after 30 seconds
  2. 2.after 2 minutes
  3. 3.after 10 minutes
  4. 4.after 1 hour
  5. 5.after 6 hours

X-Dovev-Delivery is stable across retries, so use it to make your handler idempotent.

Headers on every delivery

X-Dovev-EventWhich event this is.
X-Dovev-TimestampUnix seconds. Part of the signature.
X-Dovev-SignatureHMAC-SHA256, hex.
X-Dovev-DeliveryDelivery id, stable across retries.

Verify against the raw request body. Re-serialising the parsed JSON changes the bytes, and the signature will not match.

Verifying a delivery

The signature covers timestamp.body, not the body alone — a body-only signature is replayable forever by anyone who captures one delivery.

import { createHmac, timingSafeEqual } from "node:crypto";

    function verify(secret, timestamp, rawBody, signature) {
      const expected = createHmac("sha256", secret)
        .update(`${timestamp}.${rawBody}`)
        .digest("hex");

      const a = Buffer.from(expected, "hex");
      const b = Buffer.from(signature, "hex");
      if (a.length !== b.length) return false;

      // Reject anything older than five minutes,
      // or a captured delivery replays.
      if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

      return timingSafeEqual(a, b);
    }

Ready to build?

Keys are created and revoked in the app, and a revoked key stops working immediately. Something missing from this page? Tell us what you are trying to build.