---
title: "LinkPilot API for Developers | Short Links and Secret Links"
url: https://uselinkpilot.com/developers
description: "Create short links and burn-after-read secrets from code. Key-authenticated REST API with plan-aware limits, public no-key routes for CLIs, and honest documentation of the storage model."
lang: en
---

Integration API v1

# Short links and secrets, from code.

One bearer key, plain JSON, and the same plan rules as the dashboard. Built for extensions, CLIs, MCP servers and internal tools.

Create an API key: https://uselinkpilot.com/app/api-keys
Read the full contract: https://github.com/TetraCoreHQ/uselinkpilot/blob/main/docs/api/v1.md

**Early access.** The v1 endpoints are being switched on workspace by workspace. If a call returns `503` with code `disabled`, the API is not enabled for you yet. Keys can be created today and will work as soon as it is.

## 1. Authentication

Create a key at /app/api-keys (https://uselinkpilot.com/app/api-keys) (workspace owners and admins). The raw key is shown once; LinkPilot keeps only its SHA-256 hash. Send it on every request:

```
Authorization: Bearer lp_live_...
```

Keys are tied to a workspace, so there is no tenant parameter anywhere. Revoked, expired, unknown and malformed keys all return the same `401 unauthorized`. Keys are never written to logs.

Base URL: `https://khiydxamyihofmtmgvjz.supabase.co/functions/v1/api-v1`

## 2. Routes

| Method | Path | What it does |
| --- | --- | --- |
| GET | /me | Workspace, plan, limits, usage and your key's rate budget |
| POST | /links | Create a short link (url, optional slug, domain, title, tags) |
| GET | /links | List links, newest first, cursor paginated |
| GET | /links/{id} | Fetch one link |
| DELETE | /links/{id} | Delete a link |
| POST | /secrets | Create a secret link (ciphertext, enc_version, ttl_seconds, burn_after_read, passphrase_hash on Pro) |
| GET | /secrets | List secret metadata (never payloads) |
| DELETE | /secrets/{id} | Revoke a secret and destroy its payload |
| POST | /public/links | Create an anonymous link with a client_id, no key |
| POST | /public/secrets | Create an anonymous burn-after-read secret, no key |

Lists return `{ data, next_cursor }`; pass `next_cursor` back as `cursor` to continue. `limit` is 1 to 100.

## 3. Examples

Create a link with curl

```
curl -X POST "https://khiydxamyihofmtmgvjz.supabase.co/functions/v1/api-v1/links" \
  -H "Authorization: Bearer $LINKPILOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/launch","slug":"launch","tags":["campaign"]}'
```

Create a secret from JavaScript

```
const b64u = (b) => btoa(String.fromCharCode(...new Uint8Array(b)))
  .replace(/+/g, "-").replace(///g, "_").replace(/=+$/, "");

// 1. Encrypt locally. This key is generated here and never sent anywhere.
const key = crypto.getRandomValues(new Uint8Array(32));
const iv = crypto.getRandomValues(new Uint8Array(12));
const k = await crypto.subtle.importKey("raw", key, "AES-GCM", false, ["encrypt"]);
const ct = await crypto.subtle.encrypt(
  { name: "AES-GCM", iv, tagLength: 128 },
  k,
  new TextEncoder().encode("staging db password"),
);
const ciphertext = `v1.${b64u(iv)}.${b64u(ct)}`;

// 2. Store the blob. LinkPilot cannot read it.
const res = await fetch("https://khiydxamyihofmtmgvjz.supabase.co/functions/v1/api-v1/secrets", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.LINKPILOT_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ ciphertext, enc_version: 1, ttl_seconds: 3600 }),
});
if (!res.ok) throw new Error((await res.json()).error.code);
const { secret_url } = await res.json();

// 3. Attach the key. Without this the link can never be opened, by anyone.
const shareUrl = `${secret_url}#k=${b64u(key)}`;
```

List links from Python

```
import os, requests

r = requests.get(
    "https://khiydxamyihofmtmgvjz.supabase.co/functions/v1/api-v1/links",
    params={"limit": 20},
    headers={"Authorization": f"Bearer {os.environ['LINKPILOT_API_KEY']}"},
)
r.raise_for_status()
for link in r.json()["data"]:
    print(link["short_url"], "->", link["url"])
print("remaining this hour:", r.headers["X-RateLimit-Remaining"])
```

Create an anonymous link with no key

```
curl -X POST "https://khiydxamyihofmtmgvjz.supabase.co/functions/v1/api-v1/public/links" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com","client_id":"my-cli-0123456789abcdef"}'
```

A Postman collection and an OpenAPI 3.1 document live next to the contract in the repository under `docs/api/`.

## 4. How secrets are stored

We want to be precise, because an API client has real work to do here: the API will not accept a plaintext `payload` at all.

- **You encrypt, we store a blob.** Send `ciphertext` (AES-GCM-256, `v1.<iv>.<ct>`) with `enc_version: 1`. Sending `payload` is rejected rather than quietly accepted.
- **You must append the key yourself.** The `secret_url` we return has no key in it and cannot: we have never seen one. Add `#k=<base64url key>`. Browsers never transmit the part after the `#`, so the recipient decrypts locally.
- **There is no recovery.** Lose the key and the secret is gone, for you and for us. That is the guarantee working.
- **Destroyed on burn, expiry or revoke.** The stored ciphertext is overwritten server-side.
- **Never returned by the API.** No route echoes a payload or a ciphertext. The only reader is the recipient's reveal page, which records the view.
- **Passphrases** (Pro and above) are a real second factor: stretched with PBKDF2-SHA256 and folded into the key with HKDF, so neither the link alone nor the passphrase alone decrypts. Send only `passphrase_hash`, the SHA-256 hex \u2014 never the raw passphrase, which is half the key.
- **What it does not cover.** Anyone holding the full link holds the key, so short expiry and burn-after-read still matter. File attachments are encrypted the same way, with a key derived from the same link.

The wire format is specified in the API reference, and `@uselinkpilot/sdk` does the whole exchange in one call if you would rather not hand-roll it. The same model is described on the security architecture page (https://uselinkpilot.com/security-architecture).

## 5. Plan limits and rate limits

| Limit | Free | Pro | Agency |
| --- | --- | --- | --- |
| Links | 50 | Unlimited | Unlimited |
| Active secrets | 5 | 100 | 100 |
| Passphrase secrets | No | Yes | Yes |
| Custom domains for links | Existing verified domains | Up to 5 | Unlimited |
| Requests per key per hour | 60 | 1,000 | 5,000 |

The API applies the same plan rules as the dashboard, read from the same place, so an agency roll-up applies here too. `GET /me` returns the live numbers for your workspace. Every authenticated response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`.

Compare plans: https://uselinkpilot.com/pricing

## 6. Public routes without a key

`POST /public/links` and `POST /public/secrets` exist for clients that cannot show a browser challenge: CLIs, MCP servers, the launcher. They take an opaque `client_id` (16 to 64 characters, generated once per install) and create records in the same anonymous pool as the free web tools.

- 20 creations per client per day and 60 per network per hour, plus the pool's own per-network caps.
- Same Google Safe Browsing and threat-feed screening as every other creation path.
- No custom domain, no custom slug, no passphrase; public secrets always burn after one read and expire within 24 hours.
- The response includes a `claim_token` so a signed-in user can claim the record into a workspace later. Treat it as a credential.

## 7. Errors

Every error is `{ "error": { "code", "message", "upgrade_url"? } }`. Messages name the field, never your data. Resources in other workspaces are a 404, not a 403.

| Code | HTTP | When |
| --- | --- | --- |
| unauthorized | 401 | Missing, malformed, unknown, revoked or expired key |
| disabled | 503 | The API is not enabled for this deployment yet |
| invalid_request | 400 | Validation failure, blocked destination, taken slug, unknown domain |
| not_found | 404 | No such route, or the resource is not in your workspace |
| plan_limit | 402 | Link or active-secret quota reached (upgrade_url included) |
| pro_required | 402 | Passphrase on a plan without it (upgrade_url included) |
| rate_limited | 429 | Hourly key budget or public allowance used up (Retry-After included) |

## Questions developers ask

Which plans include the API?

API keys are created from the dashboard at /app/api-keys. Each key uses its workspace's plan limits: Free keeps 50 links and 5 active secrets with 60 requests per hour; Pro has unlimited links, 100 active secrets, passphrase-protected secrets and 1,000 requests per hour; Agency plans get 5,000 requests per hour.

Are secret payloads end-to-end encrypted?

Yes, and the API will not accept plaintext at all. You encrypt with AES-GCM-256 and send ciphertext; LinkPilot stores a blob it has no key for and cannot decrypt. Because we never see your key, the secret_url we return has no key in it — you append it yourself as a #k= fragment, which browsers never transmit. Lose the key and the secret is unrecoverable by anyone, including us. File attachments are encrypted the same way, with a key derived from the same link.

Can I create links without an API key?

Yes. POST /public/links and POST /public/secrets take an opaque client_id instead of a key and create records in the same anonymous pool as the free web tools: 20 creations per client per day, 60 per network per hour, no custom domain, no passphrase, secrets expire within 24 hours.

What happens when I hit a limit?

Plan limits return HTTP 402 with a plan_limit or pro_required code and an upgrade_url. Rate limits return 429 with a Retry-After header. Every authenticated response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset so clients can back off before that happens.

Is the API stable?

v1 routes will not change in a breaking way. Breaking changes ship as a new version. During early access the endpoints are switched on workspace by workspace; a 503 response with code disabled means the API is not enabled for your workspace yet.

## Ready to build?

Create a key in your workspace, or start with the free plan.

Go to API keys: https://uselinkpilot.com/app/api-keys
Create a free workspace: https://uselinkpilot.com/signup

## Structured data

```json
[
  {
    "@context": "https://schema.org",
    "@type": "SoftwareApplication",
    "@id": "https://uselinkpilot.com/#software",
    "name": "LinkPilot",
    "applicationCategory": "BusinessApplication",
    "operatingSystem": "Web",
    "description": "LinkPilot is a secure link management platform for creating branded short links, tracking engagement, and sharing secrets with expiring, protected, self-destructing links.",
    "url": "https://uselinkpilot.com",
    "offers": [
      {
        "@type": "Offer",
        "name": "Free",
        "price": "0",
        "priceCurrency": "USD",
        "url": "https://uselinkpilot.com/pricing"
      },
      {
        "@type": "Offer",
        "name": "Pro",
        "price": "29",
        "priceCurrency": "USD",
        "priceSpecification": {
          "@type": "UnitPriceSpecification",
          "price": "29",
          "priceCurrency": "USD",
          "billingDuration": "P1M"
        },
        "url": "https://uselinkpilot.com/pricing"
      },
      {
        "@type": "Offer",
        "name": "Agency",
        "price": "299",
        "priceCurrency": "USD",
        "priceSpecification": {
          "@type": "UnitPriceSpecification",
          "price": "299",
          "priceCurrency": "USD",
          "billingDuration": "P1M"
        },
        "url": "https://uselinkpilot.com/pricing"
      },
      {
        "@type": "Offer",
        "name": "Enterprise",
        "priceSpecification": {
          "@type": "PriceSpecification",
          "priceCurrency": "USD"
        },
        "url": "https://uselinkpilot.com/pricing"
      }
    ]
  },
  {
    "@context": "https://schema.org",
    "@type": "Organization",
    "@id": "https://uselinkpilot.com/#organization",
    "name": "LinkPilot",
    "url": "https://uselinkpilot.com",
    "parentOrganization": {
      "@type": "Organization",
      "@id": "https://tetracorehq.com/#organization",
      "name": "TetraCore",
      "url": "https://tetracorehq.com/"
    },
    "logo": {
      "@type": "ImageObject",
      "url": "https://uselinkpilot.com/logo-512.png",
      "width": 512,
      "height": 512
    },
    "description": "LinkPilot is a secure link management platform for creating branded short links, tracking engagement, and sharing secrets with expiring, protected, self-destructing links.",
    "sameAs": [
      "https://x.com/uselinkpilot",
      "https://www.linkedin.com/company/uselinkpilot",
      "https://facebook.com/uselinkpilot"
    ]
  },
  {
    "@context": "https://schema.org",
    "@type": "WebSite",
    "name": "LinkPilot",
    "url": "https://uselinkpilot.com",
    "potentialAction": {
      "@type": "SearchAction",
      "target": "https://uselinkpilot.com/blog?q={search_term_string}",
      "query-input": "required name=search_term_string"
    }
  },
  {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": [
      {
        "@type": "Question",
        "name": "Which plans include the API?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "API keys are created from the dashboard at /app/api-keys. Each key uses its workspace's plan limits: Free keeps 50 links and 5 active secrets with 60 requests per hour; Pro has unlimited links, 100 active secrets, passphrase-protected secrets and 1,000 requests per hour; Agency plans get 5,000 requests per hour."
        }
      },
      {
        "@type": "Question",
        "name": "Are secret payloads end-to-end encrypted?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Yes, and the API will not accept plaintext at all. You encrypt with AES-GCM-256 and send ciphertext; LinkPilot stores a blob it has no key for and cannot decrypt. Because we never see your key, the secret_url we return has no key in it — you append it yourself as a #k= fragment, which browsers never transmit. Lose the key and the secret is unrecoverable by anyone, including us. File attachments are encrypted the same way, with a key derived from the same link."
        }
      },
      {
        "@type": "Question",
        "name": "Can I create links without an API key?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Yes. POST /public/links and POST /public/secrets take an opaque client_id instead of a key and create records in the same anonymous pool as the free web tools: 20 creations per client per day, 60 per network per hour, no custom domain, no passphrase, secrets expire within 24 hours."
        }
      },
      {
        "@type": "Question",
        "name": "What happens when I hit a limit?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Plan limits return HTTP 402 with a plan_limit or pro_required code and an upgrade_url. Rate limits return 429 with a Retry-After header. Every authenticated response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset so clients can back off before that happens."
        }
      },
      {
        "@type": "Question",
        "name": "Is the API stable?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "v1 routes will not change in a breaking way. Breaking changes ship as a new version. During early access the endpoints are switched on workspace by workspace; a 503 response with code disabled means the API is not enabled for your workspace yet."
        }
      }
    ]
  },
  {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
      {
        "@type": "ListItem",
        "position": 1,
        "name": "Home",
        "item": "https://uselinkpilot.com/"
      },
      {
        "@type": "ListItem",
        "position": 2,
        "name": "Developers",
        "item": "https://uselinkpilot.com/developers"
      }
    ]
  }
]
```