Building with a Transcription API: First Integration
apideveloperstutorial

Building with a Transcription API: First Integration

BMMamane B. MoussaMay 26, 2026Updated July 2, 202610 min read

Summarize this article with:

The Integration in Five Steps

You can add transcription to any backend in under an hour: get an API key, POST audio (by file upload or URL), poll or receive a webhook on completion, fetch the result, and export to the format you need. The patterns here are shown against the ConvertAudioToText API, but the submit-then-poll shape is standard across every async transcription provider. If you are still choosing a provider, see the transcription API comparison 2026 first.

The five steps are: authenticate, submit a job, get the result, handle errors, and export. Each section below covers one step with runnable code.

Step 1: Get an API Key

API access on ConvertAudioToText requires the Business plan. Inside the dashboard, navigate to Settings, then API Keys, then create a new key. Give it a descriptive name tied to the integration ("production-backend", "staging-ingest"). The key is shown exactly once, so copy it immediately.

Key format: ck_live_ followed by 64 hex characters. Treat it like a password. Store it in an environment variable, never in source control.

Every request uses Authorization: Bearer <key>:

curl https://api.convertaudiototext.com/api/v1/result/some-job-id \
  -H "Authorization: Bearer ck_live_..."

In Node.js:

const headers = { 'Authorization': `Bearer ${process.env.CATT_API_KEY}` };

In Python:

headers = {'Authorization': f'Bearer {os.environ["CATT_API_KEY"]}'}

Step 2: Submit a Transcription Job

The endpoint is POST /api/v1/transcribe. It returns HTTP 201 immediately with a job_id; the actual transcription is asynchronous.

Two submission paths exist: file upload and URL. They return the same response shape.

File Upload

Use this when the audio is local or you receive it as a multipart form upload from your users.

import FormData from 'form-data';
import { createReadStream } from 'fs';
import fetch from 'node-fetch';

async function submitFileJob(filePath, language = 'en') {
  const form = new FormData();
  form.append('source', 'upload');
  form.append('language', language);
  form.append('file', createReadStream(filePath));

  const res = await fetch('https://api.convertaudiototext.com/api/v1/transcribe', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${process.env.CATT_API_KEY}` },
    body: form
  });

  if (!res.ok) throw new Error(`Submit failed: ${res.status}`);
  const { job_id, status } = await res.json();
  return { job_id, status };
}

URL Submission

Use this when audio is already in cloud storage (S3, R2, GCS, or any public HTTPS URL). The server downloads at high bandwidth from the origin, which is faster than re-uploading from your machine.

async function submitURLJob(audioURL, language = 'en') {
  const res = await fetch('https://api.convertaudiototext.com/api/v1/transcribe', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.CATT_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      source: 'url',
      input_url: audioURL,
      language: language
    })
  });

  if (!res.ok) throw new Error(`Submit failed: ${res.status}`);
  const { job_id, status } = await res.json();
  return { job_id, status };
}

Both return { job_id: "uuid-string", status: "queued" }.

ConvertAudioToText audio upload tool
ConvertAudioToText audio upload tool

The audio upload tool that the API powers. The same job pipeline runs whether you use the web UI or the API.

Step 3: Get the Result

A completed job is available at GET /api/v1/result/{job_id}. Two patterns for waiting: polling and webhooks.

Polling

Poll every 5 seconds until status is "completed" or "failed". The endpoint responds instantly; the delay is entirely on the transcription side.

async function pollForResult(jobId, maxWaitMs = 600000) {
  const start = Date.now();

  while (Date.now() - start < maxWaitMs) {
    const res = await fetch(
      `https://api.convertaudiototext.com/api/v1/result/${jobId}`,
      { headers: { 'Authorization': `Bearer ${process.env.CATT_API_KEY}` } }
    );
    const data = await res.json();

    if (data.status === 'completed') return data;
    if (data.status === 'failed') throw new Error(data.error || 'Job failed');

    await new Promise(r => setTimeout(r, 5000));
  }

  throw new Error('Timeout waiting for transcription result');
}

Do not poll faster than once per second per job. The global rate limit is 100 requests per minute per IP, so a tight loop across many jobs will trip it.

