PassFortressPassFortress
Ir a la app
Documentación para desarrolladores

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.

Visión general

Introducción

Qué es la API y cómo encaja en tu integración.

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

Quick-start

Primeros pasos

De cero a tu primera petición autenticada.

  1. 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. 2

    Haz tu primera petición: lista los metadatos de tus secretos (no requiere master key).

cURL
curl "https://api.passfortress.com/api/v1/secrets/" \
  -H "Authorization: Bearer pf_live_xxx"

La misma petición en varios lenguajes:

cURL
curl -X GET "https://api.passfortress.com/api/v1/secrets/?type=password&page=1" \
  -H "Authorization: Bearer pf_live_xxx"

¿Y los valores?

Para leer el valor descifrado de un secreto añade la cabecera X-Master-Key con la master key del usuario. Por ejemplo, GET /secrets/{uuid}/value/ requiere el scope values:read y la master key.

Seguridad

Autenticación

API keys Bearer para acceso servidor-a-servidor.

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.

Cabecera
Authorization: Bearer pf_live_xxx

Prefijo 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

Las API keys son servidor-a-servidor. No las incrustes en JavaScript de cliente, apps móviles ni repositorios públicos: cualquiera que las lea obtiene acceso a la bóveda. Úsalas solo desde tu backend.

Cifrado

Master key y cifrado

Por qué los valores necesitan una cabecera extra.

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.

Leer un valor
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

Una operación sobre un valor sin X-Master-Key responde 428 Precondition Required con código master_key_required.

403: master key incorrecta

Si la master key no valida, responde 403 con código master_key_invalid. No revela ningún secreto.

¿Por qué 428 y no 401?

Un 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

Si tu integración envía la master key, está custodiando la clave que descifra toda la bóveda. Trátala con el mismo cuidado que la propia API key: solo en memoria del backend, nunca en logs ni en el navegador.

Bóvedas compartidas

Espacios (X-Workspace-Id)

Elegir sobre qué bóveda opera cada petición.

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.

Seleccionar la bóveda
# 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

Los scopes de la API key (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.

Permisos

Scopes

Cada clave concede scopes; cada endpoint exige uno. Aplica el mínimo privilegio.

ScopeRecursoConcede
secrets:readSecretosListar y leer los metadatos de los secretos (nombre, tipo, URL, contenedor). No descifra valores.
secrets:writeSecretosCrear, editar, duplicar y eliminar secretos.
values:read
sensible
SecretosLeer 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:readContenedoresListar y leer contenedores (carpetas).
containers:writeContenedoresCrear, renombrar y eliminar contenedores.
identifiers:readIdentificadoresLeer los pares clave-valor asociados a cada secreto.
identifiers:writeIdentificadoresCrear, editar y eliminar identificadores.
websites:readSitios webListar los sitios web asociados a los secretos.
websites:writeSitios webRegistrar un host en el catálogo de sitios web (idempotente).
ips:readIPsLeer las listas blanca/negra de direcciones IP.
ips:writeIPsAñadir y eliminar reglas de IP.
profile:readPerfilLeer los datos del perfil y las preferencias.
profile:writePerfilActualizar 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:writeCompartirCompartir 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:writeImportaciónImportar secretos desde un CSV de Chrome. Requiere la master key.
workspaces:readEspaciosListar los Espacios (bóvedas compartidas) del usuario, leer sus metadatos y, si eres administrador, su lista de miembros.
workspaces:writeEspaciosCrear, 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:readAPI keysListar las API keys y sus metadatos.
apikeys:write
sensible
API keysCrear, rotar y revocar API keys. Una clave con este scope puede acuñar otras claves.

apikeys:write es peligroso

Una clave con apikeys:write puede acuñar otras claves (con cualquier scope). Concédelo solo a integraciones de aprovisionamiento de confianza.

Operación

Límites y paginación

Cuotas por clave y cómo recorrer colecciones.

Rate limits

El límite es por clave. Superarlo devuelve 429 con la cabecera Retry-After.

TierLímiteUso
default1 000 / horaLímite por defecto para nuevas claves.
high10 000 / horaPara integraciones con mayor volumen.
unlimitedSin límiteReservado 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.

Respuesta paginada
{
  "count": 673,
  "next": "https://api.passfortress.com/api/v1/secrets/?page=2",
  "previous": null,
  "results": [ /* … 50 elementos … */ ]
}

Endpoints

Referencia de la API

Cada endpoint con su método, scope, parámetros y ejemplos.

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.

POST/api/v1/auth/login/
Sin scope

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*stringNombre de usuario o correo de la cuenta.
password*stringContraseña.
Petición
cURL
curl -X POST "https://api.passfortress.com/api/v1/auth/login/" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "ada",
    "password": "mi-contraseña"
  }'
