Skip to main content

Developers

Dub videos from your own systems

The Metaphona REST API lets an LMS, a media library or a CMS upload videos, start dubbing, review the translated lines and download the dubbed videos, without the web app.

  • REST and JSON
  • Bearer API key
  • 120 requests a minute
  • Signed webhooks

Getting started

  1. A plan with the API

    The API comes with plans that include it. Without it, every call answers 403 api_not_in_plan.

  2. Create a key

    An owner or manager opens Your organization → API & webhooks, names the key and chooses its permissions. The whole key is shown once.

  3. Call the API

    Send the key in the Authorization header of every request to the base address below. Your first call can list your projects.

Base address
https://metaphona.devbuilder.dev/api/public/v1
Your first call
curl https://metaphona.devbuilder.dev/api/public/v1/projects \
  -H "Authorization: Bearer $METAPHONA_API_KEY"

Authentication

Every request carries an API key: Authorization: Bearer mph_<8 characters>_<43 characters>. The whole key is shown once, when it is created; only its SHA-256 is kept.

A key acts on behalf of the owner or manager who created it: it reaches what that person can reach. It stops working (401 api_key_invalid) when it is revoked, or when that person is removed, disabled, moves to another organization or is no longer an owner or manager. What it does appears in the activity log under their name.

No cookie and no CSRF token: the key is the only credential. Keep it on your server, never in a browser.

Permissions (scopes)

A key has one or more scopes and each route needs one. A missing scope answers 403 api_scope_missing.

read12 routes
See projects, videos, jobs, lines, voices and invoices.
upload6 routes
Create projects and upload videos.
jobs6 routes
Set a video’s languages and voices; start, cancel and retry jobs.
review3 routes
Edit and approve translated lines, or skip the review.
export6 routes
List dubbed videos, make download links, download videos and subtitles.

Limits and errors

120 requests a minute per key. Beyond that, 429 too_many_requests with a Retry-After header in seconds. Upload parts count like any request.

Errors are problem documents (application/problem+json, RFC 9457) with a stable code, a detail you can show to people, and a requestId to quote to support.

Ids of another organization answer exactly like ids that do not exist (404). Addresses that are not listed below answer 404 not_found.

Language codes come from GET /languages. Calls about one dub language take ?language=<code>; without it they mean the job’s primary language.

Codes you will meet

unauthenticated
No key was sent.
api_key_invalid
The key is unknown or revoked, or its creator can no longer use it.
api_not_in_plan
Your organization’s plan does not include the API.
api_scope_missing
The key lacks the scope this route needs.
not_found
Nothing with this id in your organization, or an address that is not part of the API.
too_many_requests
More than 120 requests in a minute: wait for Retry-After seconds.

Routes

All 33 routes, grouped by the scope they need. Paths are relative to the base address.

readSee projects, videos, jobs, lines, voices and invoices.

12 routes
  • GET/languages

    The languages and dubbing pairs your organization may use

    Answers 200

  • GET/projects

    Projects the key's creator can see (newest change first)

    ?status (optional)?query (optional)?page (optional)?size (optional)Answers 200

  • GET/projects/{projectId}

    One project

    Answers 200

  • GET/projects/{projectId}/media

    The project's videos (newest first)

    Answers 200

  • GET/projects/{projectId}/voices

    The voices the project can use for one dub language, and the cloning models

    ?language (optional)Answers 200

  • GET/media/{mediaId}

    One video, its check result, its languages and how far each dub language is

    Answers 200

  • GET/media/{mediaId}/voice-settings

    The voices used for one dub language of this video

    ?language (optional)Answers 200

  • GET/media/{mediaId}/jobs

    The video's jobs (newest first)

    Answers 200

  • GET/media/{mediaId}/translation

    The latest job's translation of one dub language, in numbers (lines, approved, flagged)

    ?language (optional)Answers 200

  • GET/jobs/{jobId}

    A job's state, progress, time estimate, stages and each dub language

    Answers 200

  • GET/jobs/{jobId}/translations

    The translated lines of one dub language, in order

    ?language (optional)?page (optional)?size (optional)Answers 200

  • GET/invoices/{invoiceId}/pdf

    A sent invoice or credit note as PDF (the invoice.sent event links here)

    Answers 200

uploadCreate projects and upload videos.

