Admin API
Shortlinker provides a full-featured HTTP Admin API for managing short links, including CRUD, batch operations, CSV import/export, and runtime config management.
Configuration
Admin API settings can come from config.toml, environment variables, or runtime config (database). See Configuration.
ADMIN_TOKEN: admin login password (recommended to set explicitly in production; if not set, the server will auto-generate one and write it once toadmin_token.txt(save it and delete the file))ADMIN_ROUTE_PREFIX: route prefix (optional, default:/admin)
All API paths include
/v1, e.g. the default login endpoint ishttp://localhost:8080/admin/v1/auth/login.
Authentication (Important)
The current implementation does not use Authorization: Bearer ... headers.
Admin API uses JWT Cookies:
- Access Token Cookie: default name
shortlinker_access,Path=/ - Refresh Token Cookie: default name
shortlinker_refresh,Path={ADMIN_ROUTE_PREFIX}/v1/auth
Cookie names, TTL, SameSite/Secure/Domain are configurable via api.* (see Configuration).
1) Login to get cookies
POST /{ADMIN_ROUTE_PREFIX}/v1/auth/login
Body:
{ "password": "your_admin_token" }Example (save cookies to cookies.txt):
curl -sS -X POST \
-H "Content-Type: application/json" \
-c cookies.txt \
-d '{"password":"your_admin_token"}' \
http://localhost:8080/admin/v1/auth/loginTokens are returned via
Set-Cookie. The response body does not include raw token strings.
2) Call other endpoints with cookies
curl -sS -b cookies.txt \
http://localhost:8080/admin/v1/links3) Refresh tokens
POST /{ADMIN_ROUTE_PREFIX}/v1/auth/refresh
curl -sS -X POST \
-b cookies.txt -c cookies.txt \
http://localhost:8080/admin/v1/auth/refresh4) Logout (clear cookies)
POST /{ADMIN_ROUTE_PREFIX}/v1/auth/logout
curl -sS -X POST -b cookies.txt -c cookies.txt \
http://localhost:8080/admin/v1/auth/logoutBase URL
Default: http://your-domain:port/admin/v1
If you changed
ADMIN_ROUTE_PREFIX, replace/adminwith your prefix.
Common JSON format
Most endpoints return:
{
"code": 0,
"data": { /* payload */ }
}code = 0: successcode = 1: error (details indata.error)- HTTP status code indicates error category (
401/404/409/500, etc.)
Link management
GET /links - List short links (pagination + filters)
curl -sS -b cookies.txt \
"http://localhost:8080/admin/v1/links?page=1&page_size=20"Query params:
| Param | Type | Description | Example |
|---|---|---|---|
page | Integer | page index (starts from 1) | ?page=1 |
page_size | Integer | page size (1-100) | ?page_size=20 |
search | String | fuzzy search on code + target | ?search=github |
created_after | RFC3339 | created_at >= | ?created_after=2024-01-01T00:00:00Z |
created_before | RFC3339 | created_at <= | ?created_before=2024-12-31T23:59:59Z |
only_expired | Boolean | only expired links | ?only_expired=true |
only_active | Boolean | only active (not expired) | ?only_active=true |
Response:
{
"code": 0,
"data": [
{
"code": "github",
"target": "https://github.com",
"created_at": "2024-12-15T14:30:22Z",
"expires_at": null,
"password": null,
"click_count": 42
}
],
"pagination": {
"page": 1,
"page_size": 20,
"total": 42,
"total_pages": 3
}
}POST /links - Create a short link
curl -sS -X POST \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{"code":"github","target":"https://github.com"}' \
http://localhost:8080/admin/v1/linksBody:
{
"code": "github",
"target": "https://github.com",
"expires_at": "2024-12-31T23:59:59Z",
"password": "secret123",
"force": false
}Notes:
codeoptional (auto-generated if omitted)targetrequiredexpires_atoptional (relative like"7d"or RFC3339)forceoptional (defaultfalse); whencodeexists andforce=false, returns409 Conflictpasswordexperimental- Admin API hashes plaintext passwords using Argon2 (if input already starts with
$argon2..., it will be stored as-is) - Redirect does not validate password in current version (stored only)
- Admin API hashes plaintext passwords using Argon2 (if input already starts with
GET /links/{code} - Get a link
curl -sS -b cookies.txt \
http://localhost:8080/admin/v1/links/githubPUT /links/{code} - Update a link
curl -sS -X PUT \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{"target":"https://github.com/new-repo","expires_at":"30d"}' \
http://localhost:8080/admin/v1/links/githubBody:
{
"target": "https://new-url.com",
"expires_at": "7d",
"password": ""
}Notes:
targetis requiredexpires_atomitted => keep existing valuepassword- omitted => keep existing
- empty string
""=> remove password - plaintext => hash with Argon2
$argon2...=> store as-is
DELETE /links/{code} - Delete a link
curl -sS -X DELETE -b cookies.txt \
http://localhost:8080/admin/v1/links/githubGET /stats - Stats
curl -sS -b cookies.txt \
http://localhost:8080/admin/v1/statsBatch operations
POST /links/batch - Batch create
The request body is an object with
links, not a raw array.
curl -sS -X POST \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{"links":[{"code":"link1","target":"https://example1.com"},{"code":"link2","target":"https://example2.com"}]}' \
http://localhost:8080/admin/v1/links/batchPUT /links/batch - Batch update
The request body uses
updates, each item includescodeandpayload.
curl -sS -X PUT \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{"updates":[{"code":"link1","payload":{"target":"https://new-example1.com"}},{"code":"link2","payload":{"target":"https://new-example2.com"}}]}' \
http://localhost:8080/admin/v1/links/batchDELETE /links/batch - Batch delete
The request body uses
codes.
curl -sS -X DELETE \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{"codes":["link1","link2","link3"]}' \
http://localhost:8080/admin/v1/links/batchCSV export/import
GET /links/export - Export CSV
The exported CSV contains a header and these columns: code,target,created_at,expires_at,password,click_count
curl -sS -b cookies.txt \
-o shortlinks_export.csv \
"http://localhost:8080/admin/v1/links/export?only_active=true"POST /links/import - Import CSV
Multipart form fields:
file: CSV filemode(optional):skip(default) /overwrite/error
curl -sS -X POST \
-b cookies.txt -c cookies.txt \
-F "mode=overwrite" \
-F "file=@./shortlinks_export.csv" \
http://localhost:8080/admin/v1/links/importRuntime config management
Config endpoints are under /{ADMIN_ROUTE_PREFIX}/v1/config. Sensitive values are masked as [REDACTED].
GET /config
curl -sS -b cookies.txt \
http://localhost:8080/admin/v1/configGET /config/schema
Returns schema metadata for all config keys (type, default value, whether restart is required, enum options, etc.). Mainly used by the admin panel to render/validate config forms.
curl -sS -b cookies.txt \
http://localhost:8080/admin/v1/config/schemaGET /config/
curl -sS -b cookies.txt \
http://localhost:8080/admin/v1/config/features.random_code_lengthPUT /config/
curl -sS -X PUT \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{"value":"8"}' \
http://localhost:8080/admin/v1/config/features.random_code_lengthGET /config/{key}/history
curl -sS -b cookies.txt \
"http://localhost:8080/admin/v1/config/features.random_code_length/history?limit=10"POST /config/reload
curl -sS -X POST -b cookies.txt \
http://localhost:8080/admin/v1/config/reloadAuth endpoints notes
POST /auth/login: no cookies required; validatesADMIN_TOKENand sets cookiesPOST /auth/refresh: no access cookie required, but refresh cookie is requiredPOST /auth/logout: no cookies required; clears cookiesGET /auth/verify: requires access cookie
Python example (requests)
import requests
class ShortlinkerAdmin:
def __init__(self, base_url: str, admin_token: str):
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
resp = self.session.post(
f"{self.base_url}/admin/v1/auth/login",
json={"password": admin_token},
timeout=10,
)
resp.raise_for_status()
def list_links(self, page=1, page_size=20):
resp = self.session.get(
f"{self.base_url}/admin/v1/links",
params={"page": page, "page_size": page_size},
timeout=10,
)
resp.raise_for_status()
return resp.json()
admin = ShortlinkerAdmin("http://localhost:8080", "your_admin_token")
print(admin.list_links())Security notes
- Use a strong
ADMIN_TOKEN(do not rely on the auto-generated one in production) - Use HTTPS in production and set
api.cookie_secure=true - Expose Admin API only to trusted networks
- Rotate
ADMIN_TOKENregularly and re-login to get new cookies