Skip to content

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

  • POST. Requisições GET e DELETE não precisam do header hmac (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: hmac em hexadecimal lowercase (a API normaliza para lowercase antes de comparar, mas sempre envie já em lowercase para evitar ambiguidade).

Como Funciona

  1. Serialize o body da requisição como JSON sem indentação (compact) com as chaves em ordem alfabética
  2. Gere a assinatura HMAC-SHA512 usando seu client_secret como chave
  3. Envie a assinatura no header hmac em formato hexadecimal lowercase
  4. 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:

  1. JSON.parse do body recebido
  2. Re-serializar com as chaves em ordem alfabética (o servidor reordena as chaves antes de calcular o HMAC esperado)
  3. JSON.stringify sem indentação (equivalente a json.dumps(obj, sort_keys=True, separators=(',', ':')) em Python)
  4. Remover 1 espaço que exista depois de cada : ou , (converte ", " para "," e ": " para ":"). Defensivo -- JSON.stringify(obj) e json.dumps(obj, separators=(',', ':')) nunca produzem esses espaços; serializadores que emitem ", " entre pares (ex: json.dumps padrão em Python) ficam automaticamente compatíveis
  5. Computar HMAC-SHA512(body_normalizado, client_secret) em hexadecimal lowercase
  6. Enviar o mesmo body normalizado (com chaves ordenadas alfabeticamente, sem espaços após : e ,) como request body e o HMAC no header hmac

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 um TreeMap<String, Object> (que mantém chaves ordenadas).
  • C#: JsonSerializerOptions não ordena; ordene via SortedDictionary<string, object> antes de serializar.
  • PHP: chame ksort($array) antes de json_encode($array).
  • Python: json.dumps(obj, sort_keys=True, separators=(',', ':')) — nativo, sem configuração extra.
  • Go: json.Marshal de um map[string]interface{} já garante ordem alfabética desde Go 1.12+.
  • JavaScript/Node: JSON.stringify preserva ordem de inserção; ordene as chaves 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 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

HeaderValorNotas
AuthorizationApiKey {client_id}:{client_secret} ou Basic {base64(client_id:client_secret)}Mesma credencial, dois formatos aceitos
Content-Typeapplication/jsonPOST sem Content-Type: application/json → 415 antes mesmo do HMAC
hmacAssinatura HMAC-SHA512 em hexadecimal lowercase128 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)

javascript
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

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

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

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#

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

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"

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

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

400 -- Body ausente ou JSON inválido

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 sem HMAC secret configurado

json
{
  "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_secret da API Key é a chave HMAC -- o mesmo valor enviado em Authorization: ApiKey {client_id}:{client_secret} (ou dentro do base64 quando usando Authorization: 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.

Owem Pay Instituição de Pagamento — ISPB 37839059