Skip to content
LinkdashLinkdash
Esc
↑↓navigate↵open⌘Jpreview
On this page

HTTP API

REST at https://api.linkdash.dev/v1 with a bearer key from the dashboard. Same operations as the CLI and MCP.

Base URL: https://api.linkdash.dev/v1

Auth: Authorization: Bearer ldash_... (create keys at Dashboard → API keys). Do not invent keys.

CLI reference: CLI. MCP: MCP. Pricing / quotas: Pricing. Discovery (no auth): GET https://api.linkdash.dev/v1. Plain markdown: api.md.

Public ids are slugs (deck, k3m9xq), not Convex _id. JSON field names match CLI stdout (id, url, kind, …).

Conventions

  • JSON Content-Type: application/json unless noted.
  • Create takes exactly one of url, text, or storageId after POST /uploads, or an variants array (see Variants). Mint returns { "uploadUrl", "storageId" }. The host PUTs bytes to uploadUrl. Small files may still use multipart file on POST /links.
  • Durations: 7d, 24h, 30m, 1w, or an ISO date. Fields: expires / expiresAt, starts / startsAt.
  • Openings limit: optional positive integer maxOpenings. The link pauses after that many successful openings. Raise maxOpenings, or clearMaxOpenings, to serve it again. Omit maxOpenings for unlimited.
  • If a slug exists on two of the owner’s domains, pass ?domain= / JSON domain.
  • Errors: { "error": "..." } plus optional code. Taken lida.sh slugs return 409 with code: "slug_taken".
  • Writes count toward the monthly event quota. List / get / stats / export / webhook list/get do not. Visit / download / play ingest still count.

Endpoints

CLI HTTP MCP
auth whoami GET /me whoami
links list GET /links (?q=&tag=&kind=&status=) links_list
links get <id> GET /links/:id (?domain=) links_get
links create --url/--file/--text POST /links links_create
links update <id> PATCH /links/:id links_update
links delete <id> DELETE /links/:id links_delete
links stats --last 24h GET /links/:id/stats?last=24h links_stats
links export GET /links/export links_export
links import POST /links/import links_import
links variants list <id> GET /links/:id/variants variants_list
links variants add <id> POST /links/:id/variants variants_add
links variants update <id> <name> PATCH /links/:id/variants/:name variants_update
links variants remove <id> <name> DELETE /links/:id/variants/:name variants_remove
(host PUT after mint) POST /uploads files_upload_url
DELETE /uploads/:storageId files_discard
webhooks list/create/get/rotate/delete GET/POST /webhooks, GET/DELETE /webhooks/:id, POST /webhooks/:id/rotate webhooks_*
domains list/add/connect/verify/remove GET/POST /domains, GET /domains/:host/connect, POST /domains/:host/verify, DELETE /domains/:host domains_*

auth create, auth login, and auth logout are browser device flows. They are not Unkey routes.

GET /me

Signed-in account. { "userId": "...", "email": "...", "name": "..." }. Email and name are omitted when unknown.

curl https://api.linkdash.dev/v1/links \
  -H "Authorization: Bearer ldash_..." \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com","slug":"welcome"}'

JSON body takes exactly one of url, text, or storageId, plus optional slug, name, tags, starts, expires, maxOpenings, password, domain, previewName, previewDescription, previewImageStorageId, sticky, forwardParams, and utm (URL links only). Shorthand creates one variant named main; top-level password, starts, expires, and maxOpenings are the link gate. Response: { "id", "url", "kind", "variants": [{ "name", "kind" }] }.

GET /links rows are { id, kind, status, variantStatuses, variantCount, name, tags, url, target, _id }; kind filter adds group, status filter adds mixed. GET /links/:id returns the full link with variants; content (destinationUrl / textContent / fileName) lives on each variant.

Upload a file

Mint an upload URL ({uploadUrl,storageId}), PUT the bytes from the host, then create the link in the same flow:

curl https://api.linkdash.dev/v1/uploads \
  -H "Authorization: Bearer ldash_..." \
  -H "Content-Type: application/json" \
  -d '{"fileSize":12345}'

curl -X PUT -H "Content-Type: application/pdf" --data-binary @./deck.pdf "$uploadUrl"

curl https://api.linkdash.dev/v1/links \
  -H "Authorization: Bearer ldash_..." \
  -H "Content-Type: application/json" \
  -d '{"storageId":"…","fileName":"deck.pdf","fileType":"application/pdf","fileSize":12345}'

Small files can still use multipart on POST /links:

curl https://api.linkdash.dev/v1/links \
  -H "Authorization: Bearer ldash_..." \
  -F "file=@./deck.pdf" \
  -F "expires=7d" \
  -F "slug=deck"

