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
| Field | Required | Description |
|---|---|---|
iss | Yes | The issuer URL registered with Paynovus for your account. |
sub | Yes | Your partner identifier in Paynovus. |
exp | Yes | Expiration time (Unix timestamp). Recommended: 60 seconds from now. |
iat | Yes | Issued-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:
| Field | Description |
|---|---|
keyId | A 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. |
algorithm | The cipher suite Paynovus uses — currently the only supported value is P521-ECDH-HKDF-SHA256-A256GCM. |
publicKey | The PEM-encoded public key. Include the full -----BEGIN PUBLIC KEY----- / -----END PUBLIC KEY----- markers; Paynovus stores it verbatim. |
status | active 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-cvvGET /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: