""" Client di esempio per le API del tuo account ConsensoSicuro (Python 3.9+, libreria requests). È un punto di partenza da copiare nel tuo gestionale, non una libreria ufficiale. Riferimento completo: https://consensosicuro.it/integrazioni/api pip install requests export CONSENSOSICURO_API_KEY=ak_test_... python client.py """ import hashlib import hmac import os import re import time import requests BASE_URL = "https://api.consensosicuro.it/api/v1/account" class ConsensoSicuroError(Exception): """Errore dell'API: status HTTP e code stabile (quando c'è).""" def __init__(self, status, body): self.status = status self.body = body if isinstance(body, dict) else {} self.code = self.body.get("code") super().__init__(self.body.get("error") or f"HTTP {status}") class ConsensoSicuroClient: def __init__(self, api_key, base_url=BASE_URL): self.base_url = base_url self.session = requests.Session() self.session.headers["Authorization"] = f"Bearer {api_key}" def _request(self, method, path, json=None, idempotency_key=None, sede_id=None): headers = {} if idempotency_key: # Stessa chiave e stesso corpo entro 24 ore: ritorna la risposta della prima volta. headers["X-Idempotency-Key"] = idempotency_key if sede_id: # Strutture: la sede su cui lavorare. headers["X-Structure-Sede-Id"] = sede_id res = self.session.request(method, self.base_url + path, json=json, headers=headers, timeout=30) data = res.json() if res.content else None if not res.ok: raise ConsensoSicuroError(res.status_code, data) return data def send_consent(self, body, idempotency_key=None, sede_id=None): """POST /consents: invia un consenso. Risponde 201 con il dettaglio.""" return self._request("POST", "/consents", json=body, idempotency_key=idempotency_key, sede_id=sede_id) def get_consent(self, consent_id): """GET /consents/{id}: stato, firmatari, consegna, documenti.""" return self._request("GET", f"/consents/{consent_id}") def consent_pdf_link(self, consent_id): """GET /consents/{id}/pdf: link firmato al PDF, valido un'ora.""" return self._request("GET", f"/consents/{consent_id}/pdf") def verify_webhook_signature(raw_body: bytes, signature_header: str, timestamp: str, secret: str, tolerance: int = 300) -> bool: """ Verifica una consegna del webhook. - timestamp = intestazione X-ConsensoSicuro-Timestamp (secondi Unix dell'invio): deve essere entro 5 minuti dal tuo orologio. Ogni riprova ne porta uno nuovo. - signature_header = intestazione X-ConsensoSicuro-Signature: uno o due valori "v1=" separati da virgola (due per 24 ore dopo una rotazione del segreto). Ognuno è l'HMAC-SHA256 di "timestamp.corpo" con il segreto whsec_... Basta che UNO torni. - raw_body sono i byte ricevuti, prima di json.loads. Confronto a tempo costante: una firma malformata dà False, mai un'eccezione. """ if not isinstance(timestamp, str) or not timestamp.isdigit(): return False if abs(int(time.time()) - int(timestamp)) > tolerance: return False expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest() for part in (signature_header or "").split(","): scheme, _, value = part.strip().partition("=") if scheme == "v1" and re.fullmatch(r"[0-9a-fA-F]{64}", value) and hmac.compare_digest(expected, value.lower()): return True return False # Protezione dal replay, oltre al timestamp: deduplica su # X-ConsensoSicuro-Delivery-Id, che resta uguale fra un tentativo e le sue # riprove. Ricorda gli id elaborati per almeno 30 giorni (il «Riprova» della # dashboard vale 30 giorni); qui un set in memoria come esempio, nel # gestionale una tabella con indice unico. _processed_deliveries = set() def is_first_delivery(delivery_id: str) -> bool: if not delivery_id or delivery_id in _processed_deliveries: return False _processed_deliveries.add(delivery_id) return True if __name__ == "__main__": cs = ConsensoSicuroClient(os.environ["CONSENSOSICURO_API_KEY"]) # Con una chiave ak_test_ il consenso nasce ma nessun messaggio parte. consent = cs.send_consent( { "patient": {"firstName": "Mario", "lastName": "Rossi", "phone": "+393331234567"}, "template": {"treatments": ["psi-colloquio"]}, "delivery": {"channel": "WHATSAPP", "language": "it"}, "metadata": {"cartella": "CRT-119"}, }, idempotency_key="visita-119", ) print(consent["id"], consent["status"]) print(cs.get_consent(consent["id"])["status"])