Respuesta
200 OK · application/json
{
  "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
  }
}
POST/api/v1/auth/login/2fa/
Sin scope

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*stringEl challenge devuelto por /auth/login/. No se reenvía el usuario.
code*stringCódigo de 6 dígitos recibido por correo o SMS.
Petición
cURL
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
200 OK · application/json
{
  "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
  }
}
POST/api/v1/auth/login/2fa/recovery/
Sin scope

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*stringEl challenge devuelto por /auth/login/ o /auth/google/.
recovery_code*stringUno de los códigos de recuperación (16 caracteres; se aceptan con o sin guiones y en minúsculas).
Petición
cURL
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
200 OK · application/json
{
  "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
  }
}
POST/api/v1/auth/google/
Sin scope

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*stringID token (JWT) emitido por Google Identity Services.
allow_signupbooleanSi es true, provisiona la cuenta cuando el correo no existe (por defecto false).
Petición
cURL
curl -X POST "https://api.passfortress.com/api/v1/auth/google/" \
  -H "Content-Type: application/json" \
  -d '{
    "credential": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
    "allow_signup": false
  }'
Respuesta
200 OK · application/json
{
  "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
}
POST/api/v1/auth/master-key/setup/
Sin scope
JWT interactivo

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*stringMaster key inicial (mínimo 8; no repetitiva, secuencial ni común).
Petición
cURL
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
200 OK · application/json
{
  "created": true
}
POST/api/v1/auth/register/
Sin scope

Registrar un nuevo usuario.

Devuelve los tokens JWT y el usuario creado (201).

Cuerpo (JSON)
username*stringNombre de usuario único.
email*stringCorreo único.
password*stringContraseña (mínimo 8).
master_key*stringMaster key inicial (mínimo 8; no repetitiva, secuencial ni común).
first_namestringNombre.
last_namestringApellidos.
Petición
cURL
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
201 Created · application/json
{
  "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
  }
}
POST/api/v1/auth/token/refresh/
Sin scope

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*stringRefresh token vigente.
Petición
cURL
curl -X POST "https://api.passfortress.com/api/v1/auth/token/refresh/" \
  -H "Content-Type: application/json" \
  -d '{
    "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  }'
Respuesta
200 OK · application/json
{
  "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
GET/api/v1/auth/me/
profile:read

Obtener el usuario autenticado.

Devuelve el objeto del usuario directamente (sin envoltorio).

Petición
cURL
curl -X GET "https://api.passfortress.com/api/v1/auth/me/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "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
}
POST/api/v1/auth/logout/
Sin scope

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*stringRefresh token a invalidar.
Petición
cURL
curl -X POST "https://api.passfortress.com/api/v1/auth/logout/" \
  -H "Content-Type: application/json" \
  -d '{
    "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  }'
Respuesta
200 OK · application/json
"205 Reset Content (sin cuerpo)"
POST/api/v1/auth/master-key/validate/
values:read

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*stringMaster key a comprobar.
Petición
cURL
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
200 OK · application/json
{
  "valid": true
}
POST/api/v1/auth/verify/request/
profile:write

Solicitar un código de verificación.

Cuerpo (JSON)
scope*stringverify_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
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
200 OK · application/json
{
  "scope": "verify_email",
  "channel": "email",
  "sent": true
}
POST/api/v1/auth/verify/confirm/
profile:write

Confirmar el código de verificación.

Cuerpo (JSON)
scope*stringEl mismo scope usado en la solicitud.
code*stringCódigo recibido.
Petición
cURL
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
200 OK · application/json
{
  "verified": true
}
POST/api/v1/auth/password-reset/
Sin scope

Solicitar el restablecimiento de contraseña.

Responde 200 siempre (no revela si el correo existe).

Cuerpo (JSON)
email*stringCorreo de la cuenta.
Petición
cURL
curl -X POST "https://api.passfortress.com/api/v1/auth/password-reset/" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "dev@example.com"
  }'
Respuesta
200 OK · application/json
{
  "sent": true
}
POST/api/v1/auth/password-reset/confirm/
Sin scope

