v1 · stable

Turn a long URL into three characters, from your own code.

Every endpoint the worker answers, documented as it actually behaves. JSON in, JSON out, and a session cookie for anything that touches an account. One credential, an API token, and a request shape that fits on a postcard. From a browser, creating a link needs no account at all; from a server it needs a token, which starts on Plus.

✓ Base https://w8.nz ✓ No SDK required ✓ Safe Browsing on every URL

01 Introduction

Waitin Zone shortens a URL to a three-to-six character code served from Cloudflare's edge. The same HTTP API the site's own pages use is the one documented here — there is no second, private interface, so anything the dashboard can do you can do.

Base URL

https://w8.nz

Conventions

  • Request bodies are JSON. Send Content-Type: application/json.
  • Every response is JSON with a boolean success field.
  • A failure carries a human-readable error string. It is written to be shown to a person as-is.
  • Times are UTC. Durations are seconds unless the field name says otherwise.
  • Short codes are case sensitive: a4f and A4F are different links.

Two credentials, two callers. Browsers use an HttpOnly session cookie, while servers use an API token created from the dashboard. Signed-out browser requests can still create six-character links after Turnstile; unattended server requests require a Plus-or-higher token.

02 Quickstart

The shortest useful request. No account, no headers beyond the content type, six-character code back.

One header does all the work. Create a token in your dashboard under API — it is shown once and never again — and send it as Authorization: Bearer w8_….

Without one, /api/shorten asks for a Cloudflare Turnstile token instead: the human check the website's own pages complete in a browser. A server cannot mint one of those, which is the whole reason API tokens exist.

# Create a link
curl -X POST https://w8.nz/api/shorten \
  -H 'Authorization: Bearer w8_…' \
  -H 'Content-Type: application/json' \
  -d '{"targetUrl":"https://example.com/a/very/long/path"}'
{
  "success": true,
  "code": "q7Bxa2",
  "shortUrl": "https://w8.nz/q7Bxa2",
  "targetUrl": "https://example.com/a/very/long/path",
  "length": 6,
  "remaining": 24,
  "limit": 25
}

From JavaScript

const res = await fetch('https://w8.nz/api/shorten', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + process.env.W8_TOKEN,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ targetUrl: 'https://example.com/' })
});
const data = await res.json();
if (!data.success) throw new Error(data.error);
console.log(data.shortUrl);

From your server, not your front-end. There is no Access-Control-Allow-Origin header on any of these endpoints, so a fetch from your own page is refused before it arrives. That is the same decision as the token rather than a separate one: a credential that has to sit in front-end JavaScript is a credential every reader of the page can take.

03 Authentication

Two credentials, for two callers. A token is how a program authenticates; a session cookie is how a browser does. Anything not listed as requiring one works without either.

API tokens

A token is w8_ followed by 64 hex characters, sent in an Authorization: Bearer header. It is created in the dashboard, shown once, and stored here only as a hash — there is no endpoint that returns it again, because one that could would let a stolen session cookie harvest every token on the account.

A token also stands in for the human check on /api/shorten, which is its main purpose. Turnstile proves a person is present; a token proves a paid account is. Either is enough to be worth screening a URL for.

The Free plan cannot mint one. POST /api/tokens answers 403 with needsPlan: "plus". That is a deliberate line rather than an upsell dressed as one: the free tier exists so a person can shorten a link, and unattended automation is what turns a shortener into somebody else's spam relay. Free accounts create links in a browser, where the human check applies.

POST/api/tokensSession required

Mints one. Optional name, up to 40 characters, to tell them apart later. Ten per account, ten an hour.

{ "success": true, "token": "w8_9f3c…", "hint": "a41b", "shownOnce": true }
GET/api/tokensSession required

Names, last-four hints, creation and last-use times — never the secrets. Also reports apiEnabled and the plan's rateLimit.

POST/api/tokens/revokeSession required

Takes name and hint. Effective immediately.

A token cannot mint another token, list the account's tokens, or revoke one — all three need the cookie. So a leaked token spends quota and nothing else: it cannot extend its own reach, and it cannot lock you out of revoking it.

Session cookies

Signing in returns a w8_session cookie: HttpOnly, SameSite=Lax, Secure over HTTPS. Send it back on any request that needs an account. When both a cookie and a token arrive, the cookie wins: it proves strictly more, and letting a header override it would let a stolen token act inside somebody's signed-in browser.