Optional multipart fields: slug, name, tags, starts / startsAt, password, expires, maxOpenings, domain, previewName, previewDescription, previewImage.

Update, stats, CSV

PATCH /links/:id is JSON or multipart. Replace file bytes with storageId after POST /uploads + host PUT, or multipart field file (single-variant links only). --status / { "status": "expired" } is rejected — use expires. { "status": "scheduled" } is rejected — use starts. Optional maxOpenings pauses the link after that many openings; clearMaxOpenings removes the limit. sticky (boolean) and restartSplit (boolean) control variant assignment; forwardParams (boolean) forwards the visitor’s query string, see Forward query parameters. Response: { "id", "url", "status", "variantStatuses" }.

GET /links/:id/stats?last=24h returns visits, downloads, plays, visitsInLast, countries, referrers, variants (per-variant totals), and visitors.

GET /links/export is CSV with columns slug,kind,name,variant,url,text,fileName,status,domain,tags,starts,expires,maxOpenings,weight,when,created, one row per variant. POST /links/import accepts raw CSV or multipart file; rows are grouped by slug + domain (first row creates the link with its variant, more rows add variants; existing links update fields, then update or add variants by name).

Variants

Every link is a container of one or more variants: content (url | file | text) plus its own gate (password, paused, starts, expires, maxOpenings), a weight, and optional visitor conditions when. The link owns slug, domain, name, tags, preview, its own gate (which wins over variant gates), sticky (default true), and splitVersion (bumped by restartSplit). Without variants a link has one variant named main. Names: lowercase letters, digits, internal hyphens, 1–40 chars, unique per link. Max 20 variants per link.

A visitor gets one variant: variants whose when matches form the pool (if none match, the unconditional variants do), then a weighted pick (sticky hash of visitor + link + splitVersion, else random). Crawlers get the first live unconditional variant. Link kind is group for several variants. Link status is the link gate when set, else the status all variants share, else mixed. variantStatuses counts { active, paused, expired, scheduled }.

Create several variants at once:

curl https://api.linkdash.dev/v1/links \
  -H "Authorization: Bearer ldash_..." \
  -H "Content-Type: application/json" \
  -d '{"slug":"promo","variants":[{"name":"a","url":"https://example.com/a"},{"name":"b","url":"https://example.com/b","weight":3,"when":{"countries":["DE","AT"]}}]}'

Each variant: name (required), one of url | text | storageId (+ fileName, fileType, fileSize), password, starts, expires, maxOpenings, utm, weight, when. Multipart create stays shorthand-only.

Route Body Response
GET /links/:id/variants — variants array
POST /links/:id/variants JSON or multipart file: name (required), content, password, starts, expires, maxOpenings, utm, weight, when the variant
PATCH /links/:id/variants/:name name (rename), url, text, storageId / file, password, clearPassword, starts, clearStarts, expires, clearExpires, maxOpenings, clearMaxOpenings, status (active | paused), weight, when, clearWhen, utm the variant
DELETE /links/:id/variants/:name — { "id", "variant", "deleted": true } (the last variant cannot be deleted)

when keys (arrays of strings; every set key must match): countries (ISO-2), devices (mobile | tablet | desktop), os (windows | macos | android | ios | linux | other), browsers (edge | chrome | firefox | safari | other), languages (prefix), referrerHosts (host suffix).

Query parameter rules

when.params is an array of up to 10 rules on the visitor’s query string (lida.sh/promo?src=newsletter); every rule must hold. Each rule is { "key", "op", "value" } with op one of equals | notEquals | includes | startsWith | endsWith | in (value is a |- or comma-separated list) | exists | missing. value is required except for exists / missing. Keys are case-sensitive, values compare exactly.

{ "params": [{ "key": "src", "op": "equals", "value": "newsletter" }, { "key": "gclid", "op": "missing" }] }

Parameter values are matched at open time and never stored in analytics.

Forward query parameters

forwardParams (boolean on the link, default false; settable on POST /links and PATCH /links/:id, returned by GET /links/:id) appends the visitor’s whole query string to URL variants. Visitor values override stored ones, utm included. File and text variants ignore it. This is routing, not access control: anyone can add parameters. Parameter values are not stored in analytics.

Errors

Status When
400 Validation, missing body, mixed create kinds
401 Missing or invalid bearer key
404 Unknown slug or domain
409 Slug taken (code: "slug_taken")
429 Monthly event quota reached
503 API keys not configured on this deployment

See webhooks and custom domains for those endpoints.

Was this page helpful?