Confirmar el restablecimiento con el código.

Cuerpo (JSON)
email*stringCorreo de la cuenta.
code*stringCódigo recibido por correo.
new_password*stringNueva contraseña (mínimo 8).
Petición
cURL
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
200 OK · application/json
{
  "reset": true
}

Capacidades

Límites efectivos del despliegue que los clientes deben aplicar antes de enviar datos.

GET/api/v1/capabilities/
Sin scope

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
curl -X GET "https://api.passfortress.com/api/v1/capabilities/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "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.

GET/api/v1/profile/
profile:read

Leer el perfil del usuario.

Petición
cURL
curl -X GET "https://api.passfortress.com/api/v1/profile/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "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
}
PATCH/api/v1/profile/
profile:write

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_namestringNombre.
last_namestringApellidos.
mobilestringTeléfono móvil.
countrystringCódigo ISO alpha-2 del país del móvil (GET /countries/); es el prefijo con el que se marca el SMS.
addressstringDirección.
languagestringIdioma preferido.
themestringlight o dark. La preferencia system es solo local del cliente web.
tfa_policystringdisabled, email o mobile (el canal debe estar verificado y completo; solo JWT interactivo, no API key).
accepts_secret_sharesbooleanPermite o bloquea nuevas invitaciones de secretos dirigidas a esta cuenta.
Petición
cURL
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
200 OK · application/json
{
  "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
}
POST/api/v1/profile/email/
Sin scope
JWT interactivo

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*stringDirección candidata; se guarda y se aplica en su forma canónica (minúsculas).
passwordstringContraseña actual de la cuenta. Obligatoria salvo que la cuenta no tenga contraseña utilizable.
Petición
cURL
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
200 OK · application/json
{
  "pending_email": "nueva@example.com",
  "sent": true,
  "expires_in": 600
}
POST/api/v1/profile/email/confirm/
Sin scope
JWT interactivo

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*stringCó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
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
200 OK · application/json
{
  "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
  }
}
DELETE/api/v1/profile/email/
Sin scope
JWT interactivo

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
curl -X DELETE "https://api.passfortress.com/api/v1/profile/email/" \
  -H "Authorization: Bearer <access-token-jwt>"
Respuesta
200 OK · application/json
{
  "cancelled": true
}
POST/api/v1/profile/password/
Sin scope
JWT interactivo

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*stringContraseña actual.
new_password*stringNueva contraseña (mínimo 8).
Petición
cURL
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
200 OK · application/json
{
  "changed": true,
  "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
POST/api/v1/profile/master-key/
Sin scope
JWT interactivo

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*stringMaster key actual.
new_master_key*stringNueva master key (mínimo 8; no repetitiva, secuencial ni común).
Petición
cURL
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
200 OK · application/json
{
  "changed": true
}
POST/api/v1/profile/tfa/recovery-codes/
Sin scope
JWT interactivo

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
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
200 OK · application/json
{
  "codes": [
    "ABCD-EFGH-JKMN-PQRS",
    "TVWX-YZ01-2345-6789"
  ],
  "remaining": 10
}
PATCH/api/v1/profile/theme/
profile:write

Cambiar el tema preferido.

Devuelve el perfil completo actualizado.

Cuerpo (JSON)
theme*stringlight o dark. La preferencia system es solo local del cliente web.
Petición
cURL
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
200 OK · application/json
{
  "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).

GET/api/v1/countries/
Sin scope

Listar países disponibles.

Devuelve un array plano (sin paginación).

Petición
cURL
curl -X GET "https://api.passfortress.com/api/v1/countries/"
Respuesta
200 OK · application/json
[
  {
    "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.

GET/api/v1/ips/
ips:read

Listar reglas de IP.

Petición
cURL
curl -X GET "https://api.passfortress.com/api/v1/ips/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "uuid": "ip1",
      "name": "Oficina",
      "ip_range": "203.0.113.0/24",
      "list": "white"
    }
  ]
}
POST/api/v1/ips/
ips:write

Añadir una regla de IP.

Cuerpo (JSON)
ip_range*stringIP o rango (CIDR).
liststringwhite o black.
namestringEtiqueta descriptiva (opcional).
Petición
cURL
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
201 Created · application/json
{
  "uuid": "ip1",
  "name": "Oficina",
  "ip_range": "203.0.113.0/24",
  "list": "white"
}
GET/api/v1/ips/{uuid}/
ips:read

Obtener una regla de IP.

Parámetros de ruta
uuid*stringUUID de la regla.
Petición
cURL
curl -X GET "https://api.passfortress.com/api/v1/ips/{uuid}/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "uuid": "ip1",
  "name": "Oficina",
  "ip_range": "203.0.113.0/24",
  "list": "white"
}
DELETE/api/v1/ips/{uuid}/
ips:write

