HMAC Signature Guide
Detailed guide for generating and troubleshooting HMAC signatures
This guide provides detailed information about generating, validating, and troubleshooting HMAC signatures for the API.
What is HMAC?
HMAC (Hash-based Message Authentication Code) is a cryptographic signature that:
- Verifies the request came from you
- Ensures the request wasn't modified in transit
- Provides an additional security layer beyond the Bearer token
HMAC Algorithm
The API uses HMAC-SHA256:
- Hash Algorithm: SHA-256
- Output: 64-character hexadecimal string
- Secret: Your API secret key
Message Construction
Format
{HTTP_METHOD}{REQUEST_PATH}{JSON_BODY}Rules
- No separators: Concatenate directly (no spaces or newlines)
- HTTP Method: Always uppercase (GET, POST, PUT, DELETE)
- Request Path: Full path starting with
/v3/ - Query Parameters: Include in path if present
- JSON Body: Compact format (no spaces) for POST/PUT only
Examples
GET Request (No Body)
Request: GET /v3/account/5001/balance
Message: GET/v3/account/5001/balanceGET with Query Parameters
Request: GET /v3/account/list-my-pix-keys?account_branch_identifier=0001&account_number=123456
Message: GET/v3/account/list-my-pix-keys?account_branch_identifier=0001&account_number=123456POST Request with Body
Request: POST /v3/cashout/create-cashout
Body: {"amount":100.50,"key":"user@example.com"}
Message: POST/v3/cashout/create-cashout{"amount":100.50,"key":"user@example.com"}Important: Body must be compact JSON (no spaces between keys/values)
Step-by-Step Generation
Step 1: Construct the Message
method = "POST"
path = "/v3/cashout/create-cashout"
body = {
"amount": 100.50,
"key": "user@example.com",
"source_account_branch_identifier": "0001",
"source_account_number": "123456"
}
# Build message
message = f"{method}{path}"
if body:
import json
# CRITICAL: Use separators=(',', ':') for compact JSON
message += json.dumps(body, separators=(',', ':'))
print(f"Message: {message}")
# Output: POST/v3/cashout/create-cashout{"amount":100.5,"key":"user@example.com","source_account_branch_identifier":"0001","source_account_number":"123456"}Step 2: Generate HMAC
import hmac
import hashlib
secret_key = "your_secret_key_here"
# Generate HMAC-SHA256
signature = hmac.new(
secret_key.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
print(f"HMAC: {signature}")
# Output: 64-character hex stringStep 3: Add to Request Header
Authorization: Bearer your_access_token
hmac: generated_signature_here
Content-Type: application/jsonCommon Pitfalls
1. Spaces in JSON
❌ Wrong:
json.dumps(body) # Default includes spaces
# Output: {"amount": 100.5, "key": "user@example.com"}✅ Correct:
json.dumps(body, separators=(',', ':')) # No spaces
# Output: {"amount":100.5,"key":"user@example.com"}2. Including Domain in Path
❌ Wrong:
path = "https://api.socredsa.com.br/v3/account/5001/balance"✅ Correct:
path = "/v3/account/5001/balance"3. Lowercase HTTP Method
❌ Wrong:
method = "get"
message = f"{method}{path}" # "get/v3/..."✅ Correct:
method = "GET" # or method.upper()
message = f"{method.upper()}{path}" # "GET/v3/..."4. Wrong Character Encoding
❌ Wrong:
signature = hmac.new(
secret_key, # Not encoded
message, # Not encoded
hashlib.sha256
).hexdigest()✅ Correct:
signature = hmac.new(
secret_key.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()5. Including Body in GET Requests
❌ Wrong:
# GET request
message = f"GET/v3/account/5001/balance{json.dumps(body)}"✅ Correct:
# GET request - no body
message = f"GET/v3/account/5001/balance"Debugging HMAC Issues
Enable Debug Logging
def generate_hmac_debug(secret_key, method, path, body=None):
"""Generate HMAC with debug logging."""
# Step 1: Construct message
message = f"{method.upper()}{path}"
print(f"1. Base message: {message}")
# Step 2: Add body if present
if body:
body_json = json.dumps(body, separators=(',', ':'))
print(f"2. Body JSON: {body_json}")
message += body_json
print(f"3. Complete message: {message}")
# Step 3: Encode
message_bytes = message.encode('utf-8')
secret_bytes = secret_key.encode('utf-8')
print(f"4. Message length: {len(message_bytes)} bytes")
print(f"5. Secret length: {len(secret_bytes)} bytes")
# Step 4: Generate HMAC
signature = hmac.new(
secret_bytes,
message_bytes,
hashlib.sha256
).hexdigest()
print(f"6. HMAC signature: {signature}")
print(f"7. Signature length: {len(signature)} characters")
return signatureVerify HMAC Locally
def verify_hmac(secret_key, message, expected_signature):
"""Verify HMAC signature matches expected value."""
actual_signature = hmac.new(
secret_key.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
if actual_signature == expected_signature:
print("✓ HMAC signatures match!")
return True
else:
print("✗ HMAC signatures DO NOT match")
print(f"Expected: {expected_signature}")
print(f"Actual: {actual_signature}")
return FalseTest with Known Values
def test_known_hmac():
"""Test with known good values."""
# Known test case
secret_key = "test_secret"
method = "GET"
path = "/v3/account/123/balance"
# Expected message
expected_message = "GET/v3/account/123/balance"
# Generate HMAC
signature = hmac.new(
secret_key.encode('utf-8'),
expected_message.encode('utf-8'),
hashlib.sha256
).hexdigest()
print(f"Test Message: {expected_message}")
print(f"Test Signature: {signature}")
# Verify signature format
assert len(signature) == 64, "Signature must be 64 characters"
assert signature.islower(), "Signature must be lowercase"
assert all(c in '0123456789abcdef' for c in signature), "Signature must be hex"
print("✓ All HMAC format checks passed")
test_known_hmac()Language-Specific Considerations
Python
Unicode Handling:
# Ensure UTF-8 encoding
message.encode('utf-8') # Explicit encodingJSON Formatting:
# Use separators for compact JSON
json.dumps(body, separators=(',', ':'))JavaScript
String Encoding:
// Node.js automatically handles UTF-8
crypto.createHmac('sha256', secretKey).update(message, 'utf8')JSON Formatting:
// JSON.stringify is compact by default (no spaces)
JSON.stringify(body)PHP
String Handling:
// UTF-8 is default in PHP
hash_hmac('sha256', $message, $secretKey)JSON Formatting:
// Use JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)Java
Charset:
// Explicitly use UTF-8
message.getBytes(StandardCharsets.UTF_8)JSON Library:
// Use Gson for consistent JSON
new Gson().toJson(body)Advanced Scenarios
Query Parameters
# Include query parameters in path
path = "/v3/account/list-my-pix-keys"
params = {
"account_branch_identifier": "0001",
"account_number": "123456"
}
# Build query string
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
full_path = f"{path}?{query_string}"
message = f"GET{full_path}"
# Result: GET/v3/account/list-my-pix-keys?account_branch_identifier=0001&account_number=123456URL Encoding
from urllib.parse import urlencode, quote
# URL encode query parameters if needed
params = {
"tag": "invoice #123", # Contains special character
}
# Option 1: Use urlencode
encoded_params = urlencode(params)
# Result: tag=invoice+%23123
# Option 2: Manual encoding
encoded_value = quote(params["tag"])
# Result: invoice%20%23123
# Use in path
path = f"/v3/transactions?{encoded_params}"Important: Include encoded version in HMAC message if that's what you're sending in the actual request.
Large Payloads
# For large JSON bodies, ensure consistent formatting
import json
body = {
"field1": "value1",
"field2": "value2",
# ... many more fields
}
# Use sort_keys for consistent order
body_json = json.dumps(body, separators=(',', ':'), sort_keys=True)Special Characters
# Handle special characters in JSON values
body = {
"description": "Payment for invoice #123 & delivery",
"tag": "invoice/2024/05"
}
# JSON encoding handles escaping automatically
body_json = json.dumps(body, separators=(',', ':'))
# Result: {"description":"Payment for invoice #123 & delivery","tag":"invoice/2024/05"}Security Considerations
1. Constant-Time Comparison
import hmac
def verify_webhook_hmac(received_signature, calculated_signature):
"""Verify HMAC using constant-time comparison."""
# Prevents timing attacks
return hmac.compare_digest(received_signature, calculated_signature)2. Secret Key Storage
# ✅ Good: Environment variables
import os
SECRET_KEY = os.getenv('API_SECRET_KEY')
# ✅ Better: Secrets manager
import boto3
def get_secret():
client = boto3.client('secretsmanager')
response = client.get_secret_value(SecretId='api-secret')
return response['SecretString']3. Key Rotation
class APIClient:
def __init__(self, secret_key, secret_key_new=None):
self.secret_key = secret_key
self.secret_key_new = secret_key_new # For rotation period
def generate_hmac(self, method, path, body=None):
# Try new key first during rotation
if self.secret_key_new:
return self._generate(self.secret_key_new, method, path, body)
return self._generate(self.secret_key, method, path, body)Performance Optimization
Cache HMAC for Retries
class APIRequest:
def __init__(self, method, path, body=None):
self.method = method
self.path = path
self.body = body
self._hmac_cache = None
def get_hmac(self, secret_key):
"""Generate or return cached HMAC."""
if self._hmac_cache is None:
self._hmac_cache = generate_hmac(
secret_key, self.method, self.path, self.body
)
return self._hmac_cacheReuse HMAC Context
import hmac
import hashlib
# For multiple signatures with same key
hmac_context = hmac.new(secret_key.encode(), digestmod=hashlib.sha256)
# Clone for each message
sig1 = hmac_context.copy()
sig1.update(message1.encode())
signature1 = sig1.hexdigest()
sig2 = hmac_context.copy()
sig2.update(message2.encode())
signature2 = sig2.hexdigest()Validation Checklist
Before making a request, verify:
- HTTP method is uppercase
- Path starts with
/v3/ - Path includes query parameters (if any)
- Body is compact JSON (no spaces) for POST/PUT
- Body is omitted for GET/DELETE
- Message uses UTF-8 encoding
- Secret key uses UTF-8 encoding
- HMAC is 64 characters long
- HMAC is lowercase hexadecimal
- HMAC is sent in
hmacheader (notHMACorx-hmac)
Testing Tools
Online HMAC Generator
Visit: https://www.freeformatter.com/hmac-generator.html
Settings:
- Algorithm: SHA-256
- Input type: Text String
- Output encoding: Hex (lowercase)
Command Line (OpenSSL)
# Generate HMAC from command line
echo -n "GET/v3/account/5001/balance" | \
openssl dgst -sha256 -hmac "your_secret_key"Python Test Script
Save as test_hmac.py:
#!/usr/bin/env python3
import hmac
import hashlib
import json
import sys
def main():
if len(sys.argv) < 4:
print("Usage: test_hmac.py <secret_key> <method> <path> [body_json]")
sys.exit(1)
secret_key = sys.argv[1]
method = sys.argv[2].upper()
path = sys.argv[3]
body = json.loads(sys.argv[4]) if len(sys.argv) > 4 else None
# Generate HMAC
message = f"{method}{path}"
if body:
message += json.dumps(body, separators=(',', ':'))
signature = hmac.new(
secret_key.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
print(f"Message: {message}")
print(f"HMAC: {signature}")
if __name__ == '__main__':
main()Usage:
python test_hmac.py "secret" "GET" "/v3/account/123/balance"
python test_hmac.py "secret" "POST" "/v3/cashout" '{"amount":100}'Next Steps
Still having issues? When contacting support, include:
- Request method and path
- Generated message (without sensitive data)
- Generated HMAC signature
- Error response from API