Skip to content

API docs

Create, update and delete short links and read their click stats from your own code. Every request goes to:

https://api.toifel.io/v1

The API is available on paid plans. Each plan allows a number of requests per minute, shared by all keys in the workspace:

PlanAPI accessRequests / min
FreeNo—
ProYes60
BusinessYes600

Not a developer? On a paid plan you can import links from a CSV file in the dashboard, no code needed.

Quickstart

  1. Create a key in Settings → API keys. Workspace owners and admins can do this. Copy the key now: it is shown only once.
  2. Store it in an environment variable called API_KEY, for example export API_KEY=sk_live_… in your shell. Every example on this page reads it from there.
  3. Check the key works:
curl -X GET 'https://api.toifel.io/v1/me' \
  -H "Authorization: Bearer $API_KEY"

Then create your first short link. Without an alias, it gets a random 7-character code.

curl -X POST 'https://api.toifel.io/v1/links' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/blog/launch?utm_source=newsletter",
  "title": "Launch post",
  "tags": [
    "newsletter"
  ]
}'

Paid plans can add ios_url and android_url. iPhone and iPad visitors get ios_url, Android visitors get android_url, and everyone else, including link-preview bots, gets url. To open your app on the right screen, use a link your app already handles as a universal/app link; in-app browsers like Instagram may still open the web page.

Authentication

Send your key in the Authorization header on every request:

Authorization: Bearer sk_live_…

A key belongs to one workspace and acts as the member who created it. It can read and change every link in that workspace, so keep it on your server and never ship it to a browser or app. If a key leaks, revoke it in Settings → API keys and create a new one. A missing, wrong or revoked key gets a 401.

Rate limits

Limits are per workspace, per minute (see the table above). Requests from a workspace whose plan includes the API get these headers back:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59

Over the limit you get 429 with type rate_limited and a Retry-After header in seconds. Wait that long, then retry.

Errors

Errors use the application/problem+json format. Branch on type, not on the human-readable title or detail.

{
  "type": "validation_failed",
  "title": "Unprocessable Content",
  "status": 422,
  "detail": "The url field must be a valid URL.",
  "errors": { "url": ["The url field must be a valid URL."] }
}
401unauthenticatedThe key is missing, wrong or revoked.
402limit_exceededYour plan’s monthly link limit is used up. Upgrade or wait for the next period.
403plan_feature_requiredThe workspace’s plan doesn’t include the API. Upgrade to Pro or Business.
403workspace_suspendedThe workspace is suspended. Contact support.
404not_foundNo such link in this workspace.
405http_errorAny other HTTP error, such as a wrong method. Check status.
409idempotency_in_progressA request with this Idempotency-Key is still running. Retry shortly.
409qr_unavailableThis link was flagged or disabled for abuse, so it has no QR code.
422validation_failedThe input is invalid; see errors for each field.
422idempotency_key_reusedThis Idempotency-Key was already used with a different body.
429rate_limitedToo many requests this minute. Wait for Retry-After seconds.
500server_errorSomething broke on our side. Safe to retry with the same Idempotency-Key.

Pagination

GET /links returns one page, newest first, plus a next_cursor. Pass it back as ?cursor= to get the next page. When next_cursor is null, you have them all.

curl -X GET 'https://api.toifel.io/v1/links?cursor=eyJpZCI6MTIzNCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ' \
  -H "Authorization: Bearer $API_KEY"

Idempotency

Networks fail. To retry POST /links safely, send an Idempotency-Key header (any unique string up to 255 characters, such as a UUID). If the same key comes back with the same body within 24 hours, you get the first response again, marked Idempotent-Replayed: true, and no second link is created. The same key with a different body gets 422 idempotency_key_reused.

curl -X POST 'https://api.toifel.io/v1/links' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: 3f1c2a9e-7b6d-4c1e-9a8f-2d5e6b7c8d9e' \
  -d '{
  "url": "https://example.com/blog/launch?utm_source=newsletter",
  "title": "Launch post",
  "tags": [
    "newsletter"
  ]
}'

Endpoints

GET/me

The key's workspace, plan and current usage

Errors: 401, 403, 429

curl -X GET 'https://api.toifel.io/v1/me' \
  -H "Authorization: Bearer $API_KEY"

200 OK

{
  "data": {
    "workspace": {
      "id": 42,
      "name": "Acme"
    },
    "plan": "Pro",
    "links": {
      "used": 120,
      "limit": 2000
    },
    "clicks": {
      "tracked": 5310,
      "untracked": 0,
      "limit": 100000
    },
    "seats": {
      "used": 3,
      "limit": 5
    }
  }
}

POST/links

Create a link

Parameters

Idempotency-Keyheader · string
Repeating a key with the same body within 24 hours replays the first response (with an Idempotent-Replayed: true header) instead of creating another link.

Body (JSON)

urlstring (uri)Required
An http or https URL.
ios_urlstring | null (uri)
Paid plans. An https URL for iPhone and iPad visitors, such as an App Store page or a universal link.
android_urlstring | null (uri)
Paid plans. An https URL for Android visitors, such as a Play Store page or an app link.
aliasstring | null
Your own code instead of a random 7-character one: letters, digits, - and _. Codes are unique across the whole service, so a taken or reserved alias gets 422.
titlestring | null
tagsstring[] | null
expires_atstring | null (date-time)
Must be in the future. After it, the link stops redirecting.

Errors: 401, 402, 403, 409, 422, 429

curl -X POST 'https://api.toifel.io/v1/links' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/blog/launch?utm_source=newsletter",
  "title": "Launch post",
  "tags": [
    "newsletter"
  ]
}'

201 Created

{
  "data": {
    "id": 1234,
    "code": "k3Xp9Qa",
    "short_url": "https://go.example.com/k3Xp9Qa",
    "target_url": "https://example.com/blog/launch?utm_source=newsletter",
    "ios_url": null,
    "android_url": null,
    "title": "Launch post",
    "is_custom": false,
    "expires_at": null,
    "tags": [
      "newsletter"
    ],
    "clicks_count": 87,
    "created_at": "2026-11-03T10:15:00+00:00"
  }
}