HMAC-SHA512
Todas las solicitudes POST en /api/external/* (cash-in, cash-out, refund, webhooks, validacion de CPF) exigen firma HMAC-SHA512 del body para garantizar integridad y autenticidad del payload.
Alcance
- Solo
POST. SolicitudesGETyDELETEno necesitan el headerhmac(el plug no esta en el pipeline de ellas). - Endpoints cubiertos:
POST /pix/cash-in,POST /pix/cash-out,POST /pix/refund,POST /webhooks,POST /cpf/validate. - Header obligatorio:
hmacen hexadecimal lowercase (la API normaliza a lowercase antes de comparar, pero siempre envie ya en lowercase para evitar ambiguedad).
Como Funciona
- Serialice el body de la solicitud como JSON sin indentacion (compact) con las claves en orden alfabetico
- Genere la firma HMAC-SHA512 usando su
client_secretcomo clave - Envie la firma en el header
hmacen formato hexadecimal lowercase - Envie el mismo body (exactamente como fue firmado) en el cuerpo de la solicitud
La API recalcula el hash sobre el body recibido (despues de la normalizacion interna) y lo compara con el valor del header hmac usando comparacion en tiempo constante (constant-time comparison), impidiendo ataques de timing que intentan descubrir la firma byte a byte.
Normalizacion del Body (critico)
El servidor normaliza el body recibido antes de calcular el HMAC. Para que la firma sea valida, el cliente debe firmar el body exactamente en la forma normalizada que usa el servidor:
JSON.parsedel body recibido- Re-serializar con las claves en orden alfabetico (el servidor reordena las claves antes de calcular el HMAC esperado)
JSON.stringifysin indentacion (equivalente ajson.dumps(obj, sort_keys=True, separators=(',', ':'))en Python)- Remover 1 espacio que exista despues de cada
:o,(convierte", "a","y": "a":"). Defensivo --JSON.stringify(obj)yjson.dumps(obj, separators=(',', ':'))nunca producen esos espacios; serializadores que emiten", "entre pares (ej:json.dumpspor defecto en Python) quedan automaticamente compatibles - Computar
HMAC-SHA512(body_normalizado, client_secret)en hexadecimal lowercase - Enviar el mismo body normalizado (con claves ordenadas alfabeticamente, sin espacios despues de
:y,) como request body y el HMAC en el headerhmac
Orden alfabetico de claves es OBLIGATORIO
El servidor reordena las claves del JSON en orden alfabetico antes de calcular el HMAC esperado. Si el cliente firma el body en otro orden, la firma no coincide y la solicitud se rechaza con HTTP 401 Invalid HMAC signature.
Ejemplo concreto:
- Cliente envia
{"external_id":"...","amount":1000}y firma exactamente esa cadena - Servidor internamente reordena a
{"amount":1000,"external_id":"..."}antes de validar - Los HMAC difieren → solicitud rechazada
Solucion: ordene las claves alfabeticamente antes de serializar y firmar, y envie exactamente la misma cadena ordenada en el cuerpo de la solicitud.
Bibliotecas que NO ordenan por defecto
Si usa Java Jackson, C# System.Text.Json o PHP json_encode, el orden de las claves no es alfabetico por defecto. Debe forzar el orden.
- Java (Jackson): use
mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)o serialice desde unTreeMap<String, Object>(mantiene las claves ordenadas). - C#:
JsonSerializerOptionsno ordena; ordene viaSortedDictionary<string, object>antes de serializar. - PHP: llame a
ksort($array)antes dejson_encode($array). - Python:
json.dumps(obj, sort_keys=True, separators=(',', ':'))— nativo, sin configuracion extra. - Go:
json.Marshaldesde unmap[string]interface{}ya garantiza orden alfabetico desde Go 1.12+. - JavaScript/Node:
JSON.stringifypreserva orden de insercion; ordene las claves 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 con JSON.stringify(obj, null, 2)
JSON.stringify(obj) (sin indentacion) produce {"a":1,"b":2} — sin espacios, ya normalizado.
JSON.stringify(obj, null, 2) (con indentacion para humanos) produce:
{
"a": 1,
"b": 2
}— con espacios despues de : y newlines. Esto rompe el HMAC. Use siempre JSON.stringify(obj) sin indentacion para computar y enviar el HMAC.
HMAC-SHA512 vs Webhook SHA256
La API usa HMAC-SHA512 para autenticar solicitudes enviadas por usted. Los webhooks enviados por Owem Pay usan HMAC-SHA256 para firma (header X-Owem-Signature). Son algoritmos diferentes -- cada uno en su contexto. Vea Webhooks para detalles de la validacion de webhooks.
Otra diferencia importante: en el request HMAC-SHA512 (sentido cliente → Owem) el servidor reordena alfabeticamente las claves antes de comparar, por lo que el cliente debe producir el body ordenado. En el webhook HMAC-SHA256 (sentido Owem → cliente) el cliente recibe el body exactamente como fue enviado (sin reordenacion posterior) y debe validar la firma sobre ese body RAW -- no re-serialize antes de validar.
Headers Obligatorios
| Header | Valor | Notas |
|---|---|---|
Authorization | ApiKey {client_id}:{client_secret} o Basic {base64(client_id:client_secret)} | Misma credencial, dos formatos aceptados |
Content-Type | application/json | POST sin Content-Type: application/json → 415 antes incluso del HMAC |
hmac | Firma HMAC-SHA512 en hexadecimal lowercase | 128 caracteres hex. Si envia en mayusculas, el servidor normaliza a lowercase antes de comparar -- pero envie ya en lowercase por convencion |
Ejemplos
JavaScript (Node.js)
const crypto = require('crypto');
// Claves en orden alfabetico (amount < description < pix_key < pix_key_type)
const payload = {
amount: 3000,
description: "Pagamento",
pix_key: "12345678901",
pix_key_type: "cpf"
};
// JSON.stringify preserva orden de insercion, NO ordena por clave.
// Incluso si el objeto anterior esta alfabetico, forzar sort explicitamente
// garantiza que el body coincida con el hash esperado por el 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 y `hmac` en el header hmac.Python
import hmac
import hashlib
import json
# sort_keys=True garantiza orden alfabetico — OBLIGATORIO
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 garantiza orden alfabetico de claves — OBLIGATORIO
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 mantiene las claves en orden alfabetico — OBLIGATORIO
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 mantiene las claves en orden alfabetico — OBLIGATORIO
// (JsonSerializerOptions NO ordena claves por defecto)
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"Validacion
La API recalcula el HMAC-SHA512 del body recibido (despues de normalizacion) y lo compara con el valor recibido en el header hmac usando comparacion en tiempo constante. Todos los errores del plug HmacValidation siguen el formato {"worked": false, "detail": "..."}:
401 -- Firma invalida o header ausente
{
"worked": false,
"detail": "Invalid HMAC signature"
}{
"worked": false,
"detail": "Missing HMAC header"
}400 -- Body ausente o JSON invalido
{
"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 sin HMAC secret configurado
{
"worked": false,
"detail": "HMAC secret not configured for this API key"
}Checklist de integracion HMAC
- Use el body exactamente como se envia en la solicitud (misma serializacion JSON, claves en orden alfabetico, sin espacios despues de
:y,). - El
client_secretde la API Key es la clave HMAC -- el mismo valor enviado enAuthorization: ApiKey {client_id}:{client_secret}(o dentro delbase64cuando usaAuthorization: Basic). - La firma debe tener 128 caracteres hexadecimales en lowercase (SHA-512 produce 64 bytes = 128 hex chars). La API normaliza el header a lowercase antes de comparar, pero envie ya en lowercase por convencion. Mayusculas o mixto (ej:
AbCdEf) funcionan pero son desaconsejados. - La comparacion se hace en tiempo constante -- no hay como descubrir la firma por timing.
- Si su cliente envia
X-Key-Case: camelCase, firme el body en la forma que esta transitando en la red (camelCase). El HMAC siempre ve el body tal como usted lo envio, antes de cualquier conversion interna -- ver Autenticacion -- Headers opcionales.