Skip to main content

Authentication

All routes except the public /health-check endpoint require a JWT Bearer token signed with the ES512 algorithm.

Overview​

Every request must include an Authorization header:

Authorization: Bearer <your-jwt-token>

The JWT payload should include at least these fields:

{
"iss": "https://your-partner-url.com",
"sub": "YOUR_PARTNER_ID",
"exp": 1700000060,
"iat": 1700000000
}

JWT payload fields​

FieldRequiredDescription
issYesThe issuer URL registered with Paynovus for your account.
subYesYour partner identifier in Paynovus.
expYesExpiration time (Unix timestamp). Recommended: 60 seconds from now.
iatYesIssued-at time (Unix timestamp).

Key generation​

Generate an EC key pair for JWT signing and share the public key with Paynovus. Keep the private key secret. This key is used only for authentication.

# Generate private key in SEC1 format
openssl ecparam -genkey -name secp521r1 -noout -out private-sec1.pem

# Convert to PKCS#8 format (required for most JWT libraries)
openssl pkcs8 -topk8 -nocrypt -in private-sec1.pem -out private.pem

# Extract the public key
openssl ec -in private-sec1.pem -pubout -out public.pem

Provide public.pem to Paynovus and use private.pem to sign your JWT tokens.

PAN and CVV encryption key​

PAN and CVV responses use a separate P-521 encryption key. Do not reuse the ES512 signing key. The signing key verifies JWT signatures; the encryption key allows Paynovus to encrypt a response that only the partner's private key can decrypt.

Generate the encryption key pair separately:

# Generate the partner encryption private key
openssl ecparam -genkey -name secp521r1 -noout -out partner-encryption-sec1.pem

# Convert it to PKCS#8 format
openssl pkcs8 -topk8 -nocrypt \
-in partner-encryption-sec1.pem \
-out partner-encryption-private.pem

# Extract the encryption public key
openssl ec -in partner-encryption-sec1.pem \
-pubout -out partner-encryption-public.pem

Provide only partner-encryption-public.pem to Paynovus and keep partner-encryption-private.pem in the partner's secret manager. Register it on the Partner record as encryption:

{
"keyId": "partner-encryption-2026-01",
"algorithm": "P521-ECDH-HKDF-SHA256-A256GCM",
"publicKey": "-----BEGIN PUBLIC KEY-----...",
"status": "active"
}

Fields:

FieldDescription
keyIdA stable id you control for this key (e.g. "partner-encryption-2026-01"). The same keyId is returned inside every encrypted envelope so the partner always knows which private key to use.
algorithmThe cipher suite Paynovus uses — currently the only supported value is P521-ECDH-HKDF-SHA256-A256GCM.
publicKeyThe PEM-encoded public key. Include the full -----BEGIN PUBLIC KEY----- / -----END PUBLIC KEY----- markers; Paynovus stores it verbatim.
statusactive for normal use, retiring during key rollover, revoked once retired. Paynovus only encrypts with keys whose status is active.

The authentication publicKey and encryption publicKey must be different key pairs, even though both use the P-521 curve. Paynovus only reads the encryption.publicKey for the sensitive-data endpoints below — the auth key is never used for encryption.

Verifying the keypair​

Before registering, confirm the public key matches the private key:

openssl ec -in partner-encryption-private.pem -pubout \
| diff - partner-encryption-public.pem
# (no output = match)

MongoDB registration example​

If you are patching the Partner record directly (e.g. via mongosh):

db.Partners.updateOne(
{ url: 'https://your-registered-url.com' },
{
$set: {
encryption: {
keyId: 'partner-encryption-2026-01',
algorithm: 'P521-ECDH-HKDF-SHA256-A256GCM',
publicKey: '-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n',
status: 'active'
}
}
}
);

Generate a JWT token (Node.js)​

const jwt = require('jsonwebtoken');
const fs = require('fs');

const privateKey = fs.readFileSync('./private.pem', 'utf8');

function createPartnerToken({ issuer, subject }) {
const now = Math.floor(Date.now() / 1000);
const payload = {
iss: issuer,
sub: subject,
exp: now + 60,
iat: now
};

return jwt.sign(payload, privateKey, { algorithm: 'ES512' });
}

Sending the token in requests​

Include the token in the Authorization header for every protected endpoint.

curl https://api.card-management-staging.paynovus.com/api/partner/me \
-H "Authorization: Bearer $TOKEN"

If the token is valid and your account is correctly configured, the request will succeed and return your account details.

Token expiry​

  • Tokens expire at the time specified by exp.
  • Recommended token lifetime: 60 seconds.
  • Expired tokens will be rejected with 401 Unauthorized.

When the encryption key is used​

The encryption key is used only for successful responses from:

  • GET /api/partner/cards/{id}/card-cvv
  • GET /api/partner/cards/{id}/card-number

The request still uses the normal ES512 Bearer JWT and the normal card ownership checks. No private key, encryption secret, or extra per-request key is sent to Paynovus. The API generates an ephemeral P-521 key for each response, derives a fresh AES-256-GCM key with HKDF-SHA256, and returns a small encrypted envelope (v p c k e iv ct t, ~280 bytes total). See Sensitive data → PAN and CVV response encryption for the exact envelope shape and a working decryption example.

Key generation​

Partners must generate an EC key pair and provide the public key to Paynovus. Keep the private key secret.

# Generate private key in SEC1 format
openssl ecparam -genkey -name secp521r1 -noout -out private-sec1.pem

# Convert to PKCS#8 format (required for most JWT libraries)
openssl pkcs8 -topk8 -nocrypt -in private-sec1.pem -out private.pem

# Extract the public key
openssl ec -in private-sec1.pem -pubout -out public.pem

Provide public.pem to Paynovus. Use private.pem to sign your JWT tokens.

Generating a JWT Token (Node.js)​

const jwt = require('jsonwebtoken');
const fs = require('fs');

const privateKey = fs.readFileSync('./private.pem', 'utf8');

function createPartnerToken({ issuer, subject }) {
const now = Math.floor(Date.now() / 1000);
const payload = {
iss: issuer,
sub: subject,
exp: now + 60,
iat: now
};

return jwt.sign(payload, privateKey, { algorithm: 'ES512' });
}

module.exports = { createPartnerToken };

Request lifecycle (per-resource)​

Every authenticated request is scoped to your partner. When a request carries an :id (tenant, cardholder, card, IBAN), the endpoint returns 404 Not Found if the id does not belong to your partner — the response is identical whether the resource does not exist or belongs to another partner, so a caller cannot probe your partner's resources.

Partner-wide listings (GET /cards, /cardholders, /transactions, /balances*) return every resource your partner owns.

What's next?​

You're ready to call protected endpoints. Head to the API Reference for the full list of routes, request/response shapes, and the Try it widget: