API Reference
Base URL: https://open.utm.bar/v1
Authentication: Authorization: Bearer sk_live_xxxx or X-API-Key: sk_live_xxxx
Short Links
Create a Short Link
POST /v1/links
Scopes required: links:write
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
original_url | string | ✅ | The destination URL |
title | string | — | Display name for the link |
custom_code | string | — | Custom short code (a-z A-Z 0-9 - _, 2–64 chars) |
external_id | string | — | Your internal reference ID, stored as-is (max 255 chars) |
expires_at | ISO 8601 | — | Expiry datetime (null = never) |
utm_source | string | — | UTM source parameter |
utm_medium | string | — | UTM medium parameter |
utm_campaign | string | — | UTM campaign parameter |
utm_term | string | — | UTM term parameter |
utm_content | string | — | UTM content parameter |
curl -X POST https://open.utm.bar/v1/links \
-H "Authorization: Bearer sk_live_xxxx" \
-H "Content-Type: application/json" \
-d '{
"original_url": "https://example.com/page",
"title": "My Link",
"custom_code": "launch-2026",
"utm_source": "twitter",
"utm_medium": "social"
}'
Response 201 Created
{
"data": {
"id": "...",
"code": "launch-2026",
"short_url": "https://utm.bar/launch-2026",
"original_url": "https://example.com/page",
"title": "My Link",
"is_active": true,
"click_count": 0,
"external_id": "order_98765",
"expires_at": null,
"utm_source": "twitter",
"utm_medium": "social",
"created_at": "2026-05-25T00:00:00Z"
}
}
custom_codeconflicts return409 Conflict.
List Short Links
GET /v1/links
Scopes required: links:read
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
page_size | integer | 20 | Items per page (max 100) |
q | string | — | Search by title or original URL |
is_active | boolean | — | Filter by active/disabled status |
curl "https://open.utm.bar/v1/links?page=1&page_size=20" \
-H "Authorization: Bearer sk_live_xxxx"
Response 200 OK
{
"data": {
"items": [ /* array of link objects */ ],
"total": 42,
"page": 1,
"page_size": 20
}
}
Get a Single Link
GET /v1/links/:code
Scopes required: links:read
curl https://open.utm.bar/v1/links/launch-2026 \
-H "Authorization: Bearer sk_live_xxxx"
Response 200 OK — returns a single link object.
Get a Link by External ID
GET /v1/links/external/:external_id
Scopes required: links:read
Look up a short link by the external_id you provided at creation time. Returns 404 if no match is found.
curl https://open.utm.bar/v1/links/external/order_98765 \
-H "Authorization: Bearer sk_live_xxxx"
Response 200 OK
{
"data": {
"id": "...",
"code": "launch-2026",
"short_url": "https://utm.bar/launch-2026",
"original_url": "https://example.com/page",
"external_id": "order_98765",
"is_active": true,
"click_count": 42,
"created_at": "2026-05-25T00:00:00Z"
}
}
Update a Link
PATCH /v1/links/:code
Scopes required: links:write
original_urlandcodecannot be changed after creation.
Request Body (all fields optional)
| Field | Type | Description |
|---|---|---|
title | string | New display name |
expires_at | ISO 8601 / null | Update or clear expiry |
utm_source | string | Update UTM source |
utm_medium | string | Update UTM medium |
utm_campaign | string | Update UTM campaign |
utm_term | string | Update UTM term |
utm_content | string | Update UTM content |
curl -X PATCH https://open.utm.bar/v1/links/launch-2026 \
-H "Authorization: Bearer sk_live_xxxx" \
-H "Content-Type: application/json" \
-d '{ "title": "Updated Title", "expires_at": "2026-12-31T23:59:59Z" }'
Response 200 OK — returns the updated link object.
Disable a Link
POST /v1/links/:code/disable
Scopes required: links:delete
curl -X POST https://open.utm.bar/v1/links/launch-2026/disable \
-H "Authorization: Bearer sk_live_xxxx"
Response 200 OK
Enable a Link
POST /v1/links/:code/enable
Scopes required: links:write
curl -X POST https://open.utm.bar/v1/links/launch-2026/enable \
-H "Authorization: Bearer sk_live_xxxx"
Response 200 OK
Delete a Link
DELETE /v1/links/:code
Scopes required: links:delete
Deletion is permanent. Consider disabling instead if you may need the link later.
curl -X DELETE https://open.utm.bar/v1/links/launch-2026 \
-H "Authorization: Bearer sk_live_xxxx"
Response 204 No Content
Get Link Statistics
GET /v1/links/:code/stats
Scopes required: links:read
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
start | YYYY-MM-DD | Last 7 days | Start date (inclusive) |
end | YYYY-MM-DD | Today | End date (inclusive) |
tz | IANA timezone | UTC | Timezone for day boundaries |
curl "https://open.utm.bar/v1/links/launch-2026/stats?start=2026-05-01&end=2026-05-25&tz=Asia/Shanghai" \
-H "Authorization: Bearer sk_live_xxxx"
Response 200 OK
{
"data": {
"total_clicks": 1024,
"unique_clicks": 876,
"tz": "Asia/Shanghai",
"daily": [
{ "date": "2026-05-01", "clicks": 40, "unique_clicks": 35 },
{ "date": "2026-05-02", "clicks": 55, "unique_clicks": 48 }
]
}
}
Rate Limits
Limits apply per team across all API keys.
| Request type | Limit |
|---|---|
| Write (create / update / disable / delete) | 60 / minute |
| Read (list / get / stats) | 300 / minute |
When a limit is exceeded, the API returns 429 Too Many Requests with these headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1748140860
Retry-After: 30
Permission Scopes
| Scope | Grants |
|---|---|
links:read | List links, get single link, get statistics |
links:write | Create links, update metadata, enable links |
links:delete | Disable and delete links |
Error Responses
All errors use the same structure:
{
"error": "error_code",
"message": "Human-readable description",
"request_id": "req_abc123"
}
| Status | error | Meaning |
|---|---|---|
400 | invalid_request | Validation failed |
401 | unauthorized | Missing or invalid API key |
403 | forbidden | Key lacks required scope |
404 | not_found | Resource does not exist |
409 | conflict | Custom code already in use |
429 | rate_limited | Too many requests |
500 | internal_error | Server error |