> ## Documentation Index
> Fetch the complete documentation index at: https://docs-openpay.nipuntheekshana.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Three authentication layers securing the Open Pay platform

# 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

<Steps>
  <Step title="Authenticate">
    Send your credentials to the login endpoint:

    ```typescript theme={null}
    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();
    ```
  </Step>

  <Step title="Use the Access Token">
    Include the access token in the `Authorization` header for all subsequent requests:

    ```typescript theme={null}
    const payments = await fetch(
      "https://olp-api.nipuntheekshana.com/v1/payments",
      {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      }
    );
    ```
  </Step>

  <Step title="Refresh When Expired">
    Access tokens expire after **15 minutes**. Use the refresh token to obtain a new pair:

    ```typescript theme={null}
    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();
    ```

    <Info>
      Refresh tokens are valid for **7 days** and are single-use. Each refresh returns a new token pair.
    </Info>
  </Step>
</Steps>

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

<Steps>
  <Step title="Enable 2FA">
    Request a TOTP secret and QR code from the setup endpoint:

    ```typescript theme={null}
    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
    ```
  </Step>

  <Step title="Verify and Activate">
    Submit a TOTP code from the authenticator app to confirm setup:

    ```typescript theme={null}
    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" }),
      }
    );
    ```
  </Step>

  <Step title="Login with 2FA">
    When 2FA is enabled, the login response returns `requires2FA: true` instead of tokens. Submit the TOTP code to complete authentication:

    ```typescript theme={null}
    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();
    }
    ```
  </Step>
</Steps>

***

## 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

| Header         | Description                                  | Example                |
| -------------- | -------------------------------------------- | ---------------------- |
| `X-API-Key`    | Your public API key                          | `ak_live_abc123def456` |
| `X-Timestamp`  | Unix timestamp in seconds (UTC)              | `1711468800`           |
| `X-Signature`  | HMAC-SHA256 hex digest of the signing string | `a1b2c3d4e5f6...`      |
| `Content-Type` | Must 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)

<Info>
  The server rejects requests where the timestamp differs from server time by more than **5 minutes** to prevent replay attacks.
</Info>

### Code Examples

<CodeGroup>
  ```typescript TypeScript theme={null}
  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,
    }
  );
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import time
  import requests
  import json
  import os

  def sign_request(api_secret: str, method: str, path: str, body: str = "") -> tuple:
      timestamp = str(int(time.time()))
      message = timestamp + method.upper() + path + body
      signature = hmac.new(
          api_secret.encode(),
          message.encode(),
          hashlib.sha256,
      ).hexdigest()
      return timestamp, signature

  # Usage
  api_key = os.environ["OPENPAY_API_KEY"]
  api_secret = os.environ["OPENPAY_API_SECRET"]

  body = json.dumps({"amount": 25.0, "currency": "USD"})
  timestamp, signature = sign_request(
      api_secret, "POST", "/v1/checkout/sessions", body
  )

  response = requests.post(
      "https://olp-api.nipuntheekshana.com/v1/checkout/sessions",
      headers={
          "Content-Type": "application/json",
          "X-API-Key": api_key,
          "X-Timestamp": timestamp,
          "X-Signature": signature,
      },
      data=body,
  )
  ```

  ```go Go theme={null}
  package main

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "fmt"
      "net/http"
      "os"
      "strings"
      "strconv"
      "time"
  )

  func signRequest(apiSecret, method, path, body string) (string, string) {
      timestamp := strconv.FormatInt(time.Now().Unix(), 10)
      message := timestamp + strings.ToUpper(method) + path + body

      mac := hmac.New(sha256.New, []byte(apiSecret))
      mac.Write([]byte(message))
      signature := hex.EncodeToString(mac.Sum(nil))

      return timestamp, signature
  }

  func main() {
      apiKey := os.Getenv("OPENPAY_API_KEY")
      apiSecret := os.Getenv("OPENPAY_API_SECRET")

      body := `{"amount":25.0,"currency":"USD"}`
      timestamp, signature := signRequest(
          apiSecret, "POST", "/v1/checkout/sessions", body,
      )

      req, _ := http.NewRequest("POST",
          "https://olp-api.nipuntheekshana.com/v1/checkout/sessions",
          strings.NewReader(body),
      )
      req.Header.Set("Content-Type", "application/json")
      req.Header.Set("X-API-Key", apiKey)
      req.Header.Set("X-Timestamp", timestamp)
      req.Header.Set("X-Signature", signature)

      client := &http.Client{}
      resp, err := client.Do(req)
      if err != nil {
          fmt.Println("Error:", err)
          return
      }
      defer resp.Body.Close()
      fmt.Println("Status:", resp.StatusCode)
  }
  ```

  ```php PHP theme={null}
  <?php

  function signRequest(
      string $apiSecret,
      string $method,
      string $path,
      string $body = ""
  ): array {
      $timestamp = (string) time();
      $message = $timestamp . strtoupper($method) . $path . $body;
      $signature = hash_hmac("sha256", $message, $apiSecret);

      return ["timestamp" => $timestamp, "signature" => $signature];
  }

  // Usage
  $apiKey = getenv("OPENPAY_API_KEY");
  $apiSecret = getenv("OPENPAY_API_SECRET");

  $body = json_encode(["amount" => 25.0, "currency" => "USD"]);
  $sign = signRequest($apiSecret, "POST", "/v1/checkout/sessions", $body);

  $ch = curl_init("https://olp-api.nipuntheekshana.com/v1/checkout/sessions");
  curl_setopt_array($ch, [
      CURLOPT_POST => true,
      CURLOPT_POSTFIELDS => $body,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => [
          "Content-Type: application/json",
          "X-API-Key: " . $apiKey,
          "X-Timestamp: " . $sign["timestamp"],
          "X-Signature: " . $sign["signature"],
      ],
  ]);

  $response = curl_exec($ch);
  curl_close($ch);
  echo $response;
  ```
