ktzr.io API reference: shorten links with one curl call
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.
- Active-link quota: 100 links per workspace (default); deleting a link frees its slot. 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"?, "temporary"?} |
| GET | /api/v1/links/{code} | Token or key | A single link |
| PATCH | /api/v1/links/{code} | Token or key | Update destination, temporary destination or tags: {"url"?, "temporary"?, "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 to 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.
A link can also be created with temporary, a temporary destination - explained below.
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",
"effectiveUrl": "https://example.com/very/long/path?with=parameters",
"temporary": null,
"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.
Temporary destination
Send a link somewhere else until a time you choose; afterwards it returns to its permanent destination on its own. PATCH the link with temporary, carrying url and until - an ISO 8601 date-time with a timezone (Z or an offset), at most 365 days ahead. The permanent destination, originalUrl, does not change, and editing url does not touch the temporary one. To end it at once, send temporary: null.
curl -X PATCH https://ktzr.io/api/v1/links/aB3xK9qZ \
-H "X-API-Key: rsvp_live_sk_..." \
-H "Content-Type: application/json" \
-d '{"temporary": {"url": "https://example.com/tonight", "until": "2027-06-01T08:00:00+03:00"}}' {
"data": {
"shortCode": "aB3xK9qZ",
"shortUrl": "https://ktzr.io/aB3xK9qZ",
"originalUrl": "https://example.com/very/long/path?with=parameters",
"effectiveUrl": "https://example.com/tonight",
"temporary": {
"url": "https://example.com/tonight",
"until": "2027-06-01T05:00:00.000Z",
"active": true
},
"domain": "ktzr.io",
"status": "active",
"tagIds": ["fJ29xPqLmA31Bc7Yd0Ek"],
"visits": 42,
"botVisits": 7,
"createdAt": "2026-08-30T10:15:00.000Z"
}
} On every link resource, effectiveUrl is where the link sends visitors right now (a disabled or deleted link answers 410 whatever it says), and temporary is null or an object with url, until and active. After the time has passed, the temporary destination is still returned, with active: false.
A temporary destination is screened against Google Web Risk too, and a flagged one is refused with 422. Setting one records a history row with kind: temporary and its until; ending an active one by hand records kind: temporaryEnded. The automatic return when the time passes is not recorded.
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 to 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 100 URLs. Please delete some existing URLs to create new ones." }