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.
| 1 | Authorization: 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.
| 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.
| 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 | } |
| Field | Type | What it does |
|---|---|---|
| unauthorized | 401 | No key, or a key that has been revoked. |
| plan_required | 403 | The key is valid but its account is not on Pro. |
| not_found | 404 | No 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_large | 413 | Over a size limit. The message says which one. |
| name_taken | 409 | That custom name is already in use. |
| quota_reached | 429 | Today's link allowance is used up. Resets at midnight UTC. |
| unavailable | 503 | Storage 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.
| Field | Type | What it does |
|---|---|---|
| file size | 2 GB | Per file. |
| one request | 16 MB | Anything larger uses the chunked flow below. |
| files per link | 500 | Beyond one, the link becomes a gallery or a collection. |
| links per day | 5,000 | Resets at midnight UTC. |
| storage | 250 GB | Across everything the account owns. |
| transfer | 1 TB | Per link, per calendar month. A link past it pauses until the first. |
| rate | 120 / minute | Per address. A 429 carries Retry-After. |
Create a link
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.
| 1 | curl -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.
| 1 | curl -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.
| 1 | curl -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
| Field | Type | What it does |
|---|---|---|
| name | string | The link name — 2l.nz/<name>. 3 to 64 characters, letters, numbers, hyphens and underscores. 409 if taken. |
| title | string | Shown in the viewer and in the unfurl. Up to 140 characters. |
| password | string | Anyone opening the link is asked for it. 4 to 200 characters. The file is not served until it matches. |
| expiresIn | seconds | Delete the link this long from now. |
| expiresAt | ms epoch | Or an absolute moment. expiresIn wins if both are sent. |
| burnAfterRead | boolean | Delete the link the first time it is opened. |
| allowDownload | boolean | Default true. False hides the download button; anyone determined can still save what their browser rendered. |
| text | string | Text instead of a file. Up to 1 MB. |
| files | array | JSON only: [{ name, type, content }] with content base64. |
List your links
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.
| 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
The full record, including view count and the list of items. Never the password — not even its hash.
Update
Send only what you want changed. Accepts title, password (null removes it), expiresAt (null means never), burnAfterRead and allowDownload.
| 1 | curl -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
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.
| Field | Type | What it does |
|---|---|---|
| POST /api/drop/begin | json | Body: { files: [{ name, type, size }], …options }. Every limit is checked here, before a byte moves. Replies with { code, partSize, items }. |
| PUT /api/drop/part | raw | Query: ?code=&i=&part=. Body is the chunk. i is the file index, part counts from 1. |
| POST /api/drop/commit | json | Body: { code }. Classifies everything, writes the record, replies exactly as create does. |
| 1 | const begun = await post('/api/drop/begin', { |
| 2 | files: [{ name: file.name, type: file.type, size: file.size }], |
| 3 | }); |
| 4 | |
| 5 | const partSize = begun.partSize; // 16 MB |
| 6 | for (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 | |
| 11 | const drop = await post('/api/drop/commit', { code: begun.code }); |
| 12 | console.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
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
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.