</CodeGroup>

### Error Responses

| HTTP Status | Error Code          | Description                                          |
| ----------- | ------------------- | ---------------------------------------------------- |
| `401`       | `INVALID_API_KEY`   | The API key does not exist or has been revoked       |
| `401`       | `INVALID_SIGNATURE` | The computed signature does not match                |
| `401`       | `TIMESTAMP_EXPIRED` | The request timestamp is outside the 5-minute window |
| `429`       | `RATE_LIMITED`      | Too 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

<Steps>
  <Step title="Retrieve the Public Key">
    Fetch the platform's ED25519 public key (you can cache this):

    ```typescript theme={null}
    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
    ```
  </Step>

  <Step title="Extract the Signature Headers">
    Each webhook request includes two signature headers:

    | Header                | Description                              |
    | --------------------- | ---------------------------------------- |
    | `X-Webhook-Signature` | Base64-encoded ED25519 signature         |
    | `X-Webhook-Timestamp` | Unix 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
    ```
  </Step>

  <Step title="Verify the Signature">
    Use the public key to verify the signature against the constructed message.

    <CodeGroup>
      ```typescript TypeScript (Node.js) theme={null}
      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 });
      });
      ```

      ```python Python theme={null}
      import base64
      from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
      from cryptography.hazmat.primitives.serialization import load_der_public_key

      def verify_webhook_signature(
          public_key_base64: str,
          signature: str,
          timestamp: str,
          body: str,
      ) -> bool:
          public_key_der = base64.b64decode(public_key_base64)
          public_key = load_der_public_key(public_key_der)

          message = f"{timestamp}.{body}".encode()
          signature_bytes = base64.b64decode(signature)

          try:
              public_key.verify(signature_bytes, message)
              return True
          except Exception:
              return False
      ```

      ```go Go theme={null}
      package main

      import (
          "crypto/ed25519"
          "encoding/base64"
          "fmt"
      )

      func verifyWebhookSignature(
          publicKeyBase64, signature, timestamp, body string,
      ) bool {
          publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKeyBase64)
          if err != nil {
              return false
          }

          // Extract the 32-byte key from the DER-encoded SPKI structure
          // For raw 32-byte keys, use publicKeyBytes directly
          pubKey := ed25519.PublicKey(publicKeyBytes[len(publicKeyBytes)-32:])

          sigBytes, err := base64.StdEncoding.DecodeString(signature)
          if err != nil {
              return false
          }

          message := []byte(timestamp + "." + body)
          return ed25519.Verify(pubKey, message, sigBytes)
      }
      ```
    </CodeGroup>
  </Step>

  <Step title="Prevent Replay Attacks">
    Always validate the timestamp to reject stale webhook deliveries:

    ```typescript theme={null}
    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;
    }
    ```

    <Warning>
      If you do not validate the timestamp, an attacker who intercepts a valid webhook payload could replay it indefinitely.
    </Warning>
  </Step>
</Steps>

### Webhook Event Types

| Event                    | Description                                 |
| ------------------------ | ------------------------------------------- |
| `payment.created`        | A new payment has been initiated            |
| `payment.completed`      | Payment confirmed on-chain and credited     |
| `payment.failed`         | Payment failed or expired                   |
| `payment.refunded`       | Payment has been refunded                   |
| `subscription.created`   | A new subscription has been activated       |
| `subscription.renewed`   | Subscription payment collected successfully |
| `subscription.cancelled` | Subscription has been cancelled             |
| `withdrawal.completed`   | Fiat withdrawal processed to merchant bank  |
| `withdrawal.failed`      | Withdrawal processing failed                |

***

## Best Practices

<CardGroup cols={2}>
  <Card title="Never Expose Secrets Client-Side" icon="eye-slash">
    API secrets and signing logic must live on your server. Never include them in frontend JavaScript or mobile app bundles.
  </Card>

  <Card title="Rotate Keys Periodically" icon="arrows-rotate">
    Generate new API keys from the Integrations page and deprecate old ones. Open Pay supports multiple active keys per merchant for zero-downtime rotation.
  </Card>

  <Card title="Always Verify Webhooks" icon="shield-check">
    Never trust webhook payloads without verifying the ED25519 signature. Treat unverified payloads as potentially malicious.
  </Card>

  <Card title="Use Environment Variables" icon="lock">
    Store API keys, secrets, and webhook public keys in environment variables or a secrets manager -- never hardcode them in source code.
  </Card>
</CardGroup>
