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).
| Option | Meaning |
|---|---|
to | Target format (required). See the table below or GET /v1/formats. |
from or type | Source format, when it cannot be detected from the file name or content. |
response | file (default) returns the bytes, base64 returns JSON with data_base64, url returns a hosted download link. |
page | PDF to image only: render a single page. Without it, up to 50 pages are returned as a zip (one page returns the image itself). |
dpi | PDF to image resolution, 36 to 300 (default 150). |
quality | JPEG quality, 1 to 100 (default 90). |
width, height | Image output: fit inside this size in pixels, keeping the aspect ratio. |
What can be converted
| From | To |
|---|---|
| png, jpg, gif, webp, bmp, tiff, ico | each other, pdf |
| svg | png, jpg, gif, webp, bmp, tiff, ico, pdf |
| png, jpg, tiff (per page), txt, docx, odt, rtf, html | |
| doc, docx, odt, rtf, html, md, txt | pdf, docx, odt, rtf, html, png, jpg, txt |
| ppt, pptx, odp | pdf, pptx, odp, png, jpg, txt |
| xls, xlsx, ods, csv | pdf, xlsx, ods, csv, html, png, jpg, txt |
| csv | json |
| json | csv, yaml, txt |
| yaml | json, txt |
| md | html, 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
| Group | Extensions |
|---|---|
.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":"..."}}.
| Status | Meaning |
|---|---|
400 | No input, missing option, or an invalid value such as a bad webhook URL. |
401 | Missing, invalid or revoked API key, or email not confirmed. |
403 | Feature, format or URL conversion not in your plan, or the account is suspended. |
404 | Job not found. |
413 | File larger than your plan allows. |
415 | Unsupported or undetectable file type, or that conversion is not offered. |
422 | The file could not be processed (corrupt, encrypted, or unreadable). |
429 | Per-minute rate limit, or a daily or monthly allowance is used up (requests, file conversions, hosted files or webhook jobs). See Retry-After. |
503 / 504 | The 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.