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/v1That 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| Status | Meaning |
|---|---|
401 | Missing, malformed, unknown or revoked key. |
403 | Valid key, but the workspace's plan no longer includes API access. The body carries code: plan_upgrade_required. |
429 | Upload rate limit — 120 new files per hour, per API key. |
404 | No such endpoint. GET /v1 lists everything this API accepts. |
Reference
Endpoints
Base URL https://api.dovev.io/v1. Every response is JSON, including errors.
/v1/meConfirms the key and names the workspace. Use it as a connection test.
Response
{
"workspace": { "id": "de64e595-…", "name": "Acme" },
"plan": "business"
}/v1/transcriptsCompleted transcripts, newest first.
Query parameters
since | ISO 8601. Only transcripts finished after it — this is what makes the endpoint a poller rather than a full re-read. |
limit | 1–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"
}
]
}/v1/transcripts/:idOne 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."
}/v1/transcriptsSubmit 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.
/v1/languagesEvery 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.completedA transcription finished and its text is ready to fetch.
job.failedA job stopped for good. The payload carries the reason.
transcript.updatedSomeone edited the transcript in the workspace.
translation.completedA 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.after 30 seconds
- 2.after 2 minutes
- 3.after 10 minutes
- 4.after 1 hour
- 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-Event | Which event this is. |
X-Dovev-Timestamp | Unix seconds. Part of the signature. |
X-Dovev-Signature | HMAC-SHA256, hex. |
X-Dovev-Delivery | Delivery 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.