API de integración v1

    Enlaces cortos y secretos, desde tu código.

    Una clave bearer, JSON sin más y las mismas reglas de plan que el panel. Pensada para extensiones, CLI, servidores MCP y herramientas internas.

    Crear una clave de APIEspecificación OpenAPI

    Acceso anticipado. Los endpoints v1 se están activando espacio de trabajo por espacio de trabajo. Si una llamada devuelve 503 con el código disabled, la API todavía no está activada para ti. Ya puedes crear claves: funcionarán en cuanto se active.

    Bibliotecas cliente

    Los secretos se cifran antes de enviarse, así que cada cliente tiene que hacer esa parte por su cuenta. Estas bibliotecas lo hacen una sola vez y bien, y se comprueban con los mismos vectores de prueba, publicados en el repositorio, que usa la aplicación web. Las tres tienen licencia MIT, se publican desde CI con procedencia de npm y solo dependen de la biblioteca estándar.

    SDK para TypeScript

    npm

    Cliente tipado para todas las rutas. Sin dependencias, solo WebCrypto, así que funciona sin cambios en Node 20+, Deno, Bun, navegadores y entornos edge.

    npm i @uselinkpilot/sdk

    Línea de comandos

    npm

    Pasa el secreto por una tubería, no como argumento: un argumento queda en el historial de la shell y se ve en la lista de procesos.

    npm i -g linkpilot
    echo 'db password: hunter2' | linkpilot secret --ttl 1h

    Servidor MCP

    npm

    Permite que un asistente de IA entregue una credencial a alguien sin dejarla en el registro del chat. Añade esto a Claude Desktop, Claude Code, Cursor o cualquier cliente MCP:

    {
      "mcpServers": {
        "linkpilot": {
          "command": "npx",
          "args": ["-y", "@uselinkpilot/mcp"],
          "env": { "LINKPILOT_API_KEY": "lp_live_..." }
        }
      }
    }

    Antes de instalarlo, ten esto en cuenta: el texto sin cifrar pasa por el contexto del asistente, porque el modelo tiene que ver el secreto para poder crearlo. Esto lo protege en tránsito, en reposo y haciendo que solo se abra una vez. No puede ocultárselo al propio modelo.

    Código fuente: linkpilot-cli (SDK y CLI) y linkpilot-mcp.

    1. Autenticación

    Crea una clave en /app/api-keys (propietarios y administradores del espacio de trabajo). La clave completa se muestra una sola vez; LinkPilot guarda solo su hash SHA-256. Envíala en cada petición:

    Authorization: Bearer lp_live_...

    Cada clave está vinculada a un espacio de trabajo, así que no hay ningún parámetro de tenant. Las claves revocadas, expiradas, desconocidas o mal formadas devuelven todas el mismo 401 unauthorized. Las claves nunca se escriben en los registros.

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

    2. Rutas

    MétodoRutaQué hace
    GET/meEspacio de trabajo, plan, límites, uso y el margen de peticiones de tu clave
    POST/linksCrea un enlace corto (url, slug opcional, domain, title, tags)
    GET/linksLista los enlaces, del más reciente al más antiguo, con paginación por cursor
    GET/links/{id}Obtiene un enlace
    DELETE/links/{id}Elimina un enlace
    POST/secretsCrea un enlace secreto (ciphertext, enc_version, ttl_seconds, burn_after_read, passphrase_hash en Pro)
    GET/secretsLista los metadatos de los secretos (nunca el contenido)
    DELETE/secrets/{id}Revoca un secreto y destruye su contenido
    POST/public/linksCrea un enlace anónimo con un client_id, sin clave
    POST/public/secretsCrea un secreto anónimo que se destruye tras la primera lectura, sin clave

    Las listas devuelven { data, next_cursor }; para seguir, envía next_cursor como cursor. limit va de 1 a 100.

    3. Ejemplos

    Crear un enlace con 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"]}'

    Crear un secreto desde 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)}`;

    Listar enlaces desde 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"])

    Crear un enlace anónimo sin clave

    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"}'

    El contrato formal se publica aquí, no en un repositorio: el documento OpenAPI 3.1, la colección de Postman y la referencia escrita. Apunta tu generador de código directamente a la URL de OpenAPI.

    4. Cómo se guardan los secretos

    Queremos ser precisos, porque aquí un cliente de la API tiene trabajo real que hacer: la API no acepta en ningún caso un payload sin cifrar.

    • Tú cifras; nosotros guardamos un blob. Envía ciphertext (AES-GCM-256, v1.<iv>.<ct>) con enc_version: 1. Si envías payload, la petición se rechaza; no se acepta en silencio.
    • Tienes que añadir la clave tú mismo. El secret_url que devolvemos no lleva la clave, y no puede llevarla: nunca la hemos visto. Añade #k=<base64url key>. Los navegadores nunca transmiten la parte del enlace que va después del #, así que el destinatario descifra en su dispositivo.
    • No hay recuperación posible. Si pierdes la clave, el secreto se pierde, para ti y para nosotros. Así es como funciona la garantía.
    • Se destruye al leerse, al expirar o al revocarse. El texto cifrado almacenado se sobrescribe en el servidor.
    • La API nunca lo devuelve. Ninguna ruta devuelve un contenido ni un texto cifrado. Solo lo lee la página donde el destinatario lo muestra, que registra la visualización.
    • Las frases de contraseña (Pro y superiores) son un segundo factor real: se refuerzan con PBKDF2-SHA256 y se integran en la clave con HKDF, así que ni el enlace solo ni la frase de contraseña sola bastan para descifrar. Envía solo passphrase_hash, el hash SHA-256 en hexadecimal de la frase de contraseña normalizada con NFKC, nunca la frase en sí, que es la mitad de la clave. Deriva la clave de esa misma forma NFKC, para que una frase de contraseña escrita con otro teclado siga abriendo el secreto.
    • Lo que no cubre. Quien tenga el enlace completo tiene la clave, así que una expiración corta y la destrucción tras la primera lectura siguen siendo importantes. Los archivos adjuntos se cifran del mismo modo, con una clave derivada del mismo enlace.

    El formato de intercambio está especificado en la referencia de la API, y @uselinkpilot/sdk hace todo el intercambio en una sola llamada si prefieres no implementarlo a mano. El mismo modelo se describe en la página de arquitectura de seguridad.

    5. Límites del plan y límites de peticiones

    LímiteFreeProAgency
    Enlaces50IlimitadosIlimitados
    Secretos activos5100100
    Secretos con frase de contraseñaNoSíSí
    Dominios propios para enlacesDominios ya verificadosHasta 5Ilimitados
    Peticiones por clave y hora6010005000

    La API aplica las mismas reglas de plan que el panel, leídas del mismo sitio, así que la agrupación de una agencia también cuenta aquí. GET /me devuelve las cifras actuales de tu espacio de trabajo. Cada respuesta autenticada incluye X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset.

    Comparar planes

    6. Rutas públicas sin clave

    POST /public/links y POST /public/secrets existen para los clientes que no pueden mostrar una verificación en el navegador: CLI, servidores MCP, el lanzador. Reciben un client_id opaco (de 16 a 64 caracteres, generado una vez por instalación) y crean registros en el mismo grupo anónimo que las herramientas web gratuitas.

    • 20 creaciones por cliente al día y 60 por red cada hora, además de los límites por red del propio grupo.
    • El mismo filtrado con fuentes de amenazas, la misma consulta de URL en Google y las mismas revisiones programadas que cualquier otra forma de crear enlaces.
    • Sin dominio propio, sin slug personalizado, sin frase de contraseña; los secretos públicos siempre se destruyen tras la primera lectura y expiran en un máximo de 24 horas.
    • La respuesta incluye un claim_token para que un usuario con sesión iniciada pueda reclamar después el registro para un espacio de trabajo. Trátalo como una credencial.

    7. Errores

    Todos los errores tienen la forma { "error": { "code", "message", "upgrade_url"? } }. Los mensajes nombran el campo, nunca tus datos. Un recurso de otro espacio de trabajo devuelve 404, no 403.

    CódigoHTTPCuándo
    unauthorized401Clave ausente, mal formada, desconocida, revocada o expirada
    disabled503La API todavía no está activada en este despliegue
    invalid_request400Error de validación, destino bloqueado, slug ocupado, dominio desconocido
    not_found404La ruta no existe o el recurso no está en tu espacio de trabajo
    plan_limit402Se ha alcanzado la cuota de enlaces o de secretos activos (incluye upgrade_url)
    pro_required402Frase de contraseña en un plan que no la incluye (incluye upgrade_url)
    rate_limited429Se ha agotado el margen por hora de la clave o la cuota pública (incluye Retry-After)

    Preguntas de desarrolladores

    ¿Qué planes incluyen la API?
    Las claves de API se crean desde el panel en /app/api-keys. Cada clave usa los límites del plan de su espacio de trabajo: el plan Free permite 50 enlaces y 5 secretos activos con 60 peticiones por hora; el plan Pro tiene enlaces ilimitados, 100 secretos activos, secretos protegidos con frase de contraseña y 1000 peticiones por hora; los planes Agency tienen 5000 peticiones por hora.
    ¿El contenido de los secretos tiene cifrado de extremo a extremo?
    Sí, y la API no acepta en ningún caso texto sin cifrar. Tú cifras con AES-GCM-256 y envías el texto cifrado; LinkPilot guarda un blob del que no tiene la clave y que no puede descifrar. Como nunca vemos tu clave, el secret_url que devolvemos no la lleva: la añades tú como fragmento #k=, que los navegadores nunca transmiten. Si pierdes la clave, nadie puede recuperar el secreto, tampoco nosotros. Los archivos adjuntos se cifran del mismo modo, con una clave derivada del mismo enlace.
    ¿Puedo crear enlaces sin clave de API?
    Sí. POST /public/links y POST /public/secrets reciben un client_id opaco en lugar de una clave y crean registros en el mismo grupo anónimo que las herramientas web gratuitas: 20 creaciones por cliente al día, 60 por red cada hora, sin dominio propio, sin frase de contraseña, y los secretos expiran en un máximo de 24 horas.
    ¿Qué pasa si llego a un límite?
    Los límites del plan devuelven HTTP 402 con el código plan_limit o pro_required y una upgrade_url. Los límites de peticiones devuelven 429 con una cabecera Retry-After. Cada respuesta autenticada incluye X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset, para que los clientes puedan frenar antes de que ocurra.
    ¿La API es estable?
    Las rutas v1 no tendrán cambios incompatibles. Los cambios incompatibles salen como una versión nueva. Durante el acceso anticipado, los endpoints se activan espacio de trabajo por espacio de trabajo; una respuesta 503 con el código disabled significa que la API todavía no está activada para tu espacio de trabajo.

    ¿Todo listo para empezar a programar?

    Crea una clave en tu espacio de trabajo o empieza con el plan Free.