Skip to content

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. Solicitudes GET y DELETE no necesitan el header hmac (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: hmac en hexadecimal lowercase (la API normaliza a lowercase antes de comparar, pero siempre envie ya en lowercase para evitar ambiguedad).

Como Funciona

  1. Serialice el body de la solicitud como JSON sin indentacion (compact) con las claves en orden alfabetico
  2. Genere la firma HMAC-SHA512 usando su client_secret como clave
  3. Envie la firma en el header hmac en formato hexadecimal lowercase
  4. 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:

  1. JSON.parse del body recibido
  2. Re-serializar con las claves en orden alfabetico (el servidor reordena las claves antes de calcular el HMAC esperado)
  3. JSON.stringify sin indentacion (equivalente a json.dumps(obj, sort_keys=True, separators=(',', ':')) en Python)
  4. Remover 1 espacio que exista despues de cada : o , (convierte ", " a "," y ": " a ":"). Defensivo -- JSON.stringify(obj) y json.dumps(obj, separators=(',', ':')) nunca producen esos espacios; serializadores que emiten ", " entre pares (ej: json.dumps por defecto en Python) quedan automaticamente compatibles
  5. Computar HMAC-SHA512(body_normalizado, client_secret) en hexadecimal lowercase
  6. Enviar el mismo body normalizado (con claves ordenadas alfabeticamente, sin espacios despues de : y ,) como request body y el HMAC en el header hmac

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 un TreeMap<String, Object> (mantiene las claves ordenadas).
  • C#: JsonSerializerOptions no ordena; ordene via SortedDictionary<string, object> antes de serializar.
  • PHP: llame a ksort($array) antes de json_encode($array).
  • Python: json.dumps(obj, sort_keys=True, separators=(',', ':')) — nativo, sin configuracion extra.
  • Go: json.Marshal desde un map[string]interface{} ya garantiza orden alfabetico desde Go 1.12+.
  • JavaScript/Node: JSON.stringify preserva orden de insercion; ordene las claves manualmente antes de serializar:
    javascript
    const 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

HeaderValorNotas
AuthorizationApiKey {client_id}:{client_secret} o Basic {base64(client_id:client_secret)}Misma credencial, dos formatos aceptados
Content-Typeapplication/jsonPOST sin Content-Type: application/json → 415 antes incluso del HMAC
hmacFirma HMAC-SHA512 en hexadecimal lowercase128 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)

javascript
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

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

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

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#

csharp
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)

bash
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

json
{
  "worked": false,
  "detail": "Invalid HMAC signature"
}
json
{
  "worked": false,
  "detail": "Missing HMAC header"
}

400 -- Body ausente o JSON invalido

json
{
  "worked": false,
  "detail": "Request body is required for HMAC validation"
}
json
{
  "worked": false,
  "detail": "Request body must be valid JSON for HMAC validation"
}

403 -- API Key sin HMAC secret configurado

json
{
  "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_secret de la API Key es la clave HMAC -- el mismo valor enviado en Authorization: ApiKey {client_id}:{client_secret} (o dentro del base64 cuando usa Authorization: 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.

Owem Pay Instituição de Pagamento — ISPB 37839059