Pular para o conteúdo principal

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":        {}
}
CampoTipoDescrição
timestampstring (RFC 3339)Hora atual em UTC. Requisições fora de ±1 minuto são rejeitadas.
correlation_idstring (UUID)UUID do relacionamento master–sub-merchant. Omitir completamente para endpoints que operam no nível do master (consulte cada endpoint).
payloadobjectCorpo 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.

Requisições GET — headers assinados

Endpoints GET não enviam corpo. Passe a autenticação em três headers adicionais:
HeaderDescrição
api-keySua chave de API do merchant master
X-TimestampTimestamp UTC no formato RFC 3339. Requisições fora de ±1 minuto são rejeitadas.
X-Correlation-IDUUID do relacionamento master–sub-merchant.
X-SignatureAssinatura 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

  1. Monte o envelope JSON com o timestamp UTC atual, opcionalmente o correlation_id, e o payload do endpoint.
  2. Serialize para bytes — não re-serialize após assinar.
  3. Calcule o SHA-256 dos bytes.
  4. Assine com sua chave privada RSA usando padding PKCS#1 v1.5.
  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') };
}

GET — assinar os headers

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:
HeaderFormatoDescrição
X-Signaturestring hexHMAC-SHA256 do corpo bruto da requisição
X-NOX-Signaturesha256=<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)