Eliminar una regla de IP.

Devuelve 204 No Content sin cuerpo.

Parámetros de ruta
uuid*stringUUID de la regla.
Petición
cURL
curl -X DELETE "https://api.passfortress.com/api/v1/ips/{uuid}/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
204 No Content
"204 No Content (sin cuerpo)"

Contenedores

Carpetas para organizar los secretos.

GET/api/v1/containers/
containers:read

Listar contenedores.

Petición
cURL
curl -X GET "https://api.passfortress.com/api/v1/containers/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "uuid": "f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff",
      "name": "Trabajo",
      "description": "",
      "secrets_count": 42
    }
  ]
}
POST/api/v1/containers/
containers:write

Crear un contenedor.

Cuerpo (JSON)
name*stringNombre del contenedor.
descriptionstringDescripción (opcional).
Petición
cURL
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
201 Created · application/json
{
  "uuid": "f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff",
  "name": "Trabajo",
  "description": "",
  "secrets_count": 0
}
GET/api/v1/containers/{uuid}/
containers:read

Obtener un contenedor.

Parámetros de ruta
uuid*stringUUID del contenedor.
Petición
cURL
curl -X GET "https://api.passfortress.com/api/v1/containers/{uuid}/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "uuid": "f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff",
  "name": "Trabajo",
  "description": "",
  "secrets_count": 42
}
PUT/api/v1/containers/{uuid}/
containers:write

Reemplazar un contenedor.

Parámetros de ruta
uuid*stringUUID del contenedor.
Cuerpo (JSON)
name*stringNuevo nombre.
descriptionstringNueva descripción.
Petición
cURL
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
200 OK · application/json
{
  "uuid": "f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff",
  "name": "Trabajo (2026)",
  "description": "",
  "secrets_count": 42
}
PATCH/api/v1/containers/{uuid}/
containers:write

Actualizar parcialmente un contenedor.

Parámetros de ruta
uuid*stringUUID del contenedor.
Cuerpo (JSON)
namestringNuevo nombre.
descriptionstringNueva descripción.
Petición
cURL
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
200 OK · application/json
{
  "uuid": "f1f2f3f4-aaaa-bbbb-cccc-ddddeeeeffff",
  "name": "Personal",
  "description": "",
  "secrets_count": 42
}
DELETE/api/v1/containers/{uuid}/
containers:write
X-Master-Key

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*stringUUID del contenedor.
Petición
cURL
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
"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.

GET/api/v1/secrets/
secrets:read

Listar secretos (solo metadatos).

Devuelve metadatos paginados (50 por página). Nunca incluye valores descifrados.

Parámetros de consulta
typestringpassword, envfile, totp o file.
containerstringUUID del contenedor para filtrar.
searchstringBúsqueda por nombre, URL o nombre de archivo.
pageintegerNúmero de página (50 por página).
Petición
cURL
curl -X GET "https://api.passfortress.com/api/v1/secrets/?type=password&page=1" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "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
    }
  ]
}
POST/api/v1/secrets/
secrets:write
X-Master-Key

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*stringpassword, envfile, totp o file.
namestringNombre del secreto.
valuestringValor 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_b64stringContenido del archivo en base64 (tipo file).
file_namestringNombre del archivo (tipo file).
urlstringURL asociada.
notesstringNotas.
containersarrayUUIDs de contenedores (alias: container_uuids); máximo 100.
identifiersarrayPares {key, value}; máximo 100.
Petición
cURL
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
201 Created · application/json
{
  "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
}
POST/api/v1/secrets/
secrets:write
X-Master-Key

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*stringDebe ser "totp".
namestringEtiqueta visible (p. ej. el issuer del servicio).
value*stringURI otpauth://totp/... completa (se guarda cifrada sin parsearse).
urlstringURL asociada.
notesstringNotas.
containersarrayUUIDs de contenedores (alias: container_uuids); máximo 100.
identifiersarrayPares {key, value} (p. ej. la cuenta); máximo 100.
Petición
cURL
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
200 OK · application/json
{
  "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
}
GET/api/v1/secrets/{uuid}/
secrets:read