6 routes
  • POST/projects

    Create a project (for example one per course) with its languages

    Body: application/jsonAnswers 201

  • POST/projects/{projectId}/media/uploads

    Start a resumable upload

    Body: application/jsonAnswers 201

    Reserves the space (checked against your organization's storage quota and the longest video your plan allows once the file is checked). The answer says the part size and how many parts to send; parts can be sent in any order and again after an interruption (GET /uploads/{uploadId} lists the parts already received).

  • GET/uploads/{uploadId}

    An upload's state and the parts already received (to resume)

    Answers 200

  • DELETE/uploads/{uploadId}

    Abandon an upload and free its reserved space

    Answers 204

  • PUT/uploads/{uploadId}/parts/{partNumber}

    Send one part (exactly partSize bytes; the last part may be shorter)

    Body: application/octet-streamAnswers 200

  • POST/uploads/{uploadId}/complete

    Finish the upload; the video is then checked (content, duration, tracks) in the background

    Answers 202

jobsSet a video’s languages and voices; start, cancel and retry jobs.

6 routes
  • PUT/projects/{projectId}/voice

    The project's voice for one dub language (a standard voice, automatic voices, or cloned voices)

    ?language (optional)Body: application/jsonAnswers 200

    Cloned voices (cloneVoices: true) are only for recordings your organization holds the rights to: the first time, send attestRights: true (recorded with the key creator's name and the time in the activity log). Cloning is not available on every plan and needs a cloning model that speaks the language.

  • PUT/media/{mediaId}/languages

    The video's own languages (otherwise the project's); applies to its next job

    Body: application/jsonAnswers 200

  • PUT/media/{mediaId}/clone-engine

    The cloning model of this video for one dub language (null = the project's)

    ?language (optional)Body: application/jsonAnswers 200

  • POST/media/{mediaId}/jobs

    Start dubbing the video into its target languages

    Body: application/jsonAnswers 201

    The video must be READY. Uses the video's languages (or the project's) and voices. Counts against your monthly minutes; refused (409/403 with a code) when a quota is reached, too many videos are being dubbed at once, or new videos are paused because an invoice is overdue.

  • POST/jobs/{jobId}/cancel

    Cancel a job (or one of its languages)

    ?language (optional)Answers 200

  • POST/jobs/{jobId}/retry

    Retry the failed or cancelled steps (finished work is kept)

    ?language (optional)Answers 200

reviewEdit and approve translated lines, or skip the review.

3 routes
  • POST/jobs/{jobId}/translations/approve-all

    Approve every line of one dub language; once all are approved, the voices are made

    ?language (optional)Answers 200

  • POST/jobs/{jobId}/skip-review

    Go on without reviewing one dub language (the machine translation is used as it is)

    ?language (optional)Answers 200

  • PATCH/translations/{lineId}

    Change one line's text and/or approve it

    Body: application/jsonAnswers 200

exportList dubbed videos, make download links, download videos and subtitles.

6 routes
  • GET/media/{mediaId}/exports

    The dubbed videos made from this video (kept 30 days)

    ?language (optional)Answers 200

  • GET/media/{mediaId}/exports/multi

    The state of the one-file export with every finished dub language

    Answers 200

  • POST/media/{mediaId}/exports/multi

    Make one video with every finished dub language as audio and subtitle tracks

    Body: application/jsonAnswers 202

  • GET/media/{mediaId}/subtitles

    Subtitles of one dub language (SRT or WebVTT)

    ?language (optional)?format (optional)Answers 200

  • POST/exports/{exportId}/link

    A download link for a dubbed video (valid 15 minutes, usable only with the same API key's creator)

    Answers 200

  • GET/downloads/{token}

    The dubbed video file (supports Range requests to resume)

    Answers 200

Webhooks

Owners and managers add https addresses on Your organization → API & webhooks and choose the events. Each event is a POST of JSON to your address.

Events

  • job.waiting_review

    A dub language waits for its translation review.

  • job.done

    A job finished: every dub language has its video.

  • job.failed

    A job stopped with an error; it can be retried.

  • invoice.sent

    An invoice was sent to your organization.

webhook.test is sent by “Send test” to check your address.

What is sent

Payloads hold ids, languages, states and links only: never transcript text or file contents.

job.done
{
  "id": "6f1c2a4e-…",
  "type": "job.done",
  "createdAt": "2026-11-09T10:00:00Z",
  "organizationId": "…",
  "data": {
    "jobId": "…", "mediaId": "…", "projectId": "…",
    "sourceLanguage": "<code>", "language": "<code>", "status": "SUCCEEDED",
    "languages": [{ "language": "<code>", "status": "SUCCEEDED" }],
    "links": {
      "job": "https://metaphona.devbuilder.dev/api/public/v1/jobs/…",
      "exports": "https://metaphona.devbuilder.dev/api/public/v1/media/…/exports?language=<code>"
    }
  }
}

Headers

X-Metaphona-Event
The event type, the same as type in the body.
X-Metaphona-Delivery
The delivery id. It stays the same when a delivery is retried or redelivered: store the ids you handled and ignore repeats.
X-Metaphona-Signature
t=<unix seconds>,v1=<hex HMAC-SHA256>, made with your signing secret.

Check the signature

Compute an HMAC-SHA256 with your signing secret (whsec_…, shown once) over t + "." + the exact bytes received. Compare it with v1 in constant time, and refuse a t more than 5 minutes away from your clock.

Python
import hashlib, hmac, time

def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
    """header: X-Metaphona-Signature; body: the exact bytes received."""
    parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
    t = int(parts.get("t", "0"))
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto'

// header: X-Metaphona-Signature; body: the raw request body (a Buffer, as received).
export function verify(secret, header, body, tolerance = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=', 2)))
  const t = Number(parts.t)
  if (!t || Math.abs(Date.now() / 1000 - t) > tolerance) return false
  const expected = createHmac('sha256', secret).update(`${t}.`).update(body).digest('hex')
  const given = Buffer.from(parts.v1 ?? '')
  return given.length === expected.length && timingSafeEqual(given, Buffer.from(expected))
}

Answer quickly

Answer any 2xx within 10 seconds and do the work afterwards. Any other answer, or none, is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours; then the delivery is marked failed and can be sent again from the delivery log.

Example: an LMS

A course platform (Moodle, Canvas or your own) sends a lesson video and gets the dubbed version back. These are the calls it makes, in order.

  1. Find or create the course’s project

    Scope:readupload

    One project per course keeps its videos, glossary and voices together.

    • GET /projects?query=…
    • POST /projects
  2. Upload the video in parts

    Scope:upload

    The answer gives the part size and the number of parts. Send them in any order; after an interruption, GET the upload to see which parts arrived.

    • POST /projects/{projectId}/media/uploads
    • PUT /uploads/{uploadId}/parts/{n}
    • POST /uploads/{uploadId}/complete
  3. Wait for the check

    Scope:read

    The video is checked in the background: wait until its status is READY.

    • GET /media/{mediaId}
  4. Start dubbing

    Scope:jobs

    Optionally set this video’s own languages first. The job’s minutes count against your monthly minutes.

    • PUT /media/{mediaId}/languages
    • POST /media/{mediaId}/jobs
  5. Follow the job

    Scope:read

    Ask for the job’s state, or let the webhooks tell you: job.waiting_review, job.done, job.failed.

    • GET /jobs/{jobId}
  6. Review, or skip the review

    Scope:readreview

    Read the translated lines, fix some, then approve them all, or go on with the machine translation.

    • GET /jobs/{jobId}/translations?language=…
    • PATCH /translations/{lineId}
    • POST /jobs/{jobId}/translations/approve-all?language=…
    • POST /jobs/{jobId}/skip-review?language=…
  7. Download the dubbed video

    Scope:export

    Each download link is valid 15 minutes and supports Range requests to resume. Dubbed videos are kept 30 days: copy them into your own storage.

    • GET /media/{mediaId}/exports?language=…
    • POST /exports/{exportId}/link
    • GET <link>
  8. If it failed

    Scope:jobs

    Retry: the finished steps are kept.

    • POST /jobs/{jobId}/retry

In an LMS

  • Store the key and the webhook secret in the LMS’s encrypted settings, never in course content or in the browser.
  • Keep the project id with the course, and the video and job ids with the lesson.
  • On job.done, download the dubbed file and attach it to the lesson as its other-language version, or link the WebVTT subtitles.

OpenAPI file

The full description of the API, OpenAPI 3.1: open it in Swagger UI or Postman, or generate a client from it. It is public: no key is needed to download it.

Metaphona public API · 1.0 · https://metaphona.devbuilder.dev/api/public/v1