API Documentation
A full REST API for links, QR codes, webhooks, UTM building, and analytics. Every endpoint below is authenticated with a per-account API key and rate-limited according to your plan.
Authentication
Every request must include your API key as a Bearer token in the Authorization header.
Authorization: Bearer YOUR_API_KEY
Generate and revoke keys from Dashboard → API Keys. A key inherits the rate limit and feature access of the plan its owner (or their team's billing owner) is on.
Rate Limits
Limits are enforced per minute, per API key, based on the key owner's plan. Every response includes:
| Header | Description |
|---|---|
X-RateLimit-Limit | Requests allowed per minute on the current plan. |
X-RateLimit-Remaining | Requests left in the current window. |
Exceeding the limit returns 429 Too Many Requests with a Retry-After header (seconds until the window resets).
Errors
| Status | Meaning |
|---|---|
401 | Missing or invalid API key. |
403 | Payment overdue, or the request needs a plan feature your plan doesn't include. |
404 | Resource not found, or it belongs to a different account (ownership is never leaked — a resource you don't own 404s, not 403s). |
422 | Validation failed — see the errors object in the response body. |
429 | Rate limit exceeded. |
Links
GET/api/v1/links
List your links, paginated (20 per page).
curl https://yourapp.test/api/v1/links \
-H "Authorization: Bearer YOUR_API_KEY"
POST/api/v1/links
Create a short link.
| Field | Type | Required | Notes |
|---|---|---|---|
url | string | Yes | Destination URL. |
custom_alias | string | No | Alphanumeric/dash/underscore, max 20 chars. Auto-generated if omitted. |
title | string | No | |
password | string | No | Requires a plan with password-protected links. |
is_one_time | boolean | No | Expires after the first click. |
max_clicks | integer | No | |
expires_at | date | No | ISO 8601. |
allowed_countries | array | No | ISO-2 country codes. |
allowed_devices | array | No | desktop, mobile, or tablet. |
curl -X POST https://yourapp.test/api/v1/links \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/product", "custom_alias": "promo"}'
{
"data": {
"id": 42,
"code": "promo",
"short_url": "https://yourapp.test/promo",
"destination_url": "https://example.com/product",
"title": null,
"is_one_time": false,
"is_used": false,
"is_active": true,
"max_clicks": null,
"click_count": 0,
"expires_at": null,
"password_protected": false,
"allowed_countries": null,
"allowed_devices": null,
"created_at": "2026-01-01T12:00:00+00:00",
"updated_at": "2026-01-01T12:00:00+00:00"
}
}
GET/api/v1/links/{id}
Fetch a single link you own.
PUT/api/v1/links/{id}
Update url, title, is_active, max_clicks, or expires_at.
DELETE/api/v1/links/{id}
Delete a link. Returns 204 No Content.
QR Codes
GET/api/v1/qr
List your QR codes, paginated.
POST/api/v1/qr
| Field | Type | Required | Notes |
|---|---|---|---|
type | string | Yes | dynamic_link, wifi, vcard, or text_url. |
link_id | integer | If type=dynamic_link | Must be a link you own. |
name | string | No | |
payload | object | Depends on type | Type-specific fields (e.g. text for text_url). |
foreground_color / background_color | string | No | Hex, e.g. #000000. |
size | integer | No | 100–2000px, default 400. |
curl -X POST https://yourapp.test/api/v1/qr \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type": "dynamic_link", "link_id": 42, "name": "Storefront QR"}'
{
"data": {
"id": 7,
"type": "dynamic_link",
"name": "Storefront QR",
"link_id": 42,
"short_url": "https://yourapp.test/promo",
"foreground_color": "#000000",
"background_color": "#FFFFFF",
"size": 400,
"margin": 4,
"downloads": {
"png": "https://yourapp.test/storage/qrcodes/7.png",
"svg": "https://yourapp.test/storage/qrcodes/7.svg",
"pdf": "https://yourapp.test/storage/qrcodes/7.pdf"
},
"created_at": "2026-01-01T12:00:00+00:00",
"updated_at": "2026-01-01T12:00:00+00:00"
}
}
GET/api/v1/qr/{id}
PUT/api/v1/qr/{id}
DELETE/api/v1/qr/{id}
POST/api/v1/bulk/qr
Upload a CSV (name,url columns) to create many links + dynamic QR codes at once. Processed asynchronously.
curl -X POST https://yourapp.test/api/v1/bulk/qr \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "csv=@products.csv"
{ "message": "CSV accepted, processing in the background." }
Webhooks
GET/api/v1/webhooks
POST/api/v1/webhooks
| Field | Type | Required | Notes |
|---|---|---|---|
url | string | Yes | Endpoint you control. |
subscribed_events | array | Yes | One or more of: link.clicked, link.expired, link.limit_reached, qr.scanned, bio_page.viewed. |
curl -X POST https://yourapp.test/api/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://yourapp.com/hooks/inbound", "subscribed_events": ["link.clicked"]}'
{
"data": {
"id": 3,
"url": "https://yourapp.com/hooks/inbound",
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"subscribed_events": ["link.clicked"],
"is_active": true,
"last_fired_at": null,
"last_status_code": null,
"last_error_message": null,
"created_at": "2026-01-01T12:00:00+00:00"
}
}
Every delivery is signed — verify it with HMAC-SHA256 over the raw request body using the webhook's secret, sent in the X-Webhook-Signature header.
GET/api/v1/webhooks/{id}
PUT/api/v1/webhooks/{id}
DELETE/api/v1/webhooks/{id}
UTM Builder
POST/api/v1/utm/build
Stateless helper — appends UTM parameters to a URL without creating any resource.
curl -X POST https://yourapp.test/api/v1/utm/build \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "utm_source": "newsletter", "utm_medium": "email", "utm_campaign": "spring_sale"}'
{
"built_url": "https://example.com?utm_source=newsletter&utm_medium=email&utm_campaign=spring_sale"
}
Analytics
All analytics endpoints are scoped to a link you own.
GET/api/v1/analytics/links/{id}/summary
{
"data": {
"total_clicks": 1280,
"unique_clicks": 940,
"max_clicks": null,
"is_active": true,
"last_clicked_at": "2026-01-05T08:12:00+00:00"
}
}
GET/api/v1/analytics/links/{id}/timeseries?days=30
Daily click counts for the last days (default 30).
GET/api/v1/analytics/links/{id}/breakdown?by=country
by is one of country (default), device, browser, or referrer.
GET/api/v1/analytics/links/{id}/campaigns
Click counts grouped by utm_campaign.
GET/api/v1/analytics/links/{id}/variants
Click counts per A/B test variant, alongside each variant's configured weight.