API docs
The dashboard is a client of this same REST API — every endpoint below is what it calls.
Authentication
Programmatic calls authenticate with an X-API-Key header carrying a key shaped like sk_live_…. The browser dashboard authenticates the same routes with a session cookie instead (set at /api/auth/login or /signup) — either one works on every route below.
Every /api/* route lives on snugl.space, the same host your shortened links resolve on — which is why the request below and the short_url it returns name the same domain.
curl -X POST https://snugl.space/api/links \
-H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{"target": "https://example.com/a-very-long-url"}'
# -> {"short_url":"https://snugl.space/x7Kbq2","code":"x7Kbq2", ...}Error codes
Every error response, on every route, shares one shape:
{
"error": "human-readable message"
}| Status | Name | Meaning |
|---|---|---|
| 400 | Bad Request | Malformed body, or an invalid target/alias/reset token |
| 401 | Unauthorized | Missing or invalid `X-API-Key`, or no session cookie |
| 402 | Payment Required | A plan limit was hit (active-link cap, Growth/Premium-only feature) |
| 403 | Forbidden | Authenticated, but not allowed to perform this action |
| 404 | Not Found | No resource at that code/id |
| 409 | Conflict | Alias/code already taken, a reserved code, or email already registered |
| 429 | Too Many Requests | The public `GET /{code}` redirect rate limit was exceeded |
| 500 | Internal Server Error | Unexpected server or database failure |
| 502 | Bad Gateway | The upstream billing provider returned an error |
| 503 | Service Unavailable | An optional integration (billing, OAuth, email) is not configured on this deployment |
Endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/health | public | Liveness probe |
| POST | /api/links | key or session | Create a link |
| GET | /api/links | key or session | List your links, newest first |
| GET | /api/links/{code} | key or session | Fetch one of your links |
| PATCH | /api/links/{code} | key or session | Update target, permanent, or is_active |
| DELETE | /api/links/{code} | key or session | Deactivate a link |
| GET | /api/links/{code}/stats | key or session | Daily click series + all-time total |
| GET | /api/billing/subscription | key or session | Current plan and effective limits |
| POST | /api/billing/checkout | key or session | Hosted checkout URL for a paid plan |
| POST | /api/billing/portal | key or session | Signed customer-portal URL |
| POST | /api/auth/signup | public | Email/password signup, sets a session cookie |
| POST | /api/auth/login | public | Email/password login, sets a session cookie |
| GET | /api/auth/oauth/{provider}/start | public | Redirects to Google or Microsoft |
| GET | /{code} | public | 302 by default, 301 when permanent; plain 404 otherwise |
Rate limits
GET /{code} — the public redirect route — is guarded per source IP by a token bucket: 10 requests/second sustained, with a burst of 20. Going over it answers 429 Too Many Requests. The authenticated /api/* surface is not rate limited by the API itself — it's low-volume by design.
Account limits
| Plan | Active links | Analytics retention | Custom aliases | Private codes |
|---|---|---|---|---|
| Free | 10 | 30 days | No | No |
| Growth / Premium | Unlimited | 365 days | Yes | Yes |
A create that would exceed the active-link limit answers 402 Payment Required, naming the limit. Deactivating a link frees the slot back up.