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 | jq

Response:

{
  "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 above

Authentication

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:

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:

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:

Requirements

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.

Expired & Tampered URLs

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
400Bad request — missing or invalid parameters
401Unauthorized — missing or invalid API key / session
403Forbidden — tampered signed URL or invalid webhook signature
404Not found — job ID doesn't exist or doesn't belong to your account
410Gone — download URL has expired
413Payload too large — file exceeds the size limit
415Unsupported media type — file type not allowed
429Too many requests — rate limit exceeded (check Retry-After header)
500Internal server error — something went wrong on our end