Skip to content

REST API

REST API quickstart

A small JSON API over HTTPS. Every request is authenticated with an API key and billed against your credit balance. Base URL: https://api.textify.me.

Running locally

Textify's Worker isn't deployed yet. To try these against a local build, run wrangler dev in apps/api and swap the base URL for http://127.0.0.1:8787. In local mock mode the endpoints answer without real provider keys, so you can develop end-to-end offline.

Authentication

Create an API key from your account, then send it on every request as a bearer token. Keys are shown once at creation — store it somewhere safe (only its hash is kept). New accounts start with 100 free credits a month.

create a key
curl -X POST https://api.textify.me/v1/account/keys \
  -H "Authorization: Bearer atx_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "label": "my-laptop" }'

The response returns the plaintext key once (prefixed atx_). Send it on every call using either header — Authorization wins if both are present:

headers
Authorization: Bearer atx_YOUR_API_KEY
# or
X-API-Key: atx_YOUR_API_KEY
Treat keys like passwords — never ship one in client-side code. Revoke a leaked key with DELETE /v1/account/keys/<id> and mint a new one.

Fetch a YouTube transcript

1 credit per video

POST /v1/youtube/transcript

Returns the caption track for a public YouTube video. Captions only — Textify never downloads the video or its audio. Caption-less videos can fall back to provider ASR (billed at the higher rate below).

Billing

1 credit when a caption track exists. Caption-less videos that fall back to provider ASR cost 3 credits; a cached transcript is free.

Request

curl
curl -X POST https://api.textify.me/v1/youtube/transcript \
  -H "Authorization: Bearer atx_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "lang": "en"
}'
TypeScript
const res = await fetch("https://api.textify.me/v1/youtube/transcript", {
  method: "POST",
  headers: {
    Authorization: "Bearer atx_YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "lang": "en"
  }),
});

const data = await res.json();
console.log(data);
Python
import requests

res = requests.post(
    "https://api.textify.me/v1/youtube/transcript",
    headers={"Authorization": "Bearer atx_YOUR_API_KEY"},
    json={
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "lang": "en"
    },
)
res.raise_for_status()
print(res.json())

Response

200 · application/json
{
  "videoId": "dQw4w9WgXcQ",
  "title": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)",
  "language": "en",
  "availableLanguages": [
    "en"
  ],
  "segments": [
    {
      "start": 0,
      "end": 2.5,
      "text": "Never gonna give you up"
    },
    {
      "start": 2.5,
      "end": 5,
      "text": "Never gonna let you down"
    }
  ],
  "text": "Never gonna give you up Never gonna let you down"
}

Transcribe an audio or video URL

15 credits per audio-hour

POST /v1/transcribe

Runs cloud speech-to-text over an audio or video file you point us at, and returns the full transcript plus timestamped segments. Pass `audio_url` and `duration_sec` (the audio length in seconds, client-measured) — duration is REQUIRED for a remote URL so the per-audio-hour price is fixed up front. Audio ≤10 minutes returns the transcript synchronously (200); longer audio returns 202 with a poll URL. A failed transcription is refunded.

Billing

Billed per audio-hour, prorated and settled to the true provider-reported duration.

Request

curl
curl -X POST https://api.textify.me/v1/transcribe \
  -H "Authorization: Bearer atx_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "audio_url": "https://cdn.example.com/interviews/ep-42.mp3",
  "duration_sec": 372,
  "language": "en"
}'
TypeScript
const res = await fetch("https://api.textify.me/v1/transcribe", {
  method: "POST",
  headers: {
    Authorization: "Bearer atx_YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "audio_url": "https://cdn.example.com/interviews/ep-42.mp3",
    "duration_sec": 372,
    "language": "en"
  }),
});

const data = await res.json();
console.log(data);
Python
import requests

res = requests.post(
    "https://api.textify.me/v1/transcribe",
    headers={"Authorization": "Bearer atx_YOUR_API_KEY"},
    json={
      "audio_url": "https://cdn.example.com/interviews/ep-42.mp3",
      "duration_sec": 372,
      "language": "en"
    },
)
res.raise_for_status()
print(res.json())

Response

200 · application/json
{
  "jobId": "url_5f3c…",
  "status": "completed",
  "language": "en",
  "durationSec": 372,
  "text": "Welcome back to the show. Today we're talking about shipping fast without breaking things.",
  "segments": [
    {
      "start": 0,
      "end": 3.1,
      "text": "Welcome back to the show."
    },
    {
      "start": 3.1,
      "end": 7.8,
      "text": "Today we're talking about shipping fast without breaking things."
    }
  ]
}

Read text out of an image

1 credit per image or page

POST /v1/ocr

Extracts text from an image using a vision model (Gemini 2.5 Flash-Lite) — handles photos, screenshots, receipts, and handwriting. Pass an `image_url`, or upload one or more images with `multipart/form-data` (field `image`).

Billing

1 credit per image. A batch of N images costs N credits; images that fail to process are refunded.

Request

curl
curl -X POST https://api.textify.me/v1/ocr \
  -H "Authorization: Bearer atx_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "image_url": "https://cdn.example.com/receipts/2026-07-01.jpg"
}'
TypeScript
const res = await fetch("https://api.textify.me/v1/ocr", {
  method: "POST",
  headers: {
    Authorization: "Bearer atx_YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "image_url": "https://cdn.example.com/receipts/2026-07-01.jpg"
  }),
});

const data = await res.json();
console.log(data);
Python
import requests

