Autenticación
Descripción General
La API de FyPay utiliza autenticación basada en OAuth 2.0 (clientId + clientSecret) combinada con un certificado de cliente X.509 (mTLS). El certificado se presenta en el handshake TLS de la llamada de generación de token; las demás rutas usan solo el token Bearer.
¿Por qué mTLS?
El mTLS (mutual TLS) ofrece mayor seguridad que las credenciales simples:
- Autenticación mutua: Tanto el cliente como el servidor se autentican en el handshake TLS
- No repudio: Los certificados vinculados a la cuenta garantizan trazabilidad
- Protección contra robo de credenciales: Aunque se filtren
clientId/clientSecret, el atacante necesitaría el certificado y su clave privada
Requisitos Previos
Antes de comenzar, necesitará:
Certificado en formato PEM, clave RSA 2048, válido por 2 años y vinculado a su cuenta, junto con su clave privada. En los ejemplos siguientes, los archivos se llaman client.crt (certificado) y client.key (clave privada).
Genere su clientId y clientSecret en el Dashboard de FyPay, en Configuración → Credenciales. El clientSecret se muestra una sola vez: cópielo y guárdelo en un almacén de secretos.
Configure su cliente HTTP para presentar el certificado y la clave privada en el handshake TLS (mecanismo nativo de TLS de su lenguaje).
El certificado debe estar vinculado a su cuenta. Los certificados no vinculados, o vinculados a otra cuenta, se rechazan aunque sean técnicamente válidos.
El certificado se entrega solo en PEM; no existe archivo .pfx. Si su stack requiere PKCS#12 (por ejemplo, algunos escenarios Windows/.NET), conviértalo localmente:
openssl pkcs12 -export -in client.crt -inkey client.key -out client.pfxEndpoint de Autenticación
POST /api/auth/token
Genera un token JWT de acceso. El tiempo de vida del token, en segundos, viene en el campo expires_in de la respuesta.
Esta es la única ruta que exige el certificado de cliente. Debe presentarse en el handshake TLS (mTLS), no en un encabezado HTTP.
Request
curl -X POST https://api.fypay.me/api/auth/token \
--cert client.crt \
--key client.key \
-H "Content-Type: application/json" \
-d '{
"clientId": "6967c0a0-3566-48d0-8c13-717038b52279",
"clientSecret": "su-client-secret"
}'| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
clientId | string (UUID v4) | Sí | Identificador de la credencial generada en el dashboard |
clientSecret | string | Sí | Secreto de la credencial, mostrado una sola vez en el dashboard |
Response (201 Created)
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 600,
"scope": "email profile"
}| Campo | Descripción |
|---|---|
access_token | Token JWT que se envía en Authorization: Bearer <token> |
token_type | Siempre Bearer |
expires_in | Tiempo de vida del token en segundos. Use este valor para programar la renovación |
scope | Alcance del token |
Ejemplo Práctico: Node.js
Instalación
npm install undiciCódigo Completo
const fs = require('fs');
const { Agent, request } = require('undici');
const mtlsAgent = new Agent({
connect: {
cert: fs.readFileSync('./client.crt'),
key: fs.readFileSync('./client.key'),
},
});
async function getToken() {
const { statusCode, body } = await request('https://api.fypay.me/api/auth/token', {
method: 'POST',
dispatcher: mtlsAgent,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clientId: process.env.FYPAY_CLIENT_ID,
clientSecret: process.env.FYPAY_CLIENT_SECRET,
}),
});
const data = await body.json();
if (statusCode !== 201) {
throw new Error(`Error al obtener el token: ${data.code} ${data.message}`);
}
console.log('¡Token obtenido con éxito!');
console.log('Expira en:', data.expires_in, 'segundos');
return data;
}
getToken().catch(console.error);Con el módulo nativo https, el equivalente es new https.Agent({ cert, key }) pasado en la opción agent de la solicitud.
Ejemplo Práctico: Python
Instalación
pip install requestsCódigo Completo
import os
import requests
url = 'https://api.fypay.me/api/auth/token'
payload = {
'clientId': os.environ.get('FYPAY_CLIENT_ID'),
'clientSecret': os.environ.get('FYPAY_CLIENT_SECRET'),
}
try:
response = requests.post(
url,
json=payload,
cert=('client.crt', 'client.key'),
timeout=30,
)
response.raise_for_status()
data = response.json()
print('¡Token obtenido con éxito!')
print(f"Expira en: {data['expires_in']} segundos")
except requests.exceptions.RequestException as e:
print(f'Error al obtener el token: {e}')
if e.response is not None:
print(f'Respuesta: {e.response.text}')Otros lenguajes
En todos ellos, el certificado y la clave se cargan mediante el mecanismo nativo de TLS del cliente HTTP:
| Lenguaje | Mecanismo |
|---|---|
| PHP (cURL) | CURLOPT_SSLCERT = client.crt y CURLOPT_SSLKEY = client.key |
| C# (.NET) | HttpClientHandler.ClientCertificates.Add(X509Certificate2.CreateFromPemFile("client.crt", "client.key")) |
| Go | tls.LoadX509KeyPair("client.crt", "client.key") en tls.Config.Certificates |
| Java | Convierta a PKCS#12 con openssl pkcs12 -export y cárguelo como KeyStore en el SSLContext |
Uso del Token
Después de obtener el token, inclúyalo en el encabezado Authorization de las demás solicitudes. Estas rutas no exigen certificado de cliente:
curl -X GET https://api.fypay.me/api/balance \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."Renovación del Token
No asuma una duración fija para el token. Lea expires_in en cada respuesta y renueve el token antes de que expire.
Estrategia Recomendada
const fs = require('fs');
const { Agent, request } = require('undici');
class TokenManager {
constructor(clientId, clientSecret, certPath, keyPath) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.agent = new Agent({
connect: {
cert: fs.readFileSync(certPath),
key: fs.readFileSync(keyPath),
},
});
this.token = null;
this.expiresAt = 0;
}
async getValidToken() {
const safetyMarginMs = 30000;
if (this.token && Date.now() < this.expiresAt - safetyMarginMs) {
return this.token;
}
return this.refreshToken();
}
async refreshToken() {
const { statusCode, body } = await request('https://api.fypay.me/api/auth/token', {
method: 'POST',
dispatcher: this.agent,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clientId: this.clientId,
clientSecret: this.clientSecret,
}),
});
const data = await body.json();
if (statusCode !== 201) {
throw new Error(`${data.code}: ${data.message}`);
}
this.token = data.access_token;
this.expiresAt = Date.now() + data.expires_in * 1000;
return this.token;
}
}
const tokenManager = new TokenManager(
process.env.FYPAY_CLIENT_ID,
process.env.FYPAY_CLIENT_SECRET,
'./client.crt',
'./client.key'
);
const token = await tokenManager.getValidToken();Validación del Certificado
El sistema realiza las siguientes validaciones sobre el certificado presentado en el handshake:
- Handshake mTLS: El certificado debe haber sido emitido por FyPay y usted debe poseer la clave privada correspondiente
- Vigencia: El certificado debe estar dentro de su período de validez (2 años desde la emisión)
- Registro: El fingerprint SHA-256 del certificado debe estar registrado y activo
- Vinculación a la cuenta: El certificado debe estar vinculado a la misma cuenta de la credencial (
clientId)
Los certificados no vinculados o vinculados a otra cuenta serán rechazados, aunque sean técnicamente válidos.
Respuestas de error
Todos los errores devueltos por POST /api/auth/token siguen el mismo formato:
{
"statusCode": 400,
"timestamp": "2026-04-24T16:59:25.577Z",
"path": "/api/auth/token",
"method": "POST",
"code": "PUB_CERT_HEADER_MISSING",
"message": "Client certificate header is missing",
"userMessage": "Client certificate is required.",
"details": {
"hint": "Present your client certificate during the TLS handshake (e.g. curl --cert client.crt --key client.key)."
},
"errorId": "81421e7a5afbd9cf25212f51dac08da1"
}code: identificador estable legible por máquina. Base su tratamiento en este campo.message: descripción técnica breve (en inglés, orientada a logs).userMessage: descripción orientada al usuario final.details.hint: orientación práctica para desarrolladores que integran la API.errorId: identificador único de la solicitud. Infórmelo al contactar al soporte.
Referencia de códigos de error
| HTTP | Código | Significado |
|---|---|---|
| 400 | PUB_CERT_HEADER_MISSING | No se presentó ningún certificado de cliente en el handshake TLS |
| 400 | PUB_CERT_MALFORMED_PEM | Certificado en formato PEM inválido |
| 400 | PUB_REQUEST_BODY_INVALID | Cuerpo JSON inválido |
| 401 | PUB_CERT_NOT_VERIFIED | El certificado presentado en el handshake fue rechazado |
| 401 | PUB_CERT_NOT_YET_VALID | La fecha de inicio de validez del certificado está en el futuro |
| 401 | PUB_CERT_EXPIRED | El certificado expiró |
| 401 | PUB_CERT_NOT_REGISTERED | El certificado no está registrado o fue revocado |
| 401 | PUB_INVALID_CREDENTIALS | clientId o clientSecret incorrectos |
| 403 | PUB_CERT_NOT_AUTHORIZED_FOR_ACCOUNT | El certificado no está vinculado a la cuenta del clientId |
| 403 | PUB_ACCOUNT_INACTIVE | Credenciales correctas, pero la cuenta no está activa |
| 502 | PUB_AUTH_UPSTREAM_ERROR | Error inesperado en el servicio de autenticación |
| 503 | PUB_AUTH_UPSTREAM_UNAVAILABLE | Servicio de autenticación temporalmente no disponible |
PUB_CERT_HEADER_MISSING (400)
No se presentó ningún certificado de cliente en el handshake TLS.
Cómo resolver: configure su cliente HTTP para enviar el certificado y la clave privada en la conexión TLS (por ejemplo, curl --cert client.crt --key client.key). Si hay un proxy o gateway entre su aplicación y FyPay que termine el TLS, la conexión con api.fypay.me debe partir de un cliente que presente el certificado.
PUB_CERT_MALFORMED_PEM (400)
Certificado en formato PEM inválido.
PUB_REQUEST_BODY_INVALID (400)
El cuerpo JSON no cumplió el esquema (campo ausente, tipo incorrecto, o clientId no es un UUID v4). Revise details.violations para identificar los campos con problema y envíe Content-Type: application/json.
PUB_CERT_NOT_VERIFIED (401)
El certificado presentado en el handshake TLS fue rechazado.
Cómo resolver: use el certificado emitido por FyPay para su cuenta y la clave privada correspondiente. No se aceptan certificados de otras autoridades certificadoras.
PUB_CERT_NOT_YET_VALID (401)
La fecha notBefore del certificado está en el futuro.
Cómo resolver: sincronice el reloj del sistema mediante NTP. Si la fecha realmente está en el futuro, espere o solicite un nuevo certificado.
PUB_CERT_EXPIRED (401)
La fecha notAfter del certificado ya pasó. Los certificados son válidos por 2 años desde su emisión.
Cómo resolver: solicite un nuevo certificado al soporte y reemplácelo en su integración. Planifique el cambio antes del vencimiento.
PUB_CERT_NOT_REGISTERED (401)
El fingerprint SHA-256 del certificado no está en el registro: nunca fue registrado o fue revocado.
Cómo resolver: calcule el fingerprint local con openssl x509 -noout -fingerprint -sha256 -in client.crt e infórmelo al soporte junto con el errorId de la respuesta.
PUB_INVALID_CREDENTIALS (401)
El clientId no es reconocido, o el clientSecret no coincide. La plataforma deliberadamente no diferencia estos casos (resistencia a enumeración).
Cómo resolver: verifique el clientId y el clientSecret. Como el clientSecret solo se muestra una vez, si lo perdió genere una nueva credencial en Configuración → Credenciales.
PUB_CERT_NOT_AUTHORIZED_FOR_ACCOUNT (403)
El certificado es válido y está registrado, pero no está vinculado a la cuenta propietaria del clientId informado.
Cómo resolver: verifique que está usando el par certificado/clientId de la misma cuenta.
PUB_ACCOUNT_INACTIVE (403)
Las credenciales son correctas, pero la cuenta no está activa. Los reintentos no tendrán éxito.
Cómo resolver: contacte al soporte con el errorId de la respuesta.
PUB_AUTH_UPSTREAM_UNAVAILABLE (503)
El servicio de autenticación está temporalmente inaccesible.
Cómo resolver: reintente con backoff exponencial (por ejemplo, 1s, 2s, 4s). Si el problema persiste por más de un minuto, contacte al soporte.
PUB_AUTH_UPSTREAM_ERROR (502)
El servicio de autenticación devolvió un error inesperado.
Cómo resolver: contacte al soporte con el errorId.
Solución de problemas
"Recibo PUB_CERT_HEADER_MISSING aunque envío el certificado"
El certificado debe ir en el handshake TLS, no en un encabezado HTTP. Confirme que el cliente HTTP está configurado con certificado y clave (por ejemplo, --cert/--key en curl, cert=(crt, key) en Python requests) y que ningún proxy intermedio termina el TLS antes de llegar a FyPay.
"Recibo ocasionalmente PUB_CERT_NOT_YET_VALID"
El reloj del sistema está atrasado respecto a la fecha notBefore del certificado. Configure NTP y mantenga el reloj del host a pocos segundos de UTC. Los contenedores pueden heredar la deriva del reloj del host; verifique con date -u dentro del contenedor.
"Recibo PUB_CERT_NOT_REGISTERED después de cambiar el certificado"
Calcule el fingerprint del nuevo certificado (openssl x509 -noout -fingerprint -sha256 -in client.crt) y confirme con el soporte que está registrado y vinculado a su cuenta antes de hacer la transición.