· Engineering · 6 min read

The API, written down

tmp0 has had an API for as long as it has had an upload button, and until now its documentation was a few curl lines and a table of eleven routes, two of them wrong. The reference has been rewritten against the code. This is what is in it, and the behaviour that was never written down.

Everything the tmp0 website does, it does through an HTTP API, and that API has always been open: no account, no key, one request to upload a file. What it did not have was documentation worth the name. The API window showed two examples, a table of eleven routes and a paragraph about limits.

Some of it was also wrong. The example answer for an upload showed an expiresAt field that the route has never returned, and left out the ones it does return, such as the checksum and the delete token. The table listed the route the website uses for chunked uploads, which needs a token from the page and was never meant for scripts. Nobody had checked the text against the code for a long time.

So we did, route by route, and rewrote the reference from what the server actually answers. It is behind the API link in the footer of every page, this one included.

What is in it

Every route now has its fields, an example request, the answer as it really looks, and the status codes it can return. In short:

Area What it is for
Files Upload, read the status, download, delete
Pastes The same for text and code, from a pipe or as JSON
Direct upload Reserve a single-use address, then send raw bytes to it
Signed links A plain link to a file that expires by itself
Live events Be told when a file is downloaded, instead of asking
Verification Check a file against the transparency log

The first two are the ones most people need:

curl -F "file=@report.pdf" -F "expires=1d" https://tmp0.cc/api/v1/upload

cat main.py | curl --data-binary @- -H "Content-Type: text/plain" \
     "https://tmp0.cc/api/snippet/create?title=main.py"

The answer to both contains the address to share, and a delete token. The token is shown once and can't be looked up later, so a script that wants to clean up after itself has to keep it.

Three things you may not have known were there

Direct upload. POST /api/v2/presign reserves an address that accepts one upload within 30 minutes. The file is then sent to it as a plain request body, without multipart encoding. The address is all the uploading side needs, so one machine can prepare an upload and another can carry it out.

Signed links. A download through the API is a POST, because fetching a file changes something: it counts, and it may be the last download allowed. That is awkward for wget or a build script. POST /api/v1/signed/generate returns a normal link, signed with HMAC-SHA256, that is valid from one second to seven days. It can be limited to a single use or to the address that asked for it. Files with a password can't be served this way.

Live events. GET /api/v2/events/{id} is a Server-Sent Events stream for one file. It reports each download, the moment the limit is reached, the last hour before expiry, and the deletion. curl -N is enough to watch it.

Behaviour that was never written down

Most of the new text is not about new features. It is about how the existing ones behave at the edges, which until now you could only find out by running into them.

  • An expiry we don't recognise becomes seven days. The accepted values are 1h, 6h, 12h, 1d, 3d, 7d, 14d and 30d. expires=2d does not fail; it quietly gives you 7d. Check the answer if the lifetime matters.
  • Five wrong passwords delete the file. This applies to pastes too, and on every route that takes a password. A script that retries with a wrong password destroys what it is trying to read.
  • A password sent to the API is checked by the server. It is stored as a hash, and the file is encrypted at rest, but this is not end-to-end encryption. For that, the file has to be encrypted before it leaves your machine, which is what the command line tool does with -e.
  • Some files are refused. Programs, scripts, Office files with macros and disk images get a 400, whether we recognise them by name or by content.
  • Asking is free, reading is not. A HEAD request on a paste or a signed link never counts as a view and never uses up a one-time link. A GET does. A link checker that only looked at an address used to destroy the paste behind it.
  • The older info routes answer 200 for a missing file. /api/file/{id}/info returns "available": false and a message. /api/v2/files/{id} returns 404 for the same case, which is easier to handle in a script.
  • Use --data-binary for pastes. curl's -d removes line breaks, and the paste arrives as one long line.
  • Lookups take at least 200 ms. Whether an ID exists or not, the answer takes the same time, so the timing can't be used to find files.

The limits are in the reference as a table. The short version: ten file uploads and five pastes a minute per address, sixty downloads, thirty lookups. An answer of 429 says how long to wait, and waiting is the right response. An address that keeps running into the upload limit is blocked for a while.

What the API is not

  • There are no accounts in it. Nothing lists "your" uploads, because the API does not know who you are. The file ID and the delete token are the only handles there are.
  • It is not for other websites. It sends no CORS headers, and a request that a foreign page makes from a visitor's browser is refused. It is meant for scripts, servers and tools.
  • It is not one tidy design. Routes under /api/file and /api/snippet are the oldest, /api/v1 and /api/v2 came later, and they don't all report errors in the same shape. The reference says which route does what instead of pretending otherwise.

If you would rather not hard-code any of this, GET /api/v2/config returns the limits and expiry values the server currently enforces. And if the reference and the server ever disagree again, the server is right and the reference has a bug. Tell us.