HMAC-SHA512
All POST requests on /api/external/* (cash-in, cash-out, refund, webhooks, CPF validation) require HMAC-SHA512 signature of the body to guarantee integrity and authenticity of the payload.
Scope
- Only
POST.GETandDELETErequests do not need thehmacheader (the plug is not in their pipeline). - Covered endpoints:
POST /pix/cash-in,POST /pix/cash-out,POST /pix/refund,POST /webhooks,POST /cpf/validate. - Required header:
hmacin lowercase hexadecimal (the API normalizes to lowercase before comparing, but always send in lowercase to avoid ambiguity).
How It Works
- Serialize the request body as JSON without indentation (compact) with keys in alphabetical order
- Generate the HMAC-SHA512 signature using your
client_secretas the key - Send the signature in the
hmacheader in lowercase hexadecimal format - Send the same body (exactly as signed) in the request body
The API recomputes the hash over the received body (after internal normalization) and compares it against the value of the hmac header using constant-time comparison, preventing timing attacks that try to discover the signature byte by byte.
Body Normalization (critical)
The server normalizes the received body before computing the HMAC. For the signature to be valid, the client must sign the body exactly in the normalized form the server uses:
JSON.parsethe received body- Re-serialize with keys in alphabetical order (the server reorders keys before computing the expected HMAC)
JSON.stringifywithout indentation (equivalent tojson.dumps(obj, sort_keys=True, separators=(',', ':'))in Python)- Remove 1 space that exists after each
:or,(converts", "to","and": "to":"). Defensive --JSON.stringify(obj)andjson.dumps(obj, separators=(',', ':'))never produce those spaces; serializers that emit", "between pairs (e.g., defaultjson.dumpsin Python) become automatically compatible - Compute
HMAC-SHA512(normalized_body, client_secret)in lowercase hexadecimal - Send the same normalized body (with keys alphabetically ordered, without spaces after
:and,) as the request body and the HMAC in thehmacheader
Alphabetical ordering of keys is MANDATORY
The server reorders JSON keys alphabetically before computing the expected HMAC. If the client signs the body in another order, the signature will not match and the request is rejected with HTTP 401 Invalid HMAC signature.
Concrete example:
- Client sends
{"external_id":"...","amount":1000}and signs exactly this string - Server internally reorders to
{"amount":1000,"external_id":"..."}before validating - HMACs differ → request rejected
Solution: order the keys alphabetically before serializing and signing, and send exactly the same ordered string in the request body.
Libraries that do NOT order by default
If you use Java Jackson, C# System.Text.Json, or PHP json_encode, the order of keys is not alphabetical by default. You need to force ordering.
- Java (Jackson): use
mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)or serialize from aTreeMap<String, Object>(which keeps keys ordered). - C#:
JsonSerializerOptionsdoes not order; order viaSortedDictionary<string, object>before serializing. - PHP: call
ksort($array)beforejson_encode($array). - Python:
json.dumps(obj, sort_keys=True, separators=(',', ':'))— native, no extra configuration. - Go:
json.Marshalof amap[string]interface{}already guarantees alphabetical order since Go 1.12+. - JavaScript/Node:
JSON.stringifypreserves insertion order; order the keys manually before serializing:javascriptconst sorted = Object.keys(obj).sort().reduce((acc, key) => { acc[key] = obj[key]; return acc; }, {}); const body = JSON.stringify(sorted);
Beware of JSON.stringify(obj, null, 2)
JSON.stringify(obj) (without indentation) produces {"a":1,"b":2} — no spaces, already normalized.
JSON.stringify(obj, null, 2) (with indentation for humans) produces:
{
"a": 1,
"b": 2
}— with spaces after : and newlines. This breaks the HMAC. Always use JSON.stringify(obj) without indentation to compute and send the HMAC.
HMAC-SHA512 vs Webhook SHA256
The API uses HMAC-SHA512 to authenticate requests sent by you. Webhooks sent by Owem Pay use HMAC-SHA256 for signature (header X-Owem-Signature). They are different algorithms -- each in its context. See Webhooks for details on webhook validation.
Another important difference: in the request HMAC-SHA512 (client → Owem direction) the server reorders keys alphabetically before comparing, so the client must produce the body ordered. In the webhook HMAC-SHA256 (Owem → client direction) the client receives the body exactly as it was sent (no later reordering) and must validate the signature over that RAW body -- do not re-serialize before validating.
Required Headers
| Header | Value | Notes |
|---|---|---|
Authorization | ApiKey {client_id}:{client_secret} or Basic {base64(client_id:client_secret)} | Same credential, two accepted formats |
Content-Type | application/json | POST without Content-Type: application/json → 415 before even reaching HMAC |
hmac | HMAC-SHA512 signature in lowercase hexadecimal | 128 hex characters. If sent in uppercase, the server normalizes to lowercase before comparing -- but send in lowercase as a convention |
Examples
JavaScript (Node.js)
const crypto = require('crypto');
// Keys in alphabetical order (amount < description < pix_key < pix_key_type)
const payload = {
amount: 3000,
description: "Pagamento",
pix_key: "12345678901",
pix_key_type: "cpf"
};
// JSON.stringify preserves insertion order, it does NOT sort by key.
// Even if the object above is alphabetical, explicitly forcing sort
// guarantees that the body matches the hash expected by the server.
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_your-client-secret')
.update(body)
.digest('hex');
// Send `body` as request body and `hmac` in the hmac header.Python
import hmac
import hashlib
import json
# sort_keys=True guarantees alphabetical order — MANDATORY
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_your-client-secret",
body.encode("utf-8"),
hashlib.sha512
).hexdigest()PHP
$data = [
'amount' => 3000,
'pix_key' => '12345678901',
'pix_key_type' => 'cpf',
'description' => 'Pagamento'
];
// ksort guarantees alphabetical key order — MANDATORY
ksort($data);
$body = json_encode($data, JSON_UNESCAPED_SLASHES);
$hmac = hash_hmac('sha512', $body, 'sk_your-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 keeps the keys in alphabetical order — MANDATORY
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_your-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 keeps the keys in alphabetical order — MANDATORY
// (JsonSerializerOptions does NOT sort keys by default)
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_your-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_your-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"Validation
The API recomputes the HMAC-SHA512 of the received body (after normalization) and compares it against the value received in the hmac header using constant-time comparison. All errors from the HmacValidation plug follow the format {"worked": false, "detail": "..."}:
401 -- Invalid signature or missing header
{
"worked": false,
"detail": "Invalid HMAC signature"
}{
"worked": false,
"detail": "Missing HMAC header"
}400 -- Missing body or invalid JSON
{
"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 without HMAC secret configured
{
"worked": false,
"detail": "HMAC secret not configured for this API key"
}HMAC integration checklist
- Use the body exactly as sent in the request (same JSON serialization, keys in alphabetical order, without spaces after
:and,). - The API Key
client_secretis the HMAC key -- the same value sent inAuthorization: ApiKey {client_id}:{client_secret}(or inside thebase64when usingAuthorization: Basic). - The signature must have 128 lowercase hexadecimal characters (SHA-512 produces 64 bytes = 128 hex chars). The API normalizes the header to lowercase before comparing, but send in lowercase as a convention. Uppercase or mixed (e.g.,
AbCdEf) works but is discouraged. - The comparison is done in constant time -- there is no way to discover the signature by timing.
- If your client sends
X-Key-Case: camelCase, sign the body in the form it is traveling on the wire (camelCase). The HMAC always sees the body as you sent it, before any internal conversion -- see Authentication -- Optional headers.