Obtener los metadatos de un secreto.

Parámetros de ruta
uuid*stringUUID del secreto.
Petición
cURL
curl -X GET "https://api.passfortress.com/api/v1/secrets/{uuid}/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "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
}
PUT/api/v1/secrets/{uuid}/
secrets:write
X-Master-Key

Reemplazar un secreto.

Requiere X-Master-Key (cifra el valor en el servidor). El tipo no se puede cambiar.

Parámetros de ruta
uuid*stringUUID del secreto.
Cuerpo (JSON)
namestringNombre.
valuestringNuevo valor (cifra en el servidor).
file_namestringNombre del FILE al reemplazar su blob.
file_content_b64stringNuevo contenido FILE en base64, sin prefijo data:.
urlstringURL asociada.
notesstringNotas.
containersarrayUUIDs de contenedores; máximo 100.
identifiersarrayPares {key, value}; máximo 100.
Petición
cURL
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
200 OK · application/json
{
  "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
}
PATCH/api/v1/secrets/{uuid}/
secrets:write
X-Master-Key

Actualizar parcialmente un secreto.

Requiere X-Master-Key (la escritura cifra en el servidor).

Parámetros de ruta
uuid*stringUUID del secreto.
Cuerpo (JSON)
namestringNombre.
valuestringNuevo valor.
containersarrayUUIDs de contenedores; máximo 100.
identifiersarrayPares {key, value}; máximo 100.
Petición
cURL
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
200 OK · application/json
{
  "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
}
DELETE/api/v1/secrets/{uuid}/
secrets:write
X-Master-Key

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*stringUUID del secreto.
Petición
cURL
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
"204 No Content (sin cuerpo)"
POST/api/v1/secrets/bulk-delete/
secrets:write
X-Master-Key

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
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
200 OK · application/json
{
  "deleted": 1,
  "not_found": [
    "b2c3d4e5-1111-2222-3333-444455556666"
  ]
}
GET/api/v1/secrets/{uuid}/value/
values:read
X-Master-Key

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*stringUUID del secreto.
Petición
cURL
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
200 OK · application/json
{
  "uuid": "a1b2c3d4-0000-1111-2222-333344445555",
  "secret_type": "password",
  "value": "s3cr3t-p4ss"
}
GET/api/v1/secrets/{uuid}/history/
values:read
X-Master-Key

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*stringUUID del secreto.
Petición
cURL
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
200 OK · application/json
[
  {
    "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"
  }
]
GET/api/v1/secrets/{uuid}/history-page/
values:read
X-Master-Key

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*stringUUID del secreto.
Parámetros de consulta
cursorstringCursor opaco devuelto por la página anterior.
Petición
cURL
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
200 OK · application/json
{
  "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"
    }
  ]
}
POST/api/v1/secrets/{uuid}/duplicate/
secrets:write
X-Master-Key

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*stringUUID del secreto a copiar.
Petición
cURL
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
201 Created · application/json
{
  "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
}
POST/api/v1/secrets/{uuid}/share/
share:write + values:read
X-Master-Key

Compartir un secreto con otros usuarios.

Acepta entre 1 y 20 destinatarios. El cliente genera un operation_id UUID y una tmp_master_key aleatoria de al menos 256 bits por destinatario. El servidor persiste sólo la huella de la clave: repetir exactamente la operación devuelve la misma copia sin duplicarla. La tmp_master_key sólo aparece en esta respuesta: el servidor no la envía por correo y el cliente debe entregarla por un canal aparte. Una fila de éxito confirma que la invitación quedó registrada de forma opaca, no que el destinatario ya la haya recibido o aceptado. Requiere X-Master-Key y los scopes share:write Y values:read.

Parámetros de ruta
uuid*stringUUID del secreto.
Cuerpo (JSON)
shares*arrayEntre 1 y 20 destinatarios con email, operation_id UUID y tmp_master_key base64url de 256 bits.
Petición
cURL
curl -X POST "https://api.passfortress.com/api/v1/secrets/{uuid}/share/" \
  -H "Authorization: Bearer pf_live_xxx" \
  -H "X-Master-Key: <tu-master-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "shares": [
      {
        "email": "colega@example.com",
        "operation_id": "8fc82f55-302d-4b50-b63f-930769079235",
        "tmp_master_key": "F9Xk3pQ7mW2vN8sR4tY6uI1oP5aD0fG2hJ7kL9zC8bA"
      }
    ]
  }'
