Links
Create, retrieve, list, and revoke shortened links.
Create a link
Creates a new shortened link. If custom_slug is omitted, a random 7-character slug is generated.
Scope: write
Request body
Request
curl -X POST https://shorten.dev/api/v1/links \
-H "Authorization: Bearer sk_..." \
-H "Content-Type: application/json" \
-d '{
"destination_url": "https://docs.example.com/getting-started",
"custom_slug": "get-started",
"tags": ["documentation"]
}'const res = await fetch("https://shorten.dev/api/v1/links", {
method: "POST",
headers: {
Authorization: "Bearer sk_...",
"Content-Type": "application/json",
},
body: JSON.stringify({
destination_url: "https://docs.example.com/getting-started",
custom_slug: "get-started",
tags: ["documentation"],
}),
});
const data = await res.json();
console.log(data.short_url); // https://r.shorten.dev/get-startedimport { Shorten } from "@shorten-dev/sdk";
const shorten = new Shorten("sk_...");
const { link, short_url } = await shorten.links.create({
destination_url: "https://docs.example.com/getting-started",
custom_slug: "get-started",
tags: ["documentation"],
});Response — 201 Created
{
"link": {
"slug": "get-started",
"destination_url": "https://docs.example.com/getting-started",
"status": "active",
"threat_type": null,
"revoked_at": null,
"tags": ["documentation"],
"created_at": "2026-02-16T00:00:00Z",
"updated_at": "2026-02-16T00:00:00Z"
},
"short_url": "https://r.shorten.dev/get-started"
}Response fields
Errors
| Status | Code | When |
|---|---|---|
400 | bad_request | Invalid URL format, invalid slug, or validation failure |
409 | conflict | Custom slug is already taken |
429 | rate_limited | Hourly creation limit exceeded |
URL safety scanning
Every destination URL is scanned for malware, phishing, and other threats at creation time. If a URL is flagged, the link is still created but with status: "flagged" and a warning field in the response. Flagged links do not redirect — they are effectively disabled. If you believe a link was incorrectly flagged, open a support ticket to request a manual review.
Bulk create links
Creates up to 500 links in a single request. The operation is all-or-nothing — if any link is invalid or a custom slug is taken, the entire batch fails.
Each link in the batch counts as one creation toward your hourly rate limit.
Scope: write
Request body
Request
curl -X POST https://shorten.dev/api/v1/links/bulk \
-H "Authorization: Bearer sk_..." \
-H "Content-Type: application/json" \
-d '{
"links": [
{ "destination_url": "https://docs.example.com/guide-1", "tags": ["docs"] },
{ "destination_url": "https://docs.example.com/guide-2", "custom_slug": "guide-2" },
{ "destination_url": "https://docs.example.com/guide-3" }
]
}'const res = await fetch("https://shorten.dev/api/v1/links/bulk", {
method: "POST",
headers: {
Authorization: "Bearer sk_...",
"Content-Type": "application/json",
},
body: JSON.stringify({
links: [
{ destination_url: "https://docs.example.com/guide-1", tags: ["docs"] },
{ destination_url: "https://docs.example.com/guide-2", custom_slug: "guide-2" },
{ destination_url: "https://docs.example.com/guide-3" },
],
}),
});
const data = await res.json();
console.log(`Created ${data.total_created} links`);const result = await shorten.links.bulkCreate({
links: [
{ destination_url: "https://docs.example.com/guide-1", tags: ["docs"] },
{ destination_url: "https://docs.example.com/guide-2", custom_slug: "guide-2" },
{ destination_url: "https://docs.example.com/guide-3" },
],
});
console.log(`Created ${result.total_created} links`);Response — 201 Created
{
"links": [
{
"link": {
"slug": "aB3nP9k",
"destination_url": "https://docs.example.com/guide-1",
"status": "active",
"threat_type": null,
"revoked_at": null,
"tags": ["docs"],
"created_at": "2026-02-23T00:00:00Z",
"updated_at": "2026-02-23T00:00:00Z"
},
"short_url": "https://r.shorten.dev/aB3nP9k"
}
],
"total_created": 3
}Each item in the links array follows the same shape as a single create response. The total_created field reflects the total number of links created.
Errors
| Status | Code | When |
|---|---|---|
400 | bad_request | Invalid URL, invalid slug, duplicate slugs in batch, exceeds 500 links, or batch size would exceed hourly creation limit |
409 | conflict | One or more custom slugs are already taken (includes taken_slugs in details) |
URL safety scanning applies to bulk creation. Any flagged links will have status: "flagged" and a warnings array is included in the response.
List links
Returns a paginated list of links for the authenticated user.
Scope: read
Query parameters
110"created_at""desc"Request
curl "https://shorten.dev/api/v1/links?status=active&limit=10" \
-H "Authorization: Bearer sk_..."const params = new URLSearchParams({ status: "active", limit: "10" });
const res = await fetch(`https://shorten.dev/api/v1/links?${params}`, {
headers: { Authorization: "Bearer sk_..." },
});
const { data, total, total_pages } = await res.json();const { data, total, total_pages } = await shorten.links.list({
status: "active",
limit: 10,
});Response — 200 OK
{
"data": [
{
"slug": "x7kQ2m",
"destination_url": "https://docs.example.com/tutorials",
"status": "active",
"threat_type": null,
"revoked_at": null,
"tags": ["marketing"],
"created_at": "2026-02-15T12:00:00Z",
"updated_at": "2026-02-15T12:00:00Z"
}
],
"total": 25,
"page": 1,
"limit": 10,
"total_pages": 3
}Get a link
Retrieves a single link by its slug.
Scope: read
Request
curl https://shorten.dev/api/v1/links/x7kQ2m \
-H "Authorization: Bearer sk_..."const res = await fetch("https://shorten.dev/api/v1/links/x7kQ2m", {
headers: { Authorization: "Bearer sk_..." },
});
const link = await res.json();const link = await shorten.links.get("x7kQ2m");Response — 200 OK
{
"slug": "x7kQ2m",
"destination_url": "https://docs.example.com/tutorials",
"status": "active",
"threat_type": null,
"revoked_at": null,
"tags": ["marketing"],
"created_at": "2026-02-15T12:00:00Z",
"updated_at": "2026-02-15T12:00:00Z"
}Errors
| Status | Code | When |
|---|---|---|
404 | not_found | Link not found or not owned by the authenticated user |
Revoke a link
Permanently revokes a link. The short URL will immediately stop redirecting. This cannot be undone.
Visitors to a revoked link are shown a public explanation page instead of the original destination.
Scope: write
Request
curl -X POST https://shorten.dev/api/v1/links/x7kQ2m/revoke \
-H "Authorization: Bearer sk_..."const res = await fetch("https://shorten.dev/api/v1/links/x7kQ2m/revoke", {
method: "POST",
headers: { Authorization: "Bearer sk_..." },
});
const link = await res.json();
console.log(link.status); // "revoked"const link = await shorten.links.revoke("x7kQ2m");
// link.status === "revoked"Response — 200 OK
{
"slug": "x7kQ2m",
"destination_url": "https://docs.example.com/tutorials",
"status": "revoked",
"threat_type": null,
"revoked_at": "2026-02-23T18:30:00Z",
"tags": ["marketing"],
"created_at": "2026-02-15T12:00:00Z",
"updated_at": "2026-02-23T18:30:00Z"
}Errors
| Status | Code | When |
|---|---|---|
400 | bad_request | Link is already revoked, or link is flagged (system-controlled) |
404 | not_found | Link not found or not owned by the authenticated user |
Revoking is permanent
Once a link is revoked, it cannot be restored. The slug is not freed up for reuse. Only active links can be revoked — flagged links are controlled by the system and cannot be revoked by the owner.