HMAC em Python

Receita hmac + hashlib + requests

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

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())
Não use 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

Troubleshooting

Erro recebidoCausa 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.