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
A machine-readable OpenAPI 3.1 specification
is available at https://shrtr.top/openapi.json — import it into
Postman, Swagger UI, or your code generator of choice.
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-09-12T05:40:29+00:00"
}
Rate limits
POST /api/v1/shorten carries rate-limit headers for its per-minute budget so you can back off proactively. Every 429 carries Retry-After, which is the authoritative signal to retry from — and the one you must actually honour, see below.
| Endpoint | Limit | Window |
|---|---|---|
POST /api/v1/shorten | 30 requests | per minute per IP |
POST /api/v1/shorten | 25 requests | per 24 hours per IP |
POST /api/v1/shorten with alias | 10 requests | per hour per IP |
GET /api/v1/stats/{code} | 120 requests | per minute per IP |
GET /api/v1/health | unlimited | — |
The daily ceiling counts requests, not successful links — a rejected destination or a taken alias still spends one. It is independent of the web form's budget, so a scripted integration and a browser sharing one address never compete. Need more than that a day? See the plans — that is what accounts are for.
Response headers
X-RateLimit-Limit: 30 # the per-minute budget, and only that one
X-RateLimit-Remaining: 29
X-RateLimit-Reset: 1776935000
Retry-After: 47 # only on 429
The X-RateLimit-* trio always describes the per-minute
30/min budget. A 429 raised by a different bucket — the daily ceiling, the
alias cap, or the per-destination limiter — carries Retry-After alone
(changed 2026-08-04; those three previously echoed the per-minute numbers, which
describe a budget that is not what refused). Drive retries from
Retry-After, with one caveat worth coding for: on the per-destination
limiter it can be many hours, and that bucket is keyed on the destination domain, so
a different destination is very likely accepted immediately. Read the
detail field to tell the cases apart.
On the per-minute and per-hour buckets, IPv6 callers are grouped per /64 rather than per /128, matching the web form's policy. The daily ceiling keys the full address.
Honouring Retry-After is not merely good manners. Once the daily ceiling
is reached every further attempt answers 429, and a client that keeps retrying through
them — even at a modest fixed interval of a few seconds — is eventually blocked at the
network layer, which returns no HTTP response at all. So an integration that suddenly
sees connection failures rather than 429s has usually been retrying too hard. Wait out
the interval the header gives you and this never applies.
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.
Frequently asked questions
How is the Shrtr API versioned?
The /api/v1/ prefix is stable. Breaking changes will ship under /api/v2/. Additive changes — new optional fields, new endpoints — may land within v1 at any time, so parse JSON leniently.
What HTTP status codes does the API return?
201 on a created short link, 200 on stats and health, 400 for malformed input, 404 for unknown codes, 409 for taken aliases, 422 for validation failures, 429 when rate-limited, and 503 on rare short-code collisions.
Can I call the API directly from a browser?
Yes. Access-Control-Allow-Origin is open on /api/v1/* and OPTIONS preflight is answered with HTTP 204 and a one-day Max-Age. The endpoints are anonymous, so no cookies or credentials are involved.