Skip to content

REST API ​

Interactive docs ​

Every Inkpage Links instance publishes API docs at:

  • https://your-domain/_docs/openapi.json — machine-readable OpenAPI
  • https://your-domain/_docs/scalar — friendly UI
  • https://your-domain/_docs/swagger — classic Swagger UI

The OpenAPI document is generated from the same schemas the server validates against, so it always matches the running deployment. For the public instance:

New here? Start with the API quickstart.

Authentication ​

Send your credential in the Authorization header:

http
Authorization: Bearer YOUR_TOKEN

Two kinds of credential work:

CredentialWho it isScope
NUXT_SITE_TOKENThe instance owner (break-glass)Full access, including instance configuration
API key (ink_…)A named integrationOnly the role the key was granted

Creating an API key ​

An API key is created once and its token is shown a single time:

http
POST /api/api-keys/create
Authorization: Bearer YOUR_SITE_TOKEN
Content-Type: application/json

{ "name": "n8n", "role": "power-user" }
json
{ "key": { "id": "…", "prefix": "ink_ab12cd34ef56", "role": "power-user" }, "token": "ink_…" }

Store the token immediately — it cannot be recovered, and only its SHA-256 hash is kept.

OperationRouteRequires
List keysGET /api/api-keysinstance:config
Create a keyPOST /api/api-keys/createinstance:config
Revoke a keyPOST /api/api-keys/revokeinstance:config

Keys accept expiresAt (Unix seconds) and can never hold the superadmin role. Listing never returns tokens or hashes.

You can also create and revoke keys in Dashboard → API keys, without curl.

Key revocation is not instant

The auth middleware caches a key for up to 60 seconds per Cloudflare location, so a revoked token may keep working for a few seconds.

Rate limits ​

Each API key is rate limited (default 120 requests per minute, configured with NUXT_API_KEY_RATE_LIMIT); exceeding it returns 429 with Retry-After. The site token is not throttled.

Sessions and Access ​

API-key and site-token requests cannot be combined with browser sessions: with Cloudflare Access enabled, browsers can additionally authenticate with a verified Access login. The MCP endpoint accepts both credential types.

CORS ​

