Skip to main content

API Guide

joe-links provides a REST API at /api/v1 for programmatic link management. All API requests require authentication via a Personal Access Token (PAT).

Creating a Personal Access Token

  1. Sign in to joe-links and go to the Dashboard.
  2. Navigate to Settings > API Tokens.
  3. Click Create Token, give it a name, and optionally set an expiration date.
  4. Copy the token immediately -- it is only shown once and cannot be retrieved later.

Authentication

Include your token in the Authorization header as a Bearer token:

curl -H "Authorization: Bearer jl_your_token_here" \
https://go.example.com/api/v1/links

Python (requests)

import requests

headers = {"Authorization": "Bearer jl_your_token_here"}
resp = requests.get("https://go.example.com/api/v1/links", headers=headers)
links = resp.json()

Pagination

List endpoints use cursor-based pagination. The response includes a next_cursor field when more results are available.

Parameters

ParameterDefaultMaxDescription
limit50200Number of items to return per page
cursor----Opaque cursor from a previous response's next_cursor

Example

Fetch the first page:

curl -H "Authorization: Bearer $TOKEN" \
"https://go.example.com/api/v1/links?limit=10"

Response:

{
"links": [ ... ],
"next_cursor": "eyJzbHVnIjoibXktbGluayJ9"
}

Fetch the next page using the cursor:

curl -H "Authorization: Bearer $TOKEN" \
"https://go.example.com/api/v1/links?limit=10&cursor=eyJzbHVnIjoibXktbGluayJ9"

When next_cursor is null, you have reached the last page.

Error Responses

All errors follow a consistent JSON shape:

{
"error": "human-readable error message",
"code": "MACHINE_READABLE_CODE"
}

Common Error Codes

HTTP StatusCodeDescription
400BAD_REQUESTInvalid request body or missing required fields
400INVALID_SLUGSlug format is invalid or uses a reserved prefix
401UNAUTHORIZEDMissing or invalid Bearer token
403FORBIDDENAuthenticated but not authorized for this resource
404NOT_FOUNDResource does not exist
409SLUG_CONFLICTSlug is already taken
409DUPLICATE_OWNERUser is already an owner of this link

API Reference

GET /api/v1/links?limit=50&cursor=...

Returns links owned by the authenticated user. Admins see all links.

POST /api/v1/links
{
"slug": "my-link",
"url": "https://example.com",
"title": "Example",
"description": "An example link",
"tags": ["example", "docs"]
}

The authenticated user becomes the primary owner. slug and url are required. title, description, and tags are optional.

GET /api/v1/links/{id}

Returns a single link. Only owners and admins may access.

PUT /api/v1/links/{id}
{
"url": "https://new-example.com",
"title": "Updated Title",
"description": "Updated description",
"tags": ["updated"]
}

Updates the link's URL, title, description, and tags. The slug is immutable and cannot be changed.

DELETE /api/v1/links/{id}

Returns 204 No Content on success. Only owners and admins may delete.

Co-Owners

List Owners

GET /api/v1/links/{id}/owners

Returns all owners of a link with their id, email, and is_primary flag.

Add a Co-Owner

POST /api/v1/links/{id}/owners
{
"email": "colleague@example.com"
}

The user must already have an account in joe-links. Returns 201 with the new owner entry.

Remove a Co-Owner

DELETE /api/v1/links/{id}/owners/{uid}

Returns 204 No Content. The primary owner cannot be removed.

Tokens

List Tokens

GET /api/v1/tokens

Returns all tokens for the authenticated user. Token hashes are never included.

Create a Token

POST /api/v1/tokens
{
"name": "CI Pipeline",
"expires_at": "2026-12-31T23:59:59Z"
}

Returns the plaintext token in the response. This is the only time the token value is returned -- store it securely.

Revoke a Token

DELETE /api/v1/tokens/{id}

Soft-deletes the token. Returns 204 No Content.

Swagger UI

For interactive API exploration, visit /api/docs/ on your joe-links instance. The Swagger UI provides a complete reference with request/response schemas and the ability to try requests directly.