The 2L API

One call turns a file into a link. Everything else on this page exists because somebody who automated that call eventually wanted to list what they had made and delete some of it.

The base is https://2l.nz/api/v1. Everything speaks JSON and everything is Pro.

Authentication

Send your key as a bearer token. Keys are made in your dashboard, start with 2l_, and are shown exactly once — they are stored only as a hash, so a key that is lost is revoked and replaced rather than recovered.

Every request
1Authorization: Bearer 2l_0f3c…

A key acts as your whole account. Make a separate one for each place you use it so that revoking one does not break the others, and revoke rather than delete when a key leaks — a revoked key is refused immediately.

Unlike the rest of this site, these endpoints send CORS headers. That is safe here precisely because they authenticate with a bearer key rather than a cookie: a key is carried deliberately or not at all, so there is no ambient authority for another origin to borrow.

Your first link

The shortest useful call is a raw body with a filename header. This is the shape curl -T already speaks, so no wrapper is needed.

curl
1curl -T report.pdf \
2 -H "Authorization: Bearer $L2_KEY" \
3 https://2l.nz/api/v1/drops
the reply
1{
2 "success": true,
3 "code": "k3f9",
4 "url": "https://2l.nz/k3f9",
5 "kind": "pdf",
6 "kindLabel": "PDF",
7 "items": 1,
8 "bytes": 284913,
9 "expiresAt": null,
10 "remaining": 4998,
11 "limit": 5000
12}

kind is what detection decided the file was, and it is what determines the page somebody sees when they open the link. You never choose it.

Errors

A failure is an HTTP status plus a body with a machine-readable error and a message written for a person to read in a log.

a refusal
1{
2 "error": "too_large",
3 "message": "A single request carries up to 16 MB. For larger files use /api/drop/begin, which takes the same key."
4}
FieldTypeWhat it does
unauthorized401No key, or a key that has been revoked.
plan_required403The key is valid but its account is not on Pro.
not_found404No such link, or it belongs to somebody else. Deliberately the same answer for both — telling a caller which one would let them enumerate real codes.
too_large413Over a size limit. The message says which one.
name_taken409That custom name is already in use.
quota_reached429Today's link allowance is used up. Resets at midnight UTC.
unavailable503Storage or the database was not reachable. Safe to retry.

Limits

These are the Pro numbers, and they are read from the same table the server enforces.

FieldTypeWhat it does
file size2 GBPer file.
one request16 MBAnything larger uses the chunked flow below.
files per link500Beyond one, the link becomes a gallery or a collection.
links per day5,000Resets at midnight UTC.
storage250 GBAcross everything the account owns.
transfer1 TBPer link, per calendar month. A link past it pauses until the first.
rate120 / minutePer address. A 429 carries Retry-After.

Create a link

POST /api/v1/drops Pro

Three body shapes are accepted, because three shapes are how people actually call an API.

A raw body

The whole body is the file. The name comes from X-Filename, or from a Content-Disposition if one is sent. Options go in the query string.

raw
1curl -X POST "https://2l.nz/api/v1/drops?name=spring-menu&expiresIn=604800" \
2 -H "Authorization: Bearer $L2_KEY" \
3 -H "X-Filename: menu.pdf" \
4 --data-binary @menu.pdf

A form

multipart/form-data. Every file part becomes an item in the link; every text part is read as an option.

multipart
1curl -X POST https://2l.nz/api/v1/drops \
2 -H "Authorization: Bearer $L2_KEY" \
3 -F "file=@shot-1.png" \
4 -F "file=@shot-2.png" \
5 -F "title=Release screenshots"

JSON

For text, or for a small file sent inline as base64. Base64 costs a third more on the wire, so use the raw form for anything large.

json
1curl -X POST https://2l.nz/api/v1/drops \
2 -H "Authorization: Bearer $L2_KEY" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "text": "{\"build\": 4417, \"ok\": true}",
6 "title": "Build 4417",
7 "expiresIn": 86400,
8 "burnAfterRead": false
9 }'

