Cloudshort
Admin API
The HTTP API the dashboard uses, for scripts that manage links.
The dashboard talks to this API at https://<admin domain>/api/. You can use it from scripts too. Every request and response body is JSON.
Logging in
The API uses a session cookie named auth_token. Log in once and send the cookie with every other request:
curl -c cookies.txt -H 'Content-Type: application/json' \
-d '{"password":"your admin password"}' https://admin.example.com/api/login
curl -b cookies.txt https://admin.example.com/api/links
The session lasts 24 hours. Without a valid cookie, every endpoint except the three login endpoints returns 401 with { "error": "Unauthorized" }. If JWT_SECRET is not set on the server, they return 500 instead.
Errors
Errors have one field: { "error": "<message>" }. A body that is not valid JSON returns 400 with Invalid request body. An unexpected problem returns 500 with Internal server error.
Login endpoints
POST /api/login
Body: { "password": "..." }
| Status | Meaning |
|---|---|
200 | { "success": true } and the session cookie is set |
400 | No password in the body |
401 | Wrong password |
429 | Too many wrong passwords from this IP. The Retry-After header says how many seconds to wait. |
500 | ADMIN_PASSWORD or JWT_SECRET is not set |
POST /api/logout
Clears the session cookie. Returns { "success": true }.
GET /api/auth/check
Returns { "authenticated": true } or { "authenticated": false }. Never returns 401.
Links
A link looks like this:
{ "id": 4, "slug": "launch", "long_url": "https://example.com/events/launch-week", "created_at": 1790682903663, "clicks": 55 }
created_at is in milliseconds since 1970 (UTC).
GET /api/links
Lists every link. Query options: sort is created_at (default) or clicks, and order is desc (default) or asc. Other values fall back to the defaults.
POST /api/links
Body: { "slug": "launch", "long_url": "https://example.com/events/launch-week" }
| Status | Meaning |
|---|---|
201 | Created. { "success": true } |
400 | The slug or URL breaks the link rules |
409 | Slug already taken |
PATCH /api/links/:slug
Changes the destination. Body: { "long_url": "https://example.com/new" }. Returns 200, 400 for a bad URL, or 404 if the slug does not exist.
DELETE /api/links/:id
Deletes the link with this id and all of its clicks. Returns 200, or 404 if the id does not exist.
Settings
GET /api/settings
Returns { "root_url": "", "not_found_url": "", "short_domain": "" }. An empty string means the setting is off.
POST /api/settings
Send any of the three fields. Fields you leave out are not changed, and an empty string turns a setting off. Every value must be an http:// or https:// URL, otherwise the request returns 400 and nothing is saved. A trailing / on short_domain is removed.
Analytics
GET /api/analytics/:slug
Returns all-time stats for one link, or 404 if it does not exist:
{
"slug": "launch",
"long_url": "https://example.com/events/launch-week",
"stats": {
"clicks_over_time": [{ "date": "2026-09-29", "count": 14 }],
"country_breakdown": [{ "country": "ID", "count": 30 }],
"referrer_breakdown": [{ "referrer": "Direct", "count": 25 }]
}
}
Days are in UTC. The country and referrer lists hold the top 10.