docs
Integration Guides

Authentication

Overview

The FyPay API uses OAuth 2.0 authentication (clientId + clientSecret) combined with an X.509 client certificate (mTLS). The certificate is presented in the TLS handshake of the token generation call; all other routes use only the Bearer token.

Why mTLS?

mTLS (mutual TLS) provides stronger security than plain credentials:

  • Mutual authentication: Both client and server authenticate each other in the TLS handshake
  • Non-repudiation: Certificates linked to the account ensure traceability
  • Protection against credential theft: Even if clientId/clientSecret leak, an attacker would also need the certificate and its private key

Prerequisites

Before you start, you will need:

A certificate in PEM format, RSA 2048 key, valid for 2 years and linked to your account, together with its private key. In the examples below, the files are named client.crt (certificate) and client.key (private key).

Generate your clientId and clientSecret in the FyPay Dashboard, under Settings → Credentials. The clientSecret is shown only once: copy it and store it in a secrets vault.

Configure your HTTP client to present the certificate and private key in the TLS handshake (your language's native TLS mechanism).

The certificate must be linked to your account. Certificates that are not linked, or linked to another account, are rejected even if technically valid.

The certificate is delivered in PEM only; there is no .pfx file. If your stack requires PKCS#12 (for example, some Windows/.NET scenarios), convert it locally:

openssl pkcs12 -export -in client.crt -inkey client.key -out client.pfx

Authentication Endpoint

POST /api/auth/token

Generates a JWT access token. The token lifetime, in seconds, is returned in the expires_in field of the response.

This is the only route that requires the client certificate. It must be presented in the TLS handshake (mTLS), not in an HTTP header.

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": "your-client-secret"
  }'
FieldTypeRequiredDescription
clientIdstring (UUID v4)YesIdentifier of the credential generated in the dashboard
clientSecretstringYesCredential secret, shown only once in the dashboard

Response (201 Created)

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 600,
  "scope": "email profile"
}
FieldDescription
access_tokenJWT to be sent in Authorization: Bearer <token>
token_typeAlways Bearer
expires_inToken lifetime in seconds. Use this value to schedule renewal
scopeToken scope

Practical Example: Node.js

Installation

npm install undici

