Skip to main content
Create a new short link.
POST /api/links

Request Body

destination
string
required
The target URL where the short link will redirect. Must be a complete URL.
domain
string
required
The domain for the short link. Must be in the DUBLY_DOMAINS environment variable.
slug
string
Custom slug for the short link. If omitted, a random 6-character slug is generated. Must be unique for the specified domain.
title
string
Optional title or description for the link.
tags
string
Optional tags for organizing links (comma-separated or any format you prefer).
notes
string
Optional private notes about the link.

Response (201 Created)

id
integer
Unique identifier for the link.
slug
string
The slug portion of the short link.
domain
string
The domain for the short link.
short_url
string
The complete short URL (https://domain/slug).
destination
string
The target URL.
title
string
The link title.
tags
string
The link tags.
notes
string
Private notes.
is_active
boolean
Whether the link is active. New links are always true.
created_at
string
ISO 8601 timestamp when the link was created.
updated_at
string
ISO 8601 timestamp when the link was last updated.

Example

curl -X POST http://localhost:8080/api/links \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "short.io",
    "destination": "https://example.com/some/long/url",
    "title": "Example Link",
    "tags": "demo",
    "slug": "custom-slug"
  }'

Auto-Generated Slug

Omit the slug field to generate a random 6-character slug:
curl -X POST http://localhost:8080/api/links \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "short.io",
    "destination": "https://example.com/article"
  }'
Response:
{
  "id": 2,
  "slug": "a3K9mX",
  "domain": "short.io",
  "short_url": "https://short.io/a3K9mX",
  "destination": "https://example.com/article",
  "title": "",
  "tags": "",
  "notes": "",
  "is_active": true,
  "created_at": "2026-03-02T10:35:00Z",
  "updated_at": "2026-03-02T10:35:00Z"
}

Errors

400 Bad Request - Missing required field:
{"error": "destination is required"}
400 Bad Request - Domain not allowed:
{"error": "domain not allowed"}
409 Conflict - Slug already exists:
{"error": "slug already exists for this domain"}

Retrieve a paginated list of links with optional search.
GET /api/links

Query Parameters

limit
integer
default:"25"
Number of links to return. Maximum: 100.
offset
integer
default:"0"
Number of links to skip for pagination.
Search term to filter links by slug, destination, title, or tags.

Response (200 OK)

Array of link objects (same structure as Create Link response).
total
integer
Total number of links matching the search criteria.
limit
integer
The limit used for this request.
offset
integer
The offset used for this request.

Example

curl "http://localhost:8080/api/links?limit=25&offset=0&search=example" \
  -H "X-API-Key: your-secret-key"

Pagination Example

Get the second page of results:
curl "http://localhost:8080/api/links?limit=25&offset=25" \
  -H "X-API-Key: your-secret-key"

Search Example

Search for links containing “github”:
curl "http://localhost:8080/api/links?search=github" \
  -H "X-API-Key: your-secret-key"
The search term is matched against:
  • Slug
  • Destination URL
  • Title
  • Tags

Retrieve a specific link by ID.
GET /api/links/{id}

Path Parameters

id
integer
required
The unique identifier of the link.

Response (200 OK)

Returns a single link object (same structure as Create Link response).

Example

curl http://localhost:8080/api/links/1 \
  -H "X-API-Key: your-secret-key"

Errors

400 Bad Request - Invalid ID:
{"error": "invalid id"}
404 Not Found - Link not found:
{"error": "not found"}

Update an existing link. Only provided fields are updated.
PATCH /api/links/{id}

Path Parameters

id
integer
required
The unique identifier of the link.

Request Body

All fields are optional. Only include fields you want to update.
slug
string
New slug for the link.
domain
string
New domain for the link. Must be in DUBLY_DOMAINS.
destination
string
New destination URL.
title
string
New title. Pass null to clear.
tags
string
New tags. Pass null to clear.
notes
string
New notes. Pass null to clear.

Response (200 OK)

Returns the updated link object.

Example

curl -X PATCH http://localhost:8080/api/links/1 \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{"destination": "https://example.com/new-url"}'

Update Multiple Fields

curl -X PATCH http://localhost:8080/api/links/1 \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "destination": "https://example.com/updated",
    "title": "Updated Title",
    "tags": "updated,demo"
  }'

Clear a Field

To clear optional fields, pass null:
curl -X PATCH http://localhost:8080/api/links/1 \
  -H "X-API-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{"title": null, "notes": null}'

Cache Invalidation

When you update a link’s slug or domain, the old cache entry is automatically invalidated to ensure redirects use the new configuration immediately.

Errors

400 Bad Request - Invalid ID:
{"error": "invalid id"}
400 Bad Request - Domain not allowed:
{"error": "domain not allowed"}
404 Not Found - Link not found:
{"error": "not found"}
409 Conflict - Slug already exists:
{"error": "slug already exists for this domain"}

Soft delete a link. The link is marked as inactive but not removed from the database.
DELETE /api/links/{id}

Path Parameters

id
integer
required
The unique identifier of the link.

Response (204 No Content)

No response body is returned on success.

Example

curl -X DELETE http://localhost:8080/api/links/1 \
  -H "X-API-Key: your-secret-key"

Soft Delete Behavior

Deleting a link:
  1. Sets is_active to false
  2. Updates the updated_at timestamp
  3. Invalidates the cache entry
  4. Keeps all data and analytics
The link is not removed from the database. Accessing the short URL will return 410 Gone (see Redirects).

Why Soft Delete?

  • Preserve analytics - Historical click data remains available
  • Prevent reuse - The slug remains reserved for that domain
  • Audit trail - You can see when links were deactivated
  • Reversible - Links can be reactivated by updating is_active directly in the database if needed

Errors

400 Bad Request - Invalid ID:
{"error": "invalid id"}
404 Not Found - Link not found:
{"error": "not found"}

Build docs developers (and LLMs) love