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": {}
}
| 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 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.
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
- Build the JSON envelope with the current UTC timestamp, optionally the
correlation_id, and the endpoint payload.
- Serialize it to bytes — do not re-serialize after signing.
- Compute SHA-256 of the bytes.
- Sign with your RSA private key using PKCS#1 v1.5 padding.
- 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') };
}
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:
| 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.
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)