Respuesta
200 OK · application/json
[
  {
    "email": "colega@example.com",
    "shared_uuid": "b2c3d4e5-1111-2222-3333-444455556666",
    "tmp_master_key": "F9Xk3pQ7mW2vN8sR4tY6uI1oP5aD0fG2hJ7kL9zC8bA",
    "error": null
  }
]
GET/api/v1/secrets/{uuid}/download/
values:read
X-Master-Key

Descargar el archivo descifrado (tipo file).

Devuelve el archivo binario descifrado. Requiere X-Master-Key.

Parámetros de ruta
uuid*stringUUID del secreto.
Petición
cURL
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>

GET/api/v1/secrets/shared/
secrets:read + share:write

Listar secretos compartidos pendientes de aceptar.

Metadatos paginados de los secretos recibidos (shared = true). La lectura también reclama invitaciones durables dirigidas al correo verificado de la cuenta, por lo que una API key necesita ambos scopes.

Petición
cURL
curl -X GET "https://api.passfortress.com/api/v1/secrets/shared/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "uuid": "share1",
      "secret_type": "password",
      "name": "GitHub",
      "file_name": null,
      "url": "https://github.com",
      "notes": "",
      "available": true,
      "shared": true,
      "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
    }
  ]
}
POST/api/v1/secrets/accept-shared/
share:write
X-Master-Key

Aceptar un secreto compartido.

Re-cifra el valor recibido con tu master key. Requiere X-Master-Key y la clave temporal devuelta al compartir.

Cuerpo (JSON)
uuid*stringUUID del secreto compartido.
tmp_master_key*stringClave temporal recibida al compartir.
Petición
cURL
curl -X POST "https://api.passfortress.com/api/v1/secrets/accept-shared/" \
  -H "Authorization: Bearer pf_live_xxx" \
  -H "X-Master-Key: <tu-master-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "uuid": "share1",
    "tmp_master_key": "F9Xk3pQ7mW2vN8sR4tY6uI1oP5aD0fG2hJ7kL9zC8bA"
  }'
Respuesta
200 OK · application/json
{
  "uuid": "share1",
  "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
}
POST/api/v1/secrets/import/
import:write
X-Master-Key

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
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
200 OK · application/json
{
  "operation_id": "d4eb9a27-8b03-4f11-9b35-4426d11dd52f",
  "imported": 128,
  "updated": 0,
  "skipped": 3,
  "errors": []
}
GET/api/v1/secrets/export/
values:read
X-Master-Key

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

POST/api/v1/secrets/import-backup/
import:write + values:read
X-Master-Key

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*fileArchivo .zip de copia de seguridad.
old_master_keystringMaster key con la que se generó la copia, si es distinta de la actual.
Petición
cURL
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
200 OK · application/json
{
  "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.

POST/api/v1/sync/bootstrap/
secrets:read + values:read
X-Master-Key

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
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
200 OK · application/json
{
  "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
}
GET/api/v1/sync/pull/
secrets:read + values:read
X-Master-Key

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
cursorintegerÚltimo cursor aplicado por el cliente (0 por defecto).
limitintegerEntradas por página, entre 1 y 200 (100 por defecto).
Petición
cURL
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
200 OK · application/json
{
  "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
}
POST/api/v1/sync/push/
secrets:write + secrets:read + values:read
X-Master-Key

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*stringIdentificador estable del dispositivo (máx. 128).
mutations*arrayDe 1 a 100 mutaciones.
mutations[].mutation_id*stringIdentificador único de la mutación en ese dispositivo.
mutations[].entity_type*stringSiempre "secret".
mutations[].entity_id*stringUUID del secreto (lo elige el cliente al crear).
mutations[].operation*string"upsert" o "delete".
mutations[].base_revision*integer0 para crear; si no, la revisión que el cliente tiene.
mutations[].payloadobjectsecret_type, name, url, notes, value e identifiers (máximo 100). secret_type no puede cambiar tras la creación.
Petición
cURL
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
200 OK · application/json
{
  "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).

GET/api/v1/identifiers/
identifiers:read

Listar identificadores.

Petición
cURL
curl -X GET "https://api.passfortress.com/api/v1/identifiers/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "uuid": "id1",
      "secret": "a1b2c3d4-0000-1111-2222-333344445555",
      "key": "usuario",
      "value": "ada@example.com"
    }
  ]
}
POST/api/v1/identifiers/
identifiers:write

