Plain Text API

API documentation

The API has two jobs: extract plain text from documents and web pages, and convert files from one format to another. Both can run immediately, or in the background with a webhook. Base URL: https://api.plaintextapi.com

Authentication

Send your API key as a bearer token. X-API-Key: ptx_... works too. Keys are created for you at sign-up and managed on the API keys page. A key only works once your email address is confirmed.

Authorization: Bearer ptx_your_key

POST /v1/convert: extract text

Reads a document and returns its text. Pick whichever input style suits you.

Upload a file

curl https://api.plaintextapi.com/v1/convert \
  -H "Authorization: Bearer ptx_your_key" \
  -F "[email protected]"

Send the raw body

Set Content-Type to the document type, or pass ?type=docx.

curl "https://api.plaintextapi.com/v1/convert?type=docx" \
  -H "Authorization: Bearer ptx_your_key" \
  --data-binary @contract.docx

JSON

curl https://api.plaintextapi.com/v1/convert \
  -H "Authorization: Bearer ptx_your_key" \
  -H "Content-Type: application/json" \
  -d '{"html": "<h1>Hello</h1><p>World</p>"}'

# other fields: "xml", "json", "text", "url", "file_base64",
# or "content" together with "type" (md, csv, yaml, eml, ...)
curl ... -d '{"url": "https://example.com/article"}'

Response

{
  "text": "Hello\n\nWorld",
  "source": "html",
  "chars": 12,
  "bytes": 31,
  "title": null,
  "pages": null,
  "ms": 1
}

Add ?format=text (or Accept: text/plain) to receive only the text, as text/plain.

POST /v1/files/convert: convert a file

Converts a file to another format. Available on plans that include file conversion. Tell the API what you want with to.

curl "https://api.plaintextapi.com/v1/files/convert?to=pdf" \
  -H "Authorization: Bearer ptx_your_key" \
  -F "[email protected]" -o letter.pdf

By default the converted file is returned as the response body, with the right Content-Type and a Content-Disposition filename. The same input styles as above work (multipart, raw body with ?type=, JSON envelope, or a url).

OptionMeaning
toTarget format (required). See the table below or GET /v1/formats.
from or typeSource format, when it cannot be detected from the file name or content.
responsefile (default) returns the bytes, base64 returns JSON with data_base64, url returns a hosted download link.
pagePDF to image only: render a single page. Without it, up to 50 pages are returned as a zip (one page returns the image itself).
dpiPDF to image resolution, 36 to 300 (default 150).
qualityJPEG quality, 1 to 100 (default 90).
width, heightImage output: fit inside this size in pixels, keeping the aspect ratio.

What can be converted

FromTo
png, jpg, gif, webp, bmp, tiff, icoeach other, pdf
svgpng, jpg, gif, webp, bmp, tiff, ico, pdf
pdfpng, jpg, tiff (per page), txt, docx, odt, rtf, html
doc, docx, odt, rtf, html, md, txtpdf, docx, odt, rtf, html, png, jpg, txt
ppt, pptx, odppdf, pptx, odp, png, jpg, txt
xls, xlsx, ods, csvpdf, xlsx, ods, csv, html, png, jpg, txt
csvjson
jsoncsv, yaml, txt
yamljson, txt
mdhtml, pdf, docx, odt, rtf, txt

Any readable source can be converted to txt. The live list for this server is at GET /v1/formats. Results depend on the source: complex layouts, fonts and macros can change during conversion, and scanned images are not read (no OCR).

Hosted file links

On plans with hosted links, add response=url to either endpoint. The result is stored temporarily and you get a download URL instead of the content.

curl "https://api.plaintextapi.com/v1/files/convert?to=pdf&response=url" \
  -H "Authorization: Bearer ptx_your_key" -F "[email protected]"

{
  "url": "https://...",
  "expires_at": "2026-11-05T10:00:00+00:00",
  "format": "pdf",
  "filename": "slides.pdf",
  "size": 482113,
  "source": "pptx"
}

For text extraction the link points to a .txt file and text is left out of the response. Links stop working at expires_at and the file is deleted afterwards. Anyone with the link can open the file until then, so treat it like a secret.

Background jobs and webhooks

