Authentication
Open Pay uses three distinct authentication mechanisms, each designed for a specific integration pattern.
Mechanism Use Case Where JWT User sessions in web portals Merchant Portal, Admin Dashboard HMAC-SHA256 Server-to-server API calls SDKs, direct API integration ED25519 Webhook signature verification Incoming webhook payloads
JWT Authentication
JWT authentication is used by the Merchant Portal and Admin Dashboard for user sessions.
Login Flow
Authenticate
Send your credentials to the login endpoint: const response = await fetch (
"https://olp-api.nipuntheekshana.com/v1/auth/login" ,
{
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({
email: "merchant@example.com" ,
password: "your-password" ,
}),
}
);
const { accessToken , refreshToken , expiresIn } = await response . json ();
Use the Access Token
Include the access token in the Authorization header for all subsequent requests: const payments = await fetch (
"https://olp-api.nipuntheekshana.com/v1/payments" ,
{
headers: {
Authorization: `Bearer ${ accessToken } ` ,
},
}
);
Refresh When Expired
Access tokens expire after 15 minutes . Use the refresh token to obtain a new pair: const response = await fetch (
"https://olp-api.nipuntheekshana.com/v1/auth/refresh" ,
{
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({
refreshToken: refreshToken ,
}),
}
);
const { accessToken : newAccessToken , refreshToken : newRefreshToken } =
await response . json ();
Refresh tokens are valid for 7 days and are single-use. Each refresh returns a new token pair.
Token Details
Property Access Token Refresh Token Lifetime 15 minutes 7 days Format JWT (RS256) Opaque token Header Authorization: Bearer <token>Request body only Revocation Expires naturally Revoked on use or logout
Two-Factor Authentication (2FA)
Open Pay supports TOTP-based two-factor authentication for portal users.
Enable 2FA
Request a TOTP secret and QR code from the setup endpoint: const setup = await fetch (
"https://olp-api.nipuntheekshana.com/v1/auth/2fa/setup" ,
{
method: "POST" ,
headers: { Authorization: `Bearer ${ accessToken } ` },
}
);
const { secret , qrCodeUrl } = await setup . json ();
// Display qrCodeUrl to the user for scanning with an authenticator app
Verify and Activate
Submit a TOTP code from the authenticator app to confirm setup: await fetch (
"https://olp-api.nipuntheekshana.com/v1/auth/2fa/verify" ,
{
method: "POST" ,
headers: {
"Content-Type" : "application/json" ,
Authorization: `Bearer ${ accessToken } ` ,
},
body: JSON . stringify ({ code: "123456" }),
}
);
Login with 2FA
When 2FA is enabled, the login response returns requires2FA: true instead of tokens. Submit the TOTP code to complete authentication: const loginResponse = await fetch (
"https://olp-api.nipuntheekshana.com/v1/auth/login" ,
{
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({
email: "merchant@example.com" ,
password: "your-password" ,
}),
}
);
const result = await loginResponse . json ();
if ( result . requires2FA ) {
// Prompt user for TOTP code, then verify
const verified = await fetch (
"https://olp-api.nipuntheekshana.com/v1/auth/2fa/verify" ,
{
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({
tempToken: result . tempToken ,
code: "123456" ,
}),
}
);
const { accessToken , refreshToken } = await verified . json ();
}
HMAC-SHA256 Authentication
HMAC-SHA256 is used for server-to-server API calls via SDKs or direct integration. Every request is signed with your API secret to ensure authenticity and prevent tampering.
Header Description Example X-API-KeyYour public API key ak_live_abc123def456X-TimestampUnix timestamp in seconds (UTC) 1711468800X-SignatureHMAC-SHA256 hex digest of the signing string a1b2c3d4e5f6...Content-TypeMust be application/json for POST/PUT application/json
Signing Algorithm
The signature is computed as:
signature = HMAC-SHA256(apiSecret, timestamp + method + path + body)
Where:
apiSecret is your secret key (from the Integrations page)
timestamp is the value of the X-Timestamp header
method is the uppercase HTTP method (GET, POST, etc.)
path is the request path including query string (e.g., /v1/payments?limit=10)
body is the raw JSON request body (empty string for GET requests)
The server rejects requests where the timestamp differs from server time by more than 5 minutes to prevent replay attacks.
Code Examples
import crypto from "crypto" ;
function signRequest (
apiSecret : string ,
method : string ,
path : string ,
body : string = ""
) : { timestamp : string ; signature : string } {
const timestamp = Math . floor ( Date . now () / 1000 ). toString ();
const message = timestamp + method . toUpperCase () + path + body ;
const signature = crypto
. createHmac ( "sha256" , apiSecret )
. update ( message )
. digest ( "hex" );
return { timestamp , signature };
}
// Usage
const apiKey = process . env . OPENPAY_API_KEY ! ;
const apiSecret = process . env . OPENPAY_API_SECRET ! ;
const body = JSON . stringify ({ amount: 25.0 , currency: "USD" });
const { timestamp , signature } = signRequest (
apiSecret , "POST" , "/v1/checkout/sessions" , body
);
const response = await fetch (
"https://olp-api.nipuntheekshana.com/v1/checkout/sessions" ,
{
method: "POST" ,
headers: {
"Content-Type" : "application/json" ,
"X-API-Key" : apiKey ,
"X-Timestamp" : timestamp ,
"X-Signature" : signature ,
},
body ,
}
);
Error Responses
HTTP Status Error Code Description 401INVALID_API_KEYThe API key does not exist or has been revoked 401INVALID_SIGNATUREThe computed signature does not match 401TIMESTAMP_EXPIREDThe request timestamp is outside the 5-minute window 429RATE_LIMITEDToo many requests (default: 100 req/min per API key)
ED25519 Webhook Signatures
All outgoing webhooks from Open Pay are signed with an ED25519 private key. This allows you to verify that a webhook genuinely originated from Open Pay and has not been tampered with.
Verification Flow
Retrieve the Public Key
Fetch the platform’s ED25519 public key (you can cache this): const response = await fetch (
"https://olp-api.nipuntheekshana.com/v1/webhooks/public-key" ,
{
headers: { "X-API-Key" : apiKey },
}
);
const { publicKey } = await response . json ();
// publicKey is a base64-encoded ED25519 public key
Extract the Signature Headers
Each webhook request includes two signature headers: Header Description X-Webhook-SignatureBase64-encoded ED25519 signature X-Webhook-TimestampUnix timestamp when the webhook was sent
The signed message is the concatenation of the timestamp and the raw JSON body: signed_message = timestamp + "." + raw_body
Verify the Signature
Use the public key to verify the signature against the constructed message. TypeScript (Node.js)
Python
Go
import crypto from "crypto" ;
function verifyWebhookSignature (
publicKeyBase64 : string ,
signature : string ,
timestamp : string ,
body : string
) : boolean {
const publicKey = crypto . createPublicKey ({
key: Buffer . from ( publicKeyBase64 , "base64" ),
format: "der" ,
type: "spki" ,
});
const message = Buffer . from ( timestamp + "." + body );
const signatureBuffer = Buffer . from ( signature , "base64" );
return crypto . verify ( null , message , publicKey , signatureBuffer );
}
// In your webhook handler
app . post ( "/webhooks/openpay" , express . raw ({ type: "application/json" }), ( req , res ) => {
const signature = req . headers [ "x-webhook-signature" ] as string ;
const timestamp = req . headers [ "x-webhook-timestamp" ] as string ;
const body = req . body . toString ();
const isValid = verifyWebhookSignature (
cachedPublicKey ,
signature ,
timestamp ,
body
);
if ( ! isValid ) {
return res . status ( 401 ). json ({ error: "Invalid signature" });
}
// Process the event
const event = JSON . parse ( body );
console . log ( "Received event:" , event . type );
res . status ( 200 ). json ({ received: true });
});
Prevent Replay Attacks
Always validate the timestamp to reject stale webhook deliveries: const WEBHOOK_TOLERANCE_SECONDS = 300 ; // 5 minutes
function isTimestampValid ( timestamp : string ) : boolean {
const webhookTime = parseInt ( timestamp , 10 );
const currentTime = Math . floor ( Date . now () / 1000 );
return Math . abs ( currentTime - webhookTime ) <= WEBHOOK_TOLERANCE_SECONDS ;
}
If you do not validate the timestamp, an attacker who intercepts a valid webhook payload could replay it indefinitely.
Webhook Event Types
Event Description payment.createdA new payment has been initiated payment.completedPayment confirmed on-chain and credited payment.failedPayment failed or expired payment.refundedPayment has been refunded subscription.createdA new subscription has been activated subscription.renewedSubscription payment collected successfully subscription.cancelledSubscription has been cancelled withdrawal.completedFiat withdrawal processed to merchant bank withdrawal.failedWithdrawal processing failed
Best Practices
Never Expose Secrets Client-Side API secrets and signing logic must live on your server. Never include them in frontend JavaScript or mobile app bundles.
Rotate Keys Periodically Generate new API keys from the Integrations page and deprecate old ones. Open Pay supports multiple active keys per merchant for zero-downtime rotation.
Always Verify Webhooks Never trust webhook payloads without verifying the ED25519 signature. Treat unverified payloads as potentially malicious.
Use Environment Variables Store API keys, secrets, and webhook public keys in environment variables or a secrets manager — never hardcode them in source code.