---
title: HTTP API
description: REST at https://api.linkdash.dev/v1 with a bearer key from the dashboard. Same operations as the CLI and MCP.
sidebar:
  label: HTTP API
  order: 4
---

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

Auth: `Authorization: Bearer ldash_...` (create keys at [Dashboard → API keys](https://linkdash.dev/dashboard/keys)). Do not invent keys.

CLI reference: [CLI](/cli). MCP: [MCP](/mcp). Pricing / quotas: [Pricing](/pricing). Discovery (no auth): `GET https://api.linkdash.dev/v1`. Plain markdown: [api.md](https://linkdash.dev/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`. 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` |
| (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.

## Create a URL link

```bash
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`, and `utm` (URL links only).

## Upload a file

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

```bash
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`:

```bash
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`. `--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.

`GET /links/:id/stats?last=24h` returns `visits`, `downloads`, `plays`, `visitsInLast`, `countries`, `referrers`, and `visitors`.

`GET /links/export` is CSV. `POST /links/import` accepts raw CSV or multipart `file`.

## 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](/webhooks) and [custom domains](/domains) for those endpoints.
