API reference
The official API for creating and managing short links, tags, statistics, and keys. Every example is real and matches the server one-to-one.
Base URL and response envelope
All requests go to:
https://ktzr.io/api A successful response is always wrapped in data; an error comes back as error with a readable message:
{ "data": { ... } } // 2xx
{ "error": "readable message" } // 4xx / 5xx Authentication
Two ways to authenticate:
A Firebase ID token (signed-in app users):
Authorization: Bearer <Firebase ID token> An API key (for servers and scripts). Keys are created in the app or via the API, always start with rsvp_live_sk_, and are shown exactly once at creation:
X-API-Key: rsvp_live_sk_... Workspace resolution
Every /api/v1 resource belongs to a workspace. Resolution works like this:
- An API-key call always operates on the workspace the key belongs to — the workspace query parameter is ignored.
- An ID-token call may pass ?workspace={slug}; without it, the account’s default workspace is used.
- A workspace that does not exist, or that you are not a member of, answers 404 — its existence is never revealed.
Rate limits and quotas
- Creating, editing, and deleting links share one budget: 20 operations per hour per workspace (default). Exceeding it answers 429 with a Retry-After header in seconds.
- Lifetime link quota: 50 links per account (default). Exceeding it answers 403.
- New accounts: up to 5 links during the first 24 hours.
- Link creation also returns an X-RateLimit-Remaining header with the remaining hourly budget.
- Public abuse reports (unauthenticated): up to 5 per hour per IP address.
All endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/register | Token only | Register the signed-in account (optional body: {"tosVersion"}) |
| GET | /api/v1/me | Token or key | Account identity and default workspace |
| DELETE | /api/v1/me | Token only | Delete the whole account (answers 202) |
| POST | /api/v1/workspace | Token only | Create a workspace: {"name"?, "slug"?, "tosVersion"?} |
| GET | /api/v1/workspace | Token or key | Workspace details, plan, and usage |
| PATCH | /api/v1/workspace | Token only | Update name or slug (workspace owner only) |
| GET | /api/v1/links | Token or key | List links with pagination and filters |
| POST | /api/v1/links | Token or key | Create a link: {"url", "domain"?, "code"?, "tagIds"?} |
| GET | /api/v1/links/{code} | Token or key | A single link |
| PATCH | /api/v1/links/{code} | Token or key | Update destination or tags: {"url"?, "tagIds"?} |
| DELETE | /api/v1/links/{code} | Token or key | Soft delete - the link answers 410 forever |
| GET | /api/v1/links/{code}/history | Token or key | Up to 50 recent destination edits |
| GET | /api/v1/tags | Token or key | The workspace’s tags |
| POST | /api/v1/tags | Token or key | New tag: {"name", "color"} |
| PATCH | /api/v1/tags/{id} | Token or key | Update name or color |
| DELETE | /api/v1/tags/{id} | Token or key | Delete a tag |
| GET | /api/v1/keys | Token only | Your keys in this workspace |
| POST | /api/v1/keys | Token only | New key: {"name", "permissions"?, "expiresIn"?} |
| PATCH | /api/v1/keys/{keyId} | Token only | Update name, permissions, or active state |
| DELETE | /api/v1/keys/{keyId} | Token only | Revoke the key permanently |
| GET | /api/v1/stats/{code}?days=N | Token or key | Click totals and daily breakdown |
| POST | /api/abuse | None | Public report of a harmful link |
Create a link
Send a URL, get a short link. domain is optional (default ktzr.io, subject to account grants); code — a custom back-half of 3–50 characters from [a-zA-Z0-9_-] — is currently available to admin accounts only (planned for Pro); tagIds — up to 10 existing tag ids.
Every destination is screened against Google Web Risk: a flagged destination is refused with 422.
curl -X POST https://ktzr.io/api/v1/links \
-H "X-API-Key: rsvp_live_sk_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/very/long/path?with=parameters"}' HTTP/2 201
X-RateLimit-Remaining: 19
{
"data": {
"shortCode": "aB3xK9qZ",
"shortUrl": "https://ktzr.io/aB3xK9qZ",
"domain": "ktzr.io",
"tagIds": []
}
} HTTP/2 422
{ "error": "This destination is flagged as unsafe (SOCIAL_ENGINEERING) and cannot be shortened." } List links
Cursor-based pagination: limit between 1 and 100 (default 25), cursor from the previous response (opaque — do not parse), status one of active / disabled / deleted (without a filter, deleted links are excluded), tag a tag id, search a short-code prefix. nextCursor is null on the last page; a page may return fewer than limit items even when more remain — keep following the cursor.
curl "https://ktzr.io/api/v1/links?limit=25&status=active&search=aB" \
-H "Authorization: Bearer <ID token>" {
"data": {
"links": [
{
"shortCode": "aB3xK9qZ",
"shortUrl": "https://ktzr.io/aB3xK9qZ",
"originalUrl": "https://example.com/very/long/path?with=parameters",
"domain": "ktzr.io",
"status": "active",
"tagIds": ["fJ29xPqLmA31Bc7Yd0Ek"],
"visits": 42,
"botVisits": 7,
"createdAt": "2026-08-30T10:15:00.000Z"
}
],
"nextCursor": null
}
} Statistics
Click totals plus a daily (UTC) breakdown of the last days: days between 1 and 90, default 30. Days without clicks come back as zeros. Bots (previews, crawlers, scripts) are counted separately in botVisits.
curl "https://ktzr.io/api/v1/stats/aB3xK9qZ?days=7" \
-H "X-API-Key: rsvp_live_sk_..." {
"data": {
"shortCode": "aB3xK9qZ",
"total": 42,
"botTotal": 7,
"daily": [
{ "date": "2026-08-24", "visits": 5, "botVisits": 1 },
{ "date": "2026-08-25", "visits": 9, "botVisits": 2 }
]
}
} Edit, delete, history
- PATCH accepts a new url (screened against Web Risk again) or new tagIds. Destination edits are recorded in the history.
- DELETE is a soft delete: the link answers 410 to everyone, and the code is never recycled. Operating on an already-deleted link answers 410.
- GET /api/v1/links/{code}/history returns up to 50 destination edits, newest first.
Tags
Name up to 32 characters, color one of: red, orange, yellow, green, teal, blue, purple, pink. A duplicate name in the same workspace answers 409. Deleting a tag never rewrites links — the id is simply filtered out on read.
API keys
Key management requires an ID token (a key cannot manage keys). Up to 10 active keys; expiresIn in days (1–365, optional); permissions are canCreate / canRead / canUpdate / canDelete, all defaulting to true. DELETE deactivates the key (revocation).
Public abuse reports
An open endpoint, no authentication. reason between 10 and 1000 characters; reporterEmail optional. The response is always 202 — whether a code exists is never revealed. Note: this endpoint’s response is not wrapped in the data envelope.
curl -X POST https://ktzr.io/api/abuse \
-H "Content-Type: application/json" \
-d '{"code": "aB3xK9qZ", "reason": "This link leads to a phishing page impersonating a bank"}' HTTP/2 202
{ "message": "Report received" } Error codes
| Status | Meaning |
|---|---|
| 400 | Invalid input (bad URL, missing parameter, broken cursor) |
| 401 | Missing/expired token or unknown key |
| 403 | Not allowed: quota reached, account suspended, unverified email, missing key permission, or an ungranted domain |
| 404 | Not found — including another workspace’s resources (existence is not leaked) |
| 409 | Conflict: code / slug / tag name already taken |
| 410 | The link was deleted (its code is never reused) |
| 422 | The destination is flagged as unsafe by Google Web Risk |
| 429 | Rate limit exceeded — the Retry-After header says how many seconds to wait |
Example 403 (quota) error:
HTTP/2 403
{ "error": "You have reached your maximum limit of 50 URLs. Please delete some existing URLs to create new ones." }