Chave assinada (par de chaves)
Você gera um par de chaves secp256k1, cadastra só a chave pública e assina cada requisição com a privada, que nunca sai do seu servidor.
Por que usar
| Risco | O que a chave assinada faz |
|---|---|
| Vazamento do segredo | Não existe segredo compartilhado: a APIX guarda só a chave pública |
| Requisição capturada e reenviada | Cada requisição vale uma vez, por até 5 minutos |
| Alteração de valor ou destino em trânsito | Método, caminho, query e body entram na assinatura |
| Uso a partir de outro servidor | A chave só funciona a partir dos IPs cadastrados |
1. Gere o par e cadastre a chave pública
openssl
# Chave privada (fica só no seu servidor)
openssl ecparam -name secp256k1 -genkey -noout -out priv.pem
# Chave pública (é ela que você cadastra no painel)
openssl ec -in priv.pem -pubout -out pub.pemNo painel, em Integrações & APIs, crie uma chave do tipo Par de chaves (assinatura), cole o conteúdo do pub.pem e informe os IPs de origem. Você recebe um Public Key ID (apix_pk_…).
Nunca envie a chave privada para a APIX nem cole em formulário nenhum.
2. Envie os quatro headers
| Header | Conteúdo |
|---|---|
| X-Public-Key-ID | O ID recebido no cadastro (apix_pk_…) |
| X-Timestamp | Momento da assinatura em ISO 8601 com fuso; até 5 minutos de diferença do servidor |
| X-Nonce | Valor aleatório único por requisição: 16 a 128 caracteres de [A-Za-z0-9._~+/=-] |
| X-Signature | Assinatura ECDSA secp256k1 + SHA-256, em DER codificado em base64 |
Não envie
X-API-Key junto com X-Public-Key-ID: a resposta é 400.3. Monte e assine a string
A string assinada é o JSON.stringify, sem espaços e nesta ordem de chaves, de:
| Campo | Valor |
|---|---|
| method | Método HTTP em maiúsculas |
| path | Caminho exatamente como enviado, antes do ? |
| query | Tudo depois do ?, sem o ?; "" quando não houver |
| body | O body idêntico ao enviado, como texto; "" quando não houver |
| timestamp | O mesmo valor do header X-Timestamp |
| nonce | O mesmo valor do header X-Nonce |
O servidor não reserializa o body: envie exatamente a string que você assinou. Serialize uma vez e reaproveite a mesma string no corpo da requisição.
| Linguagem | Como serializar para gerar os mesmos bytes |
|---|---|
| Node.js | JSON.stringify(obj) |
| PHP | json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) |
| Python | json.dumps(obj, separators=(",", ":"), ensure_ascii=False) |
| Go | json.Encoder com SetEscapeHTML(false), sem o \n final (json.Marshal escapa <, > e &) |
| Java | Jackson ObjectMapper com LinkedHashMap (preserva a ordem) |
| C# | JsonConvert.SerializeObject (Newtonsoft) na mesma ordem de propriedades |
Vetor de teste
Esta requisição precisa gerar exatamente a string abaixo. A assinatura muda a cada execução, mas o SHA-256 da string permite conferir se o seu código monta os mesmos bytes.
Requisição
POST /api/v1/direct-payments
X-Timestamp: 2026-09-16T12:00:00.000Z
X-Nonce: b7Qm4rT9xK2pLw8vZs3nYa
{"amount":990,"description":"Pedido #123","paymentMethod":"pix","customer":{"name":"João Silva","email":"joao@exemplo.com","document":"12345678909"}}String assinada
{"method":"POST","path":"/api/v1/direct-payments","query":"","body":"{\"amount\":990,\"description\":\"Pedido #123\",\"paymentMethod\":\"pix\",\"customer\":{\"name\":\"João Silva\",\"email\":\"joao@exemplo.com\",\"document\":\"12345678909\"}}","timestamp":"2026-09-16T12:00:00.000Z","nonce":"b7Qm4rT9xK2pLw8vZs3nYa"}SHA-256 (UTF-8) da string
9f3623e4ed27834921b88a1209d62300133e2faec6aa74a29007c2d0b920dbacExemplos
Node.js
import crypto from 'crypto';
import fs from 'fs';
const PUBLIC_KEY_ID = 'apix_pk_…';
const PRIVATE_KEY = fs.readFileSync('priv.pem', 'utf8');
const url = 'https://api.apix.tec.br/api/v1/direct-payments';
const body = JSON.stringify({
amount: 990,
description: 'Pedido #123',
paymentMethod: 'pix',
customer: { name: 'João Silva', email: 'joao@exemplo.com', document: '12345678909' },
});
const { pathname, search } = new URL(url);
const timestamp = new Date().toISOString();
const nonce = crypto.randomBytes(16).toString('base64url');
const signedData = JSON.stringify({ method: 'POST', path: pathname, query: search.slice(1), body, timestamp, nonce });
const signature = crypto.createSign('SHA256').update(signedData).sign(PRIVATE_KEY).toString('base64');
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Public-Key-ID': PUBLIC_KEY_ID,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
},
body, // exatamente a string assinada
});Python (cryptography + requests)
import base64
import json
import secrets
from datetime import datetime, timezone
from urllib.parse import urlsplit
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
PUBLIC_KEY_ID = "apix_pk_…"
with open("priv.pem", "rb") as f:
private_key = serialization.load_pem_private_key(f.read(), password=None)
url = "https://api.apix.tec.br/api/v1/direct-payments"
compact = {"separators": (",", ":"), "ensure_ascii": False}
body = json.dumps({
"amount": 990,
"description": "Pedido #123",
"paymentMethod": "pix",
"customer": {"name": "João Silva", "email": "joao@exemplo.com", "document": "12345678909"},
}, **compact)
parts = urlsplit(url)
timestamp = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
nonce = secrets.token_urlsafe(16)
signed_data = json.dumps({
"method": "POST",
"path": parts.path,
"query": parts.query,
"body": body,
"timestamp": timestamp,
"nonce": nonce,
}, **compact)
signature = private_key.sign(signed_data.encode("utf-8"), ec.ECDSA(hashes.SHA256())) # DER
response = requests.post(
url,
data=body.encode("utf-8"), # exatamente a string assinada
headers={
"Content-Type": "application/json",
"X-Public-Key-ID": PUBLIC_KEY_ID,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": base64.b64encode(signature).decode(),
},
)PHP
<?php
$publicKeyId = 'apix_pk_…';
$privateKey = openssl_pkey_get_private(file_get_contents('priv.pem'));
$url = 'https://api.apix.tec.br/api/v1/direct-payments';
$flags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
$body = json_encode([
'amount' => 990,
'description' => 'Pedido #123',
'paymentMethod' => 'pix',
'customer' => ['name' => 'João Silva', 'email' => 'joao@exemplo.com', 'document' => '12345678909'],
], $flags);
$parts = parse_url($url);
$micro = microtime(true);
$timestamp = gmdate('Y-m-d\TH:i:s', (int) $micro) . sprintf('.%03dZ', ($micro - floor($micro)) * 1000);
$nonce = rtrim(strtr(base64_encode(random_bytes(16)), '+/', '-_'), '=');
$signedData = json_encode([
'method' => 'POST',
'path' => $parts['path'],
'query' => $parts['query'] ?? '',
'body' => $body,
'timestamp' => $timestamp,
'nonce' => $nonce,
], $flags);
openssl_sign($signedData, $signature, $privateKey, OPENSSL_ALGO_SHA256); // DER
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body, // exatamente a string assinada
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Public-Key-ID: ' . $publicKeyId,
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature: ' . base64_encode($signature),
],
]);
$response = curl_exec($ch);Go (dcrd/secp256k1)
package main
// crypto/ecdsa não traz a curva secp256k1: use github.com/decred/dcrd/dcrec/secp256k1/v4
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/asn1"
"encoding/base64"
"encoding/json"
"encoding/pem"
"net/http"
"net/url"
"os"
"time"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
)
type signedData struct {
Method string `json:"method"`
Path string `json:"path"`
Query string `json:"query"`
Body string `json:"body"`
Timestamp string `json:"timestamp"`
Nonce string `json:"nonce"`
}
// priv.pem gerado pelo openssl é SEC1 ("EC PRIVATE KEY")
type sec1PrivateKey struct {
Version int
PrivateKey []byte
Curve asn1.ObjectIdentifier `asn1:"optional,explicit,tag:0"`
PublicKey asn1.BitString `asn1:"optional,explicit,tag:1"`
}
// JSON sem escapar <, > e & e sem a quebra de linha final do Encoder
func compactJSON(v any) []byte {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.Encode(v)
return bytes.TrimRight(buf.Bytes(), "\n")
}
func main() {
const publicKeyID = "apix_pk_…"
target := "https://api.apix.tec.br/api/v1/direct-payments"
pemBytes, _ := os.ReadFile("priv.pem")
block, _ := pem.Decode(pemBytes)
var sec1 sec1PrivateKey
if _, err := asn1.Unmarshal(block.Bytes, &sec1); err != nil {
panic(err)
}
privateKey := secp256k1.PrivKeyFromBytes(sec1.PrivateKey)
body := compactJSON(map[string]any{
"amount": 990,
"description": "Pedido #123",
"paymentMethod": "pix",
"customer": map[string]string{"name": "João Silva", "email": "joao@exemplo.com", "document": "12345678909"},
})
parsed, _ := url.Parse(target)
nonceBytes := make([]byte, 16)
rand.Read(nonceBytes)
timestamp := time.Now().UTC().Format("2006-01-02T15:04:05.000Z07:00")
nonce := base64.RawURLEncoding.EncodeToString(nonceBytes)
// struct, não map: garante a ordem method, path, query, body, timestamp, nonce
payload := compactJSON(signedData{"POST", parsed.EscapedPath(), parsed.RawQuery, string(body), timestamp, nonce})
hash := sha256.Sum256(payload)
signature := ecdsa.Sign(privateKey, hash[:]).Serialize() // DER
req, _ := http.NewRequest("POST", target, bytes.NewReader(body)) // exatamente a string assinada
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Public-Key-ID", publicKeyID)
req.Header.Set("X-Timestamp", timestamp)
req.Header.Set("X-Nonce", nonce)
req.Header.Set("X-Signature", base64.StdEncoding.EncodeToString(signature))
http.DefaultClient.Do(req)
}Erros
| Status | Mensagem | Causa |
|---|---|---|
| 400 | Envie apenas um método de autenticação | X-API-Key e X-Public-Key-ID na mesma requisição |
| 401 | Headers de assinatura incompletos / inválidos | Falta header, ou o formato do ID, nonce ou assinatura não confere |
| 401 | Timestamp fora da janela permitida | Relógio com mais de 5 minutos de diferença, ou timestamp sem fuso |
| 401 | Assinatura inválida | String assinada diferente da reconstruída, ou chave errada |
| 401 | Nonce já utilizado | Cada requisição precisa de um nonce novo, inclusive nas retentativas |
| 403 | IP não autorizado para esta API key | Origem fora da lista; a comparação é exata (atenção ao IPv6) |
| 503 | Não foi possível validar a requisição agora | Falha temporária; tente de novo com um nonce novo |