Skip to main content

Authentication

Open Pay uses three distinct authentication mechanisms, each designed for a specific integration pattern.
MechanismUse CaseWhere
JWTUser sessions in web portalsMerchant Portal, Admin Dashboard
HMAC-SHA256Server-to-server API callsSDKs, direct API integration
ED25519Webhook signature verificationIncoming webhook payloads

JWT Authentication

JWT authentication is used by the Merchant Portal and Admin Dashboard for user sessions.

Login Flow

1

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

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}`,
    },
  }
);
3

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

PropertyAccess TokenRefresh Token
Lifetime15 minutes7 days
FormatJWT (RS256)Opaque token
HeaderAuthorization: Bearer <token>Request body only
RevocationExpires naturallyRevoked on use or logout

Two-Factor Authentication (2FA)

Open Pay supports TOTP-based two-factor authentication for portal users.
1

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
2

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" }),
  }
);
3

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.

Required Headers

HeaderDescriptionExample
X-API-KeyYour public API keyak_live_abc123def456
X-TimestampUnix timestamp in seconds (UTC)1711468800
X-SignatureHMAC-SHA256 hex digest of the signing stringa1b2c3d4e5f6...
Content-TypeMust be application/json for POST/PUTapplication/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 StatusError CodeDescription
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

1

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
2

Extract the Signature Headers

Each webhook request includes two signature headers:
HeaderDescription
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
3

Verify the Signature

Use the public key to verify the signature against the constructed message.
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 });
});
4

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

EventDescription
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.