res = requests.post(
    "https://api.textify.me/v1/ocr",
    headers={"Authorization": "Bearer atx_YOUR_API_KEY"},
    json={
      "image_url": "https://cdn.example.com/receipts/2026-07-01.jpg"
    },
)
res.raise_for_status()
print(res.json())

Response

200 · application/json
{
  "results": [
    {
      "index": 0,
      "text": "WHOLE FOODS MARKET\nBananas          1.29\nOat milk         4.49\nTOTAL            5.78",
      "confidence": 0.98
    }
  ]
}

Convert a web page to Markdown

1 credit per fetch

POST /v1/web/markdown

Fetches a public web page, isolates the main article with Readability, and returns clean Markdown — no nav, ads, or clutter. Requests are SSRF-guarded (private IPs and redirect tricks are rejected) and results are cached for 24 hours.

Billing

1 credit per fetch. A cached page (served within 24h) is free and sets "cached": true.

Request

curl
curl -X POST https://api.textify.me/v1/web/markdown \
  -H "Authorization: Bearer atx_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://example.com/blog/shipping-fast"
}'
TypeScript
const res = await fetch("https://api.textify.me/v1/web/markdown", {
  method: "POST",
  headers: {
    Authorization: "Bearer atx_YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "url": "https://example.com/blog/shipping-fast"
  }),
});

const data = await res.json();
console.log(data);
Python
import requests

res = requests.post(
    "https://api.textify.me/v1/web/markdown",
    headers={"Authorization": "Bearer atx_YOUR_API_KEY"},
    json={
      "url": "https://example.com/blog/shipping-fast"
    },
)
res.raise_for_status()
print(res.json())

Response

200 · application/json
{
  "url": "https://example.com/blog/shipping-fast",
  "finalUrl": "https://example.com/blog/shipping-fast",
  "title": "Shipping Fast Without Breaking Things",
  "byline": "Jane Doe",
  "markdown": "# Shipping Fast Without Breaking Things\n\nMost teams treat speed and safety as a trade-off. They aren't...",
  "wordCount": 812,
  "cached": false
}

Convert a document to text or Markdown

1 credit per fetch

POST /v1/convert

Extracts text (or Markdown) from a DIGITAL Word/docx, PowerPoint/pptx, spreadsheet/xlsx, or EPUB document. Upload the file with `multipart/form-data` in a `file` field (this endpoint does not fetch URLs). Add `?format=markdown` (default `text`) for Markdown. PDF is NOT supported (501) — pdf.js can't run under the Workers runtime; use the free in-browser PDF tool, or send page images to /v1/ocr. An image-only document with no extractable text returns 422 (refunded) with a pointer to /v1/ocr.

Billing

1 credit per successfully-converted file. A conversion that fails or has no extractable text is refunded.

Request

curl
curl -X POST https://api.textify.me/v1/convert \
  -H "Authorization: Bearer atx_YOUR_API_KEY" \
  -F "file=@report.docx" \
  -F "format=markdown"
TypeScript
const form = new FormData();
form.append("file", fileBlob, "report.docx");
form.append("format", "markdown");

const res = await fetch("https://api.textify.me/v1/convert", {
  method: "POST",
  headers: { Authorization: "Bearer atx_YOUR_API_KEY" }, // do NOT set Content-Type — the browser sets the multipart boundary
  body: form,
});

const data = await res.json();
console.log(data);
Python
import requests

res = requests.post(
    "https://api.textify.me/v1/convert",
    headers={"Authorization": "Bearer atx_YOUR_API_KEY"},
    files={"file": open("report.docx", "rb")},
    data={"format": "markdown"},
)
res.raise_for_status()
print(res.json())

Response

200 · application/json
{
  "kind": "docx",
  "format": "markdown",
  "chars": 1284,
  "text": "# Q2 2026 Report\n\n## Summary\n\nRevenue grew 18% quarter over quarter..."
}

Errors

Errors use standard HTTP status codes and a consistent JSON envelope: a machine-readable error code and a human-readable message. Some errors add fields (e.g. balance, needed, retryAfter).

error envelope
{
  "error": "invalid_url",
  "message": "Could not find a YouTube video ID in that URL."
}
Status error Meaning
400 invalid_request / invalid_url The body was malformed or a field failed validation.
401 unauthorized Missing, invalid, or revoked API key.
402 insufficient_credits Balance too low; body carries { balance, needed }.
404 not_found / no_captions No such resource (e.g. a video without captions).
413 / 415 image_too_large / unsupported_media_type Upload too big, or the wrong content type.
429 rate_limited Too many requests; retry after the Retry-After header.
5xx provider_error An upstream provider was temporarily unavailable — safe to retry.

Rate limits & credit semantics

Metered calls are charged against your balance before the work runs, so you never overspend, and the same ledger backs the website, the API, and the MCP server — one balance, no parallel accounting.

  • Balance header. Successful metered responses include X-Credits-Balance with your remaining credits.
  • Out of credits. A short balance returns 402 insufficient_credits with { "balance", "needed" } and charges nothing. Top up on the pricing page.
  • Idempotency. Send an Idempotency-Key header to make a retry safe — the same key replays the original charge and result instead of billing twice.
  • Rate limits. A 429 rate_limited response carries a Retry-After header (seconds). Back off and retry.
  • Free where possible. Cached results (e.g. a repeated YouTube transcript or a page fetched within 24h) don't spend credits.
Next: connect the same tools to your AI agents with the hosted MCP server.