API de PassFortress
Una API REST 100% programática para gestionar tu bóveda de secretos cifrada del lado servidor. Crea una API key, llama a los endpoints y deja que tus aplicaciones lean y escriban secretos de forma segura.
Todos los endpoints viven bajo /api/v1/, relativos al host del despliegue. En estos ejemplos usamos https://api.passfortress.com como host de referencia. La API expone tus secretos, contenedores, identificadores, sitios web, IPs y perfil. Los metadatos (nombres, URLs, tipos) se sirven sin la master key; los valores descifrados exigen enviar la master key en cada petición (ver Master key y cifrado).
URL base
https://api.passfortress.com/api/v1
Autenticación
API key (Bearer)
Formato
JSON · UTF-8
- 1
Crea una API key en /developers/api-keys con los scopes que necesites. El token se muestra una sola vez: cópialo y guárdalo en un gestor de secretos o variable de entorno.
- 2
Haz tu primera petición: lista los metadatos de tus secretos (no requiere master key).
curl "https://api.passfortress.com/api/v1/secrets/" \
-H "Authorization: Bearer pf_live_xxx"La misma petición en varios lenguajes:
curl -X GET "https://api.passfortress.com/api/v1/secrets/?type=password&page=1" \
-H "Authorization: Bearer pf_live_xxx"¿Y los valores?
X-Master-Key con la master key del usuario. Por ejemplo, GET /secrets/{uuid}/value/ requiere el scope values:read y la master key.Los clientes programáticos se autentican con una API key en la cabeceraAuthorization. Las claves se crean en el panel web y se muestran una única vez.
Authorization: Bearer pf_live_xxxPrefijo de la clave
El prefijo (pf_live_ o pf_test_) es solo una etiqueta para que organices tus claves. No aísla datos: una clave pf_test_ accede exactamente a la misma bóveda real que una pf_live_, con los mismos scopes. Trata ambas como credenciales de producción.
JWT (apps de usuario)
Las apps de usuario final pueden usar el flujo JWT (/auth/login/). Para integraciones servidor-a-servidor, usa API keys.
Nunca expongas una API key en el navegador
PassFortress cifra los valores de los secretos en el servidor con la master key del usuario, que nunca se almacena (ni en el servidor ni en los backups). Para leer o escribir un valor descifrado debes enviar la cabecera X-Master-Key: <master-key> en esa petición, además de la API key. Los metadatos (listar secretos, nombres, URLs, contenedores) no la requieren.
curl "https://api.passfortress.com/api/v1/secrets/SECRET_UUID/value/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>"428: falta la master key
X-Master-Key responde 428 Precondition Required con código master_key_required.403: master key incorrecta
master_key_invalid. No revela ningún secreto.¿Por qué 428 y no 401?
401 se confunde con un token caducado y dispararía un refresh/logout en muchos clientes, expulsando al usuario de una sesión válida. El 428 deja claro que la credencial es correcta pero falta una precondición: la master key.Implicación de confianza
Una cuenta puede tener, además de su bóveda Personal, uno o varios Espacios compartidos con otras personas. La cabecera X-Workspace-Id: <uuid> selecciona sobre cuál opera la petición y afecta a todos los endpoints (secretos, contenedores, identificadores, sitios web…), no solo a los de /workspaces/. Si la omites, la petición opera sobre la bóveda Personal: por eso una integración que espera los secretos de su equipo y no envía la cabecera recibe una lista vacía o incompleta, sin ningún error.
# Los secretos del Espacio (no los personales)
curl "https://api.passfortress.com/api/v1/secrets/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Workspace-Id: 9a8b7c6d-1111-2222-3333-444455556666"
# Sin la cabecera: la bóveda Personal
curl "https://api.passfortress.com/api/v1/secrets/" \
-H "Authorization: Bearer pf_live_xxx"Ortogonal a la master key
X-Workspace-Id y X-Master-Key son independientes: la primera dice sobre qué bóveda operas y la segunda sigue siendo obligatoria para leer o escribir valores descifrados, también en un Espacio.Dos capas de permisos
workspaces:read / workspaces:write y los del recurso) y los permisos de tu membresía en el Espacio se comprueban los dos. Si la membresía no llega, la respuesta es 403 insufficient_workspace_permission, no un insufficient_scope.Usa GET /workspaces/ para enumerar los Espacios disponibles y sus uuid. Un Espacio con la invitación aún sin resolver responde 409 workspace_pending_sync; uno del que ya no eres miembro, 403 not_workspace_member. Ninguno es 401, así que no debes tratarlos como una sesión caducada.
| Scope | Recurso | Concede |
|---|---|---|
secrets:read | Secretos | Listar y leer los metadatos de los secretos (nombre, tipo, URL, contenedor). No descifra valores. |
secrets:write | Secretos | Crear, editar, duplicar y eliminar secretos. |
values:readsensible | Secretos | Leer valores descifrados. Incluye el valor actual, el historial y la descarga de archivos, pero también la exportación de la bóveda COMPLETA (GET /secrets/export/) y la instantánea completa de sincronización (POST /sync/bootstrap/): una clave con este scope puede extraer todas las contraseñas en una sola petición. En /sync/push/ sólo habilita conflicts.server_payload si la clave también tiene secrets:read. Requiere también la master key. |
containers:read | Contenedores | Listar y leer contenedores (carpetas). |
containers:write | Contenedores | Crear, renombrar y eliminar contenedores. |
identifiers:read | Identificadores | Leer los pares clave-valor asociados a cada secreto. |
identifiers:write | Identificadores | Crear, editar y eliminar identificadores. |
websites:read | Sitios web | Listar los sitios web asociados a los secretos. |
websites:write | Sitios web | Registrar un host en el catálogo de sitios web (idempotente). |
ips:read | IPs | Leer las listas blanca/negra de direcciones IP. |
ips:write | IPs | Añadir y eliminar reglas de IP. |
profile:read | Perfil | Leer los datos del perfil y las preferencias. |
profile:write | Perfil | Actualizar campos ordinarios del perfil y tema. Las operaciones de credenciales o recuperación (contraseña, correo, master key, 2FA/recovery codes) requieren una sesión JWT interactiva y rechazan API keys. |
share:write | Compartir | Compartir y aceptar secretos compartidos; junto con secrets:read también permite listar y reclamar invitaciones pendientes. Compartir y aceptar requieren la master key; listar el inbox no. |
import:write | Importación | Importar secretos desde un CSV de Chrome. Requiere la master key. |
workspaces:read | Espacios | Listar los Espacios (bóvedas compartidas) del usuario, leer sus metadatos y, si eres administrador, su lista de miembros. |
workspaces:write | Espacios | Crear, renombrar y eliminar Espacios, invitar/editar/expulsar miembros y salir de un Espacio. Las operaciones que tocan material de clave (crear, invitar, editar o expulsar) requieren también la master key. |
apikeys:read | API keys | Listar las API keys y sus metadatos. |
apikeys:writesensible | API keys | Crear, rotar y revocar API keys. Una clave con este scope puede acuñar otras claves. |
apikeys:write es peligroso
apikeys:write puede acuñar otras claves (con cualquier scope). Concédelo solo a integraciones de aprovisionamiento de confianza.Rate limits
El límite es por clave. Superarlo devuelve 429 con la cabecera Retry-After.
| Tier | Límite | Uso |
|---|---|---|
| default | 1 000 / hora | Límite por defecto para nuevas claves. |
| high | 10 000 / hora | Para integraciones con mayor volumen. |
| unlimited | Sin límite | Reservado para casos especiales. No disponible para claves con el scope values:read (devuelve invalid_rate_tier). |
Paginación
Las colecciones usan paginación por número de página, 50 elementos por página. Usa ?page=N y sigue el campo next hasta que sea null.
{
"count": 673,
"next": "https://api.passfortress.com/api/v1/secrets/?page=2",
"previous": null,
"results": [ /* … 50 elementos … */ ]
}Autenticación
Login con usuario/contraseña y gestión de la sesión JWT. Los clientes programáticos normalmente usan API keys; estos endpoints existen para apps de usuario final.
/api/v1/auth/login//api/v1/auth/login/2fa//api/v1/auth/login/2fa/recovery//api/v1/auth/google//api/v1/auth/master-key/setup//api/v1/auth/register//api/v1/auth/token/refresh//api/v1/auth/me//api/v1/auth/logout//api/v1/auth/master-key/validate//api/v1/auth/verify/request//api/v1/auth/verify/confirm//api/v1/auth/password-reset//api/v1/auth/password-reset/confirm//api/v1/auth/login/Iniciar sesión y obtener tokens JWT.
Devuelve un access token (corta duración), un refresh token (1 año) y el usuario. IMPORTANTE: un 200 NO siempre trae tokens. Si la cuenta tiene un segundo factor activo (2FA por correo o SMS), la respuesta es 200 con {tfa_required: true, challenge, channel, expires_in, sent} y SIN access/refresh: la única vía a los tokens es POST /auth/login/2fa/. Si sent es false, el challenge existe pero el envío falló; ofrece reintento o códigos de recuperación. Comprueba siempre tfa_required antes de leer access. Otros códigos: 401 invalid_credentials, 409 tfa_channel_unavailable, 429 (con Retry-After) si se agota el presupuesto de envíos del segundo factor.
Cuerpo (JSON)
| username* | string | Nombre de usuario o correo de la cuenta. |
| password* | string | Contraseña. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/auth/login/" \
-H "Content-Type: application/json" \
-d '{
"username": "ada",
"password": "mi-contraseña"
}'Respuesta
{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"uuid": "c0ffee00-1111-2222-3333-444455556666",
"username": "ada",
"email": "dev@example.com",
"pending_email": null,
"first_name": "Ada",
"last_name": "Lovelace",
"mobile": "+34600000000",
"country": "ES",
"address": "Madrid",
"language": "es",
"theme": "light",
"email_verified": true,
"mobile_verified": false,
"tfa_email_enabled": false,
"tfa_mobile_enabled": false,
"tfa_policy": "disabled",
"tfa_recovery_codes_remaining": 0,
"enabled_envfiles": true,
"enabled_files": true,
"accepts_secret_shares": true,
"has_master_key": true,
"has_usable_password": true
}
}/api/v1/auth/login/2fa/Canjear el desafío de 2FA por los tokens JWT.
Segundo paso obligatorio cuando /auth/login/ (o /auth/google/) responde tfa_required. El challenge es opaco, de un solo uso, dura 5 minutos, admite como máximo 5 intentos de código y un nuevo inicio de sesión invalida el anterior. Errores 401 con code: invalid_challenge (caducado, ya usado o superado por otro login), invalid_tfa_code (código incorrecto, el desafío sigue vivo), challenge_attempts_exceeded (desafío quemado: hay que volver a /auth/login/) y session_revoked (sesión obsoleta: descarta tokens y reinicia login).
Cuerpo (JSON)
| challenge* | string | El challenge devuelto por /auth/login/. No se reenvía el usuario. |
| code* | string | Código de 6 dígitos recibido por correo o SMS. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/auth/login/2fa/" \
-H "Content-Type: application/json" \
-d '{
"challenge": "r4nd0m-0paque-t0ken",
"code": "123456"
}'Respuesta
{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"uuid": "c0ffee00-1111-2222-3333-444455556666",
"username": "ada",
"email": "dev@example.com",
"pending_email": null,
"first_name": "Ada",
"last_name": "Lovelace",
"mobile": "+34600000000",
"country": "ES",
"address": "Madrid",
"language": "es",
"theme": "light",
"email_verified": true,
"mobile_verified": false,
"tfa_email_enabled": false,
"tfa_mobile_enabled": false,
"tfa_policy": "disabled",
"tfa_recovery_codes_remaining": 0,
"enabled_envfiles": true,
"enabled_files": true,
"accepts_secret_shares": true,
"has_master_key": true,
"has_usable_password": true
}
}/api/v1/auth/login/2fa/recovery/Canjear el desafío de 2FA con un código de recuperación.
Alternativa a /auth/login/2fa/ para quien ha perdido su segundo factor. Canjea EL MISMO challenge (mismo TTL, mismo tope de 5 intentos compartido con el endpoint anterior), pero con uno de los códigos de recuperación de un solo uso que se entregan al activar el 2FA. Es un endpoint aparte y el campo se llama recovery_code precisamente para que el servidor no tenga que adivinar qué credencial recibe. Si el canje tiene éxito el servidor DESACTIVA el segundo factor y anula el resto de códigos (el usuario debe volver a activarlo), y avisa por correo al titular. Recupera el ACCESO A LA CUENTA, nunca la bóveda: la master key no viaja al servidor y sigue siendo necesaria. Errores 401 con code: invalid_tfa_recovery_code, no_tfa_recovery_codes (la cuenta no tiene ninguno), invalid_challenge, challenge_attempts_exceeded y session_revoked.
Cuerpo (JSON)
| challenge* | string | El challenge devuelto por /auth/login/ o /auth/google/. |
| recovery_code* | string | Uno de los códigos de recuperación (16 caracteres; se aceptan con o sin guiones y en minúsculas). |
Petición
curl -X POST "https://api.passfortress.com/api/v1/auth/login/2fa/recovery/" \
-H "Content-Type: application/json" \
-d '{
"challenge": "r4nd0m-0paque-t0ken",
"recovery_code": "ABCD-EFGH-JKMN-PQRS"
}'Respuesta
{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"uuid": "c0ffee00-1111-2222-3333-444455556666",
"username": "ada",
"email": "dev@example.com",
"pending_email": null,
"first_name": "Ada",
"last_name": "Lovelace",
"mobile": "+34600000000",
"country": "ES",
"address": "Madrid",
"language": "es",
"theme": "light",
"email_verified": true,
"mobile_verified": false,
"tfa_email_enabled": false,
"tfa_mobile_enabled": false,
"tfa_policy": "disabled",
"tfa_recovery_codes_remaining": 0,
"enabled_envfiles": true,
"enabled_files": true,
"accepts_secret_shares": true,
"has_master_key": true,
"has_usable_password": true
}
}/api/v1/auth/google/Iniciar sesión (o registrarse) con un ID token de Google.
Verifica el ID token de Google e inicia sesión; con allow_signup crea la cuenta (201, created: true). Google solo autentica: NO sustituye al segundo factor (si la cuenta tiene 2FA, responde el mismo {tfa_required, challenge, channel, expires_in, sent} que /auth/login/) y NUNCA recibe, genera ni sustituye la master key. Si sent es false, el challenge existe pero el envío falló; ofrece reintento o códigos de recuperación. Una cuenta recién creada nace SIN master key (has_master_key: false) y debe establecerla en /auth/master-key/setup/. Errores: 400 invalid_google_token, 403 account_disabled, 404 google_no_account (sin allow_signup), 409 google_ambiguous_account / google_identity_conflict / google_merge_requires_recovery, 503 google_verification_unavailable.
Cuerpo (JSON)
| credential* | string | ID token (JWT) emitido por Google Identity Services. |
| allow_signup | boolean | Si es true, provisiona la cuenta cuando el correo no existe (por defecto false). |
Petición
curl -X POST "https://api.passfortress.com/api/v1/auth/google/" \
-H "Content-Type: application/json" \
-d '{
"credential": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"allow_signup": false
}'Respuesta
{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"uuid": "c0ffee00-1111-2222-3333-444455556666",
"username": "ada",
"email": "dev@example.com",
"pending_email": null,
"first_name": "Ada",
"last_name": "Lovelace",
"mobile": "+34600000000",
"country": "ES",
"address": "Madrid",
"language": "es",
"theme": "light",
"email_verified": true,
"mobile_verified": false,
"tfa_email_enabled": false,
"tfa_mobile_enabled": false,
"tfa_policy": "disabled",
"tfa_recovery_codes_remaining": 0,
"enabled_envfiles": true,
"enabled_files": true,
"accepts_secret_shares": true,
"has_master_key": true,
"has_usable_password": true
},
"created": false
}/api/v1/auth/master-key/setup/Establecer la master key INICIAL de una cuenta que no tiene.
Para cuentas provisionadas sin master key (registro con Google). Requiere sesión JWT interactiva: una API key recibe 403 jwt_session_required aunque tenga profile:write, porque este endpoint elige la clave de la bóveda. Idempotente: si ya existe una master key devuelve 409 master_key_already_set (cambiarla es /profile/master-key/, que exige la anterior). Aplica la política de fortaleza (400 weak_master_key). La master key no se persiste: solo se guarda el canario cifrado con el que se valida.
Cuerpo (JSON)
| master_key* | string | Master key inicial (mínimo 8; no repetitiva, secuencial ni común). |
Petición
curl -X POST "https://api.passfortress.com/api/v1/auth/master-key/setup/" \
-H "Authorization: Bearer <access-token-jwt>" \
-H "Content-Type: application/json" \
-d '{
"master_key": "<tu-master-key>"
}'Respuesta
{
"created": true
}/api/v1/auth/register/Registrar un nuevo usuario.
Devuelve los tokens JWT y el usuario creado (201).
Cuerpo (JSON)
| username* | string | Nombre de usuario único. |
| email* | string | Correo único. |
| password* | string | Contraseña (mínimo 8). |
| master_key* | string | Master key inicial (mínimo 8; no repetitiva, secuencial ni común). |
| first_name | string | Nombre. |
| last_name | string | Apellidos. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/auth/register/" \
-H "Content-Type: application/json" \
-d '{
"username": "ada",
"email": "nuevo@example.com",
"password": "mi-contraseña",
"master_key": "<tu-master-key>",
"first_name": "Ada"
}'Respuesta
{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"uuid": "c0ffee00-1111-2222-3333-444455556666",
"username": "ada",
"email": "dev@example.com",
"pending_email": null,
"first_name": "Ada",
"last_name": "Lovelace",
"mobile": "+34600000000",
"country": "ES",
"address": "Madrid",
"language": "es",
"theme": "light",
"email_verified": true,
"mobile_verified": false,
"tfa_email_enabled": false,
"tfa_mobile_enabled": false,
"tfa_policy": "disabled",
"tfa_recovery_codes_remaining": 0,
"enabled_envfiles": true,
"enabled_files": true,
"accepts_secret_shares": true,
"has_master_key": true,
"has_usable_password": true
}
}/api/v1/auth/token/refresh/Renovar el access token.
Un 401 con code session_revoked NO es una caducidad transitoria: la sesión fue revocada (cambio o restablecimiento de contraseña, cierre del resto de sesiones) y ningún reintento la recuperará. Descarta los tokens y vuelve a iniciar sesión.
Cuerpo (JSON)
| refresh* | string | Refresh token vigente. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/auth/token/refresh/" \
-H "Content-Type: application/json" \
-d '{
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}'Respuesta
{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}/api/v1/auth/me/Obtener el usuario autenticado.
Devuelve el objeto del usuario directamente (sin envoltorio).
Petición
curl -X GET "https://api.passfortress.com/api/v1/auth/me/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
{
"uuid": "c0ffee00-1111-2222-3333-444455556666",
"username": "ada",
"email": "dev@example.com",
"pending_email": null,
"first_name": "Ada",
"last_name": "Lovelace",
"mobile": "+34600000000",
"country": "ES",
"address": "Madrid",
"language": "es",
"theme": "light",
"email_verified": true,
"mobile_verified": false,
"tfa_email_enabled": false,
"tfa_mobile_enabled": false,
"tfa_policy": "disabled",
"tfa_recovery_codes_remaining": 0,
"enabled_envfiles": true,
"enabled_files": true,
"accepts_secret_shares": true,
"has_master_key": true,
"has_usable_password": true
}/api/v1/auth/logout/Cerrar sesión (revoca el refresh token).
Revoca el refresh token enviado y toda su cadena de rotación, es decir la sesión entera y no solo el último token. No requiere access token: basta con poseer el refresh, para que un cliente inactivo pueda cerrar sesión de verdad aunque su access haya caducado. Devuelve 205 Reset Content sin cuerpo. Idempotente.
Cuerpo (JSON)
| refresh* | string | Refresh token a invalidar. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/auth/logout/" \
-H "Content-Type: application/json" \
-d '{
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}'Respuesta
"205 Reset Content (sin cuerpo)"/api/v1/auth/master-key/validate/Validar la master key sin revelar secretos.
Para claves API requiere el scope values:read (validar la master key es la misma capacidad que leer un valor). La master key viaja en el cuerpo JSON como master_key; este endpoint no usa la cabecera X-Master-Key. Sujeto al lockout anti-fuerza-bruta: tras varios intentos fallidos devuelve 429 master_key_locked con Retry-After.
Cuerpo (JSON)
| master_key* | string | Master key a comprobar. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/auth/master-key/validate/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"master_key": "<tu-master-key>"
}'Respuesta
{
"valid": true
}/api/v1/auth/verify/request/Solicitar un código de verificación.
Cuerpo (JSON)
| scope* | string | verify_email, verify_mobile, verify_email_tfa o verify_mobile_tfa. Los scopes de móvil (SMS) están desactivados administrativamente y responden 400 sms_verification_disabled. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/auth/verify/request/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"scope": "verify_email"
}'Respuesta
{
"scope": "verify_email",
"channel": "email",
"sent": true
}/api/v1/auth/verify/confirm/Confirmar el código de verificación.
Cuerpo (JSON)
| scope* | string | El mismo scope usado en la solicitud. |
| code* | string | Código recibido. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/auth/verify/confirm/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"scope": "verify_email",
"code": "123456"
}'Respuesta
{
"verified": true
}/api/v1/auth/password-reset/Solicitar el restablecimiento de contraseña.
Responde 200 siempre (no revela si el correo existe).
Cuerpo (JSON)
| email* | string | Correo de la cuenta. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/auth/password-reset/" \
-H "Content-Type: application/json" \
-d '{
"email": "dev@example.com"
}'Respuesta
{
"sent": true
}/api/v1/auth/password-reset/confirm/Confirmar el restablecimiento con el código.
Cuerpo (JSON)
| email* | string | Correo de la cuenta. |
| code* | string | Código recibido por correo. |
| new_password* | string | Nueva contraseña (mínimo 8). |
Petición
curl -X POST "https://api.passfortress.com/api/v1/auth/password-reset/confirm/" \
-H "Content-Type: application/json" \
-d '{
"email": "dev@example.com",
"code": "123456",
"new_password": "nueva-contraseña"
}'Respuesta
{
"reset": true
}Capacidades
Límites efectivos del despliegue que los clientes deben aplicar antes de enviar datos.
/api/v1/capabilities/Consultar límites efectivos del despliegue.
Devuelve el límite de archivo realmente configurado por el operador. Requiere autenticación, pero no un scope específico ni la master key.
Petición
curl -X GET "https://api.passfortress.com/api/v1/capabilities/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
{
"max_file_size_bytes": 104857600
}Perfil
Datos del usuario, correo de la cuenta (cambio en dos pasos con verificación), contraseña, tema y rotación de la master key.
/api/v1/profile//api/v1/profile//api/v1/profile/email//api/v1/profile/email/confirm//api/v1/profile/email//api/v1/profile/password//api/v1/profile/master-key//api/v1/profile/tfa/recovery-codes//api/v1/profile/theme//api/v1/profile/Leer el perfil del usuario.
Petición
curl -X GET "https://api.passfortress.com/api/v1/profile/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
{
"uuid": "c0ffee00-1111-2222-3333-444455556666",
"username": "ada",
"email": "dev@example.com",
"pending_email": null,
"first_name": "Ada",
"last_name": "Lovelace",
"mobile": "+34600000000",
"country": "ES",
"address": "Madrid",
"language": "es",
"theme": "light",
"email_verified": true,
"mobile_verified": false,
"tfa_email_enabled": false,
"tfa_mobile_enabled": false,
"tfa_policy": "disabled",
"tfa_recovery_codes_remaining": 0,
"enabled_envfiles": true,
"enabled_files": true,
"accepts_secret_shares": true,
"has_master_key": true,
"has_usable_password": true
}/api/v1/profile/Actualizar campos del perfil.
Estricto con el cuerpo: cualquier campo no listado abajo se rechaza con 400 unknown_field en vez de descartarse en silencio (antes un campo mal escrito devolvía 200 sin aplicar nada). El correo tampoco se cambia aquí, pero tiene su propio código: 400 email_change_requires_verification, porque el cambio existe y va por POST /profile/email/ + POST /profile/email/confirm/. Cambiar el móvil o el país marca el canal como no verificado, y si el 2FA por SMS está activo la petición se rechaza con 400 tfa_mobile_locked (hay que desactivarlo en la misma petición o antes). El campo tfa_policy requiere JWT interactivo: con API key se rechaza con 403 jwt_session_required.
Cuerpo (JSON)
| first_name | string | Nombre. |
| last_name | string | Apellidos. |
| mobile | string | Teléfono móvil. |
| country | string | Código ISO alpha-2 del país del móvil (GET /countries/); es el prefijo con el que se marca el SMS. |
| address | string | Dirección. |
| language | string | Idioma preferido. |
| theme | string | light o dark. La preferencia system es solo local del cliente web. |
| tfa_policy | string | disabled, email o mobile (el canal debe estar verificado y completo; solo JWT interactivo, no API key). |
| accepts_secret_shares | boolean | Permite o bloquea nuevas invitaciones de secretos dirigidas a esta cuenta. |
Petición
curl -X PATCH "https://api.passfortress.com/api/v1/profile/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"first_name": "Ada",
"last_name": "Lovelace"
}'Respuesta
{
"uuid": "c0ffee00-1111-2222-3333-444455556666",
"username": "ada",
"email": "dev@example.com",
"pending_email": null,
"first_name": "Ada",
"last_name": "Lovelace",
"mobile": "+34600000000",
"country": "ES",
"address": "Madrid",
"language": "es",
"theme": "light",
"email_verified": true,
"mobile_verified": false,
"tfa_email_enabled": false,
"tfa_mobile_enabled": false,
"tfa_policy": "disabled",
"tfa_recovery_codes_remaining": 0,
"enabled_envfiles": true,
"enabled_files": true,
"accepts_secret_shares": true,
"has_master_key": true,
"has_usable_password": true
}/api/v1/profile/email/Solicitar el cambio de correo (paso 1 de 2).
Verificar y DESPUÉS aplicar: guarda la dirección candidata y le envía a ELLA un código de un solo uso (10 minutos). Requiere sesión JWT interactiva; una API key recibe 403 jwt_session_required. El correo de la cuenta NO cambia con esta llamada; sigue siendo el destino de la recuperación hasta que se confirme el código, así que no informes al usuario de que su correo ya se ha actualizado. Exige la contraseña de la cuenta (demostrar el buzón nuevo no demuestra quién lo pidió); las cuentas sin contraseña utilizable, altas con Google, están exentas. Refusals codificados: invalid_account_password, email_unchanged, email_already_registered (400), email_not_distinguishable (409, homógrafo de una dirección ya registrada) y verification_throttled (429 con Retry-After, presupuesto de códigos compartido con /auth/verify/request/). Pedir un código nuevo invalida el anterior. GET /profile/ expone la dirección pendiente en pending_email.
Cuerpo (JSON)
| new_email* | string | Dirección candidata; se guarda y se aplica en su forma canónica (minúsculas). |
| password | string | Contraseña actual de la cuenta. Obligatoria salvo que la cuenta no tenga contraseña utilizable. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/profile/email/" \
-H "Authorization: Bearer <access-token-jwt>" \
-H "Content-Type: application/json" \
-d '{
"new_email": "nueva@example.com",
"password": "actual"
}'Respuesta
{
"pending_email": "nueva@example.com",
"sent": true,
"expires_in": 600
}/api/v1/profile/email/confirm/Confirmar el cambio de correo (paso 2 de 2).
Único punto donde cambia la dirección de la cuenta, y solo tras redimir el código. Requiere sesión JWT interactiva; una API key recibe 403 jwt_session_required. La deja además como verificada (el código acaba de demostrar el buzón). Revoca TODAS las sesiones de la cuenta: los tokens con los que hiciste esta llamada quedan muertos y la respuesta trae un par access/refresh nuevo que el cliente DEBE guardar. Se avisa por correo a la dirección anterior. Las API keys pf_* no se revocan. Errores: no_pending_email_change, invalid_email_change_code, email_already_registered (400, si la dirección se ocupó entre paso 1 y 2), session_revoked (401) y email_not_distinguishable (409).
Cuerpo (JSON)
| code* | string | Código recibido en la dirección nueva. No se reenvía la dirección: el servidor ya sabe cuál está demostrando. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/profile/email/confirm/" \
-H "Authorization: Bearer <access-token-jwt>" \
-H "Content-Type: application/json" \
-d '{
"code": "123456"
}'Respuesta
{
"changed": true,
"email": "nueva@example.com",
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"uuid": "c0ffee00-1111-2222-3333-444455556666",
"username": "ada",
"email": "nueva@example.com",
"pending_email": null,
"first_name": "Ada",
"last_name": "Lovelace",
"mobile": "+34600000000",
"country": "ES",
"address": "Madrid",
"language": "es",
"theme": "light",
"email_verified": true,
"mobile_verified": false,
"tfa_email_enabled": false,
"tfa_mobile_enabled": false,
"tfa_policy": "disabled",
"tfa_recovery_codes_remaining": 0,
"enabled_envfiles": true,
"enabled_files": true,
"accepts_secret_shares": true,
"has_master_key": true,
"has_usable_password": true
}
}/api/v1/profile/email/Cancelar un cambio de correo pendiente.
Descarta la dirección candidata y su código. Idempotente: devuelve cancelled=false si no había nada pendiente.
Petición
curl -X DELETE "https://api.passfortress.com/api/v1/profile/email/" \
-H "Authorization: Bearer <access-token-jwt>"Respuesta
{
"cancelled": true
}/api/v1/profile/password/Cambiar la contraseña de la cuenta.
Requiere sesión JWT interactiva; una API key recibe 403 jwt_session_required. Cierra el resto de sesiones de la cuenta: los tokens con los que hiciste esta llamada quedan revocados en el acto. La respuesta trae un par access/refresh nuevo y es la única forma de seguir dentro, así que el cliente DEBE guardarlo y descartar el anterior.
Cuerpo (JSON)
| old_password* | string | Contraseña actual. |
| new_password* | string | Nueva contraseña (mínimo 8). |
Petición
curl -X POST "https://api.passfortress.com/api/v1/profile/password/" \
-H "Authorization: Bearer <access-token-jwt>" \
-H "Content-Type: application/json" \
-d '{
"old_password": "actual",
"new_password": "nueva"
}'Respuesta
{
"changed": true,
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}/api/v1/profile/master-key/Rotar la master key (re-cifra todos los secretos).
Requiere sesión JWT interactiva; una API key recibe 403 jwt_session_required. Descifra cada secreto con la master key antigua y lo vuelve a cifrar con la nueva. Ambas claves viajan en el cuerpo (no usa la cabecera X-Master-Key). Operación pesada e irreversible si se pierde la nueva clave. Validar la clave antigua cuenta para el lockout anti-fuerza-bruta: devuelve 400 si es incorrecta (o si la nueva no cumple la política de fortaleza, weak_master_key), 401 session_revoked si el JWT quedó obsoleto y 429 master_key_locked con Retry-After tras superar el umbral.
Cuerpo (JSON)
| old_master_key* | string | Master key actual. |
| new_master_key* | string | Nueva master key (mínimo 8; no repetitiva, secuencial ni común). |
Petición
curl -X POST "https://api.passfortress.com/api/v1/profile/master-key/" \
-H "Authorization: Bearer <access-token-jwt>" \
-H "Content-Type: application/json" \
-d '{
"old_master_key": "<actual>",
"new_master_key": "<nueva>"
}'Respuesta
{
"changed": true
}/api/v1/profile/tfa/recovery-codes/Generar un juego nuevo de códigos de recuperación de 2FA.
Requiere sesión JWT interactiva; una API key recibe 403 jwt_session_required. Anula los códigos anteriores y devuelve un juego nuevo. Se muestran UNA sola vez: el servidor guarda únicamente su SHA-256, igual que con los tokens de API. Al activar el 2FA (PATCH /profile/ con tfa_policy) ya se entrega un juego en la misma respuesta, dentro de recovery_codes; este endpoint es para regenerarlos y para las cuentas que activaron el 2FA antes de que existieran (tienen tfa_recovery_codes_remaining: 0). Responde 401 session_revoked si el JWT quedó obsoleto y 409 tfa_not_enabled si la cuenta no tiene segundo factor activo. Los códigos recuperan el acceso a la cuenta, nunca la bóveda.
Cuerpo (JSON)
Petición
curl -X POST "https://api.passfortress.com/api/v1/profile/tfa/recovery-codes/" \
-H "Authorization: Bearer <access-token-jwt>" \
-H "Content-Type: application/json" \
-d '{}'Respuesta
{
"codes": [
"ABCD-EFGH-JKMN-PQRS",
"TVWX-YZ01-2345-6789"
],
"remaining": 10
}/api/v1/profile/theme/Cambiar el tema preferido.
Devuelve el perfil completo actualizado.
Cuerpo (JSON)
| theme* | string | light o dark. La preferencia system es solo local del cliente web. |
Petición
curl -X PATCH "https://api.passfortress.com/api/v1/profile/theme/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"theme": "dark"
}'Respuesta
{
"uuid": "c0ffee00-1111-2222-3333-444455556666",
"username": "ada",
"email": "dev@example.com",
"pending_email": null,
"first_name": "Ada",
"last_name": "Lovelace",
"mobile": "+34600000000",
"country": "ES",
"address": "Madrid",
"language": "es",
"theme": "dark",
"email_verified": true,
"mobile_verified": false,
"tfa_email_enabled": false,
"tfa_mobile_enabled": false,
"tfa_policy": "disabled",
"tfa_recovery_codes_remaining": 0,
"enabled_envfiles": true,
"enabled_files": true,
"accepts_secret_shares": true,
"has_master_key": true,
"has_usable_password": true
}Países
Catálogo de países (datos de referencia, no requiere scope).
/api/v1/countries/Listar países disponibles.
Devuelve un array plano (sin paginación).
Petición
curl -X GET "https://api.passfortress.com/api/v1/countries/"Respuesta
[
{
"id": 1,
"name_en": "Spain",
"name_es": "España",
"iso2": "ES",
"iso3": "ESP",
"phone_code": "34"
},
{
"id": 2,
"name_en": "Mexico",
"name_es": "México",
"iso2": "MX",
"iso3": "MEX",
"phone_code": "52"
}
]IPs
Listas blanca/negra de direcciones IP para el control de acceso de la cuenta.
/api/v1/ips/Listar reglas de IP.
Petición
curl -X GET "https://api.passfortress.com/api/v1/ips/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"uuid": "ip1",
"name": "Oficina",
"ip_range": "203.0.113.0/24",
"list": "white"
}
]
}/api/v1/ips/Añadir una regla de IP.
Cuerpo (JSON)
| ip_range* | string | IP o rango (CIDR). |
| list | string | white o black. |
| name | string | Etiqueta descriptiva (opcional). |
Petición
curl -X POST "https://api.passfortress.com/api/v1/ips/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Oficina",
"ip_range": "203.0.113.0/24",
"list": "white"
}'Respuesta
{
"uuid": "ip1",
"name": "Oficina",
"ip_range": "203.0.113.0/24",
"list": "white"
}/api/v1/ips/{uuid}/Obtener una regla de IP.
Parámetros de ruta
| uuid* | string | UUID de la regla. |
Petición
curl -X GET "https://api.passfortress.com/api/v1/ips/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
{
"uuid": "ip1",
"name": "Oficina",
"ip_range": "203.0.113.0/24",
"list": "white"
}/api/v1/ips/{uuid}/Eliminar una regla de IP.
Devuelve 204 No Content sin cuerpo.
Parámetros de ruta
| uuid* | string | UUID de la regla. |
Petición
curl -X DELETE "https://api.passfortress.com/api/v1/ips/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
"204 No Content (sin cuerpo)"Contenedores
Carpetas para organizar los secretos.
/api/v1/containers//api/v1/containers//api/v1/containers/{uuid}//api/v1/containers/{uuid}//api/v1/containers/{uuid}//api/v1/containers/{uuid}//api/v1/containers/Listar contenedores.
Petición
curl -X GET "https://api.passfortress.com/api/v1/containers/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"uuid": "f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff",
"name": "Trabajo",
"description": "",
"secrets_count": 42
}
]
}/api/v1/containers/Crear un contenedor.
Cuerpo (JSON)
| name* | string | Nombre del contenedor. |
| description | string | Descripción (opcional). |
Petición
curl -X POST "https://api.passfortress.com/api/v1/containers/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Trabajo",
"description": ""
}'Respuesta
{
"uuid": "f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff",
"name": "Trabajo",
"description": "",
"secrets_count": 0
}/api/v1/containers/{uuid}/Obtener un contenedor.
Parámetros de ruta
| uuid* | string | UUID del contenedor. |
Petición
curl -X GET "https://api.passfortress.com/api/v1/containers/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
{
"uuid": "f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff",
"name": "Trabajo",
"description": "",
"secrets_count": 42
}/api/v1/containers/{uuid}/Reemplazar un contenedor.
Parámetros de ruta
| uuid* | string | UUID del contenedor. |
Cuerpo (JSON)
| name* | string | Nuevo nombre. |
| description | string | Nueva descripción. |
Petición
curl -X PUT "https://api.passfortress.com/api/v1/containers/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Trabajo (2026)",
"description": ""
}'Respuesta
{
"uuid": "f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff",
"name": "Trabajo (2026)",
"description": "",
"secrets_count": 42
}/api/v1/containers/{uuid}/Actualizar parcialmente un contenedor.
Parámetros de ruta
| uuid* | string | UUID del contenedor. |
Cuerpo (JSON)
| name | string | Nuevo nombre. |
| description | string | Nueva descripción. |
Petición
curl -X PATCH "https://api.passfortress.com/api/v1/containers/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Personal"
}'Respuesta
{
"uuid": "f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff",
"name": "Personal",
"description": "",
"secrets_count": 42
}/api/v1/containers/{uuid}/Eliminar un contenedor.
Devuelve 204 No Content sin cuerpo. Crear o renombrar un contenedor no pide master key, pero borrarlo sí: es irreversible y no hay papelera, igual que en DELETE /secrets/{uuid}/. Sin la cabecera X-Master-Key responde 428 master_key_required y con una master key incorrecta 403 master_key_invalid. Los secretos que estaban dentro NO se borran: solo pierden la carpeta.
Parámetros de ruta
| uuid* | string | UUID del contenedor. |
Petición
curl -X DELETE "https://api.passfortress.com/api/v1/containers/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>"Respuesta
"204 No Content (sin cuerpo)"Secretos
El núcleo de la API. Los METADATOS (nombre, tipo, URL, contenedores) no requieren master key; leer o escribir VALORES descifrados sí requiere la cabecera X-Master-Key.
/api/v1/secrets//api/v1/secrets//api/v1/secrets//api/v1/secrets/{uuid}//api/v1/secrets/{uuid}//api/v1/secrets/{uuid}//api/v1/secrets/{uuid}//api/v1/secrets/bulk-delete//api/v1/secrets/{uuid}/value//api/v1/secrets/{uuid}/history//api/v1/secrets/{uuid}/history-page//api/v1/secrets/{uuid}/duplicate//api/v1/secrets/{uuid}/share//api/v1/secrets/{uuid}/download//api/v1/secrets/shared//api/v1/secrets/accept-shared//api/v1/secrets/import//api/v1/secrets/export//api/v1/secrets/import-backup//api/v1/secrets/Listar secretos (solo metadatos).
Devuelve metadatos paginados (50 por página). Nunca incluye valores descifrados.
Parámetros de consulta
| type | string | password, envfile, totp o file. |
| container | string | UUID del contenedor para filtrar. |
| search | string | Búsqueda por nombre, URL o nombre de archivo. |
| page | integer | Número de página (50 por página). |
Petición
curl -X GET "https://api.passfortress.com/api/v1/secrets/?type=password&page=1" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
{
"count": 673,
"next": null,
"previous": null,
"results": [
{
"uuid": "a1b2c3d4-0000-1111-2222-333344445555",
"secret_type": "password",
"name": "GitHub",
"file_name": null,
"url": "https://github.com",
"notes": "",
"available": true,
"shared": false,
"automatic_password_change": false,
"website": {
"uuid": "w1a2b3c4-0000-1111-2222-333344445555",
"hostname": "github.com",
"login_url": "https://github.com/login",
"automatic_password_change": false
},
"containers": [
{
"uuid": "f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff",
"name": "Trabajo",
"description": "",
"secrets_count": 42
}
],
"container_uuids": [
"f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff"
],
"identifiers": [
{
"uuid": "id1",
"key": "usuario",
"value": "ada@example.com"
}
],
"has_value": true,
"imported_at": null
}
]
}/api/v1/secrets/Crear un secreto con su primer valor.
Requiere X-Master-Key porque cifra el valor en el servidor. Devuelve los metadatos (201).
Cuerpo (JSON)
| secret_type* | string | password, envfile, totp o file. |
| name | string | Nombre del secreto. |
| value | string | Valor en claro para password/envfile (se cifra en el servidor). Para totp es la URI otpauth://totp/... completa (semilla base32 + issuer + algoritmo/dígitos/periodo); el servidor la guarda cifrada sin parsearla. |
| file_content_b64 | string | Contenido del archivo en base64 (tipo file). |
| file_name | string | Nombre del archivo (tipo file). |
| url | string | URL asociada. |
| notes | string | Notas. |
| containers | array | UUIDs de contenedores (alias: container_uuids); máximo 100. |
| identifiers | array | Pares {key, value}; máximo 100. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/secrets/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>" \
-H "Content-Type: application/json" \
-d '{
"secret_type": "password",
"name": "GitHub",
"url": "https://github.com",
"value": "s3cr3t-p4ss",
"containers": [
"f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff"
],
"identifiers": [
{
"key": "usuario",
"value": "ada@example.com"
}
]
}'Respuesta
{
"uuid": "a1b2c3d4-0000-1111-2222-333344445555",
"secret_type": "password",
"name": "GitHub",
"file_name": null,
"url": "https://github.com",
"notes": "",
"available": true,
"shared": false,
"automatic_password_change": false,
"website": {
"uuid": "w1a2b3c4-0000-1111-2222-333344445555",
"hostname": "github.com",
"login_url": "https://github.com/login",
"automatic_password_change": false
},
"containers": [
{
"uuid": "f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff",
"name": "Trabajo",
"description": "",
"secrets_count": 42
}
],
"container_uuids": [
"f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff"
],
"identifiers": [
{
"uuid": "id1",
"key": "usuario",
"value": "ada@example.com"
}
],
"has_value": true,
"imported_at": null
}/api/v1/secrets/Crear un autenticador TOTP (segundo factor).
Mismo endpoint que crear cualquier secreto, con secret_type: "totp". El value es la URI otpauth://totp/... completa (semilla base32 + issuer + algoritmo/dígitos/periodo). El servidor la cifra bajo la master key sin parsearla (conocimiento cero): NUNCA calcula códigos. El código de 6 dígitos se genera en el CLIENTE tras leer el valor. Compatible con el estándar TOTP (RFC 6238). Requiere X-Master-Key.
Cuerpo (JSON)
| secret_type* | string | Debe ser "totp". |
| name | string | Etiqueta visible (p. ej. el issuer del servicio). |
| value* | string | URI otpauth://totp/... completa (se guarda cifrada sin parsearse). |
| url | string | URL asociada. |
| notes | string | Notas. |
| containers | array | UUIDs de contenedores (alias: container_uuids); máximo 100. |
| identifiers | array | Pares {key, value} (p. ej. la cuenta); máximo 100. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/secrets/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>" \
-H "Content-Type: application/json" \
-d '{
"secret_type": "totp",
"name": "GitHub",
"value": "otpauth://totp/GitHub:ada@example.com?secret=JBSWY3DPEHPK3PXP&issuer=GitHub&algorithm=SHA1&digits=6&period=30",
"url": "https://github.com",
"identifiers": [
{
"key": "account",
"value": "ada@example.com"
}
]
}'Respuesta
{
"uuid": "a1b2c3d4-0000-1111-2222-333344445555",
"secret_type": "totp",
"name": "GitHub",
"file_name": null,
"url": "https://github.com",
"notes": "",
"available": true,
"shared": false,
"automatic_password_change": false,
"website": {
"uuid": "w1a2b3c4-0000-1111-2222-333344445555",
"hostname": "github.com",
"login_url": "https://github.com/login",
"automatic_password_change": false
},
"containers": [
{
"uuid": "f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff",
"name": "Trabajo",
"description": "",
"secrets_count": 42
}
],
"container_uuids": [
"f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff"
],
"identifiers": [
{
"uuid": "id1",
"key": "usuario",
"value": "ada@example.com"
}
],
"has_value": true,
"imported_at": null
}/api/v1/secrets/{uuid}/Obtener los metadatos de un secreto.
Parámetros de ruta
| uuid* | string | UUID del secreto. |
Petición
curl -X GET "https://api.passfortress.com/api/v1/secrets/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
{
"uuid": "a1b2c3d4-0000-1111-2222-333344445555",
"secret_type": "password",
"name": "GitHub",
"file_name": null,
"url": "https://github.com",
"notes": "",
"available": true,
"shared": false,
"automatic_password_change": false,
"website": {
"uuid": "w1a2b3c4-0000-1111-2222-333344445555",
"hostname": "github.com",
"login_url": "https://github.com/login",
"automatic_password_change": false
},
"containers": [
{
"uuid": "f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff",
"name": "Trabajo",
"description": "",
"secrets_count": 42
}
],
"container_uuids": [
"f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff"
],
"identifiers": [
{
"uuid": "id1",
"key": "usuario",
"value": "ada@example.com"
}
],
"has_value": true,
"imported_at": null
}/api/v1/secrets/{uuid}/Reemplazar un secreto.
Requiere X-Master-Key (cifra el valor en el servidor). El tipo no se puede cambiar.
Parámetros de ruta
| uuid* | string | UUID del secreto. |
Cuerpo (JSON)
| name | string | Nombre. |
| value | string | Nuevo valor (cifra en el servidor). |
| file_name | string | Nombre del FILE al reemplazar su blob. |
| file_content_b64 | string | Nuevo contenido FILE en base64, sin prefijo data:. |
| url | string | URL asociada. |
| notes | string | Notas. |
| containers | array | UUIDs de contenedores; máximo 100. |
| identifiers | array | Pares {key, value}; máximo 100. |
Petición
curl -X PUT "https://api.passfortress.com/api/v1/secrets/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>" \
-H "Content-Type: application/json" \
-d '{
"name": "GitHub",
"value": "nuevo-valor"
}'Respuesta
{
"uuid": "a1b2c3d4-0000-1111-2222-333344445555",
"secret_type": "password",
"name": "GitHub",
"file_name": null,
"url": "https://github.com",
"notes": "",
"available": true,
"shared": false,
"automatic_password_change": false,
"website": {
"uuid": "w1a2b3c4-0000-1111-2222-333344445555",
"hostname": "github.com",
"login_url": "https://github.com/login",
"automatic_password_change": false
},
"containers": [
{
"uuid": "f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff",
"name": "Trabajo",
"description": "",
"secrets_count": 42
}
],
"container_uuids": [
"f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff"
],
"identifiers": [
{
"uuid": "id1",
"key": "usuario",
"value": "ada@example.com"
}
],
"has_value": true,
"imported_at": null
}/api/v1/secrets/{uuid}/Actualizar parcialmente un secreto.
Requiere X-Master-Key (la escritura cifra en el servidor).
Parámetros de ruta
| uuid* | string | UUID del secreto. |
Cuerpo (JSON)
| name | string | Nombre. |
| value | string | Nuevo valor. |
| containers | array | UUIDs de contenedores; máximo 100. |
| identifiers | array | Pares {key, value}; máximo 100. |
Petición
curl -X PATCH "https://api.passfortress.com/api/v1/secrets/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>" \
-H "Content-Type: application/json" \
-d '{
"value": "nuevo-valor"
}'Respuesta
{
"uuid": "a1b2c3d4-0000-1111-2222-333344445555",
"secret_type": "password",
"name": "GitHub",
"file_name": null,
"url": "https://github.com",
"notes": "",
"available": true,
"shared": false,
"automatic_password_change": false,
"website": {
"uuid": "w1a2b3c4-0000-1111-2222-333344445555",
"hostname": "github.com",
"login_url": "https://github.com/login",
"automatic_password_change": false
},
"containers": [
{
"uuid": "f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff",
"name": "Trabajo",
"description": "",
"secrets_count": 42
}
],
"container_uuids": [
"f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff"
],
"identifiers": [
{
"uuid": "id1",
"key": "usuario",
"value": "ada@example.com"
}
],
"has_value": true,
"imported_at": null
}/api/v1/secrets/{uuid}/Eliminar un secreto.
Devuelve 204 No Content sin cuerpo. Borrar es una escritura y es irreversible (no hay papelera: se lleva por delante todo el historial de valores y el archivo adjunto), así que exige X-Master-Key válida igual que el resto de escrituras: sin la cabecera responde 428 master_key_required y con una master key incorrecta 403 master_key_invalid. Única excepción: rechazar un secreto compartido pendiente de aceptar (los que devuelve /secrets/shared/) no la pide, porque está cifrado con la clave de un solo uso del emisor y el destinatario nunca podrá presentar una master key que lo abra. Repetir el DELETE devuelve 404.
Parámetros de ruta
| uuid* | string | UUID del secreto. |
Petición
curl -X DELETE "https://api.passfortress.com/api/v1/secrets/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>"Respuesta
"204 No Content (sin cuerpo)"/api/v1/secrets/bulk-delete/Eliminar varios secretos en una transacción.
Elimina entre 1 y 200 UUIDs de la bóveda activa. Es irreversible y exige X-Master-Key; los UUIDs que ya no existen se devuelven en not_found sin abortar el resto. En un Espacio se vuelve a comprobar el permiso de borrado para cada tipo de recurso presente.
Cuerpo (JSON)
| uuids* | string[] | Entre 1 y 200 UUIDs; los repetidos se cuentan una sola vez. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/secrets/bulk-delete/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>" \
-H "Content-Type: application/json" \
-d '{
"uuids": [
"a1b2c3d4-0000-1111-2222-333344445555",
"b2c3d4e5-1111-2222-3333-444455556666"
]
}'Respuesta
{
"deleted": 1,
"not_found": [
"b2c3d4e5-1111-2222-3333-444455556666"
]
}/api/v1/secrets/{uuid}/value/Leer el valor descifrado de un secreto.
Descifra el valor en memoria usando la master key. Devuelve 428 si falta X-Master-Key y 403 si es incorrecta. Los intentos fallidos cuentan para el lockout: tras superar el umbral devuelve 429 master_key_locked con Retry-After. Para un secreto totp el value es la URI otpauth://...; el código de 6 dígitos se calcula en el cliente (el servidor no genera códigos).
Parámetros de ruta
| uuid* | string | UUID del secreto. |
Petición
curl -X GET "https://api.passfortress.com/api/v1/secrets/{uuid}/value/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>"Respuesta
{
"uuid": "a1b2c3d4-0000-1111-2222-333344445555",
"secret_type": "password",
"value": "s3cr3t-p4ss"
}/api/v1/secrets/{uuid}/history/Historial descifrado con contrato legacy.
Mantiene el array histórico para clientes existentes. Si el historial supera el presupuesto seguro responde 413 history_pagination_required; usa history-page para recorrerlo completo.
Parámetros de ruta
| uuid* | string | UUID del secreto. |
Petición
curl -X GET "https://api.passfortress.com/api/v1/secrets/{uuid}/history/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>"Respuesta
[
{
"uuid": "hv1",
"value": "s3cr3t-p4ss",
"created_date_time": "2026-06-01T11:00:00Z"
},
{
"uuid": "hv2",
"value": "old-pass",
"created_date_time": "2026-01-12T09:30:00Z"
}
]/api/v1/secrets/{uuid}/history-page/Recorrer el historial con cursor y presupuesto de bytes.
Devuelve un snapshot estable: las revisiones creadas tras la primera página no desplazan ni duplican las siguientes. next_cursor es opaco y caduca.
Parámetros de ruta
| uuid* | string | UUID del secreto. |
Parámetros de consulta
| cursor | string | Cursor opaco devuelto por la página anterior. |
Petición
curl -X GET "https://api.passfortress.com/api/v1/secrets/{uuid}/history-page/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>"Respuesta
{
"count": 2,
"next_cursor": null,
"results": [
{
"uuid": "hv1",
"value": "s3cr3t-p4ss",
"created_date_time": "2026-06-01T11:00:00Z"
},
{
"uuid": "hv2",
"value": "old-pass",
"created_date_time": "2026-01-12T09:30:00Z"
}
]
}/api/v1/secrets/{uuid}/duplicate/Duplicar un secreto.
Devuelve los metadatos de la copia (201). La copia parte del valor actual; el historial indefinido permanece asociado al secreto original. En un secreto de tipo file escribe otra copia completa del blob cifrado. Requiere X-Master-Key: responde 428 master_key_required si falta y 403 master_key_invalid si no es válida.
Parámetros de ruta
| uuid* | string | UUID del secreto a copiar. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/secrets/{uuid}/duplicate/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>"Respuesta
{
"uuid": "nuevo-uuid",
"secret_type": "password",
"name": "GitHub (copia)",
"file_name": null,
"url": "https://github.com",
"notes": "",
"available": true,
"shared": false,
"automatic_password_change": false,
"website": {
"uuid": "w1a2b3c4-0000-1111-2222-333344445555",
"hostname": "github.com",
"login_url": "https://github.com/login",
"automatic_password_change": false
},
"containers": [
{
"uuid": "f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff",
"name": "Trabajo",
"description": "",
"secrets_count": 42
}
],
"container_uuids": [
"f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff"
],
"identifiers": [
{
"uuid": "id1",
"key": "usuario",
"value": "ada@example.com"
}
],
"has_value": true,
"imported_at": null
}/api/v1/secrets/{uuid}/download/Descargar el archivo descifrado (tipo file).
Devuelve el archivo binario descifrado. Requiere X-Master-Key.
Parámetros de ruta
| uuid* | string | UUID del secreto. |
Petición
curl -X GET "https://api.passfortress.com/api/v1/secrets/{uuid}/download/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>"Respuesta (binaria)
<contenido binario del archivo>
/api/v1/secrets/import/Importar contraseñas desde un CSV de Chrome.
multipart/form-data con el CSV exportado de Chrome, un operation_id UUID generado por el cliente y on_duplicate=duplicate|overwrite. En overwrite, una coincidencia por hostname e identificador actualiza solo la contraseña y conserva el historial y los metadatos. Las filas válidas y el recibo durable se confirman juntos en una única transacción para todo el fichero; repetir exactamente la misma petición y operation_id devuelve el resultado anterior, mientras reutilizar el UUID con otro fichero o modo devuelve 409 import_operation_conflict. Los errores de filas rechazadas se devuelven como {row, error}. El tamaño se comprueba antes de leer: 413 import_file_too_large. Un CSV ilegible o que supera el máximo de filas devuelve 400 import_bad_csv/import_too_many_rows sin escribir ninguna fila. Requiere X-Master-Key.
Petición
curl -X POST "https://api.passfortress.com/api/v1/secrets/import/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>" \
-F "operation_id=d4eb9a27-8b03-4f11-9b35-4426d11dd52f" \
-F "on_duplicate=duplicate" \
-F "file=@passwords.csv"Respuesta
{
"operation_id": "d4eb9a27-8b03-4f11-9b35-4426d11dd52f",
"imported": 128,
"updated": 0,
"skipped": 3,
"errors": []
}/api/v1/secrets/export/Descargar una copia de seguridad cifrada de toda la bóveda.
Devuelve un .zip cifrado con TODAS las contraseñas del titular en una sola petición: es la lectura masiva de la bóveda, no un endpoint de metadatos. Requiere X-Master-Key y el scope values:read (tenlo en cuenta al conceder una clave de "solo lectura"). Solo opera sobre la bóveda Personal: con X-Workspace-Id activo devuelve 409 workspace_export_unsupported. Si la copia no puede cifrarse con la master key actual devuelve 409 export_master_key_stale; si un secreto no descifra o no tiene revisión almacenada devuelve 422 export_decrypt_failed/export_storage_unavailable; si el resultado superaría el presupuesto importable configurado del servidor devuelve 422 export_too_large.
Petición
curl -X GET "https://api.passfortress.com/api/v1/secrets/export/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>"Respuesta (binaria)
<archivo .zip cifrado: passfortress-boveda-20260807-101500.zip>
/api/v1/secrets/import-backup/Restaurar una copia de seguridad cifrada.
multipart/form-data con el .zip generado por /secrets/export/. Es idempotente: repetir la importación no duplica secretos. Para lograrlo descifra todas las contraseñas vivas y responde cuántas ha omitido, así que revela si un valor ya está en la bóveda: por eso la clave API necesita import:write Y values:read (mismo criterio que /secrets/{uuid}/share/). Requiere X-Master-Key válida; si la copia se hizo con una master key anterior, envíala en old_master_key (se usa solo para descifrar el archivo y los valores se vuelven a cifrar con la master key actual). Errores principales: 422 import_master_key_mismatch; 413 import_file_too_large solo si el upload en bruto rebasa su límite; y 400 import_bad_file (incluido un miembro que excede el límite descomprimido), import_bad_version, import_decrypt_failed, import_too_many_entries e import_master_key_stale. Solo bóveda Personal: con X-Workspace-Id devuelve 409 workspace_import_unsupported.
Formulario (multipart)
| file* | file | Archivo .zip de copia de seguridad. |
| old_master_key | string | Master key con la que se generó la copia, si es distinta de la actual. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/secrets/import-backup/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>" \
-F "file=@passfortress-backup.zip"Respuesta
{
"imported": 128,
"skipped": 3,
"errors": []
}Sincronización offline
Journal de sincronización que usa el cliente de escritorio para trabajar sin conexión. Devuelve y acepta VALORES DESCIFRADOS, así que las tres rutas exigen X-Master-Key además de sus scopes. Alcance actual: solo la bóveda Personal y solo secretos password, envfile y totp (los de tipo file, los Espacios, la compartición y los contenedores quedan fuera). Con la cabecera X-Workspace-Id las tres responden 409 sync_personal_only.
/api/v1/sync/bootstrap/Instantánea completa de la bóveda Personal más el cursor inicial.
Devuelve un registro por secreto soportado, con su revisión y su valor DESCIFRADO, y el cursor desde el que empezar a llamar a /sync/pull/. Es una lectura masiva de la bóveda completa, igual que /secrets/export/. Sin paginación todavía. Si algún secreto no puede descifrarse con la master key enviada devuelve 409 sync_secret_unreadable.
Petición
curl -X POST "https://api.passfortress.com/api/v1/sync/bootstrap/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>"Respuesta
{
"records": [
{
"entity_type": "secret",
"entity_id": "a1b2c3d4-0000-1111-2222-333344445555",
"revision": 1,
"payload": {
"secret_type": "password",
"name": "GitHub",
"url": "https://github.com",
"notes": null,
"available": true,
"automatic_password_change": false,
"identifiers": [
{
"key": "usuario",
"value": "ada@example.com"
}
],
"value": "s3cr3t-p4ss"
}
}
],
"cursor": 0
}/api/v1/sync/pull/Cambios posteriores a un cursor.
Devuelve los cambios del journal con cursor mayor que el enviado, colapsados a uno por entidad. Una entidad borrada llega como operation "delete" con payload null (lápida). Repite hasta que has_more sea false y guarda next_cursor. El cursor todavía no tiene generación ni retención: ante un next_cursor desconocido, vuelve a llamar a /sync/bootstrap/.
Parámetros de consulta
| cursor | integer | Último cursor aplicado por el cliente (0 por defecto). |
| limit | integer | Entradas por página, entre 1 y 200 (100 por defecto). |
Petición
curl -X GET "https://api.passfortress.com/api/v1/sync/pull/?cursor=0&limit=100" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>"Respuesta
{
"changes": [
{
"cursor": 42,
"entity_type": "secret",
"entity_id": "a1b2c3d4-0000-1111-2222-333344445555",
"revision": 2,
"operation": "upsert",
"payload": {
"secret_type": "password",
"name": "GitHub",
"url": "https://github.com",
"notes": null,
"available": true,
"automatic_password_change": false,
"identifiers": [
{
"key": "usuario",
"value": "ada@example.com"
}
],
"value": "nuevo-valor"
}
}
],
"next_cursor": 42,
"has_more": false
}/api/v1/sync/push/Enviar mutaciones hechas sin conexión.
Lote de hasta 100 mutaciones. Una validación terminal del cuerpo rechaza todo el lote antes de escribir, pero los conflictos de revisión se devuelven por mutación: la respuesta puede mezclar accepted y conflicts, y el cliente debe confirmar localmente solo las accepted. Cada mutación lleva un mutation_id propio del dispositivo que la hace idempotente (reenviar el mismo id devuelve la misma respuesta; reutilizarlo con un contenido distinto devuelve 409 mutation_id_reused). base_revision 0 crea; cualquier otro valor exige coincidir con la revisión del servidor, y si no coincide la mutación vuelve en conflicts con code revision_conflict o entity_deleted y el estado vivo del servidor en server_payload. Necesita secrets:write, secrets:read y values:read: server_payload mezcla valor vivo y metadatos del secreto. Requiere también X-Master-Key.
Cuerpo (JSON)
| device_id* | string | Identificador estable del dispositivo (máx. 128). |
| mutations* | array | De 1 a 100 mutaciones. |
| mutations[].mutation_id* | string | Identificador único de la mutación en ese dispositivo. |
| mutations[].entity_type* | string | Siempre "secret". |
| mutations[].entity_id* | string | UUID del secreto (lo elige el cliente al crear). |
| mutations[].operation* | string | "upsert" o "delete". |
| mutations[].base_revision* | integer | 0 para crear; si no, la revisión que el cliente tiene. |
| mutations[].payload | object | secret_type, name, url, notes, value e identifiers (máximo 100). secret_type no puede cambiar tras la creación. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/sync/push/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>" \
-H "Content-Type: application/json" \
-d '{
"device_id": "desktop-ada",
"mutations": [
{
"mutation_id": "8f14e45f-ceea-467a-9cf5-9a3f1d2b7c10",
"entity_type": "secret",
"entity_id": "a1b2c3d4-0000-1111-2222-333344445555",
"operation": "upsert",
"base_revision": 1,
"payload": {
"value": "nuevo-valor"
}
}
]
}'Respuesta
{
"accepted": [
{
"mutation_id": "8f14e45f-ceea-467a-9cf5-9a3f1d2b7c10",
"entity_type": "secret",
"entity_id": "a1b2c3d4-0000-1111-2222-333344445555",
"operation": "upsert",
"revision": 2
}
],
"conflicts": [],
"cursor": 42
}Identificadores
Pares clave-valor asociados a un secreto (p. ej. usuario, email, nota).
/api/v1/identifiers//api/v1/identifiers//api/v1/identifiers/{uuid}//api/v1/identifiers/{uuid}//api/v1/identifiers/{uuid}//api/v1/identifiers/{uuid}//api/v1/identifiers/Listar identificadores.
Petición
curl -X GET "https://api.passfortress.com/api/v1/identifiers/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"uuid": "id1",
"secret": "a1b2c3d4-0000-1111-2222-333344445555",
"key": "usuario",
"value": "ada@example.com"
}
]
}/api/v1/identifiers/Crear un identificador.
Cuerpo (JSON)
| secret* | string | UUID del secreto. |
| key | string | Clave. |
| value* | string | Valor. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/identifiers/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"secret": "a1b2c3d4-0000-1111-2222-333344445555",
"key": "usuario",
"value": "ada@example.com"
}'Respuesta
{
"uuid": "id1",
"secret": "a1b2c3d4-0000-1111-2222-333344445555",
"key": "usuario",
"value": "ada@example.com"
}/api/v1/identifiers/{uuid}/Obtener un identificador.
Parámetros de ruta
| uuid* | string | UUID del identificador. |
Petición
curl -X GET "https://api.passfortress.com/api/v1/identifiers/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
{
"uuid": "id1",
"secret": "a1b2c3d4-0000-1111-2222-333344445555",
"key": "usuario",
"value": "ada@example.com"
}/api/v1/identifiers/{uuid}/Reemplazar un identificador.
Parámetros de ruta
| uuid* | string | UUID del identificador. |
Cuerpo (JSON)
| key | string | Clave. |
| value* | string | Valor. |
Petición
curl -X PUT "https://api.passfortress.com/api/v1/identifiers/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"key": "usuario",
"value": "ada.lovelace@example.com"
}'Respuesta
{
"uuid": "id1",
"secret": "a1b2c3d4-0000-1111-2222-333344445555",
"key": "usuario",
"value": "ada.lovelace@example.com"
}/api/v1/identifiers/{uuid}/Actualizar parcialmente un identificador.
Parámetros de ruta
| uuid* | string | UUID del identificador. |
Cuerpo (JSON)
| value | string | Nuevo valor. |
Petición
curl -X PATCH "https://api.passfortress.com/api/v1/identifiers/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"value": "nuevo-valor"
}'Respuesta
{
"uuid": "id1",
"secret": "a1b2c3d4-0000-1111-2222-333344445555",
"key": "usuario",
"value": "nuevo-valor"
}/api/v1/identifiers/{uuid}/Eliminar un identificador.
Devuelve 204 No Content sin cuerpo. Crear, editar o reasignar un identificador no pide master key, pero borrarlo sí: es irreversible y no hay papelera, igual que en DELETE /secrets/{uuid}/. Sin la cabecera X-Master-Key responde 428 master_key_required y con una master key incorrecta 403 master_key_invalid.
Parámetros de ruta
| uuid* | string | UUID del identificador. |
Petición
curl -X DELETE "https://api.passfortress.com/api/v1/identifiers/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>"Respuesta
"204 No Content (sin cuerpo)"Sitios web
Sitios web vinculados a los secretos (para favicons y autocompletado).
/api/v1/websites/Listar sitios web.
Petición
curl -X GET "https://api.passfortress.com/api/v1/websites/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"uuid": "w1a2b3c4-0000-1111-2222-333344445555",
"hostname": "github.com",
"login_url": "https://github.com/login",
"automatic_password_change": false
}
]
}/api/v1/websites/Registrar un sitio web.
Idempotente: si el host ya existe devuelve la misma fila, y en ambos casos responde 201, así que la respuesta no revela si alguien más ya lo tenía guardado. hostname es el único campo que aporta quien llama; login_url y automatic_password_change son de solo lectura (el catálogo no tiene dueño, así que nadie puede sembrar campos en los metadatos de otra cuenta).
Cuerpo (JSON)
| hostname* | string | Host del sitio (p. ej. github.com). |
Petición
curl -X POST "https://api.passfortress.com/api/v1/websites/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"hostname": "github.com"
}'Respuesta
{
"uuid": "w1a2b3c4-0000-1111-2222-333344445555",
"hostname": "github.com",
"login_url": "https://github.com/login",
"automatic_password_change": false
}Espacios de trabajo
Bóvedas compartidas. Toda la API existente ya opera sobre un Espacio con la cabecera X-Workspace-Id (ausente = Personal); estos endpoints administran los propios Espacios y su lista de miembros. La API key necesita workspaces:read / workspaces:write, y las operaciones que tocan material de clave (crear, invitar, editar o expulsar) necesitan además X-Master-Key.
/api/v1/workspaces//api/v1/workspaces//api/v1/workspaces/{uuid}//api/v1/workspaces/{uuid}//api/v1/workspaces/{uuid}//api/v1/workspaces/{uuid}//api/v1/workspaces/{uuid}/members//api/v1/workspaces/{uuid}/members//api/v1/workspaces/{uuid}/members/{m_uuid}//api/v1/workspaces/{uuid}/members/{m_uuid}//api/v1/workspaces/{uuid}/members/sync//api/v1/workspaces/{uuid}/resume-key-rotation//api/v1/workspaces/{uuid}/leave//api/v1/workspaces/Listar los Espacios del usuario (incluido el Personal).
Array plano, sin paginar. El Personal aparece siempre el primero con is_personal: true; su uuid es también el valor "personal" de X-Workspace-Id (o simplemente omite la cabecera). permissions es la matriz efectiva de tu membresía.
Petición
curl -X GET "https://api.passfortress.com/api/v1/workspaces/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
[
{
"uuid": "11112222-3333-4444-5555-666677778888",
"name": "Personal",
"is_personal": true,
"is_owner": true,
"role": "admin",
"status": "active",
"permissions": {
"passwords": "edit_delete",
"envfiles": "edit_delete",
"totp": "edit_delete",
"containers": "edit_delete",
"shared": true
}
},
{
"uuid": "9a8b7c6d-1111-2222-3333-444455556666",
"name": "Equipo de infraestructura",
"is_personal": false,
"is_owner": true,
"role": "admin",
"status": "active",
"permissions": {
"passwords": "edit_delete",
"envfiles": "edit_delete",
"totp": "edit_delete",
"containers": "edit_delete",
"shared": true
}
}
]/api/v1/workspaces/Crear un Espacio compartido.
Genera la clave del Espacio y la sella bajo tu clave pública, por lo que requiere X-Master-Key. El creador queda como administrador.
Cuerpo (JSON)
| name* | string | Nombre del Espacio (máx. 64 caracteres). |
Petición
curl -X POST "https://api.passfortress.com/api/v1/workspaces/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>" \
-H "Content-Type: application/json" \
-d '{
"name": "Equipo de infraestructura"
}'Respuesta
{
"uuid": "9a8b7c6d-1111-2222-3333-444455556666",
"name": "Equipo de infraestructura",
"is_personal": false,
"is_owner": true,
"role": "admin",
"status": "active",
"permissions": {
"passwords": "edit_delete",
"envfiles": "edit_delete",
"totp": "edit_delete",
"containers": "edit_delete",
"shared": true
}
}/api/v1/workspaces/{uuid}/Obtener un Espacio.
Parámetros de ruta
| uuid* | string | uuid del Espacio. |
Petición
curl -X GET "https://api.passfortress.com/api/v1/workspaces/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
{
"uuid": "9a8b7c6d-1111-2222-3333-444455556666",
"name": "Equipo de infraestructura",
"is_personal": false,
"is_owner": true,
"role": "admin",
"status": "active",
"permissions": {
"passwords": "edit_delete",
"envfiles": "edit_delete",
"totp": "edit_delete",
"containers": "edit_delete",
"shared": true
}
}/api/v1/workspaces/{uuid}/Renombrar un Espacio (solo administradores).
name es obligatorio. El Espacio Personal se puede renombrar, pero no eliminar.
Parámetros de ruta
| uuid* | string | uuid del Espacio. |
Cuerpo (JSON)
| name* | string | Nuevo nombre. |
Petición
curl -X PATCH "https://api.passfortress.com/api/v1/workspaces/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Infraestructura"
}'Respuesta
{
"uuid": "9a8b7c6d-1111-2222-3333-444455556666",
"name": "Infraestructura",
"is_personal": false,
"is_owner": true,
"role": "admin",
"status": "active",
"permissions": {
"passwords": "edit_delete",
"envfiles": "edit_delete",
"totp": "edit_delete",
"containers": "edit_delete",
"shared": true
}
}/api/v1/workspaces/{uuid}/Renombrar un Espacio reemplazando el recurso (alias soportado).
Mismo contrato de nombre y permisos que PATCH; name es obligatorio.
Parámetros de ruta
| uuid* | string | uuid del Espacio. |
Cuerpo (JSON)
| name* | string | Nuevo nombre. |
Petición
curl -X PUT "https://api.passfortress.com/api/v1/workspaces/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Infraestructura"
}'Respuesta
{
"uuid": "9a8b7c6d-1111-2222-3333-444455556666",
"name": "Infraestructura",
"is_personal": false,
"is_owner": true,
"role": "admin",
"status": "active",
"permissions": {
"passwords": "edit_delete",
"envfiles": "edit_delete",
"totp": "edit_delete",
"containers": "edit_delete",
"shared": true
}
}/api/v1/workspaces/{uuid}/Eliminar un Espacio (solo administradores).
Responde 204 sin cuerpo. El Espacio Personal no se puede eliminar (400 workspace_not_deletable). Es la operación más destructiva de la API: arrastra en cascada TODOS los secretos, contenedores, identificadores, historiales de valores y archivos del Espacio, para todos sus miembros, y no hay papelera. Por eso exige X-Master-Key igual que DELETE /secrets/{uuid}/: sin la cabecera responde 428 master_key_required y con una master key incorrecta 403 master_key_invalid. Si no eres administrador del Espacio responde 403 not_workspace_member antes de mirar la master key.
Parámetros de ruta
| uuid* | string | uuid del Espacio. |
Petición
curl -X DELETE "https://api.passfortress.com/api/v1/workspaces/{uuid}/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>"Respuesta
null/api/v1/workspaces/{uuid}/members/Listar los miembros de un Espacio (solo administradores).
Array plano, sin paginar. Un miembro no administrador recibe 403 not_workspace_member: la lista contiene correos de terceros e invitaciones pendientes.
Parámetros de ruta
| uuid* | string | uuid del Espacio. |
Petición
curl -X GET "https://api.passfortress.com/api/v1/workspaces/{uuid}/members/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
[
{
"uuid": "aaaabbbb-cccc-dddd-eeee-ffff00001111",
"email": "colega@example.com",
"display_name": "Grace Hopper",
"role": "custom",
"permissions": {
"passwords": "edit_no_delete",
"envfiles": "read",
"totp": "read",
"containers": "read",
"shared": false
},
"status": "active",
"is_owner": false,
"is_self": false,
"invited_email": null,
"expires_at": null
}
]/api/v1/workspaces/{uuid}/members/Invitar a un miembro por correo (solo administradores).
Sin aceptación por parte del invitado: el acceso es inmediato si la dirección ya tiene cuenta (201) y queda pendiente 30 días si no (status: pending, se resuelve solo o con members/sync/). Sella la clave del Espacio bajo la clave pública del invitado, así que requiere X-Master-Key (pero NUNCA la del invitado). permissions solo se acepta con role: "custom".
Parámetros de ruta
| uuid* | string | uuid del Espacio. |
Cuerpo (JSON)
| email* | string | Correo de la persona invitada. |
| role | string | admin, guest (por defecto) o custom. |
| permissions | object | Matriz {passwords, envfiles, totp, containers} con nivel read | create | edit_no_delete | edit_delete, más shared (boolean). Solo con role: "custom". |
Petición
curl -X POST "https://api.passfortress.com/api/v1/workspaces/{uuid}/members/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>" \
-H "Content-Type: application/json" \
-d '{
"email": "colega@example.com",
"role": "custom",
"permissions": {
"passwords": "edit_no_delete",
"envfiles": "read",
"totp": "read",
"containers": "read",
"shared": false
}
}'Respuesta
{
"uuid": "aaaabbbb-cccc-dddd-eeee-ffff00001111",
"email": "colega@example.com",
"display_name": "Grace Hopper",
"role": "custom",
"permissions": {
"passwords": "edit_no_delete",
"envfiles": "read",
"totp": "read",
"containers": "read",
"shared": false
},
"status": "active",
"is_owner": false,
"is_self": false,
"invited_email": null,
"expires_at": null
}/api/v1/workspaces/{uuid}/members/{m_uuid}/Editar el rol / los permisos de un miembro, o re-incluir a uno expulsado.
Solo administradores. Para tocar permissions hay que enviar role: "custom" (si no, 400 permissions_requires_custom_role). Degradar al último administrador responde 409 last_admin.
Parámetros de ruta
| uuid* | string | uuid del Espacio. |
| m_uuid* | string | uuid de la membresía. |
Cuerpo (JSON)
| role | string | admin, guest o custom. |
| permissions | object | Matriz de permisos (solo con role: "custom"). |
Petición
curl -X PATCH "https://api.passfortress.com/api/v1/workspaces/{uuid}/members/{m_uuid}/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>" \
-H "Content-Type: application/json" \
-d '{
"role": "admin"
}'Respuesta
{
"uuid": "aaaabbbb-cccc-dddd-eeee-ffff00001111",
"email": "colega@example.com",
"display_name": "Grace Hopper",
"role": "admin",
"permissions": {
"passwords": "edit_no_delete",
"envfiles": "read",
"totp": "read",
"containers": "read",
"shared": false
},
"status": "active",
"is_owner": false,
"is_self": false,
"invited_email": null,
"expires_at": null
}/api/v1/workspaces/{uuid}/members/{m_uuid}/Expulsar a un miembro (solo administradores).
Responde 204. Expulsar ROTA la clave del Espacio (revocación real), por lo que requiere X-Master-Key. Expulsar al último administrador responde 409 last_admin.
Parámetros de ruta
| uuid* | string | uuid del Espacio. |
| m_uuid* | string | uuid de la membresía. |
Petición
curl -X DELETE "https://api.passfortress.com/api/v1/workspaces/{uuid}/members/{m_uuid}/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>"Respuesta
null/api/v1/workspaces/{uuid}/members/sync/Resolver las invitaciones pendientes del Espacio.
Sella la clave del Espacio para los invitados que ya tienen cuenta y aún estaban pendientes. Requiere ser administrador y X-Master-Key. Devuelve cuántos se resolvieron.
Parámetros de ruta
| uuid* | string | uuid del Espacio. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/workspaces/{uuid}/members/sync/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>"Respuesta
{
"resolved": 1
}/api/v1/workspaces/{uuid}/resume-key-rotation/Reanudar una rotación de clave pendiente.
Idempotente y solo para administradores. Reintenta una revocación cuya expulsión ya se confirmó, aunque la fila del miembro expulsado ya no exista. Requiere X-Master-Key y responde 204.
Parámetros de ruta
| uuid* | string | uuid del Espacio. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/workspaces/{uuid}/resume-key-rotation/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "X-Master-Key: <tu-master-key>"Respuesta
null/api/v1/workspaces/{uuid}/leave/Salir de un Espacio.
Responde 204. No rota la clave del Espacio (salir no es una revocación). El último administrador no puede salir (409 last_admin) y del Personal no se puede salir (400 workspace_not_leavable).
Parámetros de ruta
| uuid* | string | uuid del Espacio. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/workspaces/{uuid}/leave/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
nullAPI keys
Gestión programática de las propias API keys. Normalmente se administran desde el panel web; usar estos endpoints requiere el scope apikeys:*.
/api/v1/api-keys//api/v1/api-keys//api/v1/api-keys/{public_id}//api/v1/api-keys/{public_id}//api/v1/api-keys/{public_id}/revoke//api/v1/api-keys/{public_id}/rotate//api/v1/api-keys/Listar las API keys.
Petición
curl -X GET "https://api.passfortress.com/api/v1/api-keys/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"public_id": "key_abc123",
"name": "CI deploy",
"prefix": "pf_live_",
"last4": "a1b2",
"masked": "pf_live_…a1b2",
"scopes": [
"secrets:read",
"values:read"
],
"is_active": true,
"expires_at": null,
"last_used_at": "2026-06-20T08:00:00Z",
"created_at": "2026-05-01T10:00:00Z",
"revoked_at": null,
"rate_tier": "default",
"ip_allowlist": [],
"environment": "live"
}
]
}/api/v1/api-keys/Crear una API key (el token se muestra una sola vez).
La respuesta añade el campo token (texto plano) que NO se vuelve a mostrar.
Cuerpo (JSON)
| name* | string | Nombre descriptivo. |
| scopes | array | Lista de scopes. |
| environment | string | live o test. |
| rate_tier | string | default, high o unlimited. |
| expires_at | string | Fecha de expiración ISO-8601 (opcional). |
| ip_allowlist | array | Allowlist de IPs/CIDR (opcional). |
Petición
curl -X POST "https://api.passfortress.com/api/v1/api-keys/" \
-H "Authorization: Bearer pf_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "CI deploy",
"scopes": [
"secrets:read",
"values:read"
],
"environment": "live",
"rate_tier": "default"
}'Respuesta
{
"public_id": "key_abc123",
"name": "CI deploy",
"prefix": "pf_live_",
"last4": "a1b2",
"masked": "pf_live_…a1b2",
"scopes": [
"secrets:read",
"values:read"
],
"is_active": true,
"expires_at": null,
"last_used_at": null,
"created_at": "2026-05-01T10:00:00Z",
"revoked_at": null,
"rate_tier": "default",
"ip_allowlist": [],
"environment": "live",
"token": "pf_live_3f9a2c8e7b1d4f0a6c5e9d8b7a1f2e3c"
}/api/v1/api-keys/{public_id}/Obtener una API key.
Parámetros de ruta
| public_id* | string | Identificador público de la clave. |
Petición
curl -X GET "https://api.passfortress.com/api/v1/api-keys/{public_id}/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
{
"public_id": "key_abc123",
"name": "CI deploy",
"prefix": "pf_live_",
"last4": "a1b2",
"masked": "pf_live_…a1b2",
"scopes": [
"secrets:read",
"values:read"
],
"is_active": true,
"expires_at": null,
"last_used_at": "2026-06-20T08:00:00Z",
"created_at": "2026-05-01T10:00:00Z",
"revoked_at": null,
"rate_tier": "default",
"ip_allowlist": [],
"environment": "live"
}/api/v1/api-keys/{public_id}/Eliminar permanentemente una API key.
Responde 204 sin cuerpo. Para invalidarla conservando sus metadatos, usa revoke.
Parámetros de ruta
| public_id* | string | Identificador público de la clave. |
Petición
curl -X DELETE "https://api.passfortress.com/api/v1/api-keys/{public_id}/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
null/api/v1/api-keys/{public_id}/revoke/Revocar una API key.
Devuelve la clave actualizada (is_active = false, revoked_at fijado).
Parámetros de ruta
| public_id* | string | Identificador público de la clave. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/api-keys/{public_id}/revoke/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
{
"public_id": "key_abc123",
"name": "CI deploy",
"prefix": "pf_live_",
"last4": "a1b2",
"masked": "pf_live_…a1b2",
"scopes": [
"secrets:read",
"values:read"
],
"is_active": false,
"expires_at": null,
"last_used_at": "2026-06-20T08:00:00Z",
"created_at": "2026-05-01T10:00:00Z",
"revoked_at": "2026-06-26T12:00:00Z",
"rate_tier": "default",
"ip_allowlist": [],
"environment": "live"
}/api/v1/api-keys/{public_id}/rotate/Rotar una API key (genera un token nuevo).
Devuelve los campos de lectura más un token de un solo uso.
Parámetros de ruta
| public_id* | string | Identificador público de la clave. |
Petición
curl -X POST "https://api.passfortress.com/api/v1/api-keys/{public_id}/rotate/" \
-H "Authorization: Bearer pf_live_xxx"Respuesta
{
"public_id": "key_abc123",
"name": "CI deploy",
"prefix": "pf_live_",
"last4": "c5e9",
"masked": "pf_live_…c5e9",
"scopes": [
"secrets:read",
"values:read"
],
"is_active": true,
"expires_at": null,
"last_used_at": "2026-06-20T08:00:00Z",
"created_at": "2026-05-01T10:00:00Z",
"revoked_at": null,
"rate_tier": "default",
"ip_allowlist": [],
"environment": "live",
"token": "pf_live_9d8b7a1f2e3c3f9a2c8e7b1d4f0a6c5e"
}Toda respuesta de error tiene la forma { "detail": "…", "code": "…" }. Usa el code (no el texto) para ramificar tu lógica.
| code | HTTP | Causa | Solución |
|---|---|---|---|
invalid_api_key | 401 | La API key no existe o está mal formada. | Revisa la cabecera Authorization y usa una clave válida (pf_live_ / pf_test_). |
api_key_revoked | 401 | La clave fue revocada desde el panel o de forma automática tras acumular demasiados intentos fallidos de master key (auto-revoke por fuerza bruta). | Crea una nueva API key en /developers/api-keys. |
api_key_expired | 401 | La clave superó su fecha de expiración. | Rota la clave o crea una nueva sin expiración. |
ip_not_allowed | 401 | La petición proviene de una IP fuera del allowlist de la clave. | Añade la IP de origen al allowlist de la clave o emite la petición desde una IP permitida. |
insufficient_scope | 403 | La clave no tiene el scope que exige el endpoint. | Crea o rota la clave incluyendo el scope necesario (ver tabla de scopes). |
jwt_session_required | 403 | El endpoint modifica credenciales, sesión o material de recuperación y solo acepta una sesión JWT interactiva. Una API key, aunque tenga profile:write, no puede ejecutar esta operación. | Realiza la operación desde una sesión de usuario con Authorization: Bearer <access-token-jwt>. |
master_key_required | 428 | Operación sobre un valor sin la cabecera X-Master-Key. | Reenvía la petición incluyendo X-Master-Key con la master key del usuario. |
master_key_invalid | 403 | La master key enviada no valida contra el test_passwd del usuario. | Comprueba que la master key es exactamente la del propietario de la bóveda. |
master_key_locked | 429 | Demasiados intentos fallidos de master key (umbral por usuario, por defecto 10/hora); el endpoint queda bloqueado temporalmente con backoff exponencial. | Espera el tiempo indicado en la cabecera Retry-After y reintenta con la master key correcta (una validación correcta resetea el contador). |
master_key_guard_unavailable | 503 | El almacén donde el servidor cuenta los intentos fallidos de master key no admite escrituras, así que no puede aplicarse la protección anti fuerza bruta. El servidor falla en cerrado y rechaza por igual una master key correcta y una incorrecta: la respuesta no dice nada sobre la clave enviada. | No es un problema de tu clave ni de tus scopes: reintenta con backoff en unos minutos. Si persiste, es una incidencia del servicio. |
not_authenticated | 401 | Falta toda credencial (ni API key ni JWT). | Incluye la cabecera Authorization: Bearer <token>. |
invalid_credentials | 401 | El usuario/correo o la contraseña del inicio de sesión no son válidos. | Revisa las credenciales y vuelve a iniciar sesión. |
no_customer | 403 | La identidad autenticada no tiene un perfil Customer utilizable. | Contacta con soporte para reparar el perfil de la cuenta. |
refresh_required | 400 | La revocación de sesión no incluyó el refresh token. | Envía el campo refresh con el token de renovación de la sesión. |
session_revoked | 401 | El JWT pertenece a una sesión revocada (restablecimiento o cambio de contraseña, cambio de correo verificado, cierre del resto de sesiones). Afecta tanto al access token como al refresh, así que /auth/token/refresh/ también responde 401. | No reintentes: descarta ambos tokens y vuelve a autenticar al usuario en /auth/login/. |
invalid_google_token | 400 | La credencial de Google no es válida. | Obtén una credencial nueva desde Google Identity Services. |
google_verification_unavailable | 503 | No se pudo verificar temporalmente la credencial contra Google. | Reintenta con backoff; no trates el token como definitivamente inválido. |
google_no_account | 404 | El acceso con Google no encontró una cuenta y el alta no estaba permitida. | Registra la cuenta o habilita allow_signup. |
account_disabled | 403 | La cuenta asociada está desactivada. | Contacta con soporte; reintentar no la reactiva. |
google_ambiguous_account | 409 | Varias cuentas legacy coinciden con la identidad de Google. | Solicita una deduplicación administrativa antes de enlazar. |
google_identity_conflict | 409 | La identidad de Google ya está enlazada de forma incompatible. | Recupera la cuenta ya enlazada o contacta con soporte. |
google_merge_requires_recovery | 409 | La fusión no puede conservar con seguridad el segundo factor existente. | Entra con contraseña y 2FA o completa la recuperación antes de enlazar Google. |
master_key_already_set | 409 | Se intentó configurar una master key inicial en una cuenta que ya tiene una. | Usa el flujo de cambio de master key, no el de configuración inicial. |
invalid_verification_code | 400 | El código de verificación de correo/móvil es incorrecto, expiró o ya se usó. | Solicita un código nuevo y vuelve a confirmarlo. |
recovery_throttled | 429 | Se agotó el presupuesto de intentos de recuperación. | Respeta Retry-After antes de volver a intentarlo. |
invalid_recovery_code | 400 | El código de recuperación de contraseña es incorrecto o expiró. | Solicita un flujo de recuperación nuevo. |
invalid_challenge | 401 | El desafío de 2FA enviado a /auth/login/2fa/ no existe, caducó (5 min), ya se usó o lo superó un inicio de sesión posterior. | Vuelve a llamar a /auth/login/ para obtener un desafío nuevo. |
invalid_tfa_code | 401 | El código de 6 dígitos no es correcto. El desafío sigue vivo hasta agotar sus 5 intentos. | Pide al usuario el código correcto y reintenta con el MISMO challenge. |
invalid_tfa_recovery_code | 401 | El código de recuperación enviado a /auth/login/2fa/recovery/ no existe o ya se usó (cada uno sirve una sola vez). Cuenta contra los mismos 5 intentos del desafío. Es un code DISTINTO de invalid_recovery_code, que pertenece al restablecimiento de contraseña. | Prueba con otro de los códigos guardados, o completa el inicio de sesión con el código enviado por correo/SMS. |
no_tfa_recovery_codes | 401 | La cuenta tiene un segundo factor activo pero no tiene ningún código de recuperación (típico de cuentas que activaron el 2FA antes de que existieran). No gasta intento. | Entra con el código enviado por correo/SMS y genera un juego en POST /profile/tfa/recovery-codes/. |
tfa_not_enabled | 409 | Se pidieron códigos de recuperación en /profile/tfa/recovery-codes/ para una cuenta sin segundo factor activo. | Activa el 2FA con PATCH /profile/ (tfa_policy): la respuesta ya trae un juego de códigos. |
tfa_channel_unverified | 400 | PATCH /profile/ intentó activar un tfa_policy cuyo canal (correo o móvil) todavía no está verificado. Armar ese factor crearía un segundo factor cuyo código nunca llegaría. | Verifica antes el canal (POST /auth/verify/request/ y /auth/verify/confirm/ para el correo) y repite el PATCH. |
sms_verification_disabled | 400 | El canal SMS está desactivado administrativamente en el servidor: POST /auth/verify/request/ con scope verify_mobile o verify_mobile_tfa, o PATCH /profile/ intentando armar tfa_policy "mobile". El número de móvil del perfil es solo un dato de contacto y no se verifica. | Usa el correo: verify_email para la verificación y tfa_policy "email" para la verificación en dos pasos. |
tfa_channel_incomplete | 400 | El canal del segundo factor que se quiere activar está incompleto: la cuenta no tiene guardado el dato que ese canal necesita (por ejemplo, tfa_policy "mobile" sin número de móvil). | Completa el dato del canal en PATCH /profile/ y vuelve a activar el tfa_policy. |
tfa_mobile_locked | 400 | Se intentó cambiar el móvil del perfil con la verificación en dos pasos por SMS activa. El número es el destino de ese factor, así que cambiarlo en caliente dejaría la cuenta sin segundo factor alcanzable. | Pon tfa_policy en disabled (o cámbialo a email), actualiza el móvil, verifícalo y vuelve a activar el 2FA por SMS. |
invalid_old_master_key | 400 | El cambio de master key envió una master key actual que no es correcta. Nada se ha re-cifrado. | Reenvía con la master key vigente. No reintentes a ciegas: los fallos consumen el presupuesto anti fuerza bruta y acaban en 429 master_key_locked. |
master_key_reencrypt_failed | 409 | El cambio de master key se abortó del lado del servidor al re-cifrar la bóveda (material de clave o almacenamiento). No es un error de lo que enviaste: la operación es atómica y se deshizo entera, así que la master key ANTERIOR sigue siendo válida y la bóveda está intacta. | No cambies de master key ni reintentes en bucle: sigue usando la anterior y revisa los secretos que no se lean bien con GET /secrets/{uuid}/value/ (secret_value_undecryptable señala cuáles). Es una incidencia del servicio. |
challenge_attempts_exceeded | 401 | Se agotaron los intentos del desafío de 2FA, que queda quemado. | Reinicia el inicio de sesión en /auth/login/. |
permission_denied | 403 | El recurso pertenece a otro usuario o la acción no está permitida. | Usa el uuid de un recurso que pertenezca al titular de la clave. |
not_found | 404 | El recurso no existe o no es visible para el titular. | Verifica el uuid / public_id de la ruta. |
validation_error | 400 | El cuerpo no cumple el esquema (campos faltantes o inválidos). | Revisa el detalle por campo en la respuesta y corrige el payload. |
invalid_rate_tier | 400 | Se intentó crear o rotar una clave con el scope values:read y el tier unlimited, combinación no permitida. | Elige el tier default o high para claves que puedan leer valores descifrados. |
invalid_scope | 400 | Se pidieron scopes que la propia API key que firma la petición no tiene. Una clave solo puede acuñar o rotar hacia un SUBCONJUNTO de sus scopes (solo se de-escala, nunca se escala). Desde una sesión JWT no hay esta restricción. | Pide únicamente scopes que la clave emisora ya posea, o crea la clave nueva desde la web (/developers/api-keys) con una sesión JWT. |
weak_master_key | 400 | La nueva master key (en registro o cambio de master key) no cumple la política mínima: ≥8 caracteres, no toda idéntica, sin secuencias triviales ni claves comunes. | Elige una master key más fuerte. Las master keys existentes mantienen su validez hasta que las cambies. |
unsupported_character | 400 | El texto enviado tiene caracteres que la columna no puede almacenar. El esquema procede de un volcado heredado y las columnas de texto de la bóveda son latin1: emojis, chino, cirílico, griego o árabe no caben. Afecta a cualquier campo de texto libre (nombre, notas y URL del secreto, nombre de archivo, clave y valor de un identifier, nombre y descripción de un contenedor, hostname de un sitio, dirección y móvil del perfil) y también al push de sincronización. | Quita esos caracteres del campo y reenvía. Los acentos y la ñ SÍ se admiten. El servidor nunca recorta ni transcribe el valor en silencio: o se guarda tal cual, o se rechaza. |
throttled | 429 | Se superó un límite de peticiones. Puede ser el de la clave (tier default/high) o uno de los presupuestos por operación: compartir secretos, 60/hora; y las lecturas que descifran la bóveda ENTERA en una sola petición (GET /secrets/export/, POST /secrets/import/, POST /secrets/import-backup/ y POST /sync/bootstrap/), 10/hora. Estas últimas cuestan varios segundos de CPU cada una, así que su presupuesto es deliberadamente pequeño. | Respeta la cabecera Retry-After y reintenta tras la ventana. Para sincronizar, usa GET /sync/pull/ con cursor en vez de repetir el bootstrap; para leer valores sueltos, GET /secrets/{uuid}/value/ en vez de exportar la bóveda. Subir de tier no levanta los límites por operación. |
unknown_field | 400 | El cuerpo incluye campos que el endpoint no admite (nombre mal escrito, campo de solo lectura o campo de otra versión). PATCH /profile/ y PATCH /profile/theme/ los rechazan en vez de descartarlos en silencio. | Envía solo los campos documentados del endpoint; el detalle de la respuesta enumera los no reconocidos. |
email_change_requires_verification | 400 | Se envió email a PATCH /profile/. El correo de la cuenta es el destino de la recuperación, así que no se cambia en un solo paso. | Usa POST /profile/email/ (envía un código a la dirección nueva) y luego POST /profile/email/confirm/ con ese código. |
invalid_account_password | 400 | La contraseña de la cuenta enviada en POST /profile/email/ no es correcta. Se exige porque demostrar el buzón nuevo no demuestra quién lo pidió. | Reenvía la petición con la contraseña actual de la cuenta. Las cuentas sin contraseña utilizable (alta con Google) están exentas. |
email_unchanged | 400 | La dirección solicitada ya es la de la cuenta (la comparación es sobre la forma canónica, en minúsculas). | Elige una dirección distinta. |
email_already_registered | 400 | Otra cuenta ya tiene esa dirección. También se devuelve al confirmar si la dirección se ocupó entre la solicitud y la confirmación. | Elige otra dirección; auth_user.email es único. |
email_not_distinguishable | 409 | La dirección se pliega sobre otra ya registrada: la comparación del almacén ignora mayúsculas Y acentos, así que un homógrafo (victim@gmaíl.com) no puede distinguirse de victim@gmail.com. El servidor falla en cerrado en vez de resolverla sobre la cuenta ajena. | Usa una dirección que no sea un homógrafo de otra ya existente. |
no_pending_email_change | 400 | Se llamó a POST /profile/email/confirm/ sin una solicitud viva: nunca se pidió, se canceló o caducó (10 minutos). | Vuelve a empezar con POST /profile/email/. |
invalid_email_change_code | 400 | El código es incorrecto, caducó o ya se usó. El correo de la cuenta no se ha tocado. | Pide un código nuevo con POST /profile/email/ (invalida el anterior). |
verification_throttled | 429 | Se agotó el presupuesto de códigos de la cuenta (compartido entre /auth/verify/request/, el desafío 2FA y POST /profile/email/). | Espera lo que indique Retry-After. Una solicitud viva NO se pierde por este 429. |
not_workspace_member | 403 | La cabecera X-Workspace-Id apunta a un Espacio del que ya no eres miembro, o la operación exige ser administrador del Espacio y no lo eres. | Usa el uuid de un Espacio con membresía activa (GET /workspaces/ los enumera) o pide a un administrador que realice la operación. |
insufficient_workspace_permission | 403 | Tu membresía en ese Espacio no llega al nivel que exige la acción sobre ese recurso (matriz passwords/envfiles/totp/containers × read/create/edit_no_delete/edit_delete). | Pide a un administrador del Espacio que suba tu nivel para ese recurso. Es independiente de los scopes de la API key: se comprueban los dos. |
workspace_pending_sync | 409 | Tu invitación al Espacio existe pero aún no se ha resuelto: la clave del Espacio todavía no está sellada para ti. | Llama a POST /workspaces/{uuid}/members/sync/ (con la master key) o espera a que se resuelva; entonces reintenta. |
last_admin | 409 | La operación dejaría el Espacio sin ningún administrador (degradar, expulsar o salir siendo el último). | Nombra antes a otro administrador y repite la operación. |
self_invite | 400 | Se intentó invitar al propio titular de la cuenta al Espacio. | Invita a la dirección de otra persona; ya eres miembro. |
workspace_not_shareable | 400 | Se intentó invitar o sincronizar miembros en el Espacio Personal, que no tiene miembros. | Crea un Espacio compartido (POST /workspaces/) e invita ahí. |
workspace_not_leavable | 400 | Se intentó abandonar el Espacio Personal. | Cambia a otro Espacio; el Personal forma parte permanente de la cuenta. |
permissions_requires_custom_role | 400 | Se envió el objeto permissions junto a un role distinto de "custom". Los roles admin e invitado usan su preset fijo, así que el payload granular se habría descartado en silencio. | Envía role: "custom" con tus permisos granulares, o quita permissions y quédate con el preset del rol. |
workspace_gone | 409 | El Espacio se borró mientras la petición lo estaba escribiendo o rotando su clave (una carrera con DELETE /workspaces/{uuid}/). Antes salía como un 500 sin código. | No reintentes: el Espacio y sus secretos ya no existen. Recarga la lista con GET /workspaces/. |
sync_personal_only | 409 | Se llamó a /sync/bootstrap/, /sync/pull/ o /sync/push/ con la cabecera X-Workspace-Id. La sincronización offline solo cubre la bóveda Personal. | Repite la petición sin X-Workspace-Id. |
sync_secret_unreadable | 409 | Un secreto Personal no pudo descifrarse o no tiene su contenido almacenado, así que la instantánea no puede completarse. Si la master key ya no es la actual, la API responde master_key_invalid en vez de este código. | No reintentes en bucle. Abre el secreto afectado en la web para identificarlo; si persiste, el operador debe reparar o volver a cargar el contenido almacenado. |
mutation_id_reused | 409 | Un mutation_id ya usado por ese device_id se reenvió con un contenido distinto. El reenvío idéntico sí es idempotente y devuelve la respuesta original. | Usa un mutation_id nuevo para cada mutación distinta; reserva el reenvío del mismo id para reintentar exactamente la misma mutación. |
sync_receipt_quota_exceeded | 409 | La cuenta acumula más recibos de idempotencia de los permitidos. Cada mutación deja uno, incluidas las que terminan en conflicto y no cambian nada. | Resuelve los conflictos en vez de reenviarlos y espera a que la retención libere los recibos antiguos. |
secret_value_undecryptable | 409 | El secreto SÍ tiene cifrado almacenado, pero no descifra con la master key de esta petición: normalmente es un secreto compartido todavía pendiente de aceptar (está cifrado con la clave de un solo uso del emisor), o una rotación de master key que quedó a medias. Lo devuelven GET /secrets/{uuid}/value/, GET /secrets/{uuid}/history/ y GET /secrets/{uuid}/download/ en vez de un 200 con el valor vacío, que era indistinguible de un secreto sin contenido. | Si el secreto viene de GET /secrets/shared/, acéptalo primero con POST /secrets/accept-shared/ y vuelve a leerlo. Si no, comprueba que la master key es la actual del titular. No reintentes en bucle: sin ese paso el resultado no cambia. |
secret_storage_unavailable | 409 | La operación falló porque el CONTENIDO ALMACENADO del secreto falta o está corrupto, no porque la master key sea incorrecta. Lo pueden devolver GET /secrets/{uuid}/value/, GET /secrets/{uuid}/history-page/, GET /secrets/{uuid}/download/, POST /secrets/{uuid}/share/, POST /secrets/{uuid}/duplicate/ y PUT/PATCH /secrets/{uuid}/ cuando el secreto anuncia contenido almacenado pero la fila o el blob ya no existen. | No reintentes con otra master key: no cambia nada. El titular debe pedir al operador que ejecute repair_orphaned_file_secrets o repair_secretvalue_ownership sobre la cuenta, y volver a subir el contenido del secreto afectado. |
history_pagination_required | 413 | El array legacy de GET /secrets/{uuid}/history/ superaría el presupuesto seguro de una respuesta. | Recorre el historial completo con GET /secrets/{uuid}/history-page/ y su next_cursor. |
invalid_history_cursor | 400 | El cursor de history-page está mal formado, pertenece a otro secreto o ha caducado. | Descarta el cursor y vuelve a empezar por la primera página del mismo UUID. |
secret_pending_acceptance | 409 | Se intentó editar, duplicar o volver a compartir un secreto que todavía es una copia entrante pendiente de aceptar (shared=true). Su cifrado está bajo la clave de un solo uso del emisor, así que la operación no puede completarse con seguridad. | Acepta el secreto con POST /secrets/accept-shared/ (queda re-cifrado con tu master key) y repite la operación. |
share_reencrypt_failed | 409 | El servidor no pudo re-cifrar el secreto para la operación solicitada (compartir o aceptar) porque el valor actual no descifra con la clave aportada: el caso más común es un secreto compartido todavía pendiente de aceptar o una rotación de master key que quedó a medias. | Comprueba que la master key es la actual y que el secreto se lee bien con GET /secrets/{uuid}/value/ antes de compartirlo. Si el problema es almacenamiento faltante o corrupto, la API responde secret_storage_unavailable, no este código. |
export_master_key_stale | 409 | La master key cambió mientras /secrets/export/ estaba construyendo la copia, así que el servidor se niega a producir un backup mezclado. | Repite la exportación con la master key actual. |
export_decrypt_failed | 422 | Una contraseña de la bóveda Personal no pudo descifrarse durante /secrets/export/, así que exportar continuando produciría una copia con pérdida silenciosa. | Abre el secreto indicado por UUID y repara o vuelve a guardar su valor antes de exportar. |
export_storage_unavailable | 422 | Una contraseña de la bóveda Personal no tiene una revisión almacenada accesible para incluirla en /secrets/export/. | Repara la fila afectada o vuelve a guardar el secreto antes de exportar. |
export_too_large | 422 | La copia cifrada de /secrets/export/ superaría el presupuesto importable configurado del servidor (límites de subida y/o de miembro descomprimido del backup). | Reduce el volumen de la bóveda exportada o eleva el límite operativo antes de volver a exportar; si no, la copia no será reimportable. |
export_ambiguous_containers | 422 | El formato de backup v1 identifica carpetas por nombre y no puede restaurar de forma inequívoca un secreto vinculado a carpetas Personales homónimas. | Renombra las carpetas homónimas antes de exportar. Un formato v2 futuro conservará identificadores y enlaces estables. |
share_operation_conflict | 409 | El operation_id de share ya se usó con otro secreto, destinatario o huella de clave. | No reutilices operation_id entre operaciones. Para reintentar una respuesta perdida, reenvía exactamente el mismo objeto. |
share_operation_in_progress | 409 | Otra petición todavía prepara la misma operación idempotente. | Espera unos segundos y reintenta exactamente el mismo objeto, sin generar otra clave ni otro operation_id. |
workspace_rotation_pending | 409 | Una expulsión ya revocó al miembro, pero el recifrado completo del Espacio todavía no ha terminado; mientras tanto se bloquean escrituras y cambios de membresía. | Llama a POST /workspaces/{uuid}/resume-key-rotation/ para reanudar la rotación. Las lecturas de los miembros restantes continúan disponibles. |
workspace_key_unavailable | 409 | La petición apunta a un Espacio pero su workspace_key no pudo abrirse para ti (falta el par de claves, falta la clave sellada, o la master key no abre tu clave privada). También aparece si una rotación de la clave del Espacio se cruzó con la lectura. | Reintenta una vez; si persiste, llama a POST /workspaces/{uuid}/members/sync/ con la master key y comprueba que sigues siendo miembro del Espacio. |
workspace_share_unsupported | 409 | Se llamó a POST /secrets/{uuid}/share/ con la cabecera X-Workspace-Id. Compartir un secreto DESDE un Espacio todavía no está soportado: su valor está cifrado con la clave del Espacio, no con tu master key. | Repite la petición sin X-Workspace-Id (bóveda Personal). Para dar acceso dentro de un Espacio, invita a la persona como miembro. |
workspace_shared_unsupported | 409 | Se llamó a GET /secrets/shared/ o a POST /secrets/accept-shared/ con la cabecera X-Workspace-Id. Los secretos compartidos entrantes viven solo en la bóveda Personal. | Repite la petición sin X-Workspace-Id. |
workspace_export_unsupported | 409 | Se llamó a GET /secrets/export/ con la cabecera X-Workspace-Id. La exportación cubre solo la bóveda Personal. | Repite la petición sin X-Workspace-Id. |
workspace_import_unsupported | 409 | Se llamó a POST /secrets/import/ o POST /secrets/import-backup/ con la cabecera X-Workspace-Id. La importación escribe solo en la bóveda Personal. | Repite la petición sin X-Workspace-Id. |
identifiers_not_supported_for_type | 400 | Se enviaron identifiers (pares clave-valor) a un secreto cuyo tipo no puede guardarlos: solo password y totp los admiten, no envfile ni file. Lo devuelven POST y PATCH /secrets/{uuid}/ y también POST /sync/push/. Una lista VACÍA nunca se rechaza: sigue significando "desvincula los que haya" para cualquier tipo. | Quita identifiers del cuerpo para envfile y file, o guarda ese dato en el propio valor. Para limpiar los que ya existan, envía identifiers: []. |
unknown_container | 400 | Un uuid de container_uuids no existe en el espacio del secreto (borrado, mal escrito o de otra bóveda). Antes se descartaba en silencio y la respuesta era 201/200: en un PATCH eso era peor, porque containers es un REEMPLAZO y un solo uuid caducado desvinculaba el secreto de TODAS sus carpetas. Un uuid ajeno y uno inexistente dan la MISMA respuesta a propósito, para no convertir esto en un oráculo de existencia entre cuentas. | Relee las carpetas con GET /containers/ y reenvía solo uuids vigentes de la misma bóveda (la cabecera X-Workspace-Id debe ser la del secreto). El detalle de la respuesta enumera los uuids rechazados. |
secret_type_immutable | 400 | Un PATCH /secrets/{uuid}/ envió un secret_type distinto del que ya tiene el secreto. El tipo determina dónde se guarda el cifrado, así que no puede cambiarse después de crearlo. Reenviar el MISMO tipo sí se acepta (no pide ningún cambio). | Crea un secreto nuevo del tipo que necesites y borra el anterior; o quita secret_type del cuerpo del PATCH. |
import_master_key_mismatch | 422 | La copia de seguridad de POST /secrets/import-backup/ se hizo con una master key distinta de la actual, así que no se puede descifrar con la que envías. | Reenvía la petición añadiendo el campo old_master_key con la master key que estaba vigente cuando se generó la copia; los valores se re-cifran con la actual al insertarlos. |
import_file_too_large | 413 | El CSV de POST /secrets/import/ o el .zip de POST /secrets/import-backup/ supera su límite de subida en bruto. Para CSV se usa el menor límite configurado entre archivo y backup; para .zip, el límite de subida de backup. Se decide antes de leer, decodificar o descomprimir. | Reduce el fichero y vuelve a subirlo. Un miembro .zip que supera el límite descomprimido no devuelve este 413: se rechaza como 400 import_bad_file. |
import_master_key_stale | 400 | La master key actual de la cuenta cambió entre la validación de la petición y el inicio de POST /secrets/import-backup/. | Repite la importación con la master key actual. |
import_bad_file | 400 | El .zip de POST /secrets/import-backup/ no es un backup PassFortress legible: zip inválido, entradas ausentes o ilegibles, manifest corrupto, vault.enc malformado, demasiados miembros o un miembro que supera el límite descomprimido. | Vuelve a exportar el backup desde PassFortress y sube el .zip original sin modificar. El límite de expansión forma parte de este 400, no del 413 import_file_too_large. |
import_bad_version | 400 | El backup tiene un formato PassFortress reconocido pero una versión que este servidor no soporta. | Importa la copia en una versión compatible o genera un backup nuevo desde esta instancia. |
import_decrypt_failed | 400 | El backup no pudo descifrarse con la master key proporcionada, o su payload descifrado está corrupto. | Comprueba old_master_key si el backup es antiguo; si persiste, vuelve a generar la copia. |
import_too_many_entries | 400 | El backup declara más secretos que el máximo por restauración. | Divide la copia en lotes más pequeños o eleva el límite operativo antes de importarla. |
file_too_large | 413 | El file_content_b64 supera el límite efectivo publicado por /capabilities/ (100 MiB como techo; MAX_FILE_SIZE puede reducirlo). Se mide sobre el contenido DECODIFICADO antes de descodificar, cifrar o bloquear. | Sube un fichero más pequeño. Elevar el techo exige otro transporte; no basta con cambiar nginx. |
value_too_large | 413 | El campo value, el campo notes o la suma value + notes supera el límite efectivo publicado por /capabilities/ (100 MiB como techo; MAX_FILE_SIZE puede reducirlo). El backend mide el payload agregado antes de cifrar. | Envía contenido más pequeño; para un .env grande, considera un secreto de tipo file. |
import_too_many_rows | 400 | El CSV enviado a POST /secrets/import/ declara más filas que el máximo por importación (MAX_BACKUP_ENTRIES, 10000). La importación es atómica para todo el fichero: las filas válidas se confirman juntas y un fallo de la operación no deja un prefijo parcialmente importado. | Divide el CSV en ficheros de como mucho 10000 filas y súbelos por separado. El rechazo ocurre antes de escribir ninguna fila, así que no hay nada que deshacer. |
import_bad_csv | 400 | El fichero enviado a POST /secrets/import/ no se pudo interpretar como CSV: no es CSV válido, o una de sus celdas supera el límite del analizador (131072 caracteres), que es lo que acota una celda suelta en esta ruta. Un fichero de texto sin ningún salto de línea en sus primeros 128 KiB cae aquí. Antes escapaba como un 500 sin código y sin cuerpo imported/skipped/errors. | Vuelve a exportar el CSV desde el navegador (columnas name,url,username,password,note) y comprueba que ninguna celda es un bloque gigante de texto. El rechazo ocurre antes de escribir ninguna fila. |
import_operation_conflict | 409 | El operation_id de importación CSV ya se utilizó con un fichero o modo on_duplicate diferente. | Para recuperar una respuesta perdida, reenvía el mismo CSV y modo con el mismo operation_id. Para otra petición, genera un UUID nuevo. |
import_receipt_quota_exceeded | 429 | La cuenta ya conserva el máximo de recibos idempotentes de importación CSV dentro de la ventana de recuperación. | Reutiliza el operation_id original para recuperar una operación existente o espera a que expire la ventana antes de iniciar otra importación. |
workspace_unsupported | 409 | Rechazo genérico de las acciones que operan solo sobre la bóveda Personal cuando llega la cabecera X-Workspace-Id. Los casos concretos tienen su propio code (workspace_export_unsupported, workspace_import_unsupported, workspace_share_unsupported, workspace_shared_unsupported); este es el que se devuelve si la acción no está en ese mapa. | Repite la petición sin X-Workspace-Id. |
428 vs 401
master_key_required es un 428, no un 401. Trátalo como "falta la master key", no como "sesión caducada": reintenta añadiendo la cabecera X-Master-Key, no renueves la API key.Mínimo privilegio
Concede a cada clave solo los scopes que necesita. Evita apikeys:write salvo en aprovisionamiento.
Rotación periódica
Rota las claves con regularidad y tras cualquier sospecha de filtración. La rotación invalida el token anterior al instante.
Allowlist de IPs
Restringe cada clave a las IPs de tu backend. Las peticiones desde otras IPs reciben ip_not_allowed.
Nunca en el navegador
No incrustes claves ni la master key en JavaScript de cliente, apps móviles ni repositorios.
No hay entorno de pruebas aislado
pf_test_ es una etiqueta, no un sandbox: una clave pf_test_ autentica al mismo usuario contra la misma bóveda real que una pf_live_. Si la filtras en un repositorio o en la configuración de CI, expone tus datos reales igual que una clave de producción. Trata cualquier API key como una credencial de producción y usa una clave distinta por integración para poder revocarla sola.