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

FieldTypeRequiredDescription
original_urlstringThe destination URL
titlestringDisplay name for the link
custom_codestringCustom short code (a-z A-Z 0-9 - _, 2–64 chars)
external_idstringYour internal reference ID, stored as-is (max 255 chars)
expires_atISO 8601Expiry datetime (null = never)
utm_sourcestringUTM source parameter
utm_mediumstringUTM medium parameter
utm_campaignstringUTM campaign parameter
utm_termstringUTM term parameter
utm_contentstringUTM 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_code conflicts return 409 Conflict.


List Short Links

GET /v1/links

Scopes required: links:read

Query Parameters

ParameterTypeDefaultDescription
pageinteger1Page number
page_sizeinteger20Items per page (max 100)
qstringSearch by title or original URL
is_activebooleanFilter 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_url and code cannot be changed after creation.

Request Body (all fields optional)

FieldTypeDescription
titlestringNew display name
expires_atISO 8601 / nullUpdate or clear expiry
utm_sourcestringUpdate UTM source
utm_mediumstringUpdate UTM medium
utm_campaignstringUpdate UTM campaign
utm_termstringUpdate UTM term
utm_contentstringUpdate 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

ParameterTypeDefaultDescription
startYYYY-MM-DDLast 7 daysStart date (inclusive)
endYYYY-MM-DDTodayEnd date (inclusive)
tzIANA timezoneUTCTimezone 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 typeLimit
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

ScopeGrants
links:readList links, get single link, get statistics
links:writeCreate links, update metadata, enable links
links:deleteDisable and delete links

Error Responses

All errors use the same structure:

{
  "error": "error_code",
  "message": "Human-readable description",
  "request_id": "req_abc123"
}
StatuserrorMeaning
400invalid_requestValidation failed
401unauthorizedMissing or invalid API key
403forbiddenKey lacks required scope
404not_foundResource does not exist
409conflictCustom code already in use
429rate_limitedToo many requests
500internal_errorServer error