Developers
API reference
Everything the worker answers, as it actually behaves. JSON in, JSON out, session cookie for anything that touches an account.
01 Getting started
You do not need an account to create a link. Signed out you get a six-character code, fifty a day per network. Every request and response is JSON.
curl -X POST https://w8.nz/api/shorten \
-H 'Content-Type: application/json' \
-d '{"targetUrl":"https://example.com/a/very/long/path"}'
Response:
{
"success": true,
"code": "a4f9Kq",
"shortUrl": "https://w8.nz/a4f9Kq",
"targetUrl": "https://example.com/a/very/long/path",
"length": 6,
"remaining": 49,
"limit": 50
}
TURNSTILE_SECRET is configured — it is, in production — /api/shorten requires a valid turnstileToken and rejects the request without one. A verification that cannot complete fails the request rather than passing it, so this endpoint is not usable from a script today. A proper API-key path is the next thing on the list.
02 Authentication
Signing in sets w8_session: HttpOnly, SameSite=Lax, Secure, seven days. Send it back on anything that touches an account.
| Endpoint | Body |
|---|---|
POST /api/auth/register | username, email, password. Does not sign you in — the account stays inactive until the emailed code is entered. |
POST /api/auth/verify-email | username, code. Sets the session cookie on success. |
POST /api/auth/resend-code | username. One per minute, five per hour per account. |
POST /api/auth/login | username (or an email address), password, and twoFactorCode if 2FA is on. Answers {"requires2FA":true} with HTTP 200 when the code is needed but absent. |
POST /api/auth/logout | — |
GET /api/auth/me | Always 200. Returns {"user":null} when signed out. |
POST /api/auth/forgot-password | email. Always answers {"success":true}, whether or not the address is registered. |
POST /api/auth/reset-password | token, password. Bumps the session epoch, so every device is signed out. |
GET /api/auth/check-reset?token= | Validates a reset link before a password box is offered for it. |
POST /api/auth/change-email | email. Sends a code to the new address; nothing changes until it comes back. |
POST /api/auth/confirm-email-change | code. |
Usernames are 3–20 characters of [a-z0-9_]. Passwords are at least 8 characters, must not contain the username, and are rejected if they appear on a short breach list.
03 Shortening
POST /api/shorten
| Field | Notes |
|---|---|
targetUrl | Required. http or https, up to 2,048 characters. A w8.nz link is refused. |
customLength | 3, 4, 5 or 6. Defaults to 5 signed in, 6 signed out. desiredLength is accepted as an alias. |
turnstileToken | Required whenever Turnstile is configured. |
Limits are enforced against the plan currently in force, which is not always the plan stored on the account: a lapsed subscription falls back to free-tier rules after its grace period, regardless of whether the provider has sent an expiry event yet.
| Length | Free | Plus | Pro |
|---|---|---|---|
6 | 100/day | 1,000/day | 5,000/day |
5 | 100/day | 1,000/day | unlimited |
4 | — | 100/day | 1,000/day |
3 | — | — | 100/day |
04 Redirects
GET https://w8.nz/{code} answers 302 with Cache-Control: no-store.
302 rather than 301 is deliberate. A permanent redirect is cached by the browser indefinitely, which means the second click never reaches us: the counter stops moving, and a link removed for abuse keeps working for everyone who already followed it once.
A code belonging to a suspended subscription answers 402 with a page explaining the pause. An unknown, deleted or recycled code answers 404.
05 Analytics
| Endpoint | Purpose |
|---|---|
GET /api/dashboard | Every link you own, fourteen days of daily totals, aggregates, and a week-over-week trend. |
GET /api/analytics/{code} | One link in detail. |
DELETE /api/link/delete/{code} | Removes the link, its click history and its index entry. The code returns to the pool. |
Fields are gated by plan: Free gets totals, Plus adds devices and referrers, Pro adds countries, browsers and recentClicks (the last 20). Fields your plan does not include come back as null rather than being omitted.
Each click record is { timestamp, country, device, browser, referrer }. The last hundred per link are kept. The dashboard reports on the 200 most recent links.
06 Two-factor authentication
RFC 6238 TOTP, SHA-1, six digits, thirty-second period, with one window of drift accepted on either side.
| Endpoint | Purpose |
|---|---|
POST /api/auth/2fa/setup | Returns secret and otpauthUrl. The server keeps its own copy of the pending secret for fifteen minutes. |
POST /api/auth/2fa/verify | code. Turns 2FA on and returns ten backupCodes. |
POST /api/auth/2fa/disable | code — a TOTP or a backup code. Required. |
/verify. Only their SHA-256 hashes are stored, so they cannot be shown again or recovered from the database. /setup deliberately returns no codes: it is reachable with nothing but an open session, and handing out ten permanent login bypasses at that point would defeat the feature.
07 Billing
| Endpoint | Purpose |
|---|---|
POST /api/billing/checkout | plan (plus|pro), cycle (monthly|annual). Returns a Vapih Pay URL. |
POST /api/billing/webhook | Provider only. Authenticated by the x-vapih-secret header, compared in constant time. |
There is no endpoint that changes your own plan. Plans are granted only by the webhook, once the payment provider has confirmed a payment, and cancelling happens in the provider's portal — the only place that can actually stop a card being charged. The checkout URL is built from your session, never from a username in the request body.
08 Errors and limits
Failures are {"success": false, "error": "a sentence you can show a person"}. Some carry a hint: needsAccount, needsPlan, requires2FA, needsVerification, quotaReached, blocked.
| Status | Means |
|---|---|
400 | Bad input, or a wrong password or code. |
401 | Not signed in, or the action needs an account. |
402 | The link's owner has a suspended subscription. |
403 | Signed in, but the plan does not allow it — or the destination was blocked by Safe Browsing. |
404 | No such link. |
429 | Rate limited or out of daily quota. Carries Retry-After. |
502 | A verification email could not be delivered. |
Rate limits, per fixed window: failed sign-ins 8 per account and 100 per IP every 15 minutes; registrations 15 per IP per hour; reset emails 3 per account and 15 per IP per hour; abuse reports 10 per IP per hour. Only failures are counted against the sign-in limits, so signing in successfully all morning never locks you out.