Shrtr API reference
A small JSON API for programmatic URL shortening. No signup, no API key, permissive CORS. Rate-limited per IP. Errors follow RFC 7807.
Base URL
https://shrtr.top/api/v1
Endpoints
/api/v1/shorten
Create a short link. Body: {"url": "…", "alias": "optional"}.
Returns 201 with the created link representation.
/api/v1/stats/{code}
Fetch aggregate stats for a short link (clicks_count, last_clicked_at, created_at, enabled). Never returns per-click data.
/api/v1/health
Uptime probe. Returns {"status": "ok"}. Polling-friendly
with Cache-Control: no-store.
Shorten a URL
Send a JSON POST with Content-Type: application/json. The alias field is optional.
curl
curl -sS -X POST 'https://shrtr.top/api/v1/shorten' \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com/some/long/path"}'
JavaScript (fetch)
const res = await fetch('https://shrtr.top/api/v1/shorten', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({url: 'https://example.com/some/long/path'}),
});
const data = await res.json();
console.log(data.short_url);
Python (requests)
import requests
res = requests.post(
'https://shrtr.top/api/v1/shorten',
json={'url': 'https://example.com/some/long/path'},
timeout=5,
)
res.raise_for_status()
print(res.json()['short_url'])
Go (net/http)
body := strings.NewReader(`{"url":"https://example.com/some/long/path"}`)
req, _ := http.NewRequest("POST", "https://shrtr.top/api/v1/shorten", body)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
Success response (201)
{
"code": "aBc1234",
"short_url": "https://shrtr.top/s/aBc1234",
"original_url": "https://example.com/some/long/path",
"created_at": "2026-04-23T12:03:18+00:00"
}
Rate limits
Every response (2xx and 4xx) carries rate-limit headers so you can back off proactively.
| Endpoint | Limit | Window |
|---|---|---|
POST /api/v1/shorten | 30 requests | per minute per IP |
POST /api/v1/shorten with alias | 10 requests | per hour per IP |
GET /api/v1/stats/{code} | unlimited | — |
GET /api/v1/health | unlimited | — |
Response headers
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 29
X-RateLimit-Reset: 1776935000
Retry-After: 47 # only on 429
IPv6 callers are bucketed per /64, not per /128, matching the web form's policy.
Errors (RFC 7807 problem+json)
Every non-2xx response has Content-Type: application/problem+json and the following shape:
{
"type": "about:blank",
"title": "Unprocessable Entity",
"status": 422,
"detail": "The URL scheme must be http or https.",
"errors": {"url": ["This is not a valid URL."]}
}
| Status | When |
|---|---|
400 | malformed JSON, missing Content-Type: application/json, oversized body |
404 | GET /api/v1/stats/{code} for a code that does not exist |
409 | alias already taken |
422 | validation failure (invalid URL, alias shape, reserved word) |
429 | rate limit exceeded — respect Retry-After |
503 | rare short-code collision; retry once with a different body |
CORS
Access-Control-Allow-Origin: * on every /api/v1/* response. The API is anonymous and there are no cookies, so this is safe. You can call the endpoint directly from a browser without a proxy.
Preflight OPTIONS is handled without running the route handler and returns 204 with a Max-Age of one day.
Stability
The /api/v1/ prefix is stable. Breaking changes will ship under /api/v2/. Additive changes (new optional fields, new endpoints) may land at any time within v1 — parse JSON leniently.