Optional. Set NUXT_API_CORS=true at build time to allow browser apps on other sites to call /api/**. Login is still required. See configuration.

Storage must be ready

Until you open Dashboard → Links once after deploy, most /api/link/** calls fail with “storage not ready” (HTTP 423). See storage setup.

  • upsert creates when free; if the short code exists, returns it with status: "existing" (does not overwrite)
  • search matches short code, URL, comment, and tags
  • check probes target URLs from the server
  • verify checks how you are authenticated
  • location returns approximate coordinates when Cloudflare provides them
  • Image upload: multipart/form-data upload requiring both file and target link slug; requires R2 (JPEG/PNG/WebP/GIF, max 5 MB)

Endpoint groups ​

Use the OpenAPI UI for full request/response details.

GroupRoutes
Links/api/link/create, edit, upsert, delete, query, search, list, check, tags
Subdomains/api/domains, /api/domains/create, /api/domains/update, /api/domains/delete — Subdomains
API keys/api/api-keys, /api/api-keys/create, /api/api-keys/revoke
Import/export/api/link/import, /api/link/export — Import and Export
Storage setup/api/link/migration/status, /api/link/migration/run — storage setup
AI/api/link/ai, /api/link/og-ai — Workers AI
Analytics/api/stats/**, /api/logs/** — Analytics
Utilities/api/verify, /api/location, /api/upload/image, /api/backup
MCP/api/mcp — MCP Server

Everything the dashboard can do ​

A link carries its full configuration in one payload — nothing is dashboard-only:

json
{
  "url": "https://example.com/target",
  "slug": "promo",
  "comment": "internal note",
  "password": "visitor-password",
  "cloaking": true,
  "unsafe": true,
  "redirectWithQuery": true,
  "expiration": 1798761600,
  "title": "Preview title",
  "description": "Preview description",
  "image": "https://cdn.example.com/cover.png",
  "apple": "https://apps.apple.com/app/id000000",
  "google": "https://play.google.com/store/apps/details?id=com.example",
  "proxy": false,
  "tags": ["campaign", "q4"],
  "geo": { "BR": "https://example.com/brasil", "US": "https://example.com/us" }
}
  • cloaking keeps the short link in the address bar instead of revealing the destination
  • password is hashed on the server; reads return a mask, and an empty string clears it on edit
  • geo routes visitors by two-letter country code
  • image is stored as given and used directly in the preview tag: either a public http(s) URL or the key returned by /api/upload/image. Internal paths are capped at 128 characters; absolute http(s) URLs are capped at the standard URL length limit

Pagination ​

List endpoints return an opaque cursor instead of an offset. Pass the returned cursor to fetch the next page, and stop when list_complete is true. Each list keeps its own array key:

RouteArray keyNotes
/api/link/listlinksAlso accepts sort, tag, and status
/api/domainsdomainsAlso accepts q and status
/api/api-keyskeyslimit maxes at 100, default 50
json
{
  "links": [],
  "cursor": "eyJzbHVnIjoicHJvbW8ifQ==",
  "list_complete": false
}

When list_complete is true, cursor is omitted. Never build a cursor yourself — treat it as opaque. The limit parameter caps page size per route; the OpenAPI document lists each route's maximum.

Errors ​

Errors use standard HTTP status codes. The body carries h3's statusCode and statusMessage, plus data when there is more to say — a validation failure lists the offending fields:

json
{ "statusCode": 400, "statusMessage": "Bad Request", "data": { "issues": [] } }
StatusMeaning
400Request body or query failed validation; the body names the invalid fields
401Missing, malformed, expired, or revoked credential
403Credential is valid but the role lacks the required permission
404The link, subdomain, or domain does not exist
409Conflict — slug taken, subdomain exists, link replaced, or slug unallocatable
413/api/upload/image only: the file exceeds 5 MB
423Storage is not initialized, or a D1 migration is pending
429API-key rate limit exceeded; retry after Retry-After seconds

A 401 never reveals whether a token existed, so treat it as "credential not usable" and create a new key rather than probing for one.

Recipes ​

All examples use https://your-domain as the base URL and read the token from the environment.

sh
curl -X POST https://your-domain/api/link/create \
  -H "Authorization: Bearer $INKPAGE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/secret-report",
    "slug": "report",
    "password": "visitor-password",
    "cloaking": true
  }'

/api/domains/create takes the label in name, and accepts url instead of slug. It creates both records atomically:

sh
curl -X POST https://your-domain/api/domains/create \
  -H "Authorization: Bearer $INKPAGE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "promo", "url": "https://example.com/landing" }'
json
{ "domain": { "name": "promo", "slug": "k3f9xq", "status": "active" } }

Pass slug instead of url to attach the subdomain to a link that already exists. Sending both, or neither, is a validation error.

Issue a key for another integration ​

Creating a key requires the owner credential, not another API key:

sh
export INKPAGE_SITE_TOKEN="your_instance_site_token"

curl -X POST https://your-domain/api/api-keys/create \
  -H "Authorization: Bearer $INKPAGE_SITE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "zapier", "role": "power-user", "expiresAt": 1798761600 }'

The payload needs a version and a links array, matching the export format:

sh
curl -X POST https://your-domain/api/link/import \
  -H "Authorization: Bearer $INKPAGE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "version": "1.0",
    "links": [ { "url": "https://example.com/a", "slug": "a" } ]
  }'

Call the API from an AI client ​

The MCP endpoint speaks the Model Context Protocol over HTTP and accepts the same bearer token:

sh
claude mcp add --transport http inkpage https://your-domain/api/mcp \
  --header "Authorization: Bearer $INKPAGE_TOKEN"

See Integrations for the available tools.

Versioning ​

The API follows the deployment, not a version number: /api/link/** and /api/domains/** keep their field names and semantics across releases, and new optional fields may appear. Read /_docs/openapi.json from the instance you are integrating with — it is the authoritative contract for that deployment, and it is regenerated on every build.