# LinkZen > LinkZen is a URL shortener with analytics. It turns a long address into a short link on > `qr3.fr`, renders a QR code for it, and records every visit (device, source, referrer, > optional geolocation). The web app lives at https://app.linkzen.io, the REST API at > https://api.linkzen.io. An agent can drive the link operations with a long-lived API key: no > browser, no refresh loop. Everything else needs an interactive login. The machine-readable contract is the OpenAPI 3.1 document. Read it before writing any request: it carries the real request bodies, response shapes and status codes, endpoint by endpoint. ## Start here - [OpenAPI 3.1 specification](https://linkzen.io/openapi.yaml): the source of truth. Every documented path exists in the running backend. Covers auth, account, API keys, urls, qr, subscription and plans. - [Pricing and plan limits](https://linkzen.io/pricing/): which plan includes API access, custom short codes, link editing, custom QR colours, and how long visit statistics are kept. ## What an API key can and cannot do Read this before designing anything. An API key drives **short links, and nothing else**. **Yes:** list and create links (`/api/urls`), read, edit, soft-delete, disable, enable and pin a link (`/api/urls/{code}` and its sub-paths), and read its statistics and visits. **No:** read the account profile or plan, render or download a QR code, touch subscriptions and billing, or manage API keys. Those are all JWT-only, which means an interactive login. There is no key-based route to them, and there is no plan that unlocks them. So an agent can run the whole link lifecycle unattended. It cannot check its own quota before creating a link, and it cannot fetch a QR image. Plan for that rather than discovering it later. ## Authentication in one paragraph Send `Authorization: Bearer `. There are two kinds of token and the server tells them apart by shape. An **API key** starts with `lz_` followed by 64 hexadecimal characters and is what an integration should use. A **JWT access token** is three dot-separated base64 segments, comes from `POST /api/auth/login` and expires after 15 minutes. There is no `X-API-Key` header, do not invent one. To get a key: log in to https://app.linkzen.io, open the account page, create a key. Or call `POST /api/account/api-keys` with a JWT and a `label`. **Creating a key requires a plan that includes API access**; on a plan without it you get 403 `account.api_key_plan_required`. Either way, the plaintext key appears exactly once, in that one response. Only its SHA-256 is stored, so nothing can ever show it to you again. Copy it to your secret store immediately. A key normally never expires. You may give it an explicit expiry when you create it, and you can revoke it at any time, with immediate effect. ## Six things that will trip you up 1. **The `lz_` prefix is the whole distinction.** Same header, same scheme, two transports. A token without the prefix is parsed as a JWT and will fail as one. 2. **Read the 401 body, it tells you which problem you have.** There are three, and each one is accurate wherever it appears: - `Invalid or expired credentials.` A JWT-only route refused what you sent. It names no credential type on purpose. **If you sent a key that works on `/api/urls`, this means wrong route, not dead key.** Check whether the operation lists `apiKeyBearer` in the spec before you re-mint anything. - `Invalid or expired API key.` The link routes refused your key. Here the key really is the problem. - `Missing or invalid Authorization header.` No bearer token arrived at all. 3. **Every genuine key refusal is the same 401.** On the link routes, an unknown key, a malformed key, an expired key, a deleted account and a deactivated account all return `{"status":"error","message":"Invalid or expired API key."}`, byte for byte. This is deliberate: you can tell that the key was refused, never why. Do not build logic that tries. 4. **`DELETE /api/urls/{code}` does not delete.** It sets the link inactive. The row survives, the visit history survives, and the short code stays reserved forever, so nobody else can ever claim it. Neither can you: creating a link on a code you deleted returns 409 `url.code_already_used`, and that holds in the web app too, not just the API. A short code, once used, is spent for good. Call `PATCH /api/urls/{code}/enable` to bring the original back. Calling DELETE twice returns 404 the second time, which here means "already inactive", not "unknown code". 5. **Key management is JWT-only.** An agent cannot mint or revoke its own keys. Trying it with a key gives `Invalid or expired credentials.`, not a helpful "use a JWT here", so treat that body on `/api/account/api-keys` as "this needs a login". The admin routes under `api/admin/*` are JWT-only too, and are not part of the public API. 6. **`message` is usually an i18n key, not a sentence.** You will get `url.not_found`, `auth.invalid_credentials`, `account.api_key_plan_required`. Never show it to a human as-is. Three families break the pattern and return French prose instead: plan-limit refusals, throttle messages and the authentication filter's own refusals. Handle both. ## Limits - 60 requests per minute, per key, on the link routes. Over that: 429 with a `Retry-After` header in seconds. The bucket is per key, not per IP, so your key never shares it with anyone. - Plan quotas (links per day, active links, statistics retention) are enforced. An agent cannot read them with a key, so treat a 403 carrying a `limit_details` block as the signal that a quota is exhausted. - The pricing page advertises a daily API request quota on the Pro plan. That quota is **not enforced** by the backend today. The 60 per minute above is the only real limit. ## A first call ``` curl -H "Authorization: Bearer lz_REPLACE_WITH_YOUR_KEY" \ "https://api.linkzen.io/api/urls?per_page=10" ``` That lists your links and confirms the key works before you try to write anything. To create a link, `POST /api/urls` with `{"url": "https://..."}`; the response carries the `short_url`. Note that the list response also embeds a `plan` block with your limits and usage, which is the only way a key holder can see them. ## Optional - [Web application](https://app.linkzen.io): sign up, manage links, create and revoke API keys. - [Marketing site](https://linkzen.io): what the product does. - [Source repository](https://github.com/Liam-Nothing/Linkzen.io): backend (CodeIgniter 4), dashboard and website.