Webhooks

For better resource use at scale, webhooks replace polling entirely. Register a webhook URL once:

curl -X POST https://api.convertaudiototext.com/api/v1/dashboard/webhooks \
  -H "Authorization: Bearer $CATT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/transcription",
    "events": ["job.completed", "job.failed"]
  }'

The response includes a secret field. Save it. The server will POST to your URL on each matching event. The payload looks like:

{
  "event": "job.completed",
  "timestamp": "2026-07-01T14:22:05Z",
  "data": {
    "job_id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "completed",
    "duration": 1842,
    "result_url": "https://api.convertaudiototext.com/api/v1/result/550e8400-...",
    "completed_at": "2026-07-01T14:22:04Z"
  }
}

Verify every delivery with HMAC-SHA256. The server sends the signature in X-Webhook-Signature as sha256=<hex>:

import crypto from 'crypto';

function verifyWebhookSignature(rawBody, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

// In your Express/Fastify handler:
app.post('/webhooks/transcription', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-webhook-signature'];
  if (!verifyWebhookSignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const { event, data } = JSON.parse(req.body);
  if (event === 'job.completed') {
    // fetch full transcript from data.result_url
  }
  res.status(200).end();
});

Webhooks require the Business plan and a publicly reachable HTTPS URL. For a deeper comparison of the two approaches, see the webhook vs polling for transcripts guide.

Step 4: Parse the Result

A completed job response nests the transcript under a transcript key. The exact shape is controlled by the server-side formatter, but the fields you will always find are:

{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "completed",
  "language": "en",
  "duration": 1842,
  "filename": "interview.mp3",
  "transcript": {
    "text": "Hello, welcome to the show...",
    "summary": "An interview about...",
    "confidence": 0.97,
    "word_count": 4210,
    "timeline": [
      {
        "speaker": "Speaker 0",
        "text": "Hello, welcome to the show.",
        "start": 0.12,
        "end": 1.87
      }
    ],
    "topics": [...],
    "sentiments": [...]
  }
}

The text field is the full concatenated transcript. The timeline array gives you speaker-attributed utterances with timestamps. The summary, topics, and sentiments fields are populated for English audio on plans that include AI insights; they will be stripped or absent on the Starter plan.

Step 5: Export Formats

Download formatted files via:

curl "https://api.convertaudiototext.com/api/v1/result/$JOB_ID/srt" \
  -H "Authorization: Bearer $CATT_API_KEY" > captions.srt  # SRT for video captions

curl "https://api.convertaudiototext.com/api/v1/result/$JOB_ID/vtt" \
  -H "Authorization: Bearer $CATT_API_KEY" > captions.vtt  # WebVTT

curl "https://api.convertaudiototext.com/api/v1/result/$JOB_ID/txt" \
  -H "Authorization: Bearer $CATT_API_KEY" > transcript.txt  # Plain text with speaker labels

curl "https://api.convertaudiototext.com/api/v1/result/$JOB_ID/json" \
  -H "Authorization: Bearer $CATT_API_KEY" > transcript.json  # Structured JSON

curl "https://api.convertaudiototext.com/api/v1/result/$JOB_ID/docx" \
  -H "Authorization: Bearer $CATT_API_KEY" > transcript.docx  # DOCX

All export endpoints require a paid plan. Free-tier requests return HTTP 402 with an upgrade_url field. For building subtitle workflows at scale, the SRT and VTT endpoints work well in batch pipelines.

Error Handling

Three categories of errors to handle, each with a different recovery strategy.

4xx errors are your bug. Bad auth (401), missing required fields (400), quota exhausted (402), or rate limit hit (429). Do not retry without fixing the request. On 429, read the retry_after field from the response body.

5xx errors are transient. Retry with exponential backoff. Most clear within 30 seconds.

Job-level failures. A job can be submitted successfully (HTTP 201) but later fail during processing. status becomes "failed" and error carries the reason. Common causes: corrupted audio, silent-throughout recordings, unsupported codecs. Retry the job only if the audio source is valid.

async function transcribeWithRetry(audioPath, maxAttempts = 3) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      const { job_id } = await submitFileJob(audioPath);
      return await pollForResult(job_id);
    } catch (err) {
      // 4xx: fix the request, do not retry
      if (err.status >= 400 && err.status < 500) throw err;
      // Last attempt: give up
      if (attempt === maxAttempts - 1) throw err;
      // 5xx: wait and try again
      const delayMs = 1000 * Math.pow(2, attempt);
      await new Promise(r => setTimeout(r, delayMs));
    }
  }
}

