cleanthis.io
API Documentation
Quick Start
Get up and running in four steps. All examples use curl.
1 Create an account
cleanthis.io uses anonymous, Mullvad-style accounts — no email or password required. A random account number is your only credential.
curl -s -X POST https://cleanthis.io/api/v1/account | jq
Response:
{
"accountNumber": "CT-7K9M-X2P4-R8N1-Q5W3-J6L0-V4T2",
"prefix": "CT-7K9M",
"warning": "Save this account number now. It cannot be recovered — we do not store it."
}2 Log in and create an API key
# Log in (sets session cookie)
curl -s -X POST https://cleanthis.io/api/v1/account/login \
-H "Content-Type: application/json" \
-d '{"accountNumber": "CT-7K9M-X2P4-R8N1-Q5W3-J6L0-V4T2"}' \
-c cookies.txt | jq
# Create an API key
curl -s -X POST https://cleanthis.io/api/v1/account/keys \
-H "Content-Type: application/json" \
-d '{"label": "production", "mode": "live"}' \
-b cookies.txt | jqResponse:
{
"rawKey": "ct_live_Ab3xK9mP...",
"keyId": "key_01",
"prefix": "ct_live",
"last4": "9mPq",
"label": "production",
"warning": "Save this API key now. It will not be shown again."
}3 Sanitize a file (upload → poll → download)
# Upload
curl -s -X POST https://cleanthis.io/api/v1/sanitize \
-H "Authorization: Bearer ct_live_Ab3xK9mP..." \
-F "file=@document.pdf" | jq
# Response:
# {
# "jobId": "a1b2c3d4-...",
# "status": "queued",
# "statusUrl": "https://cleanthis.io/api/v1/job/a1b2c3d4-..."
# }
# Poll until complete
curl -s https://cleanthis.io/api/v1/job/JOB_ID \
-H "Authorization: Bearer ct_live_Ab3xK9mP..." | jq
# Response when complete includes a signed downloadUrl:
# {
# "status": "completed",
# "downloadUrl": "https://cleanthis.io/api/v1/download/JOB_ID?expires=...&sig=..."
# }
# Download using the signed URL from the response
curl -o clean.pdf "DOWNLOAD_URL_FROM_RESPONSE" \
-H "Authorization: Bearer ct_live_Ab3xK9mP..."4 Sanitize from URL
curl -s -X POST https://cleanthis.io/api/v1/sanitize-url \
-H "Authorization: Bearer ct_live_Ab3xK9mP..." \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/doc.pdf"}' | jq
# Same poll → download flow as aboveAuthentication
Anonymous Accounts
cleanthis.io uses Mullvad-style anonymous accounts. No email, no password — just a random account number (CT-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX) generated at signup. This number is shown once and cannot be recovered — store it securely.
API Keys
API keys are created via the dashboard or the POST /api/v1/account/keys endpoint. Two modes are available:
ct_live_*— Production keys. Files count against your quota.ct_test_*— Test keys. Requests are validated but files are not processed.
Bearer Token Auth
All sanitization endpoints use Bearer token authentication:
Authorization: Bearer ct_live_YOUR_KEY
Session Auth
Dashboard and account management endpoints use cookie-based session authentication. Sessions are HttpOnly, Secure, and SameSite=Strict. Log in via POST /api/v1/account/login to receive the session cookie.
Endpoints — Sanitization
| Method | Endpoint | Auth | Rate Limit | Description |
|---|---|---|---|---|
| POST | /api/v1/sanitize |
Bearer | 20/min | Upload file for sanitization. Optional webhook field. |
| POST | /api/v1/sanitize-url |
Bearer | 20/min | Fetch from URL and sanitize. Optional webhook field in JSON body. |
| GET | /api/v1/job/:id |
Bearer | 120/min | Poll job status. Returns downloadUrl when complete. |
| GET | /api/v1/download/:id |
Bearer | 30/min | Download sanitized file. Requires valid signed URL params (expires, sig). |
| POST | /api/v1/cancel/:id |
Bearer | 20/min | Cancel a queued or running job. |
POST /api/v1/sanitize
Upload a file for sanitization. Optionally include a webhook URL to receive a callback when the job completes.
POST /api/v1/sanitize Authorization: Bearer ct_live_YOUR_KEY Content-Type: multipart/form-data file: (binary) webhook: https://your-server.com/callback (optional)
Response 200 OK:
{
"jobId": "a1b2c3d4-...",
"statusUrl": "https://cleanthis.io/api/v1/job/a1b2c3d4-...",
"webhook": { "url": "https://your-server.com/callback", "status": "registered" }
}POST /api/v1/sanitize-url
Provide a URL to fetch and sanitize. The server downloads the file, then processes it.
POST /api/v1/sanitize-url
Authorization: Bearer ct_live_YOUR_KEY
Content-Type: application/json
{
"url": "https://example.com/doc.pdf",
"webhook": "https://your-server.com/callback"
}Response 200 OK:
{
"jobId": "e5f6g7h8-...",
"statusUrl": "https://cleanthis.io/api/v1/job/e5f6g7h8-...",
"webhook": { "url": "https://your-server.com/callback", "status": "registered" }
}GET /api/v1/job/:id
Poll job status. The response changes as the job progresses.
In progress:
{
"status": "processing",
"originalName": "doc.pdf"
}Completed:
{
"status": "completed",
"originalName": "doc.pdf",
"downloadName": "doc_clean.pdf",
"downloadUrl": "https://cleanthis.io/api/v1/download/a1b2c3d4-...?expires=...&sig=...",
"report": { "..." }
}GET /api/v1/download/:id
Download the sanitized file. Use the downloadUrl from the poll or webhook response — it includes the required expires and sig query parameters.
curl -o clean.pdf "https://cleanthis.io/api/v1/download/JOB_ID?expires=...&sig=..." \ -H "Authorization: Bearer ct_live_YOUR_KEY"
POST /api/v1/cancel/:id
Cancel a queued or in-progress job.
curl -X POST https://cleanthis.io/api/v1/cancel/a1b2c3d4-... \ -H "Authorization: Bearer ct_live_YOUR_KEY"
Response 200 OK:
{ "status": "cancelled" }Endpoints — Account Management
| Method | Endpoint | Auth | Rate Limit | Description |
|---|---|---|---|---|
| POST | /api/v1/account |
— | 5/hr | Create anonymous account |
| POST | /api/v1/account/login |
— | 10/15min | Log in (sets session cookie) |
| POST | /api/v1/account/logout |
Session | — | Clear session |
| GET | /api/v1/account |
Session | 30/min | Get account info + usage |
| DELETE | /api/v1/account |
Session | 30/min | Delete account and all data |
| GET | /api/v1/account/keys |
Session | 30/min | List API keys |
| POST | /api/v1/account/keys |
Session | 30/min | Create API key |
| DELETE | /api/v1/account/keys/:id |
Session | 30/min | Revoke API key |
| GET | /api/v1/account/usage |
Session | 30/min | Usage stats |
| GET | /api/v1/account/webhook-secret |
Session | 30/min | Get webhook signing secret |
| POST | /api/v1/account/webhook-secret/rotate |
Session | 30/min | Rotate webhook secret |
Webhooks
How to use
Include a webhook URL in your sanitize request. When the job completes (or fails), cleanthis.io sends a POST request to that URL with the result.
# Upload with webhook curl -X POST https://cleanthis.io/api/v1/sanitize \ -H "Authorization: Bearer ct_live_YOUR_KEY" \ -F file=@document.pdf \ -F webhook=https://your-server.com/callback
Payload
The webhook POST body is JSON:
{
"event": "job.completed",
"jobId": "a1b2c3d4-...",
"status": "completed",
"downloadUrl": "https://cleanthis.io/api/v1/download/...?expires=...&sig=...",
"downloadName": "doc_clean.pdf",
"report": { "..." },
"timestamp": "2026-04-23T01:23:00.000Z"
}Signature Verification
Every webhook includes two headers for verification:
X-CleanThis-Signature— HMAC-SHA256 of the raw request body, prefixed withsha256=X-CleanThis-Timestamp— ISO 8601 timestamp of when the webhook was sent
Always verify the signature before trusting the payload. Use a timing-safe comparison to prevent timing attacks.
const crypto = require('crypto');
function verifyWebhook(body, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express handler
app.post('/callback', express.text({ type: '*/*' }), (req, res) => {
const sig = req.headers['x-cleanthis-signature'];
if (!verifyWebhook(req.body, sig, WEBHOOK_SECRET)) {
return res.status(403).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log('Completed:', event.downloadUrl);
res.sendStatus(200);
});
Retry Behavior
If your endpoint returns a non-2xx status code or doesn't respond within 10 seconds, cleanthis.io retries up to 3 times with exponential backoff:
- Attempt 1 — immediate
- Attempt 2 — after 2 seconds
- Attempt 3 — after 8 seconds
- Attempt 4 (final) — after 32 seconds
Requirements
- HTTPS only — webhook URLs must use
https:// - Public IP — the URL must resolve to a public IP address (SSRF-protected; private/loopback ranges are rejected)
Managing Your Secret
Retrieve your webhook signing secret from the dashboard or programmatically:
GET /api/v1/account/webhook-secret Cookie: session=...
To rotate your secret (the old secret is immediately invalidated):
POST /api/v1/account/webhook-secret/rotate Cookie: session=...
Signed Download URLs
All download URLs use HMAC-SHA256 signed URLs with an expiry timestamp. You never need to construct these yourself — the downloadUrl returned by the poll endpoint and webhook payload is ready to use.
- API downloads — signed URLs expire in 15 minutes
- Web UI downloads — signed URLs expire in 5 minutes
Expired & Tampered URLs
- Expired URLs return
410 Gonewith a descriptive message - Tampered URLs (modified
sigorexpires) return403 Forbidden
If your download URL has expired, simply poll GET /api/v1/job/:id again to receive a fresh signed URL.
Rate Limits
| Endpoint Group | Limit | Keyed By |
|---|---|---|
| API v1 sanitize / cancel | 20/min | Account ID |
| API v1 job poll | 120/min | Account ID |
| API v1 download | 30/min | Account ID |
| Account create | 5/hr | IP |
| Account login | 10/15min | IP |
| Account management | 30/min | Session |
| Web UI upload | 10/min | IP |
| Web UI poll | 120/min | IP |
| Web UI download | 30/min | IP |
API v1 endpoints are keyed by account ID (not IP), so multiple servers behind NAT share a generous per-account limit. When rate-limited, responses include Retry-After header with the number of seconds to wait.
Error Responses
All errors return JSON with a single error field:
{ "error": "File type not allowed: .exe" }Common Status Codes
| Code | Meaning |
|---|---|
400 | Bad request — missing or invalid parameters |
401 | Unauthorized — missing or invalid API key / session |
403 | Forbidden — tampered signed URL or invalid webhook signature |
404 | Not found — job ID doesn't exist or doesn't belong to your account |
410 | Gone — download URL has expired |
413 | Payload too large — file exceeds the size limit |
415 | Unsupported media type — file type not allowed |
429 | Too many requests — rate limit exceeded (check Retry-After header) |
500 | Internal server error — something went wrong on our end |