HTTP API | THE FILE IS SENT

The same bench,
with no browser in the way.

Everything these pages do, a program can do too: one endpoint per tool, the file in the request body, the result in the response. It is the same code the pages run — the same qpdf, the same alignment, the same container format — reached through a different door.

[ How a call is shaped ]

Every tool is one POST to /api/tools/<tool>. Where the input is a single file, the file is the body and the options are in the query string, which is what makes a plain curl work. Where it is several files or a piece of text, the body is JSON. The answer is the result itself — a PDF, an image, a ZIP — so `curl -o` gives you the file; send Accept: application/json and you get the same thing base64'd with the extras alongside it.

[ Ask it what it can do ]

GET /api/tools answers with this page as data: every endpoint, every parameter, its type and its default, plus the limits that apply to you right now. It is generated from the same contract as the reference below, so a client built from it cannot be out of date with the server answering it.

[ Signing in, and not having to ]

None of this needs an account. An API token raises the ceilings and nothing else: send it as Authorization: Bearer hushhh_token_… and the bigger of the two limits below applies. Tokens are made under Account → API tokens, and the token itself is shown once.

[ Limits ]

Anonymous: 25 MB per file and 60 calls per 15 minutes. With a token: 100 MB per file and 600 calls. A call that goes over gets a 413 that says which limit it hit and what the limit was — no truncation, ever, because half a PDF is worse than an error.

[ When it says no ]

Errors are JSON with an `error` field. 400 means the request was wrong — a missing parameter, a value outside its list — and names what. 422 means the request was fine and the file was not: a PDF that needs a password, a range that does not fit the document, a wrong password. 413 is the size limit and tells you which one. 429 is the rate limit. 500 is ours.

[ Everywhere ]

?filename= names the result in the Content-Disposition, so a download keeps a sensible name. It never reaches any tool.

Add Accept: application/json to get it base64'd in a JSON envelope instead.

POST /api/tools/pdf-unlock Unlock PDF →

Removes the password and the restrictions from a PDF, leaving the document untouched. The first attempt is made without a password: a file with owner-only restrictions opens with no password at all, and the answer says whether one was needed.

Request

The file itself, as the request body.

Response

The resulting file.

Parameters
  • password query string

    The password the document opens with. Leave it out first: a PDF with owner-only restrictions needs none.

Example
curl -X POST --data-binary @locked.pdf \
     "https://hushhh.cc/api/tools/pdf-unlock?password=hunter2" \
     -o unlocked.pdf
POST /api/tools/pdf-merge Merge PDF →

Joins documents into one, in the order they appear in the array. Each entry can carry its own password and its own page range.

Request

JSON: { "files": [ { "name": "…", "data": "<base64>" } ] }

Response

The resulting file.

Parameters
  • files[].password body string

    The password for this file, if it has one.

  • files[].range body string

    The pages to take from this file. Defaults to all of them.

Example
jq -n --arg a "$(base64 -w0 one.pdf)" --arg b "$(base64 -w0 two.pdf)" \
   '{files:[{name:"one.pdf",data:$a},{name:"two.pdf",data:$b}]}' |
curl -X POST -H 'Content-Type: application/json' --data-binary @- \
     https://hushhh.cc/api/tools/pdf-merge -o merged.pdf
POST /api/tools/pdf-split Split PDF →

Splits a document three ways: every page as its own file, only the pages you name, or everything except them.

Request

The file itself, as the request body.

Response

A ZIP when mode=each, otherwise one PDF.

Parameters
  • mode query enum default: each

    each: one file per page, returned as a ZIP. keep: only the pages in range. drop: everything except them.

    one of: each, keep, drop

  • range query string

    Which pages, for keep and drop. "end" is the last page, so "end-2" is the last three.

  • password query string

    The password the document opens with. Leave it out first: a PDF with owner-only restrictions needs none.

Example
curl -X POST --data-binary @report.pdf \
     "https://hushhh.cc/api/tools/pdf-split?mode=keep&range=1-3,8" \
     -o first-pages.pdf
POST /api/tools/pdf-rotate Rotate PDF →

Turns pages clockwise, all of them or the ones you name.

Request

The file itself, as the request body.

Response

The resulting file.

Parameters
  • angle query enum default: 90

    Degrees clockwise.

    one of: 90, 180, 270, -90

  • range query string

    Which pages to turn. All of them if left out.

  • password query string

    The password the document opens with. Leave it out first: a PDF with owner-only restrictions needs none.

Example
curl -X POST --data-binary @scan.pdf \
     "https://hushhh.cc/api/tools/pdf-rotate?angle=90&range=2-end" \
     -o straightened.pdf
POST /api/tools/image-convert Convert images →

Converts an image to WebP, JPEG, PNG or AVIF. Metadata is not carried over — a converted photo arrives without the coordinates it was taken at.

Request

The file itself, as the request body.

Response

The resulting file.

Parameters
  • format query enum required

    What to convert to.

    one of: webp, jpeg, png, avif

  • quality query int default: 82

    1–100, for the lossy formats. Ignored for PNG, which is lossless.

Example
curl -X POST --data-binary @photo.png \
     "https://hushhh.cc/api/tools/image-convert?format=webp&quality=80" \
     -o photo.webp