Rate Limits

The limits that apply to authenticated API requests:

LayerLimitScope
Global100 requests/minPer IP address
Transcription jobs10 submissions/minPer authenticated user
Upload multipart (sign calls)1000/minPer IP, separate ceiling

The transcription job limit means you can submit up to 10 jobs per minute, not 10 concurrent; jobs beyond that threshold queue on the server.

Hitting either limit returns HTTP 429 with retry_after in seconds in the response body.

A Complete Working Example

import { createReadStream } from 'fs';
import FormData from 'form-data';
import fetch from 'node-fetch';

const API_KEY = process.env.CATT_API_KEY;
const BASE    = 'https://api.convertaudiototext.com/api/v1';

async function submit(filePath, language = 'en') {
  const form = new FormData();
  form.append('source', 'upload');
  form.append('language', language);
  form.append('file', createReadStream(filePath));

  const res = await fetch(`${BASE}/transcribe`, {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${API_KEY}` },
    body: form
  });

  if (!res.ok) throw Object.assign(new Error('Submit failed'), { status: res.status });
  return res.json();
}

async function poll(jobId, maxWaitMs = 600000) {
  const deadline = Date.now() + maxWaitMs;

  while (Date.now() < deadline) {
    const res = await fetch(`${BASE}/result/${jobId}`, {
      headers: { 'Authorization': `Bearer ${API_KEY}` }
    });
    const data = await res.json();

    if (data.status === 'completed') return data;
    if (data.status === 'failed') throw new Error(data.error || 'Transcription failed');

    await new Promise(r => setTimeout(r, 5000));
  }
  throw new Error('Timeout');
}

export async function transcribeFile(filePath, language = 'en') {
  const { job_id } = await submit(filePath, language);
  const result     = await poll(job_id);
  return result.transcript?.text ?? '';
}

From here, add caching for transcription results to avoid re-processing identical files, and swap polling for webhooks once you have more than a handful of concurrent jobs.

My take: the submit-poll shape is the same across all major async transcription providers, so the client code transfers almost verbatim. The parts that differ are the authentication header format, the field names in the response, and whether advanced features like speaker labels require a higher plan. Verify each against the provider's current documentation before shipping.

If you only need an occasional clean transcript without managing API keys or plan tiers, ConvertAudioToText lets you upload and download without any setup.

Frequently Asked Questions

Which plan do I need to use the API?

API key creation on ConvertAudioToText requires the Business plan. The Developer plan (called PlanPro internally) does not unlock API access per the current billing code. Check /pricing for the latest tiers before purchasing.

What is the rate limit for transcription jobs?

The global limit is 100 requests per minute per IP address. Transcription job submissions are further capped at 10 jobs per minute per user. Hitting either limit returns HTTP 429 with a Retry-After header.

Polling or webhooks: which should I use?

Polling works for low-volume or one-off integrations and requires no public endpoint. Webhooks are better once you have multiple concurrent jobs: instead of polling N jobs every few seconds, your server receives one POST per completion. Webhooks require the Business plan and a publicly reachable HTTPS URL.

How do I verify a webhook payload is genuine?

Each webhook registration returns a secret string. When the server delivers a payload it computes HMAC-SHA256 of the raw JSON body using that secret and sends the result in the X-Webhook-Signature header as sha256=hex. Re-compute the same HMAC on your side and compare. Reject any request where they do not match.

What export formats does the API support?

Completed jobs can be exported as SRT, VTT, TXT, JSON, DOCX, and PDF via GET /api/v1/result//. All export endpoints require a paid plan; free-tier requests return HTTP 402.

Sources

Try transcription free

Convert any audio or video to clean, unwatermarked text — speaker labels, timestamps, and AI summaries included. First 30 minutes free, no account.

Related Articles