Skip to content

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. GET and DELETE requests do not need the hmac header (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: hmac in lowercase hexadecimal (the API normalizes to lowercase before comparing, but always send in lowercase to avoid ambiguity).

How It Works

  1. Serialize the request body as JSON without indentation (compact) with keys in alphabetical order
  2. Generate the HMAC-SHA512 signature using your client_secret as the key
  3. Send the signature in the hmac header in lowercase hexadecimal format
  4. 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:

  1. JSON.parse the received body
  2. Re-serialize with keys in alphabetical order (the server reorders keys before computing the expected HMAC)
  3. JSON.stringify without indentation (equivalent to json.dumps(obj, sort_keys=True, separators=(',', ':')) in Python)
  4. Remove 1 space that exists after each : or , (converts ", " to "," and ": " to ":"). Defensive -- JSON.stringify(obj) and json.dumps(obj, separators=(',', ':')) never produce those spaces; serializers that emit ", " between pairs (e.g., default json.dumps in Python) become automatically compatible
  5. Compute HMAC-SHA512(normalized_body, client_secret) in lowercase hexadecimal
  6. Send the same normalized body (with keys alphabetically ordered, without spaces after : and ,) as the request body and the HMAC in the hmac header

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 a TreeMap<String, Object> (which keeps keys ordered).
  • C#: JsonSerializerOptions does not order; order via SortedDictionary<string, object> before serializing.
  • PHP: call ksort($array) before json_encode($array).
  • Python: json.dumps(obj, sort_keys=True, separators=(',', ':')) — native, no extra configuration.
  • Go: json.Marshal of a map[string]interface{} already guarantees alphabetical order since Go 1.12+.
  • JavaScript/Node: JSON.stringify preserves insertion order; order the keys manually before serializing:
    javascript
    const 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

HeaderValueNotes
AuthorizationApiKey {client_id}:{client_secret} or Basic {base64(client_id:client_secret)}Same credential, two accepted formats
Content-Typeapplication/jsonPOST without Content-Type: application/json → 415 before even reaching HMAC
hmacHMAC-SHA512 signature in lowercase hexadecimal128 hex characters. If sent in uppercase, the server normalizes to lowercase before comparing -- but send in lowercase as a convention

Examples

JavaScript (Node.js)

javascript
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

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

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

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#

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

bash
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

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

400 -- Missing body or invalid JSON

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 without HMAC secret configured

json
{
  "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_secret is the HMAC key -- the same value sent in Authorization: ApiKey {client_id}:{client_secret} (or inside the base64 when using Authorization: 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.

Owem Pay Instituição de Pagamento — ISPB 37839059