POST/api/auth/loginPublic

Exchanges credentials for a session cookie.

FieldNotes
usernameRequiredUsername or email address.
passwordRequired
twoFactorCodeConditionalSix-digit TOTP, or one backup code. Required only once the first attempt answers requires2FA.
{ "success": false, "requires2FA": true }   // send the code and repeat
GET/api/auth/meOptional session

Who the current cookie belongs to. Returns { "success": true, "user": null } when there is no session, rather than a 401, so it is safe to call on every page load.

{
  "success": true,
  "user": {
    "username": "sami",
    "email": "[email protected]",
    "plan": "pro",
    "effectivePlan": "pro",
    "subscriptionStatus": "ACTIVE",
    "subscriptionEndsAt": 1818764275027,
    "cancelAtPeriodEnd": false,
    "twoFactorEnabled": true,
    "backupCodesRemaining": 10,
    "emailVerified": true,
    "createdAt": 1785312000000
  }
}

The address comes back in full, not masked. Nothing secret is here — no password material, no TOTP secret, no backup codes — but it is still the account's own email, so treat the response as private to the session that asked for it.

plan versus effectivePlan. plan is what was bought; effectivePlan is what is currently granted. A lapsed subscription leaves the first as pro and drops the second to free. Always gate features on effectivePlan.

POST/api/auth/logoutPublic

Clears the cookie. Always answers 200, session or not.

The rest of the account surface

EndpointSessionPurpose
POST /api/auth/registerNoCreates an unverified account and emails a six-digit code.
POST /api/auth/verify-emailNoConfirms that code and signs the account in.
POST /api/auth/resend-codeNoSends another. Refused within 60 seconds of the last one.
POST /api/auth/forgot-passwordNoEmails a reset link. Answers the same whether or not the address exists.
POST /api/auth/reset-passwordNoConsumes the token and signs every device out.
GET /api/auth/check-resetNoTests a reset token before showing a password field for it.
POST /api/auth/change-emailYesSends a confirmation to the new address.
POST /api/auth/confirm-email-changeYesCompletes the change.
POST /api/auth/2fa/setupYesReturns a TOTP secret and its otpauth URI. No backup codes yet.
POST /api/auth/2fa/verifyYesConfirms one code, switches 2FA on, and returns the ten backup codes once.
POST /api/auth/2fa/disableYesRequires a valid TOTP or backup code.

Setup deliberately withholds the backup codes until verify succeeds. Handing them out at setup would mean anyone who reached that endpoint walked away with ten permanent bypasses whether or not they ever proved they could generate a code.

04 Create a link

POST/api/shortenOptional session

Screens the destination, picks a free code and stores the link. This is the only endpoint most integrations need.

FieldNotes
targetUrlRequiredhttp or https, at most 2,048 characters. A w8.nz URL is refused.
customCodeScaleChoose the code instead of being handed one. 3–32 characters of letters, numbers, hyphens and underscores, beginning and ending with a letter or number. Wins over customLength — a chosen code is its own length — and spends the separate custom quota rather than a length quota.
customLengthOptional3, 4, 5 or 6. Defaults to 5 with a session, 6 without. Anything else is ignored rather than rejected.
turnstileTokenBrowsersA single-use Cloudflare Turnstile token. Needed only when the request carries no Authorization header — an API token replaces it. With neither, the call is refused 400 carrying needsHumanCheck: true.

Success

{
  "success": true,
  "code": "a4f",
  "shortUrl": "https://w8.nz/a4f",
  "targetUrl": "https://example.com/...",
  "length": 3,
  "remaining": 99,
  "limit": 100
}

Refusals worth handling

StatusBody carriesMeaning
400errorNot a URL, wrong scheme, over 2,048 characters, already a w8.nz link, or the human check failed.
401needsAccountThat length needs an account. Free gets five characters.
403needsPlanThat length needs a paid plan. The value is the lowest plan that has it.
403blockedGoogle Safe Browsing matched the destination. The code is never created.
409takenThat customCode already belongs to a link.
429quotaReachedMonthly quota for that length is spent. No Retry-After on this one — the counter turns over at the start of the next UTC month, and the message says when.
429Retry-AfterThe token's per-minute rate limit, which does carry the header.

A lapsed subscription reads as a plan error, not an auth error. When a Pro account falls back to free, a request for a three-character code answers 403 with a message naming the lapsed plan. Show it as-is; it already explains what renewing will restore.

