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

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"?, "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

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 100 URLs. Please delete some existing URLs to create new ones." }