APIXdocs

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

RiscoO que a chave assinada faz
Vazamento do segredoNão existe segredo compartilhado: a APIX guarda só a chave pública
Requisição capturada e reenviadaCada requisição vale uma vez, por até 5 minutos
Alteração de valor ou destino em trânsitoMétodo, caminho, query e body entram na assinatura
Uso a partir de outro servidorA 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.pem

No 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

HeaderConteúdo
X-Public-Key-IDO ID recebido no cadastro (apix_pk_…)
X-TimestampMomento da assinatura em ISO 8601 com fuso; até 5 minutos de diferença do servidor
X-NonceValor aleatório único por requisição: 16 a 128 caracteres de [A-Za-z0-9._~+/=-]
X-SignatureAssinatura 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:

CampoValor
methodMétodo HTTP em maiúsculas
pathCaminho exatamente como enviado, antes do ?
queryTudo depois do ?, sem o ?; "" quando não houver
bodyO body idêntico ao enviado, como texto; "" quando não houver
timestampO mesmo valor do header X-Timestamp
nonceO 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.
LinguagemComo serializar para gerar os mesmos bytes
Node.jsJSON.stringify(obj)
PHPjson_encode($obj, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
Pythonjson.dumps(obj, separators=(",", ":"), ensure_ascii=False)
Gojson.Encoder com SetEscapeHTML(false), sem o \n final (json.Marshal escapa <, > e &)
JavaJackson 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
9f3623e4ed27834921b88a1209d62300133e2faec6aa74a29007c2d0b920dbac

Exemplos

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

StatusMensagemCausa
400Envie apenas um método de autenticaçãoX-API-Key e X-Public-Key-ID na mesma requisição
401Headers de assinatura incompletos / inválidosFalta header, ou o formato do ID, nonce ou assinatura não confere
401Timestamp fora da janela permitidaRelógio com mais de 5 minutos de diferença, ou timestamp sem fuso
401Assinatura inválidaString assinada diferente da reconstruída, ou chave errada
401Nonce já utilizadoCada requisição precisa de um nonce novo, inclusive nas retentativas
403IP não autorizado para esta API keyOrigem fora da lista; a comparação é exata (atenção ao IPv6)
503Não foi possível validar a requisição agoraFalha temporária; tente de novo com um nonce novo