For large files or fire-and-forget workflows, add a webhook_url. The API replies immediately with 202 and a job id, processes the file in the background, then POSTs the result to your URL. Available on plans with webhooks. You can also pass async=true without a webhook and poll the job instead.

curl "https://api.plaintextapi.com/v1/files/convert?to=pdf" \
  -H "Authorization: Bearer ptx_your_key" \
  -F "[email protected]" \
  -F "webhook_url=https://example.com/hooks/ptx"

# 202 Accepted
{ "id": "5c1e...", "status": "queued", "status_url": "/v1/jobs/5c1e..." }

webhook_url can be a query parameter, a form or JSON field, or an X-Webhook-Url header. It must be a public http or https address.

What your endpoint receives

POST /hooks/ptx
Content-Type: application/json
X-PlainText-Event: job.completed        (or job.failed)
X-PlainText-Job: 5c1e...
X-PlainText-Timestamp: 1793700000
X-PlainText-Signature: sha256=9f2c...

{
  "id": "5c1e...",
  "event": "job.completed",
  "kind": "convert",
  "status": "done",
  "created_at": "...", "finished_at": "...",
  "result": { "format": "pdf", "url": "https://...", "expires_at": "...", "size": 91234 },
  "error": null
}

When the plan includes hosted links, file results arrive as a url. Otherwise results up to 8 MB arrive inline as data_base64. Text extraction results contain text. A failed job has "error": {"code": "...", "message": "..."}.

Verify the signature

The signature is HMAC-SHA256(secret, timestamp + "." + raw_body) in hex, prefixed with sha256=. Your secret is on the API keys page. Reject calls with an old timestamp to block replays.

// Node.js
const crypto = require('crypto');
const expected = 'sha256=' + crypto.createHmac('sha256', SECRET)
  .update(timestamp + '.' + rawBody).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));

# Python
expected = 'sha256=' + hmac.new(SECRET.encode(), f'{timestamp}.'.encode() + raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, signature)

Retries

Reply with any 2xx status to acknowledge. Otherwise we retry after 10 seconds, 1 minute, 10 minutes, 1 hour and 6 hours, then give up. You can always fetch the job yourself while it is retained (24 hours).

GET /v1/jobs/{id}

curl https://api.plaintextapi.com/v1/jobs/5c1e... -H "Authorization: Bearer ptx_your_key"

{
  "id": "5c1e...",
  "kind": "convert",
  "status": "done",                // queued | processing | done | failed
  "result": { ... },
  "error": null,
  "webhook": { "status": "delivered", "attempts": 1 }
}

GET /v1/formats

No key needed. Lists the formats the extractor reads and the conversions this server can perform right now.

GET /v1/usage

Returns your plan, limits (including hosted file and webhook allowances) and current usage.

curl https://api.plaintextapi.com/v1/usage -H "Authorization: Bearer ptx_your_key"

Formats we can read

GroupExtensions
PDF.pdf (text layer; no OCR)
Word processors.docx .docm .dotx .doc .odt .ott .rtf
Presentations.pptx .pptm .ppsx .ppt .odp
Spreadsheets.xlsx .xlsm .xls .ods .csv .tsv
Web and markup.html .htm .xhtml .xml .svg .rss .atom, or a public URL
Data.json .jsonl .yaml .yml
E-books.epub
Email, calendar, contacts.eml .ics .vcf
Text and subtitles.txt .md .log .ini .toml .tex .rst .srt .vtt

Your plan may limit which formats, file sizes and features are available.

Errors and limits

Errors look like {"error":{"code":"quota_exceeded","message":"..."}}.

StatusMeaning
400No input, missing option, or an invalid value such as a bad webhook URL.
401Missing, invalid or revoked API key, or email not confirmed.
403Feature, format or URL conversion not in your plan, or the account is suspended.
404Job not found.
413File larger than your plan allows.
415Unsupported or undetectable file type, or that conversion is not offered.
422The file could not be processed (corrupt, encrypted, or unreadable).
429Per-minute rate limit, or a daily or monthly allowance is used up (requests, file conversions, hosted files or webhook jobs). See Retry-After.
503 / 504The service is busy or the conversion timed out. Retry shortly.

Responses carry X-RateLimit-Limit and X-RateLimit-Remaining for the per-minute limit. Failed requests are not counted against your quota.