Crear un identificador.

Cuerpo (JSON)
secret*stringUUID del secreto.
keystringClave.
value*stringValor.
Petición
cURL
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
201 Created · application/json
{
  "uuid": "id1",
  "secret": "a1b2c3d4-0000-1111-2222-333344445555",
  "key": "usuario",
  "value": "ada@example.com"
}
GET/api/v1/identifiers/{uuid}/
identifiers:read

Obtener un identificador.

Parámetros de ruta
uuid*stringUUID del identificador.
Petición
cURL
curl -X GET "https://api.passfortress.com/api/v1/identifiers/{uuid}/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "uuid": "id1",
  "secret": "a1b2c3d4-0000-1111-2222-333344445555",
  "key": "usuario",
  "value": "ada@example.com"
}
PUT/api/v1/identifiers/{uuid}/
identifiers:write

Reemplazar un identificador.

Parámetros de ruta
uuid*stringUUID del identificador.
Cuerpo (JSON)
keystringClave.
value*stringValor.
Petición
cURL
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
200 OK · application/json
{
  "uuid": "id1",
  "secret": "a1b2c3d4-0000-1111-2222-333344445555",
  "key": "usuario",
  "value": "ada.lovelace@example.com"
}
PATCH/api/v1/identifiers/{uuid}/
identifiers:write

Actualizar parcialmente un identificador.

Parámetros de ruta
uuid*stringUUID del identificador.
Cuerpo (JSON)
valuestringNuevo valor.
Petición
cURL
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
200 OK · application/json
{
  "uuid": "id1",
  "secret": "a1b2c3d4-0000-1111-2222-333344445555",
  "key": "usuario",
  "value": "nuevo-valor"
}
DELETE/api/v1/identifiers/{uuid}/
identifiers:write
X-Master-Key

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*stringUUID del identificador.
Petición
cURL
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
"204 No Content (sin cuerpo)"

Sitios web

Sitios web vinculados a los secretos (para favicons y autocompletado).

GET/api/v1/websites/
websites:read

Listar sitios web.

Petición
cURL
curl -X GET "https://api.passfortress.com/api/v1/websites/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "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
    }
  ]
}
POST/api/v1/websites/
websites:write

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*stringHost del sitio (p. ej. github.com).
Petición
cURL
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
201 Created · application/json
{
  "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.

GET/api/v1/workspaces/
workspaces:read

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
curl -X GET "https://api.passfortress.com/api/v1/workspaces/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
[
  {
    "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
    }
  }
]
POST/api/v1/workspaces/
workspaces:write
X-Master-Key

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*stringNombre del Espacio (máx. 64 caracteres).
Petición
cURL
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
201 Created · application/json
{
  "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
  }
}
GET/api/v1/workspaces/{uuid}/
workspaces:read

Obtener un Espacio.

Parámetros de ruta
uuid*stringuuid del Espacio.
Petición
cURL
curl -X GET "https://api.passfortress.com/api/v1/workspaces/{uuid}/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "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
  }
}
PATCH/api/v1/workspaces/{uuid}/
workspaces:write

Renombrar un Espacio (solo administradores).

name es obligatorio. El Espacio Personal se puede renombrar, pero no eliminar.

Parámetros de ruta
uuid*stringuuid del Espacio.
Cuerpo (JSON)
name*stringNuevo nombre.
Petición
cURL
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
200 OK · application/json
{
  "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
  }
}
PUT/api/v1/workspaces/{uuid}/
workspaces:write

Renombrar un Espacio reemplazando el recurso (alias soportado).

Mismo contrato de nombre y permisos que PATCH; name es obligatorio.

