O que é o NaaS?
O Noxpay as a Service (NaaS) permite que um merchant master opere contas de sub-merchants de forma programática. Usando sua chave de API master junto com uma assinatura criptográfica, você pode cadastrar sub-merchants, iniciar onboarding KYB, criar links de checkout, gerenciar carteiras, iniciar saques e consultar splits — tudo vinculado a um sub-merchant específico via correlation_id.
URL base
https://checkout.noxpay.io
Todos os endpoints NaaS estão sob o prefixo /v2/naas/.
Autenticação
Toda requisição NaaS exige o header api-key. O mecanismo de autenticação difere pelo método HTTP:
- Requisições POST carregam a autenticação no corpo da requisição como um envelope JSON assinado.
- Requisições GET carregam a autenticação inteiramente em headers — nenhum corpo é enviado.
Requisições POST — envelope assinado
Todo corpo POST é um envelope JSON:
{
"timestamp": "2026-05-19T14:30:00Z",
"correlation_id": "a1b2c3d4-e5f6-4789-abcd-000000000001",
"payload": {}
}
| Campo | Tipo | Descrição |
|---|
timestamp | string (RFC 3339) | Hora atual em UTC. Requisições fora de ±1 minuto são rejeitadas. |
correlation_id | string (UUID) | UUID do relacionamento master–sub-merchant. Omitir completamente para endpoints que operam no nível do master (consulte cada endpoint). |
payload | object | Corpo específico do endpoint. |
Serialize o envelope para bytes, calcule o SHA-256, assine com RSA PKCS#1 v1.5, codifique o resultado em Base64 e envie como X-Signature.
Endpoints GET não enviam corpo. Passe a autenticação em três headers adicionais:
| Header | Descrição |
|---|
api-key | Sua chave de API do merchant master |
X-Timestamp | Timestamp UTC no formato RFC 3339. Requisições fora de ±1 minuto são rejeitadas. |
X-Correlation-ID | UUID do relacionamento master–sub-merchant. |
X-Signature | Assinatura RSA-PKCS1v15-SHA256 da string canônica <X-Timestamp>\n<X-Correlation-ID>, codificada em Base64 |
Assine os bytes UTF-8 da string canônica: o valor exato de X-Timestamp, uma quebra de linha (\n), seguido do valor exato de X-Correlation-ID.
GET /v2/naas/submerchants/onboarding_status é o único endpoint GET que usa apenas api-key — não são necessários headers assinados.
Configuração de chave
1. Gerar um par de chaves RSA
A chave deve ter pelo menos 2048 bits. Recomendamos 4096 bits para chaves de longa duração.
# Gerar chave privada de 4096 bits
openssl genrsa -out naas_private.pem 4096
# Extrair a chave pública
openssl rsa -in naas_private.pem -pubout -out naas_public.pem
O formato PKCS#1 de chave pública também é aceito:
openssl rsa -in naas_private.pem -RSAPublicKey_out -out naas_public_pkcs1.pem
Mantenha naas_private.pem em seus servidores e nunca a compartilhe. Apenas a chave pública é registrada na Noxpay.
2. Registrar a chave pública
No painel da Noxpay, acesse NaaS → Configuração → Chave Pública, cole o conteúdo de naas_public.pem e salve. A chave entra em vigor imediatamente. Você pode rotacioná-la a qualquer momento — a chave anterior é desativada automaticamente.
3. Assinar requisições
POST — assinar o envelope
- Monte o envelope JSON com o timestamp UTC atual, opcionalmente o
correlation_id, e o payload do endpoint.
- Serialize para bytes — não re-serialize após assinar.
- Calcule o SHA-256 dos bytes.
- Assine com sua chave privada RSA usando padding PKCS#1 v1.5.
- Codifique o resultado em Base64 e envie como
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') };
}
Monte a string canônica <X-Timestamp>\n<X-Correlation-ID>, assine o hash SHA-256 com sua chave RSA (PKCS#1 v1.5) e codifique o resultado em Base64 como 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
}
// Uso:
// 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'),
};
}
Proteção contra replay
Cada combinação (timestamp, correlation_id, signature) é tratada como um token de uso único. Sempre use o horário atual ao montar o envelope ou assinar headers — nunca reutilize uma requisição previamente assinada.
Verificação de webhook
Todos os webhooks do NaaS são assinados com HMAC-SHA256 usando seu segredo de webhook. A assinatura é entregue em dois headers:
| Header | Formato | Descrição |
|---|
X-Signature | string hex | HMAC-SHA256 do corpo bruto da requisição |
X-NOX-Signature | sha256=<hex> | Mesmo digest com o prefixo sha256= (estilo GitHub) |
Verifique qualquer um dos headers — eles carregam o mesmo digest.
Registre seu segredo de webhook em NaaS → Configuração → Segredo de Webhook. O segredo deve ter entre 32 e 512 caracteres imprimíveis, sem espaços.
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)