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

# NaaS — Authentication & Setup

> How to authenticate as a master merchant and sign requests for the Noxpay as a Service (NaaS) API.

## What is NaaS?

Noxpay as a Service (NaaS) lets a **master merchant** operate sub-merchant accounts programmatically. Using your master API key plus a cryptographic signature, you can enroll sub-merchants, trigger KYB onboarding, create checkout links, manage wallets, initiate withdrawals, and query splits — all scoped to a specific sub-merchant via a `correlation_id`.

## Base URL

```
https://checkout.noxpay.io
```

All NaaS endpoints are under the `/v2/naas/` prefix.

## Authentication

Every NaaS request requires an `api-key` header. The authentication mechanism differs by HTTP method:

* **POST** requests carry authentication in the **request body** as a signed JSON envelope.
* **GET** requests carry authentication entirely in **request headers** — no body is sent.

### POST requests — signed envelope

Every POST body is a JSON envelope:

```json theme={null}
{
  "timestamp":      "2026-05-19T14:30:00Z",
  "correlation_id": "a1b2c3d4-e5f6-4789-abcd-000000000001",
  "payload":        {}
}
```

| Field            | Type              | Description                                                                                                                         |
| ---------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `timestamp`      | string (RFC 3339) | Current UTC time. Requests outside ±1 minute are rejected.                                                                          |
| `correlation_id` | string (UUID)     | UUID of the master–sub-merchant relationship. **Omit entirely** for endpoints that operate at the master level (see each endpoint). |
| `payload`        | object            | Endpoint-specific body.                                                                                                             |

Serialize the envelope to bytes, compute SHA-256, sign with RSA PKCS#1 v1.5, Base64-encode the result, and send it as `X-Signature`.

### GET requests — signed headers

GET endpoints carry no body. Pass authentication in three additional headers:

| Header             | Description                                                                         |
| ------------------ | ----------------------------------------------------------------------------------- |
| `api-key`          | Your master merchant API key                                                        |
| `X-Timestamp`      | RFC 3339 UTC timestamp. Requests outside ±1 minute are rejected.                    |
| `X-Correlation-ID` | UUID of the master–sub-merchant relationship.                                       |
| `X-Signature`      | Base64-encoded RSA-PKCS1v15-SHA256 signature of `<X-Timestamp>\n<X-Correlation-ID>` |

Sign the UTF-8 bytes of the canonical string: the exact `X-Timestamp` value, a newline (`\n`), then the exact `X-Correlation-ID` value.

<Note>
  `GET /v2/naas/submerchants/onboarding_status` is the only GET endpoint that uses only `api-key` — no signed headers required.
</Note>

## Key setup

### 1. Generate an RSA key pair

The key must be at least 2048 bits. 4096 bits is recommended for long-lived keys.

```bash theme={null}
# Generate a 4096-bit private key
openssl genrsa -out naas_private.pem 4096

# Extract the public key
openssl rsa -in naas_private.pem -pubout -out naas_public.pem
```

A PKCS#1 public key format is also accepted:

```bash theme={null}
openssl rsa -in naas_private.pem -RSAPublicKey_out -out naas_public_pkcs1.pem
```

Keep `naas_private.pem` on your servers and never share it. Only the public key is registered with Noxpay.

### 2. Register the public key

In the Noxpay dashboard, go to **NaaS → Setup → Public Key**, paste the contents of `naas_public.pem`, and save. The key takes effect immediately. You can rotate it at any time — the previous key is deactivated automatically.

### 3. Sign requests

#### POST — sign the envelope body

1. Build the JSON envelope with the current UTC timestamp, optionally the `correlation_id`, and the endpoint payload.
2. Serialize it to bytes — do not re-serialize after signing.
3. Compute SHA-256 of the bytes.
4. Sign with your RSA private key using PKCS#1 v1.5 padding.
5. Base64-encode the signature and send it as `X-Signature`.

