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.
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
npmCliente 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/sdkLínea de comandos
npmPasa 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 1hServidor MCP
npmPermite 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étodo | Ruta | Qué hace |
|---|---|---|
| GET | /me | Espacio de trabajo, plan, límites, uso y el margen de peticiones de tu clave |
| POST | /links | Crea un enlace corto (url, slug opcional, domain, title, tags) |
| GET | /links | Lista 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 | /secrets | Crea un enlace secreto (ciphertext, enc_version, ttl_seconds, burn_after_read, passphrase_hash en Pro) |
| GET | /secrets | Lista los metadatos de los secretos (nunca el contenido) |
| DELETE | /secrets/{id} | Revoca un secreto y destruye su contenido |
| POST | /public/links | Crea un enlace anónimo con un client_id, sin clave |
| POST | /public/secrets | Crea 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>) conenc_version: 1. Si envíaspayload, la petición se rechaza; no se acepta en silencio. - Tienes que añadir la clave tú mismo. El
secret_urlque 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ímite | Free | Pro | Agency |
|---|---|---|---|
| Enlaces | 50 | Ilimitados | Ilimitados |
| Secretos activos | 5 | 100 | 100 |
| Secretos con frase de contraseña | No | Sí | Sí |
| Dominios propios para enlaces | Dominios ya verificados | Hasta 5 | Ilimitados |
| Peticiones por clave y hora | 60 | 1000 | 5000 |
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.
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_tokenpara 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ódigo | HTTP | Cuándo |
|---|---|---|
| unauthorized | 401 | Clave ausente, mal formada, desconocida, revocada o expirada |
| disabled | 503 | La API todavía no está activada en este despliegue |
| invalid_request | 400 | Error de validación, destino bloqueado, slug ocupado, dominio desconocido |
| not_found | 404 | La ruta no existe o el recurso no está en tu espacio de trabajo |
| plan_limit | 402 | Se ha alcanzado la cuota de enlaces o de secretos activos (incluye upgrade_url) |
| pro_required | 402 | Frase de contraseña en un plan que no la incluye (incluye upgrade_url) |
| rate_limited | 429 | Se 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.