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 keyRead the full contract

    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 (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.

    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

    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.