HMAC-SHA256 em Python (hmac + requests)
Receita reutilizável em Python 3.8+. A biblioteca
requests é a única dependência externa — tudo
mais (HMAC, SHA-256, timestamp) é stdlib.
Pré-requisitos
- Python 3.8+ (CPython ou PyPy)
requests≥ 2.28 (pip install requests)- Relógio do sistema sincronizado por NTP (janela ±300s)
1. Função utilitária reusável
Esta função aceita URL absoluta ou path relativo e devolve o dicionário de headers já assinado. Reaproveite em todas as chamadas.
import hmac
import hashlib
import time
from urllib.parse import urlparse
API_KEY = "svp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
API_SECRET = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
def sign_request(method: str, url_or_path: str, body: str = "") -> dict:
"""
Calcula os 3 headers HMAC para uma requisicao.
- method: 'GET', 'POST', etc.
- url_or_path: URL absoluta ('https://api.sivoe.med.br/v1/ping')
ou path relativo ('/v1/ping?limit=5').
- body: corpo cru (string). Para GET, ''.
"""
ts = str(int(time.time()))
# Extrai apenas path + query (REQUEST_URI). Bate com o que o
# gateway usa para refazer a assinatura no lado servidor.
parsed = urlparse(url_or_path)
path_q = parsed.path + (f"?{parsed.query}" if parsed.query else "")
if not path_q:
# path_q vazio significa que url_or_path nao tinha path -- raro
path_q = "/"
payload = f"{ts}.{method.upper()} {path_q}\n{body}"
signature = hmac.new(
API_SECRET.encode("utf-8"),
payload.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return {
"X-API-Key": API_KEY,
"X-Timestamp": ts,
"X-Signature": signature,
}
2. Uso com requests — GET
import requests
url = "https://api.sivoe.med.br/v1/ping"
headers = sign_request("GET", url)
resp = requests.get(url, headers=headers, timeout=10)
print(resp.status_code, resp.json())
Saída esperada
200 {'data': {'ok': True, 'service': 'Sivoe API Gateway', 'version': 'v1', ...},
'meta': {'request_id': '...', 'duracao_ms': 12},
'error': None}
3. Uso com requests — POST com body JSON
A pegadinha aqui: você assina a string final do body, mas o
requests ao receber json= serializa
e envia. Para garantir que o que você assinou == o que
vai pela rede, serialize manualmente com
json.dumps() e use data=.
import json
import requests
url = "https://api.sivoe.med.br/v1/ping"
# Serialize ANTES de assinar -- sem espacos, sort_keys para determinismo
body = json.dumps({"hello": "world", "numero": 42}, separators=(",", ":"))
headers = sign_request("POST", url, body)
headers["Content-Type"] = "application/json"
resp = requests.post(url, headers=headers, data=body, timeout=10)
print(resp.status_code, resp.json())
json= em POST assinado.
O parâmetro json= do requests chama
json.dumps() internamente com formatação padrão,
que pode incluir espaços diferentes do que você assinou —
gerando AUTH_INVALID_SIGNATURE. Sempre
data=<string serializada>.
4. GET com query string
Se você usar params= do requests,
monte a URL completa primeiro e passe para a
função de assinatura — caso contrário a query string ficará
fora do path_com_query.
from urllib.parse import urlencode
base = "https://api.sivoe.med.br/v1/exames"
query = urlencode({"status_laudo": "EMITIDO", "limit": 10})
url = f"{base}?{query}"
headers = sign_request("GET", url)
resp = requests.get(url, headers=headers, timeout=10)
print(resp.status_code)
Pegadinhas comuns
-
time.time()retornafloat— sempreint(time.time())antes de virar string. -
requestscanonicaliza a URL. Se você passaparams=, a ordem/encoding da query pode mudar do que você assinou. Monte a URL completa antes da assinatura. -
Encoding do body:
payload.encode("utf-8")ebody.encode("utf-8")garantem bytes; sem isso, caracteres acentuados podem quebrar. -
json.dumpsdefault difere por versão. Em Python 3.6+ o default é estável, mas useseparators=(",", ":")para garantir o mesmo output em qualquer versão. - Timeout mínimo recomendado: 10s. Cold calls do gateway chegam a ~2.3s; 5s é o piso, 10s é folgado.
Troubleshooting
| Erro recebido | Causa provável |
|---|---|
AUTH_INVALID_SIGNATURE |
Body diferente do que foi assinado (uso de json=); query string fora do path_q; encoding errado |
AUTH_TIMESTAMP_EXPIRED |
Relógio fora ±300s; verifique int(time.time()) vs date -u +%s |
requests.exceptions.Timeout |
Cold call passou do seu timeout — aumente para 10s |
ssl.SSLError |
Cliente sem CA bundle atualizado — atualize certifi ou Python |
Catálogo completo: Códigos de erro.