POST /api/tools/text-compare Text compare →

Compares two texts and returns the edit script, or a unified diff that `patch` will apply. The alignment is Myers' algorithm, the same code the page runs.

Request

A JSON body.

Response

JSON.

Parameters
  • a body string required

    The first text.

  • b body string required

    The second text.

  • granularity body enum default: line

    What to compare by. Word and character return one flowing run of spans instead of rows, because at that grain a line number answers nothing.

    one of: line, word, character

  • ignoreCase body bool default: false

    Treat upper and lower case as the same.

  • ignoreWhitespace body bool default: false

    Treat runs of spaces and tabs as one, and ignore them at the ends of lines.

  • format body enum default: json

    json: the edit script. unified: a real unified diff, line granularity only.

    one of: json, unified

Example
curl -X POST -H 'Content-Type: application/json' \
     -d '{"a":"one\ntwo","b":"one\nTWO","format":"unified"}' \
     https://hushhh.cc/api/tools/text-compare
POST /api/tools/file-encrypt Encrypt a file →

Locks a file under a password, or opens one that was locked. AES-256-GCM with the key from PBKDF2-SHA-256 over 600,000 rounds; the container is the one documented on the tool's page, so a file locked here opens there and the other way round.

Request

The file itself, as the request body.

Response

The resulting file.

Parameters
  • direction query enum default: lock

    lock to encrypt, unlock to decrypt.

    one of: lock, unlock

  • password query string required

    The password. There is no recovery: a file nobody can open is a file you have lost.

  • filename query string

    The original name, stored inside the container and given back when it is opened.

Example
curl -X POST --data-binary @taxes.pdf \
     "https://hushhh.cc/api/tools/file-encrypt?password=correct-horse&filename=taxes.pdf" \
     -o taxes.pdf.hushhh
GET / POST /api/tools/password Password generator →

Generates passwords or passphrases from the system's random source, with rejection sampling so no character is likelier than any other.

Request

A JSON body.

Response

JSON.

Parameters
  • mode body enum default: characters

    characters for a random string, words for a passphrase from the 512-word list.

    one of: characters, words

  • length body int default: 20

    Characters, in characters mode.

  • words body int default: 5

    Words, in words mode.

  • separator body string default: -

    What goes between the words.

  • count body int default: 1

    How many to generate in one call.

  • uppercase body bool default: true

    Include A–Z.

  • digits body bool default: true

    Include 0–9.

  • symbols body bool default: false

    Include punctuation.

Example
curl "https://hushhh.cc/api/tools/password?mode=words&words=5"
POST /api/tools/file-hash File checksum →

Returns the MD5, SHA-1 and/or SHA-256 of a file, and answers the question a checksum is actually asked: does it match the one you were given?

Request

The file itself, as the request body.

Response

JSON.

Parameters
  • algorithms query list default: sha256

    Which digests to compute, comma-separated.

    one of: md5, sha1, sha256

  • expected query string

    The checksum you were given. The answer comes back with matches: true or false, compared case-insensitively.

Example
curl -X POST --data-binary @ubuntu.iso \
     "https://hushhh.cc/api/tools/file-hash?algorithms=sha256,md5"
[ What happens to the file you send ]

This is the one part of the bench where the honest answer changes. On a tool page the file is opened in your own tab and this server never sees it — that is the promise those pages make and it stays true. Call the API and you have uploaded the file, because there is no way to run qpdf on a document without the document. So the promise here is the next one down, and it is kept in code rather than in prose: the bytes live in this process's memory for as long as the request takes, they are never written to disk, never written to the log — the request logger is mounted behind this endpoint on purpose — and never put in the database. When the response is sent there is nothing left. The test suite checks the data directory byte for byte before and after a run of every operation.

The same one thing is counted as on the pages: which tool ran, whether it worked, and an IP address with its last part zeroed. Not the file, not its name, not its size, not the result.

[ FAQ ]

Frequently Asked Questions

Q: So the API does upload my file?

Yes, and that is the whole difference between it and the pages. A tool page runs the work in your browser and this server never sees the file; the API cannot do that, because the work happens here. What it does instead is hold the bytes in memory for the length of the request and never write them anywhere — not to disk, not to the log, not to the database — which is checked by a test that compares the data directory byte for byte around every operation. If that trade is not one you want to make for a particular file, the page for that tool does the same job without it.

Q: Do I need an account or a key?

No. Everything works anonymously, with smaller limits. A token raises the size and rate ceilings and nothing else — it does not unlock features and it does not change what is stored, which is still nothing.

Q: Is it the same code as the tool pages?

For the parts where it can be, yes, and deliberately: the same qpdf build with the same arguments, the same page-range reading, the same diff algorithm and the same container format, shared as modules rather than copied. Where it cannot be — the browser encrypts with WebCrypto and hashes by hand, this process has node:crypto — the two are pinned together by tests that make a file on one side and open it on the other.

Q: What happens if I go over a limit?

You get a 413 with the limit in it, or a 429 with the window. Nothing is ever truncated to fit: half a PDF that looks like a whole one is worse than an error.

Q: Can I send a batch?

pdf-merge takes several files in one call because joining is what it is for. Everything else is one file per call — which makes each call independent, retryable and easy to run in parallel from your side, where you know how much of the machine you want to use.