05 Following a link

GET/{code}Public

Answers 302 Found with the destination in Location, and records the click.

$ curl -sI https://w8.nz/a4f | head -3
HTTP/2 302
location: https://example.com/...
cache-control: no-store

302, never 301. A permanent redirect is cached by the browser forever: the click counter stops moving after the first visit, and a link removed for abuse keeps working for everyone who already followed it. The round trip is the price of keeping control of the link, and no-store is what makes that price actually get paid.

HEAD is answered exactly like GET with the body dropped, which is what Slack, Discord and WhatsApp send before they unfurl a link.

An unknown code returns the 404 page. A code whose owner's subscription has lapsed past its grace period returns a paused page instead of redirecting — the link is not gone, and renewing restores it.

What a click records

Country, device class, browser and referring host — all of it read from what Cloudflare already attaches to the request. No cookie is set on the person clicking and no identifier follows them anywhere. The last hundred clicks per link are kept.

06 Analytics

GET/api/analytics/{code}Session required

Everything recorded for one of your links. Fields above your plan come back as null rather than being omitted, so the shape never changes.

{
  "success": true,
  "code": "a4f",
  "plan": "pro",           // which tier the fields below reflect
  "targetUrl": "https://example.com/...",
  "totalHits": 1284,
  "createdAt": 1787228279291, // epoch ms, not a date string
  "devices": { ... },        // null on free
  "referrers": { ... },      // null on free
  "countries": { ... },      // pro only
  "browsers": { ... },       // pro only
  "recentClicks": [ ... ]    // pro only, last 20
}

Requesting a link you do not own answers 403, not 404 — the code demonstrably exists, so pretending otherwise would be theatre.

There is no per-day series on this endpoint. Daily totals are an account-wide figure, not a per-link one, and they come back from /api/dashboard as hitsByDay alongside the matching dayLabels.

07 List and delete

GET/api/dashboardSession required

Every link you own with its click total, plus the account's last fourteen days of traffic as hitsByDay and dayLabels, and the showDevices / showReferrers / showCountries / showBrowsers flags for what your plan may see. It does not report remaining quota — that only comes back on a /api/shorten response, as remaining.

DELETE/api/link/delete/{code}Session required

Removes the link. The code returns to the pool and may be handed to somebody else later, so treat deletion as permanent.

PATCH/api/link/{code}Scale

Repoint a live code at a new destination. The code, its click history and its totals all stay where they are; only the destination moves.

This is the endpoint that makes a short link worth owning. A code goes on a poster, a business card, the QR sticker under a menu — and printed things cannot be recalled. Changing where w8.nz/menu goes, without changing what is printed, is the difference between a shortener and a redirect you control.

curl -X PATCH https://w8.nz/api/link/spring-sale   -H 'Authorization: Bearer w8_…'   -d '{"targetUrl":"https://example.com/spring-v2"}'

{ "success": true, "code": "spring-sale",
  "targetUrl": "https://example.com/spring-v2",
  "previousUrl": "https://example.com/spring" }

The new destination goes through Safe Browsing exactly as a new link would. Without that, this endpoint would be a way to launder a blocked URL through a code that was screened while pointing somewhere harmless.

POST/api/shorten/bulkScale

Many links, one request. Takes { "links": [ … ] } where each entry is the same body /api/shorten accepts. The per-call ceiling is 100 on Scale 1 rising to 1,000 on Scale 4 and 5.

Partial success is the normal outcome, and the response reports it per item rather than failing the batch. One blocked URL out of five hundred should not discard the other four hundred and ninety-nine, and a caller forced to re-send everything to discover which one was bad will simply re-send everything.

{ "success": true, "created": 2, "failed": 1,
  "results": [
    { "index": 0, "status": 200, "code": "yxIjm" },
    { "index": 1, "status": 400, "error": "That doesn't look like a URL…" },
    { "index": 2, "status": 200, "code": "launch-2027" }
  ] }

Every item runs through the single-link handler itself, so quota, plan gates, code validation and screening cannot drift between the two endpoints — and this is exactly where a second copy of those rules would go unnoticed.

curl -X DELETE https://w8.nz/api/link/delete/a4f \
  -H 'Cookie: w8_session=…'
POST/api/report-abusePublic

Reports a link. Requires the code and a reason. Reported links are retired by hand and their codes are never reissued.

08 Errors