Complete Code

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 obtaining token: ${data.code} ${data.message}`);
  }

  console.log('Token obtained successfully!');
  console.log('Expires in:', data.expires_in, 'seconds');

  return data;
}

getToken().catch(console.error);

With the native https module, the equivalent is new https.Agent({ cert, key }) passed in the request's agent option.

Practical Example: Python

Installation

pip install requests

Complete Code

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 obtained successfully!')
    print(f"Expires in: {data['expires_in']} seconds")

except requests.exceptions.RequestException as e:
    print(f'Error obtaining token: {e}')
    if e.response is not None:
        print(f'Response: {e.response.text}')

Other languages

In all of them, the certificate and key are loaded through the HTTP client's native TLS mechanism:

LanguageMechanism
PHP (cURL)CURLOPT_SSLCERT = client.crt and CURLOPT_SSLKEY = client.key
C# (.NET)HttpClientHandler.ClientCertificates.Add(X509Certificate2.CreateFromPemFile("client.crt", "client.key"))
Gotls.LoadX509KeyPair("client.crt", "client.key") in tls.Config.Certificates
JavaConvert to PKCS#12 with openssl pkcs12 -export and load it as a KeyStore in the SSLContext

Using the Token

After obtaining the token, include it in the Authorization header of all other requests. These routes do not require a client certificate:

curl -X GET https://api.fypay.me/api/balance \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Token Renewal

Do not assume a fixed token lifetime. Read expires_in in every response and renew the token before it expires.

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();

Certificate Validation

The system performs the following validations on the certificate presented in the handshake:

  1. mTLS handshake: The certificate must have been issued by FyPay and you must hold the matching private key
  2. Validity: The certificate must be within its validity period (2 years from issuance)
  3. Registration: The certificate's SHA-256 fingerprint must be registered and active
  4. Account linkage: The certificate must be linked to the same account as the credential (clientId)

Certificates that are not linked, or linked to another account, will be rejected even if technically valid.

Error responses

All errors returned by POST /api/auth/token follow the same format:

{
  "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: stable machine-readable identifier. Branch your handling on this field.
  • message: short technical description (in English, meant for logs).
  • userMessage: end-user facing description.
  • details.hint: practical guidance for developers integrating the API.
  • errorId: unique request identifier. Include it when contacting support.

Error code reference

HTTPCodeMeaning
400PUB_CERT_HEADER_MISSINGNo client certificate was presented in the TLS handshake
400PUB_CERT_MALFORMED_PEMCertificate in invalid PEM format
400PUB_REQUEST_BODY_INVALIDInvalid JSON body
401PUB_CERT_NOT_VERIFIEDThe certificate presented in the handshake was rejected
401PUB_CERT_NOT_YET_VALIDThe certificate's validity start date is in the future
401PUB_CERT_EXPIREDThe certificate has expired
401PUB_CERT_NOT_REGISTEREDThe certificate is not registered or has been revoked
401PUB_INVALID_CREDENTIALSWrong clientId or clientSecret
403PUB_CERT_NOT_AUTHORIZED_FOR_ACCOUNTThe certificate is not linked to the clientId account
403PUB_ACCOUNT_INACTIVECorrect credentials, but the account is not active
502PUB_AUTH_UPSTREAM_ERRORUnexpected error in the authentication service
503PUB_AUTH_UPSTREAM_UNAVAILABLEAuthentication service temporarily unavailable

PUB_CERT_HEADER_MISSING (400)

No client certificate was presented in the TLS handshake.

How to fix: configure your HTTP client to send the certificate and private key in the TLS connection (for example, curl --cert client.crt --key client.key). If there is a proxy or gateway between your application and FyPay that terminates TLS, the connection to api.fypay.me must come from a client that presents the certificate.

PUB_CERT_MALFORMED_PEM (400)

Certificate in invalid PEM format.

PUB_REQUEST_BODY_INVALID (400)

The JSON body did not match the schema (missing field, wrong type, or clientId is not a UUID v4). Check details.violations to identify the offending field(s) and send Content-Type: application/json.

PUB_CERT_NOT_VERIFIED (401)

The certificate presented in the TLS handshake was rejected.

How to fix: use the certificate issued by FyPay for your account and its matching private key. Certificates from other certificate authorities are not accepted.

PUB_CERT_NOT_YET_VALID (401)

The certificate's notBefore date is in the future.

How to fix: synchronize the system clock via NTP. If the date really is in the future, wait or request a new certificate.

PUB_CERT_EXPIRED (401)

The certificate's notAfter date has passed. Certificates are valid for 2 years from issuance.

How to fix: request a new certificate from support and replace it in your integration. Plan the replacement before expiration.

PUB_CERT_NOT_REGISTERED (401)

The certificate's SHA-256 fingerprint is not in the registry: it was never registered or has been revoked.

How to fix: compute the local fingerprint with openssl x509 -noout -fingerprint -sha256 -in client.crt and send it to support together with the errorId from the response.

PUB_INVALID_CREDENTIALS (401)

The clientId is not recognized, or the clientSecret does not match. The platform deliberately does not distinguish these cases (enumeration resistance).

How to fix: check both clientId and clientSecret. Since the clientSecret is shown only once, if you lost it generate a new credential under Settings → Credentials.

PUB_CERT_NOT_AUTHORIZED_FOR_ACCOUNT (403)

The certificate is valid and registered, but it is not linked to the account that owns the given clientId.

How to fix: make sure you are using the certificate/clientId pair from the same account.

PUB_ACCOUNT_INACTIVE (403)

The credentials are correct, but the account is not active. Retrying will not succeed.

How to fix: contact support with the errorId from the response.

PUB_AUTH_UPSTREAM_UNAVAILABLE (503)

The authentication service is temporarily unreachable.

How to fix: retry with exponential backoff (for example, 1s, 2s, 4s). If the problem persists for more than one minute, contact support.

PUB_AUTH_UPSTREAM_ERROR (502)

The authentication service returned an unexpected error.

How to fix: contact support with the errorId.

Troubleshooting

"I get PUB_CERT_HEADER_MISSING even though I send the certificate"

The certificate must go in the TLS handshake, not in an HTTP header. Make sure your HTTP client is configured with certificate and key (for example, --cert/--key in curl, cert=(crt, key) in Python requests) and that no intermediate proxy terminates TLS before reaching FyPay.

"I occasionally get PUB_CERT_NOT_YET_VALID"

The system clock is behind the certificate's notBefore date. Configure NTP and keep the host clock within a few seconds of UTC. Containers may inherit host clock drift; check with date -u inside the container.

"I get PUB_CERT_NOT_REGISTERED after replacing the certificate"

Compute the new certificate's fingerprint (openssl x509 -noout -fingerprint -sha256 -in client.crt) and confirm with support that it is registered and linked to your account before switching over.

Best Practices

Next Steps

On this page