Parámetros de ruta
uuid*stringuuid del Espacio.
Cuerpo (JSON)
name*stringNuevo nombre.
Petición
cURL
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
200 OK · application/json
{
  "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
  }
}
DELETE/api/v1/workspaces/{uuid}/
workspaces:write
X-Master-Key

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*stringuuid del Espacio.
Petición
cURL
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
204 No Content
null
GET/api/v1/workspaces/{uuid}/members/
workspaces:read

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*stringuuid del Espacio.
Petición
cURL
curl -X GET "https://api.passfortress.com/api/v1/workspaces/{uuid}/members/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
[
  {
    "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
  }
]
POST/api/v1/workspaces/{uuid}/members/
workspaces:write
X-Master-Key

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*stringuuid del Espacio.
Cuerpo (JSON)
email*stringCorreo de la persona invitada.
rolestringadmin, guest (por defecto) o custom.
permissionsobjectMatriz {passwords, envfiles, totp, containers} con nivel read | create | edit_no_delete | edit_delete, más shared (boolean). Solo con role: "custom".
Petición
cURL
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
200 OK · application/json
{
  "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
}
PATCH/api/v1/workspaces/{uuid}/members/{m_uuid}/
workspaces:write
X-Master-Key

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*stringuuid del Espacio.
m_uuid*stringuuid de la membresía.
Cuerpo (JSON)
rolestringadmin, guest o custom.
permissionsobjectMatriz de permisos (solo con role: "custom").
Petición
cURL
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
200 OK · application/json
{
  "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
}
DELETE/api/v1/workspaces/{uuid}/members/{m_uuid}/
workspaces:write
X-Master-Key

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*stringuuid del Espacio.
m_uuid*stringuuid de la membresía.
Petición
cURL
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
204 No Content
null
POST/api/v1/workspaces/{uuid}/members/sync/
workspaces:write
X-Master-Key

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*stringuuid del Espacio.
Petición
cURL
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
200 OK · application/json
{
  "resolved": 1
}
POST/api/v1/workspaces/{uuid}/resume-key-rotation/
workspaces:write
X-Master-Key

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*stringuuid del Espacio.
Petición
cURL
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
204 No Content
null
POST/api/v1/workspaces/{uuid}/leave/
workspaces:write

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*stringuuid del Espacio.
Petición
cURL
curl -X POST "https://api.passfortress.com/api/v1/workspaces/{uuid}/leave/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
204 No Content
null

API keys

Gestión programática de las propias API keys. Normalmente se administran desde el panel web; usar estos endpoints requiere el scope apikeys:*.

GET/api/v1/api-keys/
apikeys:read

Listar las API keys.

Petición
cURL
curl -X GET "https://api.passfortress.com/api/v1/api-keys/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "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"
    }
  ]
}
POST/api/v1/api-keys/
apikeys:write

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*stringNombre descriptivo.
scopesarrayLista de scopes.
environmentstringlive o test.
rate_tierstringdefault, high o unlimited.
expires_atstringFecha de expiración ISO-8601 (opcional).
ip_allowlistarrayAllowlist de IPs/CIDR (opcional).
Petición
cURL
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
201 Created · application/json
{
  "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"
}
GET/api/v1/api-keys/{public_id}/
apikeys:read

Obtener una API key.

Parámetros de ruta
public_id*stringIdentificador público de la clave.
Petición
cURL
curl -X GET "https://api.passfortress.com/api/v1/api-keys/{public_id}/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "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"
}
DELETE/api/v1/api-keys/{public_id}/
apikeys:write

Eliminar permanentemente una API key.

Responde 204 sin cuerpo. Para invalidarla conservando sus metadatos, usa revoke.

Parámetros de ruta
public_id*stringIdentificador público de la clave.
Petición
cURL
curl -X DELETE "https://api.passfortress.com/api/v1/api-keys/{public_id}/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
204 No Content
null
POST/api/v1/api-keys/{public_id}/revoke/
apikeys:write

Revocar una API key.

Devuelve la clave actualizada (is_active = false, revoked_at fijado).

Parámetros de ruta
public_id*stringIdentificador público de la clave.
Petición
cURL
curl -X POST "https://api.passfortress.com/api/v1/api-keys/{public_id}/revoke/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "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"
}
POST/api/v1/api-keys/{public_id}/rotate/
apikeys:write

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*stringIdentificador público de la clave.
Petición
cURL
curl -X POST "https://api.passfortress.com/api/v1/api-keys/{public_id}/rotate/" \
  -H "Authorization: Bearer pf_live_xxx"
Respuesta
200 OK · application/json
{
  "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"
}

Diagnóstico

Errores

Envoltura de error y catálogo de códigos.

Toda respuesta de error tiene la forma { "detail": "…", "code": "…" }. Usa el code (no el texto) para ramificar tu lógica.

codeHTTPCausaSolució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

Recuerda: 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.

Buenas prácticas

Seguridad

Recomendaciones para integrar PassFortress de forma segura.

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

El prefijo 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.