<CodeGroup>
  ```go Go theme={null}
  import (
      "crypto"
      "crypto/rand"
      "crypto/rsa"
      "crypto/sha256"
      "crypto/x509"
      "encoding/base64"
      "encoding/json"
      "encoding/pem"
      "time"
  )

  func signEnvelope(privateKeyPEM []byte, correlationID string, payload any) (body []byte, sig string, err error) {
      block, _ := pem.Decode(privateKeyPEM)
      key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
      if err != nil {
          return nil, "", err
      }
      envelope := map[string]any{
          "timestamp": time.Now().UTC().Format(time.RFC3339),
          "payload":   payload,
      }
      if correlationID != "" {
          envelope["correlation_id"] = correlationID
      }
      body, err = json.Marshal(envelope)
      if err != nil {
          return nil, "", err
      }
      hash := sha256.Sum256(body)
      sigBytes, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, hash[:])
      if err != nil {
          return nil, "", err
      }
      return body, base64.StdEncoding.EncodeToString(sigBytes), nil
  }
  ```

  ```python Python theme={null}
  import base64, json, time
  from cryptography.hazmat.primitives import hashes, serialization
  from cryptography.hazmat.primitives.asymmetric import padding

  with open("naas_private.pem", "rb") as f:
      private_key = serialization.load_pem_private_key(f.read(), password=None)

  def sign_envelope(payload: dict, correlation_id: str = None) -> tuple[bytes, str]:
      envelope = {
          "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
          "payload": payload,
      }
      if correlation_id:
          envelope["correlation_id"] = correlation_id
      body = json.dumps(envelope, separators=(",", ":")).encode()
      sig = private_key.sign(body, padding.PKCS1v15(), hashes.SHA256())
      return body, base64.b64encode(sig).decode()
  ```

  ```javascript Node.js theme={null}
  const crypto = require('crypto');
  const fs = require('fs');

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

  function signEnvelope(payload, correlationId = null) {
    const envelope = {
      timestamp: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
      payload,
      ...(correlationId ? { correlation_id: correlationId } : {}),
    };
    const body = Buffer.from(JSON.stringify(envelope));
    const sig = crypto.sign('sha256', body, {
      key: privateKey,
      padding: crypto.constants.RSA_PKCS1_PADDING,
    });
    return { body, signature: sig.toString('base64') };
  }
  ```
</CodeGroup>

#### GET — sign the headers

Build the canonical string `<X-Timestamp>\n<X-Correlation-ID>`, sign its SHA-256 hash with your RSA private key (PKCS#1 v1.5), and Base64-encode the result as `X-Signature`.

<CodeGroup>
  ```go Go theme={null}
  func signGETHeaders(privateKeyPEM []byte, correlationID string) (timestamp, sig string, err error) {
      block, _ := pem.Decode(privateKeyPEM)
      key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
      if err != nil {
          return "", "", err
      }
      timestamp = time.Now().UTC().Format(time.RFC3339)
      canonical := timestamp + "\n" + correlationID
      hash := sha256.Sum256([]byte(canonical))
      sigBytes, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, hash[:])
      if err != nil {
          return "", "", err
      }
      return timestamp, base64.StdEncoding.EncodeToString(sigBytes), nil
  }

  // Usage:
  // ts, sig, _ := signGETHeaders(privKeyPEM, correlationID)
  // req.Header.Set("X-Timestamp", ts)
  // req.Header.Set("X-Correlation-ID", correlationID)
  // req.Header.Set("X-Signature", sig)
  ```

  ```python Python theme={null}
  import base64, time
  from cryptography.hazmat.primitives import hashes
  from cryptography.hazmat.primitives.asymmetric import padding

  def sign_get_headers(private_key, correlation_id: str) -> dict:
      timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
      canonical = f"{timestamp}\n{correlation_id}".encode()
      sig = private_key.sign(canonical, padding.PKCS1v15(), hashes.SHA256())
      return {
          "X-Timestamp": timestamp,
          "X-Correlation-ID": correlation_id,
          "X-Signature": base64.b64encode(sig).decode(),
      }
  ```

  ```javascript Node.js theme={null}
  function signGETHeaders(correlationId) {
    const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
    const canonical = Buffer.from(`${timestamp}\n${correlationId}`);
    const sig = crypto.sign('sha256', canonical, {
      key: privateKey,
      padding: crypto.constants.RSA_PKCS1_PADDING,
    });
    return {
      'X-Timestamp': timestamp,
      'X-Correlation-ID': correlationId,
      'X-Signature': sig.toString('base64'),
    };
  }
  ```
</CodeGroup>

## Replay protection

Each `(timestamp, correlation_id, signature)` triple is treated as a one-time token. Always use the current time when building the envelope or signing headers — never reuse a previously signed request.

## Webhook verification

All NaaS webhooks are signed with HMAC-SHA256 using your webhook secret. The signature is delivered in two headers:

| Header            | Format         | Description                                        |
| ----------------- | -------------- | -------------------------------------------------- |
| `X-Signature`     | hex string     | HMAC-SHA256 of the raw request body                |
| `X-NOX-Signature` | `sha256=<hex>` | Same digest prefixed with `sha256=` (GitHub-style) |

Verify either header — they carry the same digest.

Register your webhook secret at **NaaS → Setup → Webhook Secret**. The secret must be 32–512 printable characters with no spaces.

<CodeGroup>
  ```go Go theme={null}
  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
  )

  func verifyWebhook(body []byte, secret, receivedSig string) bool {
      mac := hmac.New(sha256.New, []byte(secret))
      mac.Write(body)
      expected := hex.EncodeToString(mac.Sum(nil))
      return hmac.Equal([]byte(expected), []byte(receivedSig))
  }
  ```

  ```python Python theme={null}
  import hashlib, hmac

  def verify_webhook(body: bytes, secret: str, received_sig: str) -> bool:
      expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, received_sig)
  ```
</CodeGroup>
