API reference
REST API for converting files programmatically. Business and Enterprise plans — create a key from your dashboard. Secrets are hashed at rest; plaintext is shown once.
Choose sync or async first
Start with POST /api/v1/convert for a bounded conversion supported by the synchronous engine policy. If the API returns that the engine or input requires async processing, call POST /api/v1/uploads, PUT the bytes to its signed URL, then create the job with POST /api/v1/jobs. Poll the returned job ID until it is terminal.
Authentication
Pass Authorization: Bearer cvx_live_…. Keys support scopes (convert:write, jobs:read, convert:read), optional expiry, revocation, and rotation. Expired or revoked keys return 401.
curl https://convyx.io/api/v1/jobs \
-H "Authorization: Bearer cvx_live_xxxxxxxxxxxx"Uploads & conversion
Synchronous POST /api/v1/convert accepts low-cost conversions up to 25 MB and returns a signed download URL. Larger files and media, office, or archive work use POST /api/v1/uploads for a direct-to-storage PUT, followed by a metadata-only POST /api/v1/jobs. The job endpoint returns 202 and supports Idempotency-Key (8–128 chars) so retries never double-convert.
cURL — sync
curl -X POST https://convyx.io/api/v1/convert \
-H "Authorization: Bearer cvx_live_xxxxxxxxxxxx" \
-F "[email protected]" \
-F "sourceFormat=png" \
-F "targetFormat=webp" \
-F "retention=24h"cURL — async + idempotency
# 1. Prepare a direct-to-storage upload
curl -X POST https://convyx.io/api/v1/uploads \
-H "Authorization: Bearer cvx_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"fileName":"lecture.mp4","fileSizeBytes":123456,"sourceFormat":"mp4"}'
# 2. PUT bytes to the returned uploadUrl with its contentType/requiredHeaders
# 3. Submit jobId, storageKey, file metadata and target (no file bytes)
curl -X POST https://convyx.io/api/v1/jobs \
-H "Authorization: Bearer cvx_live_xxxxxxxxxxxx" \
-H "Idempotency-Key: order-42-mp4-webm" \
-H "Content-Type: application/json" \
-d '{"jobId":"...","storageKey":"uploads/...","fileName":"lecture.mp4","fileSizeBytes":123456,"sourceFormat":"mp4","targetFormat":"webm","retention":"24h"}'Sync response:
{
"jobId": "35562e3e-4268-49c5-8534-a702b16c0b5f",
"status": "completed",
"sourceFormat": "png",
"targetFormat": "webp",
"downloadUrl": "https://storage.convyx.io/...",
"expiresAt": "2026-08-02T17:17:50.316Z"
}Status & downloads
GET /api/v1/jobs/:id requires jobs:read (or legacy convert:write). You only see your own jobs — other users' IDs return JOB_NOT_FOUND. Completed jobs include a short-lived signed downloadUrl.
curl https://convyx.io/api/v1/jobs/35562e3e-4268-49c5-8534-a702b16c0b5f \
-H "Authorization: Bearer cvx_live_xxxxxxxxxxxx"JavaScript @convyx/sdk
The JavaScript and Python clients are maintained and tested in this repository. Public npm and PyPI publishing is being prepared; use the plain HTTP examples below until package releases are announced in the changelog.
import { ConvyxClient } from "@convyx/sdk";
const client = new ConvyxClient(process.env.CONVYX_API_KEY!);
const { downloadUrl } = await client.convert({
file: fileBuffer,
fileName: "photo.png",
sourceFormat: "png",
targetFormat: "webp",
});
const job = await client.createJob({
file: videoBuffer,
fileName: "lecture.mp4",
sourceFormat: "mp4",
targetFormat: "webm",
idempotencyKey: "order-42-mp4-webm",
});
const finished = await client.waitForJob(job.jobId);Or plain fetch:
const form = new FormData();
form.set("file", fileBlob);
form.set("sourceFormat", "png");
form.set("targetFormat", "webp");
const res = await fetch("https://convyx.io/api/v1/convert", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.CONVYX_API_KEY}` },
body: form,
});
const { downloadUrl } = await res.json();TypeScript
Use the same SDK with typed responses:
type JobStatus =
| "queued"
| "processing"
| "completed"
| "failed"
| "expired"
| "cancelled";
interface JobResponse {
jobId: string;
status: JobStatus;
downloadUrl?: string | null;
errorMessage?: string | null;
}Python convyx
from convyx import ConvyxClient
import os
client = ConvyxClient(os.environ["CONVYX_API_KEY"])
with open("photo.png", "rb") as f:
result = client.convert(f, "photo.png", "png", "webp")
print(result["downloadUrl"])
with open("lecture.mp4", "rb") as f:
job = client.create_job(
f, "lecture.mp4", "mp4", "webm",
idempotency_key="order-42-mp4-webm",
)
finished = client.wait_for_job(job["jobId"])
print(finished.download_url)Or requests:
import os, requests
with open("photo.png", "rb") as f:
res = requests.post(
"https://convyx.io/api/v1/convert",
headers={"Authorization": f"Bearer {os.environ['CONVYX_API_KEY']}"},
files={"file": f},
data={"sourceFormat": "png", "targetFormat": "webp"},
)
print(res.json()["downloadUrl"])PHP
<?php
$ch = curl_init("https://convyx.io/api/v1/convert");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("CONVYX_API_KEY"),
],
CURLOPT_POSTFIELDS => [
"file" => new CURLFile("photo.png", "image/png", "photo.png"),
"sourceFormat" => "png",
"targetFormat" => "webp",
],
CURLOPT_RETURNTRANSFER => true,
]);
$body = json_decode(curl_exec($ch), true);
echo $body["downloadUrl"];Go
package main
import (
"bytes"
"io"
"mime/multipart"
"net/http"
"os"
)
func main() {
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
fw, _ := w.CreateFormFile("file", "photo.png")
f, _ := os.Open("photo.png")
io.Copy(fw, f)
w.WriteField("sourceFormat", "png")
w.WriteField("targetFormat", "webp")
w.Close()
req, _ := http.NewRequest("POST", "https://convyx.io/api/v1/convert", &buf)
req.Header.Set("Authorization", "Bearer "+os.Getenv("CONVYX_API_KEY"))
req.Header.Set("Content-Type", w.FormDataContentType())
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
// decode JSON → downloadUrl
}Webhooks
Events: conversion.completed, conversion.failed, conversion.expired (legacy job.* aliases still match). Deliveries include X-Convyx-Delivery-Id, HMAC signature, 10s timeout, up to 5 attempts with exponential backoff, and dashboard history with manual retry.
{
"event": "conversion.completed",
"deliveryId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"jobId": "35562e3e-4268-49c5-8534-a702b16c0b5f",
"status": "completed",
"sourceFormat": "mp4",
"targetFormat": "webm",
"fileName": "lecture.mp4",
"createdAt": "2026-08-02T17:15:02.100Z",
"completedAt": "2026-08-02T17:16:48.221Z",
"errorMessage": null
}import crypto from "node:crypto";
function isValid(rawBody, signatureHeader, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signatureHeader, "hex"),
);
}
// Headers: X-Convyx-Event, X-Convyx-Signature, X-Convyx-Delivery-Id
app.post("/webhooks/convyx", express.raw({ type: "*/*" }), (req, res) => {
const ok = isValid(req.body, req.headers["x-convyx-signature"], process.env.CONVYX_WEBHOOK_SECRET);
if (!ok) return res.status(401).end();
// Deduplicate with X-Convyx-Delivery-Id on retries / manual replay
res.status(200).end();
});Errors
Responses use { "error": string, "code": string }. Common codes: UNAUTHORIZED, FORBIDDEN, VALIDATION_ERROR, RATE_LIMITED, QUOTA_EXCEEDED, JOB_NOT_FOUND, UNSUPPORTED_FORMAT, FILE_TOO_LARGE.
Rate limits
Enforced per API key. Convert: 60/min Business, 600/min Enterprise. Async jobs: higher tier (120 / 1200). Every response includes X-RateLimit-* headers.