API reference
HTTP API.
Create links and read their stats from your own code: JSON over HTTPS with a personal token. Included in Free.
- Base URL
- https://api.dwnl.cc/v1
- Auth
- Bearer token
- Format
- JSON, camelCase
Basics
Authentication
Every request carries a personal token in the Authorization header. Create one on the Developers page of the app and pick its scopes; the secret is shown once, so store it as one. Revoke a token on the same page and it stops working at once.
Scopes
- links:read
- List and read links
- links:write
- Create, edit and delete links
- stats:read
- Read click stats
- traffic:read
- Read sources, offers, campaigns, conversions (Pro)
- traffic:write
- Change them (Pro)
Request
curl https://api.dwnl.cc/v1/me \ -H "Authorization: Bearer $DWNL_TOKEN"
Rate limits
Each token gets 600 requests a minute; the CLI counts as one token per signed-in machine. Every response says how many are left. Over the limit, the API answers 429 and says how long to wait.
Response headers
- RateLimit-Limit
- Requests allowed in the current minute
- RateLimit-Remaining
- Requests left in it
- RateLimit-Reset
- Seconds until it starts over
- Retry-After
- On a 429 only: seconds to wait
429 Too Many Requests
HTTP/2 429
RateLimit-Limit: 600
RateLimit-Remaining: 0
RateLimit-Reset: 12
Retry-After: 12
{
"code": "rate_limited",
"detail": "600 requests per minute per token. Retry after 12 s."
}Errors
A failed request returns a 4xx or 5xx status and a JSON body: a stable code to branch on and a sentence for people.
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | The body is not JSON, or a field has the wrong type |
| 401 | unauthorized | No token, or an expired or revoked one |
| 403 | insufficient_scope | The token lacks the scope this call needs |
| 404 | not_found | No link with that code in your account |
| 409 | code_taken | The back-half you asked for is in use |
| 422 | invalid_url | The URL is not http(s), has spaces or points to dwnl.cc |
| 429 | rate_limited | Over the rate limit |
404 Not Found
HTTP/2 404
{
"code": "not_found",
"detail": "No link with the code a8Kd3xQ."
}Links
POST/linksScope: links:write
Create a link
Shortens a URL. Leave out the scheme and https:// is added. Without code, a 7-character code is generated.
Body
- urlstring Required
- Where the link points, up to 2,048 characters
- codestring
- Your own back-half: 4 to 64 letters, digits, _ or -
- titlestring
- A name for the dashboard; never shown to visitors
Request
curl -X POST https://api.dwnl.cc/v1/links \
-H "Authorization: Bearer $DWNL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/blog/launch-notes","title":"Launch notes"}'201 Created
{
"code": "a8Kd3xQ",
"shortUrl": "https://dwnl.cc/a8Kd3xQ",
"url": "https://example.com/blog/launch-notes",
"title": "Launch notes",
"enabled": true,
"clicks": 0,
"createdAt": "2026-09-01T09:30:00Z",
"updatedAt": "2026-09-01T09:30:00Z"
}GET/linksScope: links:read
List links
Your links, newest first, a page at a time. Pass nextCursor back as cursor for the next page; it is null on the last one.
Query
- limitinteger
- 1 to 100, default 50
- cursorstring
- From the previous page
- enabledboolean
- Only links that are on, or off
Request
curl "https://api.dwnl.cc/v1/links?limit=2" \ -H "Authorization: Bearer $DWNL_TOKEN"
200 OK
{
"items": [
{
"code": "a8Kd3xQ",
"shortUrl": "https://dwnl.cc/a8Kd3xQ",
"url": "https://example.com/blog/launch-notes",
"title": "Launch notes",
"enabled": true,
"clicks": 11250,
"createdAt": "2026-09-01T09:30:00Z",
"updatedAt": "2026-09-01T09:30:00Z"
},
{
"code": "Qm4tZ2p",
"shortUrl": "https://dwnl.cc/Qm4tZ2p",
"url": "https://example.com/pricing",
"title": "Pricing",
"enabled": true,
"clicks": 3420,
"createdAt": "2026-08-14T16:02:00Z",
"updatedAt": "2026-09-20T11:45:00Z"
}
],
"nextCursor": "eyJpZCI6IlFtNHRaMnAifQ"
}GET/links/{code}Scope: links:read
Get a link
One link by its code, with the all-time click count.
Request
curl https://api.dwnl.cc/v1/links/a8Kd3xQ \ -H "Authorization: Bearer $DWNL_TOKEN"
200 OK
{
"code": "a8Kd3xQ",
"shortUrl": "https://dwnl.cc/a8Kd3xQ",
"url": "https://example.com/blog/launch-notes",
"title": "Launch notes",
"enabled": true,
"clicks": 11250,
"createdAt": "2026-09-01T09:30:00Z",
"updatedAt": "2026-09-01T09:30:00Z"
}PATCH/links/{code}Scope: links:write
Update a link
Changes only the fields you send. The short link and its click history stay the same.
Body
- urlstring
- A new destination
- titlestring
- A new name
- enabledboolean
- false switches the link off: visitors get a 404
Request
curl -X PATCH https://api.dwnl.cc/v1/links/a8Kd3xQ \
-H "Authorization: Bearer $DWNL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled":false}'200 OK
{
"code": "a8Kd3xQ",
"shortUrl": "https://dwnl.cc/a8Kd3xQ",
"url": "https://example.com/blog/launch-notes",
"title": "Launch notes",
"enabled": false,
"clicks": 11250,
"createdAt": "2026-09-01T09:30:00Z",
"updatedAt": "2026-10-15T08:12:00Z"
}DELETE/links/{code}Scope: links:write
Delete a link
Removes the link and its stats for good; the code is never handed out again. To stop a link but keep its history, switch it off with PATCH instead.
Request
curl -X DELETE https://api.dwnl.cc/v1/links/a8Kd3xQ \ -H "Authorization: Bearer $DWNL_TOKEN"
204 No Content No body
GET/links/{code}/statsScope: stats:read
Link stats
Clicks per day and the top referrers, countries, devices and UTM campaigns for a period. Known bots are left out.
Query
- rangestring
- 24h, 7d, 30d or 90d; default 30d
- tzstring
- IANA time zone for the daily buckets; default UTC
Request
curl "https://api.dwnl.cc/v1/links/a8Kd3xQ/stats?range=30d" \ -H "Authorization: Bearer $DWNL_TOKEN"
200 OK
{
"code": "a8Kd3xQ",
"range": "30d",
"from": "2026-09-15",
"to": "2026-10-14",
"clicks": 11250,
"uniqueVisitors": 7200,
"daily": [
{ "date": "2026-09-15", "clicks": 980 },
{ "date": "2026-09-16", "clicks": 820 }
],
"referrers": [
{ "host": "github.com", "clicks": 3100 },
{ "host": "news.ycombinator.com", "clicks": 2600 }
],
"countries": [
{ "country": "US", "clicks": 3400 },
{ "country": "DE", "clicks": 1900 }
],
"devices": [
{ "type": "desktop", "os": "macOS", "clicks": 3600 },
{ "type": "mobile", "os": "iOS", "clicks": 2100 }
],
"utm": [
{
"source": "newsletter",
"medium": "email",
"campaign": "launch",
"clicks": 2400
}
]
}Account
GET/me
Current account
Who the token belongs to, the account's plan and the token's scopes. Works with any valid token, so it is the quick way to check one.
Request
curl https://api.dwnl.cc/v1/me \ -H "Authorization: Bearer $DWNL_TOKEN"
200 OK
{
"id": "usr_8f3k2m",
"name": "Lena Hartmann",
"email": "lena@example.com",
"plan": "pro",
"links": 48,
"createdAt": "2026-03-02T10:00:00Z",
"token": {
"prefix": "dwnl_pat_7Kq2",
"scopes": ["links:read", "links:write"]
}
}Command line
The dwnl CLI
dwnl is a single binary for macOS and Linux that calls this API. It signs in with a device code, so there is no token to paste, and prints only the short link, so its output goes straight into other commands.
- dwnl login
- Sign in: confirm the code it prints in the browser
- dwnl <url>
- Shorten a URL and print the short link
- dwnl ls
- Your links, newest first; --limit and --after page through them
- dwnl stats <code>
- Clicks for one link; --range 7d|30d|90d, --json
- DWNL_TOKEN
- A personal token instead of login, e.g. in CI
Terminal
# Install to ~/.local/bin (or: brew install isalikov/tap/dwnl)
curl -fsSL https://raw.githubusercontent.com/isalikov/dwnl/master/scripts/install.sh | sh
# Sign in once
dwnl login
Open https://app.dwnl.cc/developers/cli/device and enter WDJB-MJHT
Signed in as you@example.com
# Shorten and copy
dwnl https://example.com/pricing | pbcopy
# Last week of one link, as JSON
dwnl stats a8Kd3xQ --range 7d --json
# In CI: a personal token instead of login
DWNL_TOKEN="$SECRET" dwnl https://example.com/changelog
https://dwnl.cc/Vt7wP0e
Need a higher rate limit, or the traffic endpoints that come with Pro? Tell us what you are building.
Talk to us