DOCS
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. Keys created inside a workspace act for that workspace (its plan, shared quota and jobs) and stop working if their creator leaves it.
Example
curl "https://convyx.io/api/v1/jobs?limit=10" \
-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
Example
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
Example
# 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 (201):
Example
{
"jobId": "35562e3e-4268-49c5-8534-a702b16c0b5f",
"status": "completed",
"sourceFormat": "png",
"targetFormat": "webp",
"outputFileName": "converted.webp",
"fileSizeBytes": 48213,
"downloadUrl": "https://storage.convyx.io/...",
"expiresAt": "2026-08-02T17:17:50.316Z"
}Async response:
Example
// 202 Accepted (new job)
{
"jobId": "35562e3e-4268-49c5-8534-a702b16c0b5f",
"status": "queued",
"sourceFormat": "mp4",
"targetFormat": "webm",
"fileName": "lecture.mp4",
"fileSizeBytes": 123456,
"createdAt": "2026-08-02T17:15:02.100Z"
}
// Replaying the same Idempotency-Key → 200 with the job's current status and
// "idempotentReplay": true. Reusing a key for a different file → 409 CONFLICT.File size: the lower of your plan limit and the conversion engine's limit (images and documents 200 MB, PDFs and spreadsheets 100 MB, audio, video and archives 512 MB). POST /api/v1/uploads rejects oversize files before you upload. Every started job counts toward the monthly quota, including failed ones.
Status & downloads
GET /api/v1/jobs/:id requires jobs:read (or legacy convert:write). Personal keys see the key owner's jobs; workspace keys see the workspace's jobs. Any other ID returns 404 JOB_NOT_FOUND. Completed jobs include a signed downloadUrl valid for 15 minutes; GET /api/v1/jobs/:id/download redirects to a fresh one. After the job's retention window the output is deleted: status becomes expired, downloadUrl is null and /download returns 410 JOB_EXPIRED (409 JOB_NOT_READY before completion).
Example
# Status (includes a 15-minute downloadUrl once completed)
curl https://convyx.io/api/v1/jobs/35562e3e-4268-49c5-8534-a702b16c0b5f \
-H "Authorization: Bearer cvx_live_xxxxxxxxxxxx"
# Fresh download link at any time: 302 to a signed URL, 410 JOB_EXPIRED after retention
curl -L -o result.webm https://convyx.io/api/v1/jobs/35562e3e-4268-49c5-8534-a702b16c0b5f/download \
-H "Authorization: Bearer cvx_live_xxxxxxxxxxxx"
# List jobs, newest first (cursor: pass nextBefore back as ?before=)
curl "https://convyx.io/api/v1/jobs?limit=25&status=completed" \
-H "Authorization: Bearer cvx_live_xxxxxxxxxxxx"Example
{
"jobId": "35562e3e-4268-49c5-8534-a702b16c0b5f",
"status": "completed",
"sourceFormat": "mp4",
"targetFormat": "webm",
"fileName": "lecture.mp4",
"fileSizeBytes": 123456,
"retention": "24h",
"downloadUrl": "https://storage.convyx.io/...",
"createdAt": "2026-08-02T17:15:02.100Z",
"completedAt": "2026-08-02T17:16:48.221Z",
"expiresAt": "2026-08-03T17:16:48.221Z",
"errorMessage": null
}
// GET /api/v1/jobs → { "data": [ ...same shape... ], "hasMore": true, "nextBefore": "2026-08-02T17:15:02.100Z" }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.
Example
import { ConvyxClient, ConvyxApiError } 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);
// Later — a fresh link even after the 15-minute downloadUrl lapsed:
const url = await client.getDownloadUrl(job.jobId);
const { data, nextBefore } = await client.listJobs({ limit: 25 });
try {
await client.convert({ /* ... */ });
} catch (err) {
if (err instanceof ConvyxApiError && err.code === "QUOTA_EXCEEDED") {
// err.status === 429, err.details = { used, limit }
}
}Or plain fetch:
Example
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:
Example
import type { JobResult, JobStatus, JobList } from "@convyx/sdk";
// JobStatus:
// in flight: "created" | "queued" | "processing" | "finalizing"
// terminal: "completed" | "failed" | "cancelled" | "expired"
// Every error response:
interface ApiError {
error: string; // human-readable
code: string; // e.g. "QUOTA_EXCEEDED"
details?: Record<string, unknown>;
}Python convyx
Example
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)
print(client.get_download_url(job["jobId"])) # fresh signed URL any time
page = client.list_jobs(limit=25, status="failed")
for j in page.data:
print(j.job_id, j.error_message)Or requests:
Example
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
Example
<?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
Example
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-Event and X-Convyx-Delivery-Id. Non-2xx responses and timeouts (10s) are retried up to 5 attempts with exponential backoff; the dashboard keeps 90 days of delivery history with manual retry. Personal webhooks receive your personal jobs; workspace webhooks receive that workspace's jobs. Deliveries pause while the account is not on a Business or Enterprise plan.
Verifying signatures (v2, recommended)
Every delivery carries X-Convyx-Timestamp (unix seconds, set per attempt) and X-Convyx-Signature-V2: the hex HMAC-SHA256 of `${timestamp}.${rawBody}` with your webhook secret. Recompute it over the raw, unparsed body, compare in constant time, and reject timestamps more than 5 minutes from your clock in either direction. That stops a captured delivery from being replayed later; store X-Convyx-Delivery-Id to ignore repeats inside the window.
Example
{
"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
}Example
import crypto from "node:crypto";
const TOLERANCE_SECONDS = 300; // reject deliveries older (or newer) than 5 minutes
// v2 (recommended): HMAC-SHA256 over `${timestamp}.${rawBody}`
function isValid(rawBody, signatureHeader, timestampHeader, secret) {
if (!/^\d+$/.test(timestampHeader ?? "") || !signatureHeader) return false;
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestampHeader));
if (age > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestampHeader}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(signatureHeader, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Headers: X-Convyx-Event, X-Convyx-Delivery-Id, X-Convyx-Timestamp,
// X-Convyx-Signature-V2 (recommended) and X-Convyx-Signature (legacy v1)
app.post("/webhooks/convyx", express.raw({ type: "*/*" }), (req, res) => {
const ok = isValid(
req.body.toString("utf8"),
req.headers["x-convyx-signature-v2"],
req.headers["x-convyx-timestamp"],
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();
});
// Or with the SDKs:
// JS: verifyWebhookSignatureV2(rawBody, sigV2, timestamp, secret) (@convyx/sdk)
// Python: verify_webhook_signature_v2(raw_body, sig_v2, timestamp, secret) (convyx)Legacy signature (v1)
X-Convyx-Signature (hex HMAC-SHA256 of the raw body only) is still sent unchanged so existing integrations keep working. It has no timestamp, so it cannot detect replays. Move to v2 when you can.
Example
// v1 (legacy): HMAC-SHA256 over the raw body only — no replay protection.
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
// compare with req.headers["x-convyx-signature"] using crypto.timingSafeEqualErrors
Every error uses { "error": string, "code": string, "details"?: object }: 400 VALIDATION_ERROR (malformed JSON or fields), 401 UNAUTHORIZED, 403 FORBIDDEN, 403 RETENTION_NOT_ALLOWED, 404 JOB_NOT_FOUND, 409 CONFLICT (Idempotency-Key reused for a different request), 409 JOB_NOT_READY, 410 JOB_EXPIRED, 413 FILE_TOO_LARGE, 422 UNSUPPORTED_FORMAT, 422 CONVERSION_FAILED (the input could not be converted), 429 RATE_LIMITED / 429 QUOTA_EXCEEDED, 503 STORAGE_ERROR (temporary — retry with the same Idempotency-Key).
Rate limits
Enforced per API key. Convert: 60/min Business, 600/min Enterprise. Async jobs: higher tier (120 / 1200), shared with uploads. Convert, upload and job-submission responses include X-RateLimit-Limit, -Remaining and -Reset headers. Concurrent conversions are also capped per plan (Business 8, Enterprise 20).