Every failure has the same shape. There are no numeric error codes to look up — the string is the contract, and it is written to be displayed.

{ "success": false, "error": "That URL is longer than 2048 characters." }
200Done. success is still worth checking.
400The request itself is wrong: malformed JSON, a bad URL, a failed human check.
401No session, or one is needed for what was asked.
403Signed in, but not allowed: someone else's link, a plan you are not on, or a destination Safe Browsing refused.
404No such link, or no such endpoint.
429Rate limited. Auth and abuse endpoints carry Retry-After in seconds; the monthly link quota does not, because it resets on the UTC calendar rather than after an interval.
500Our fault. Nothing was written; retrying is safe.

09 Limits & plans

Quotas are per calendar month and per length, counted in UTC, so every counter turns over together at 00:00 UTC on the first day of the month. A dash means that length is not available on the plan at all.

Per monthNoneFreePlusProScale 1Scale 2Scale 3Scale 4Scale 5
6 characters251005001,0005,00012,00030,00075,000150,000
5 characters—502507503,0008,00020,00050,000100,000
4 characters——1003001,0002,5006,00015,00030,000
3 characters———1003007502,0005,00012,500
Custom codes————1505002,0007,50020,000

A customCode spends the custom row, never a length row. A chosen code is not a length: spring-sale is not a six-character link, and charging it to that counter would let an account drain a bucket it never uses.

What a token may do per minute

FreePlusProScale 1Scale 2Scale 3Scale 4Scale 5
API requests—601203006001,2002,4006,000
Bulk per call———1002005001,0001,000
Repoint a code———✓✓✓✓✓

Guest quota is counted per network address; account quota is counted per account. Sign-in attempts are limited separately, per account and per network. The per-minute rate limit is per account, not per token, so ten tokens share one budget rather than multiplying it.

Analytics depth follows the plan too: Free sees totals, Plus adds devices and referrers, and Pro — along with every Scale step — adds countries, browsers and the last twenty individual clicks.

Limits move with the plan in force right now, not the plan at the time a token was created. A lapsed subscription takes Scale features away from an existing token the moment the provider says it ended; renewing gives them back with no need to re-issue anything.

10 Security

Destinations

Every URL is checked against Google Safe Browsing — malware, social engineering, unwanted software, harmful applications — before a code is generated. A link that fails never exists, so there is nothing to disable later.

Accounts

  • Passwords: PBKDF2-SHA256 with a per-account salt, three chained rounds of 100,000 iterations — 300,000 iterations of work in total. It is chained because Workers refuses more than 100,000 iterations in a single call, and one round at the cap is well under what is recommended. Both the iteration count and the round count are stored on each record, so either can be raised later without locking anyone out.
  • Two-factor: RFC 6238 TOTP over a 30-second period, accepting one step of drift either side.
  • Backup codes: ten, single use, stored only as hashes.
  • Changing a password invalidates every existing session.

The session cookie is the credential. It is HttpOnly, so page scripts cannot read it, and SameSite=Lax, so another site cannot make an authenticated request on your behalf. If you store it server-side to call this API, treat it exactly as you would a password.

11 Questions

Can I choose my own code?

Yes, on Scale. Send customCode and you get exactly that code: 3–32 characters of letters, numbers, hyphens and underscores. Names that would shadow a real page on this site — docs, dashboard, privacy and the rest — are refused with a reason rather than quietly swapped for a generated one.

It is a paid feature because that is the reservation model. Free vanity codes on a shortener are a land-grab: the good words go in an afternoon, to whoever scripts it first, and none of them go to the people who would have used them.

Do links expire?

No. A link lives until you delete it, it is reported and retired, or the owning subscription lapses for more than fourteen days — seven days working, seven days paused, then removed.

Is there a bulk endpoint?

Yes, on Scale: POST /api/shorten/bulk, 100 to 1,000 links per call depending on the step. The screening runs concurrently, so a batch costs about one round trip rather than one per link.

Below Scale, call /api/shorten in a loop and respect the quota. Each call is one Safe Browsing round trip and roughly 300 ms.

Are codes case sensitive?

Yes. The alphabet is base62, so a4f and A4F are two different links. Do not lowercase a code before looking it up.

Can I see who clicked?

No, and neither can we. Country, device class, browser and referring host are all that is recorded, none of it joined to a person.

© 2026 WAITIN ZONE Built and run on Cloudflare · w8.nz