Skip to main content

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:
{
  "timestamp":      "2026-05-19T14:30:00Z",
  "correlation_id": "a1b2c3d4-e5f6-4789-abcd-000000000001",
  "payload":        {}
}
FieldTypeDescription
timestampstring (RFC 3339)Current UTC time. Requests outside ±1 minute are rejected.
correlation_idstring (UUID)UUID of the master–sub-merchant relationship. Omit entirely for endpoints that operate at the master level (see each endpoint).
payloadobjectEndpoint-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:
HeaderDescription
api-keyYour master merchant API key
X-TimestampRFC 3339 UTC timestamp. Requests outside ±1 minute are rejected.
X-Correlation-IDUUID of the master–sub-merchant relationship.
X-SignatureBase64-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.
GET /v2/naas/submerchants/onboarding_status is the only GET endpoint that uses only api-key — no signed headers required.

Key setup

1. Generate an RSA key pair

The key must be at least 2048 bits. 4096 bits is recommended for long-lived keys.
# 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:
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.
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
}
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()
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') };
}

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.
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)
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(),
    }
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'),
  };
}

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:
HeaderFormatDescription
X-Signaturehex stringHMAC-SHA256 of the raw request body
X-NOX-Signaturesha256=<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.
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))
}
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)