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

MethodPathAuthDescription
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

StatusMeaning
400Invalid input (bad URL, missing parameter, broken cursor)
401Missing/expired token or unknown key
403Not allowed: quota reached, account suspended, unverified email, missing key permission, or an ungranted domain
404Not found — including another workspace’s resources (existence is not leaked)
409Conflict: code / slug / tag name already taken
410The link was deleted (its code is never reused)
422The destination is flagged as unsafe by Google Web Risk
429Rate 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." }