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/v1The API is available on paid plans. Each plan allows a number of requests per minute, shared by all keys in the workspace:
| Plan | API access | Requests / min |
|---|---|---|
| Free | No | — |
| Pro | Yes | 60 |
| Business | Yes | 600 |
Not a developer? On a paid plan you can import links from a CSV file in the dashboard, no code needed.
Quickstart
- Create a key in Settings → API keys. Workspace owners and admins can do this. Copy the key now: it is shown only once.
- Store it in an environment variable called
API_KEY, for exampleexport API_KEY=sk_live_…in your shell. Every example on this page reads it from there. - 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."] }
}| Status | Type | Meaning |
|---|---|---|
| 401 | unauthenticated | The key is missing, wrong or revoked. |
| 402 | limit_exceeded | Your plan’s monthly link limit is used up. Upgrade or wait for the next period. |
| 403 | plan_feature_required | The workspace’s plan doesn’t include the API. Upgrade to Pro or Business. |
| 403 | workspace_suspended | The workspace is suspended. Contact support. |
| 404 | not_found | No such link in this workspace. |
| 405 | http_error | Any other HTTP error, such as a wrong method. Check status. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still running. Retry shortly. |
| 409 | qr_unavailable | This link was flagged or disabled for abuse, so it has no QR code. |
| 422 | validation_failed | The input is invalid; see errors for each field. |
| 422 | idempotency_key_reused | This Idempotency-Key was already used with a different body. |
| 429 | rate_limited | Too many requests this minute. Wait for Retry-After seconds. |
| 500 | server_error | Something 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
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
}
}
}GET/links
List links, newest first, 25 per page
Parameters
qquery · string- Matches title, code or target URL.
tagquery · string- Only links with this tag.
created_afterquery · string- Only links created after this date or time (UTC). A bare date such as 2026-11-03 means midnight at the start of that day, so links from that day are included.
cursorquery · string- The previous page's next_cursor. Send q, tag and created_after again with it.
curl -X GET 'https://api.toifel.io/v1/links' \
-H "Authorization: Bearer $API_KEY"200 OK
{
"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"
}
],
"next_cursor": "eyJpZCI6MTIzNCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ"
}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 | nulltagsstring[] | nullexpires_atstring | null (date-time)- Must be in the future. After it, the link stops redirecting.
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"
}
}GET/links/{id}
Show a link
curl -X GET 'https://api.toifel.io/v1/links/1234' \
-H "Authorization: Bearer $API_KEY"200 OK
{
"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"
}
}DELETE/links/{id}
Delete a link. Its code is never reused.
PATCH/links/{id}
Update a link. Send only the fields to change; the code can't be changed.
Parameters
idpath · integerRequired
Body (JSON)
urlstring (uri)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.
titlestring | nulltagsstring[] | nullexpires_atstring | null (date-time)
curl -X PATCH 'https://api.toifel.io/v1/links/1234' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"title": "Launch post (updated)",
"tags": [
"newsletter",
"q4"
]
}'200 OK
{
"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 (updated)",
"is_custom": false,
"expires_at": null,
"tags": [
"newsletter",
"q4"
],
"clicks_count": 87,
"created_at": "2026-11-03T10:15:00+00:00"
}
}GET/links/{id}/stats
Daily click stats for a link
Parameters
idpath · integerRequiredfromquery · string (date)- Defaults to 30 days before to.
toquery · string (date)- Defaults to today (UTC). The range can be up to 366 days.
intervalquery · string (day)
curl -X GET 'https://api.toifel.io/v1/links/1234/stats' \
-H "Authorization: Bearer $API_KEY"200 OK
{
"data": {
"from": "2026-10-28",
"to": "2026-11-03",
"interval": "day",
"totals": {
"clicks": 87,
"unique_visitors": 64,
"previews": 12
},
"series": [
{
"date": "2026-11-02",
"clicks": 31,
"unique_visitors": 25
},
{
"date": "2026-11-03",
"clicks": 56,
"unique_visitors": 39
}
],
"referrers": {
"t.co": 40,
"google.com": 22,
"other": 25
},
"countries": {
"IN": 51,
"US": 20,
"other": 16
},
"devices": {
"mobile": 60,
"desktop": 27
},
"browsers": {
"Chrome": 58,
"Safari": 21,
"other": 8
},
"sources": {
"link": 61,
"qr": 26
},
"in_apps": {
"browser": 52,
"instagram": 30,
"facebook": 5
},
"languages": {
"en": 60,
"hi": 22,
"unknown": 5
},
"preview_apps": {
"whatsapp": 10,
"telegram": 2
},
"hours": {
"1-9": 14,
"6-20": 31
},
"last_24h": [
{
"hour": "2026-11-03T09:00:00+00:00",
"clicks": 12
}
]
}
}GET/links/{id}/qr
QR code for a link's short URL
Parameters
idpath · integerRequiredformatquery · string (svg, png)
curl -X GET 'https://api.toifel.io/v1/links/1234/qr' \
-H "Authorization: Bearer $API_KEY" \
-o qr.svg200 The QR code image
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">…</svg>