shorten.dev/docs
shorten.dev/docs
Getting StartedAuthentication
OverviewLinksAnalyticsErrors & Rate Limits
API Reference

Links

Create, retrieve, list, and revoke shortened links.

Create a link

POST/api/v1/links

Creates a new shortened link. If custom_slug is omitted, a random 7-character slug is generated.

Scope: write

Request body

destination_url
stringrequired
The URL to redirect to. Must be a valid http or https URL.
custom_slug
string
Custom slug for the short URL. Must be 3–50 characters, alphanumeric with hyphens and underscores.
tags
string[]
Tags for organization. Maximum 3 tags, each up to 50 characters.

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-started
import { 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

link.slug
string
The unique slug for the short URL.
link.destination_url
string
The original destination URL.
link.status
"active" | "flagged" | "revoked"
Current status of the link.
link.threat_type
string | null
Threat classification if the URL was flagged by safety scanning, otherwise null.
link.revoked_at
string | null
ISO 8601 timestamp of when the link was revoked, otherwise null.
link.tags
string[]
Tags attached to this link.
link.created_at
string
ISO 8601 creation timestamp.
link.updated_at
string
ISO 8601 last-updated timestamp.
short_url
string
The full shortened URL ready to share.

Errors

StatusCodeWhen
400bad_requestInvalid URL format, invalid slug, or validation failure
409conflictCustom slug is already taken
429rate_limitedHourly 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

POST/api/v1/links/bulk

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

links
object[]required
Array of link objects to create. Minimum 1, maximum 500.
links[].destination_url
stringrequired
The URL to redirect to.
links[].custom_slug
string
Custom slug (3–50 characters). Auto-generated if omitted.
links[].tags
string[]
Tags for organization (max 3, each up to 50 characters).

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

StatusCodeWhen
400bad_requestInvalid URL, invalid slug, duplicate slugs in batch, exceeds 500 links, or batch size would exceed hourly creation limit
409conflictOne 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

GET/api/v1/links

Returns a paginated list of links for the authenticated user.

Scope: read

Query parameters

page
integer= 1
Page number.
limit
integer= 10
Items per page (max 50).
sort
string= "created_at"
Sort field. One of `created_at`, `updated_at`, `slug`, `destination_url`, or `status`.
order
"asc" | "desc"= "desc"
Sort direction.
status
"active" | "flagged" | "revoked"
Filter by link status.
status_not
"active" | "flagged" | "revoked"
Exclude links with this status.
search
string
Search in slug and destination URL (max 200 characters).
tag
string
Filter by exact tag match (max 50 characters).
date_from
string
Filter links created on or after this date (ISO 8601).
date_to
string
Filter links created on or before this date (ISO 8601).

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

GET/api/v1/links/:slug

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

StatusCodeWhen
404not_foundLink not found or not owned by the authenticated user

Revoke a link

POST/api/v1/links/:slug/revoke

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

StatusCodeWhen
400bad_requestLink is already revoked, or link is flagged (system-controlled)
404not_foundLink 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.

Overview

Complete reference for the Shorten REST API.

Analytics

Retrieve click analytics and traffic data for your links.

On this page

Create a linkBulk create linksList linksGet a linkRevoke a link