Detection runs on text too: that body arrives as valid JSON, so the link opens a JSON tree rather than a wall of characters.

Options

FieldTypeWhat it does
namestringThe link name — 2l.nz/<name>. 3 to 64 characters, letters, numbers, hyphens and underscores. 409 if taken.
titlestringShown in the viewer and in the unfurl. Up to 140 characters.
passwordstringAnyone opening the link is asked for it. 4 to 200 characters. The file is not served until it matches.
expiresInsecondsDelete the link this long from now.
expiresAtms epochOr an absolute moment. expiresIn wins if both are sent.
burnAfterReadbooleanDelete the link the first time it is opened.
allowDownloadbooleanDefault true. False hides the download button; anyone determined can still save what their browser rendered.
textstringText instead of a file. Up to 1 MB.
filesarrayJSON only: [{ name, type, content }] with content base64.

List your links

GET /api/v1/drops?page=0&q= Pro

Fifty at a time, newest first. q matches the code and the title. The reply carries hasMore rather than a total, because counting every row on every page is a cost paid for a number almost nobody reads.

reply
1{
2 "drops": [
3 { "code": "spring-menu", "url": "https://2l.nz/spring-menu", "kind": "pdf",
4 "title": "Spring menu", "bytes": 284913, "items": 1,
5 "hasPassword": false, "createdAt": 1755800000000, "expiresAt": null }
6 ],
7 "page": 0,
8 "hasMore": false
9}

Read one

GET /api/v1/drops/{code} Pro

The full record, including view count and the list of items. Never the password — not even its hash.

Update

PATCH /api/v1/drops/{code} Pro

Send only what you want changed. Accepts title, password (null removes it), expiresAt (null means never), burnAfterRead and allowDownload.

example
1curl -X PATCH https://2l.nz/api/v1/drops/spring-menu \
2 -H "Authorization: Bearer $L2_KEY" \
3 -H "Content-Type: application/json" \
4 -d '{"password": null, "expiresAt": null}'

Delete

DELETE /api/v1/drops/{code} Pro

Removes the record, the analytics and the stored bytes. The bytes go first, so a link taken down for a reason stops serving content immediately even while an edge cache still holds the record.

Files over 16 MB

The same three calls the website itself uses, and they take the same bearer key. Declare what is coming, send it in chunks, then commit.

FieldTypeWhat it does
POST /api/drop/beginjsonBody: { files: [{ name, type, size }], …options }. Every limit is checked here, before a byte moves. Replies with { code, partSize, items }.
PUT /api/drop/partrawQuery: ?code=&i=&part=. Body is the chunk. i is the file index, part counts from 1.
POST /api/drop/commitjsonBody: { code }. Classifies everything, writes the record, replies exactly as create does.
a two-gigabyte upload, in outline
1const begun = await post('/api/drop/begin', {
2 files: [{ name: file.name, type: file.type, size: file.size }],
3});
4
5const partSize = begun.partSize; // 16 MB
6for (let p = 1; p <= Math.ceil(file.size / partSize); p++) {
7 const chunk = file.slice((p - 1) * partSize, p * partSize);
8 await put(`/api/drop/part?code=${begun.code}&i=0&part=${p}`, chunk);
9}
10
11const drop = await post('/api/drop/commit', { code: begun.code });
12console.log(drop.url);

A part that fails is worth retrying — parts are addressed by number, so sending the same one twice replaces it rather than appending. A 4xx other than 429 is a decision, not a hiccup, and retrying it only makes the same answer arrive three times.

An unfinished upload is discarded after six hours and costs nothing.

Usage

GET /api/v1/account Pro

Storage used, storage allowed, and every limit that applies to this key. Worth reading before a batch job rather than discovering the ceiling half way through one.

Keys

GET /api/v1/keys Pro

Lists your keys by prefix, name and when each was last used — never the keys themselves. Making and revoking keys is deliberately dashboard-only: a key that can mint more keys is a key whose loss cannot be contained.

Anything else

Write to support@2l.nz. If something in this page is wrong, that is a bug and worth telling us about.