HMAC-SHA512
Todas as requisições POST em /api/external/* (cash-in, cash-out, refund, webhooks, validação de CPF) exigem assinatura HMAC-SHA512 do body para garantir integridade e autenticidade do payload.
Escopo
- Só
POST. RequisiçõesGETeDELETEnão precisam do headerhmac(o plug não está no pipeline delas). - Endpoints cobertos:
POST /pix/cash-in,POST /pix/cash-out,POST /pix/refund,POST /webhooks,POST /cpf/validate. - Header obrigatório:
hmacem hexadecimal lowercase (a API normaliza para lowercase antes de comparar, mas sempre envie já em lowercase para evitar ambiguidade).
Como Funciona
- Serialize o body da requisição como JSON sem indentação (compact) com as chaves em ordem alfabética
- Gere a assinatura HMAC-SHA512 usando seu
client_secretcomo chave - Envie a assinatura no header
hmacem formato hexadecimal lowercase - Envie o mesmo body (exatamente como foi assinado) no corpo da requisição
A API recalcula o hash sobre o body recebido (após a normalização interna) e compara com o valor do header hmac usando comparação em tempo constante (constant-time comparison), impedindo ataques de timing que tentam descobrir a assinatura byte a byte.
Normalização do Body (crítico)
O servidor normaliza o body recebido antes de calcular o HMAC. Para a assinatura ser válida, o cliente deve assinar o body exatamente na forma normalizada que o servidor usa:
JSON.parsedo body recebido- Re-serializar com as chaves em ordem alfabética (o servidor reordena as chaves antes de calcular o HMAC esperado)
JSON.stringifysem indentação (equivalente ajson.dumps(obj, sort_keys=True, separators=(',', ':'))em Python)- Remover 1 espaço que exista depois de cada
:ou,(converte", "para","e": "para":"). Defensivo --JSON.stringify(obj)ejson.dumps(obj, separators=(',', ':'))nunca produzem esses espaços; serializadores que emitem", "entre pares (ex:json.dumpspadrão em Python) ficam automaticamente compatíveis - Computar
HMAC-SHA512(body_normalizado, client_secret)em hexadecimal lowercase - Enviar o mesmo body normalizado (com chaves ordenadas alfabeticamente, sem espaços após
:e,) como request body e o HMAC no headerhmac
Ordenação alfabética de chaves é OBRIGATÓRIA
O servidor reordena as chaves do JSON em ordem alfabética antes de calcular o HMAC esperado. Se o cliente assina o body em outra ordem, a assinatura não bate e a requisição é rejeitada com HTTP 401 Invalid HMAC signature.
Exemplo concreto:
- Cliente envia
{"external_id":"...","amount":1000}e assina exatamente essa string - Servidor internamente reordena para
{"amount":1000,"external_id":"..."}antes de validar - HMACs diferem → requisição rejeitada
Solução: ordene as chaves alfabeticamente antes de serializar e assinar, e envie exatamente essa mesma string ordenada no corpo da requisição.
Bibliotecas que NÃO ordenam por padrão
Se você usa Java Jackson, C# System.Text.Json ou PHP json_encode, a ordem das chaves não é alfabética por padrão. Você precisa forçar a ordenação.
- Java (Jackson): use
mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)ou serialize a partir de umTreeMap<String, Object>(que mantém chaves ordenadas). - C#:
JsonSerializerOptionsnão ordena; ordene viaSortedDictionary<string, object>antes de serializar. - PHP: chame
ksort($array)antes dejson_encode($array). - Python:
json.dumps(obj, sort_keys=True, separators=(',', ':'))— nativo, sem configuração extra. - Go:
json.Marshalde ummap[string]interface{}já garante ordem alfabética desde Go 1.12+. - JavaScript/Node:
JSON.stringifypreserva ordem de inserção; ordene as chaves manualmente antes de serializar:javascriptconst sorted = Object.keys(obj).sort().reduce((acc, key) => { acc[key] = obj[key]; return acc; }, {}); const body = JSON.stringify(sorted);
Cuidado com JSON.stringify(obj, null, 2)
JSON.stringify(obj) (sem indentação) produz {"a":1,"b":2} — sem espaços, já normalizado.
JSON.stringify(obj, null, 2) (com indentação para humanos) produz:
{
"a": 1,
"b": 2
}— com espaços depois de : e newlines. Isso quebra o HMAC. Use sempre JSON.stringify(obj) sem indentação para computar e enviar o HMAC.
HMAC-SHA512 vs Webhook SHA256
A API usa HMAC-SHA512 para autenticar requisições enviadas por você. Os webhooks enviados pela Owem Pay usam HMAC-SHA256 para assinatura (header X-Owem-Signature). São algoritmos diferentes -- cada um no seu contexto. Veja Webhooks para detalhes da validação de webhooks.
Outra diferença importante: no request HMAC-SHA512 (sentido cliente → Owem) o servidor reordena alfabeticamente as chaves antes de comparar, então o cliente deve produzir o body ordenado. No webhook HMAC-SHA256 (sentido Owem → cliente) o cliente recebe o body exatamente como foi enviado (sem reordenação posterior) e deve validar a assinatura sobre esse body RAW -- não re-serialize antes de validar.
Headers Obrigatórios
| Header | Valor | Notas |
|---|---|---|
Authorization | ApiKey {client_id}:{client_secret} ou Basic {base64(client_id:client_secret)} | Mesma credencial, dois formatos aceitos |
Content-Type | application/json | POST sem Content-Type: application/json → 415 antes mesmo do HMAC |
hmac | Assinatura HMAC-SHA512 em hexadecimal lowercase | 128 caracteres hex. Se enviar em maiúsculas, o servidor normaliza para lowercase antes de comparar -- mas envie já em lowercase por convenção |
Exemplos
JavaScript (Node.js)
const crypto = require('crypto');
// Keys em ordem alfabética (amount < description < pix_key < pix_key_type)
const payload = {
amount: 3000,
description: "Pagamento",
pix_key: "12345678901",
pix_key_type: "cpf"
};
// JSON.stringify preserva ordem de inserção, NÃO ordena por chave.
// Mesmo que o objeto acima esteja alfabético, forçar sort explicitamente
// garante que o body casa com o hash esperado pelo servidor.
const sortedKeys = Object.keys(payload).sort();
const body = JSON.stringify(
sortedKeys.reduce((acc, key) => ({ ...acc, [key]: payload[key] }), {})
);
// body = '{"amount":3000,"description":"Pagamento","pix_key":"12345678901","pix_key_type":"cpf"}'
const hmac = crypto
.createHmac('sha512', 'sk_seu-client-secret')
.update(body)
.digest('hex');
// Enviar `body` como request body e `hmac` no header hmac.Python
import hmac
import hashlib
import json
# sort_keys=True garante ordem alfabética — OBRIGATÓRIO
body = json.dumps(
{
"amount": 3000,
"pix_key": "12345678901",
"pix_key_type": "cpf",
"description": "Pagamento"
},
sort_keys=True,
separators=(',', ':')
)
# body = '{"amount":3000,"description":"Pagamento","pix_key":"12345678901","pix_key_type":"cpf"}'
signature = hmac.new(
b"sk_seu-client-secret",
body.encode("utf-8"),
hashlib.sha512
).hexdigest()PHP
$data = [
'amount' => 3000,
'pix_key' => '12345678901',
'pix_key_type' => 'cpf',
'description' => 'Pagamento'
];
// ksort garante ordem alfabética de chaves — OBRIGATÓRIO
ksort($data);
$body = json_encode($data, JSON_UNESCAPED_SLASHES);
$hmac = hash_hmac('sha512', $body, 'sk_seu-client-secret');Java
import java.util.TreeMap;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
// TreeMap mantém as chaves em ordem alfabética — OBRIGATÓRIO
TreeMap<String, Object> bodyMap = new TreeMap<>();
bodyMap.put("amount", 3000);
bodyMap.put("pix_key", "12345678901");
bodyMap.put("pix_key_type", "cpf");
bodyMap.put("description", "Pagamento");
ObjectMapper mapper = new ObjectMapper();
String body = mapper.writeValueAsString(bodyMap);
// body = {"amount":3000,"description":"Pagamento","pix_key":"12345678901","pix_key_type":"cpf"}
String secret = "sk_seu-client-secret";
Mac mac = Mac.getInstance("HmacSHA512");
SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA512");
mac.init(keySpec);
byte[] hash = mac.doFinal(body.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : hash) {
sb.append(String.format("%02x", b));
}
String hmac = sb.toString();C#
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
// SortedDictionary mantém as chaves em ordem alfabética — OBRIGATÓRIO
// (JsonSerializerOptions NÃO ordena chaves por padrão)
var payload = new SortedDictionary<string, object>
{
["amount"] = 3000,
["description"] = "Pagamento",
["pix_key"] = "12345678901",
["pix_key_type"] = "cpf"
};
var body = JsonSerializer.Serialize(payload);
// body = {"amount":3000,"description":"Pagamento","pix_key":"12345678901","pix_key_type":"cpf"}
var secret = Encoding.UTF8.GetBytes("sk_seu-client-secret");
using var hmacSha512 = new HMACSHA512(secret);
var hash = hmacSha512.ComputeHash(Encoding.UTF8.GetBytes(body));
var hmac = BitConverter.ToString(hash).Replace("-", "").ToLower();Bash (curl)
BODY='{"amount":3000,"description":"Pagamento","pix_key":"12345678901","pix_key_type":"cpf"}'
SECRET="sk_seu-client-secret"
HMAC=$(echo -n "$BODY" | openssl dgst -sha512 -hmac "$SECRET" | awk '{print $2}')
curl -X POST https://api.owem.com.br/api/external/pix/cash-out \
-H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "hmac: $HMAC" \
-d "$BODY"Validação
A API recalcula o HMAC-SHA512 do body recebido (após normalização) e compara com o valor recebido no header hmac usando comparação em tempo constante. Todos os erros do plug HmacValidation seguem o formato {"worked": false, "detail": "..."}:
401 -- Assinatura inválida ou header ausente
{
"worked": false,
"detail": "Invalid HMAC signature"
}{
"worked": false,
"detail": "Missing HMAC header"
}400 -- Body ausente ou JSON inválido
{
"worked": false,
"detail": "Request body is required for HMAC validation"
}{
"worked": false,
"detail": "Request body must be valid JSON for HMAC validation"
}403 -- API Key sem HMAC secret configurado
{
"worked": false,
"detail": "HMAC secret not configured for this API key"
}Checklist de integração HMAC
- Use o body exatamente como enviado na requisição (mesma serialização JSON, chaves em ordem alfabética, sem espaços após
:e,). - O
client_secretda API Key é a chave HMAC -- o mesmo valor enviado emAuthorization: ApiKey {client_id}:{client_secret}(ou dentro dobase64quando usandoAuthorization: Basic). - A assinatura deve ter 128 caracteres hexadecimais em lowercase (SHA-512 produz 64 bytes = 128 hex chars). A API normaliza o header para lowercase antes de comparar, mas envie já em lowercase por convenção. Maiúsculas ou misto (ex:
AbCdEf) funcionam mas são discouraged. - A comparação é feita em tempo constante -- não há como descobrir a assinatura por timing.
- Se o seu cliente envia
X-Key-Case: camelCase, assine o body na forma que está trafegando na rede (camelCase). O HMAC sempre vê o body tal como você enviou, antes de qualquer conversão interna -- ver Autenticação -- Headers opcionais.