shortn
Docs/API

API reference

Thirteen endpoints, all plain HTTP. There is also an interactive version of this reference where you can type a request and see the response without leaving the browser.

Base URL https://shortn.pages.dev · limits on the limits page · errors on the errors page

Responses are JSON except /api/export, which returns text. Errors are always JSON with an error field holding a sentence meant to be shown to a person.

CORS is open on the read endpoints, so you can call them from a browser page anywhere. The two that create things answer OPTIONS as well, and accept a JSON request body.

Making links

POST /api/create

Makes one short link.

Request

url
Required. The address to shorten. A missing scheme is added, so example.com becomes https://example.com.
custom
Optional. A slug of your choosing, 3 to 32 characters. Rejected if reserved or already taken.
title
Optional. A short label for your own records.
note
Optional. A longer note, same purpose.
pass
Optional. A password of 4 to 64 characters. Turns the redirect into a gate.
curl -X POST https://shortn.pages.dev/api/create   -H 'content-type: application/json'   -d '{"url":"example.com/a/very/long/path","title":"Docs"}'

Response

Answers 201 with the slug, the short URL, the destination, how many days it has left and whether it is protected.

{"slug":"k3mq7x","shortUrl":"https://shortn.pages.dev/k3mq7x",
 "url":"https://example.com/a/very/long/path",
 "title":"Docs","protected":false,"expiresInDays":7}

Only url is required. Everything else can be left out and the link still works. The slug rules are on the links page.

POST /api/bulk

Makes up to ten links in one request.

Request

The body is {"urls": ["...", "..."]}, optionally with a pass applied to every one of them. More than ten is refused with max 10 links per request rather than silently truncated.

curl -X POST https://shortn.pages.dev/api/bulk   -H 'content-type: application/json'   -d '{"urls":["example.com","example.org","example.net"]}'

Response

An array with one entry per input, each carrying either ok: true and the new link, or ok: false and the reason it failed. A failure partway through does not undo the links already made, so a partial success is a normal outcome and not an error.

DELETE /api/remove?slug=

Deletes one link immediately, before its seven days are up. This is the only way to take something back.

On protected links

Deleting does not need the password. The password protects the redirect from anyone following the link; it is not a claim on who is allowed to remove it.

Asking about links

GET /api/check

Whether a slug exists. Takes ?slug=. Unlimited, and safe to call in a loop.

Returns {slug, exists, reserved}. An expired slug answers exists: false and is queued for deletion, which means the name is genuinely free again rather than merely unreachable.

GET /api/unshorten

Where a slug goes, plus its click count, when it was made and how long it has left.

Request

Takes ?slug=. It accepts a full short URL as well as a bare slug, because a full pasted link is what you actually have. It only expands a link whose host is shortn.pages.dev or www.shortn.pages.dev; anything else is invalid slug.

That restriction is deliberate. This endpoint is for revealing our own links, and a general link expander pointed at a server is not something to leave lying around.

Response

The destination, or the reason there is not one. Note that the destination is withheld from a protected link until the password has been accepted.

GET /api/preview

Fetches an address and reports what it found. Takes ?url=.

Response

The final URL after redirects, the host, the page title, a description, a site name, an image and a canonical link. Any of them may be empty.

An unreachable page is not an error. It comes back 200 with the fields empty, because "I could not reach it" is an answer and a failed request is not. The only thing refused outright is an address we will not fetch at all — a private or loopback address, or a port that is not an ordinary one — and that is a 400 carrying reason of host, private or port. The reasoning is on the security page.

GET /api/links

Every link, as a bare JSON array — not wrapped in an object. Each entry has the slug, destination, host, title, note, click count, expiresInDays, whether it is protected, and the times it was created and last used.

Internal bookkeeping keys are filtered out, so nothing that is not a link appears. If you were expecting {"links": [...]} you will get an array, and that is not a mistake in your code.

GET /api/export

The same list as /api/links as plain text, one line per link. Meant for curl into a file.

GET /api/stats

Service-wide totals: links made, clicks recorded, a per-day breakdown for the last seven days, the most-clicked links, and referrer, device and country counts.

On expiry figures

Every figure that depends on a link's age is computed on the server from when it expires. Nothing here is counted down in the browser, so two people reading the page at different times cannot see different answers. Only the rows on your own links page do arithmetic client-side, and those are yours alone.

Protected links

POST /api/unlock

Attempts the password on a protected link.

Request

Body is {"slug":"...","pass":"..."}.

Response

A wrong password is 403 with {"error":"wrong password"}. A right one is 200 with the unlock cookie set, after which the link redirects normally.

The destination is in neither response body. The cookie is the answer.

Housekeeping

GET /api/version and /api/changelog

The running version and the full release history as JSON. Both are static documents rather than stored records, so they work even when storage is unhappy.

GET, DELETE and POST /api/sweep

Maintenance. GET and DELETE list what the sweeper would remove; POST with {"confirm":"sweep"} runs it. The confirmation is required, so this cannot be triggered by a stray link or a crawler.

Not part of the public API. It is documented because it exists and because knowing it exists is better than finding out. The three methods share one allowance rather than having one each — see the limits page.