Seguire una chiamata e tenere d'occhio i consumi
Due cose che prima o poi ogni integrazione si costruisce da sola, e nessuna delle due è il tuo prodotto. Eccole, così non devi farlo tu.
Ogni risposta porta un id
Ogni risposta di questo servizio — dell'API e delle pagine della console — porta l'header
X-Request-Id:
X-Request-Id: req_9f2c6b1a4e7d40f0a1b2c3d4e5f60718
Registralo accanto alla tua richiesta. È l'unica cosa che trasforma "una chiamata stamattina è fallita" in una riga che possiamo guardare: è scritto sulla generazione stessa, quindi citarlo identifica il job esatto, il suo template, quanto è durato e cosa ha risposto.
In caso di errore non devi nemmeno conservare l'header. Lo stesso id è nel corpo, così un errore che incolli in un messaggio al supporto ci ha già detto di quale chiamata si tratta:
{
"code": "TEMPLATE_VERSION_NOT_ACTIVE",
"number": 1204,
"message": "version 2 of template 'contract' is not active",
"retryable": false,
"requestId": "req_9f2c6b1a4e7d40f0a1b2c3d4e5f60718"
}
L'id è nostro, generato per ogni richiesta. Se mandi un tuo X-Request-Id lo ignoriamo, ed è voluto: due
tue richieste con lo stesso id sarebbero indistinguibili nei nostri log, che è proprio la proprietà per cui l'id
esiste. Registra quello che ti restituiamo accanto al tuo id di correlazione e hai entrambe le metà.
Una chiamata per quello che hai usato
GET /api/v1/usage risponde a quattro domande in una richiesta: qual è il tuo
saldo, quanto ti costano le operazioni, quanto hai speso, e quali limiti ti rifiuteranno.
curl https://your-host/api/v1/usage \
-H "Authorization: Bearer YOUR_API_KEY"
{
"organisation": { "id": "...", "name": "Acme",
"deliveryMode": "prepaid", // deprecato
"suites": { "documents": true, "signatures": true, "flow": true } },
"balance": { "credits": 4310, "overdraft": 0,
"rates": { "document": 5, "signature": 10, "sms": 2, "email": 0,
"seal": 1, "timestamp": 3 } },
"spent": { "windowDays": 30, "since": "...",
"credits": { "document": 640, "signature": 150 },
"operations": { "document": 128, "signature": 15 } },
"thisMonth": { "since": "...",
"documents": { "production": { "succeeded": 96, "failed": 2 },
"test": { "succeeded": 5, "failed": 0 } },
"documentsSucceeded": 101, "documentsFailed": 2, // deprecati
"credits": { "document": 480 } },
"testItems": { "used": 5, "limit": 20, "remaining": 15,
"resetsAt": "2026-10-16T00:00:00+00:00" },
"limits": { "requestsPerMinute": 30, "documentsPerMonth": null,
"jsonBytes": 10485760, "templateBytes": 26214400,
"assetBytes": 10485760, "maxPages": 5000,
"downloadLinks": { "defaultSeconds": 3600, "maxSeconds": 172800 },
"storedFilesDays": 30 },
"asOf": "..."
}
Tutto quello che sta sotto limits è un numero che produce un rifiuto, nelle stesse unità del rifiuto
— così puoi far scattare un allarme prima di un 429 invece che dopo. Contiene ogni riga della pagina
Limiti della console: il payload JSON, il template e il file caricato più grandi (assetBytes:
un'immagine, un allegato o un PDF da firmare), le pagine per documento, quanto vive un link di download per
impostazione predefinita e al massimo (downloadLinks: una richiesta più lunga è rifiutata, mai accorciata)
e quanti giorni si conserva un file caricato. balance.credits confrontato con balance.rates ti
dice quante operazioni di ciascun tipo puoi ancora fare davvero.
Appartiene al tuo account, non a un prodotto. Il saldo è lo stesso fatto sia che tu abbia acquistato la composizione, la firma o il flusso — per questo l'endpoint continua a rispondere anche quando uno dei tre è spento per la tua organizzazione, che è proprio il momento in cui vuoi guardarlo.
Produzione e test non sono mai un numero solo. thisMonth.documents conta quello che hai
generato davvero sotto production e le chiamate isTest a parte sotto test; le chiamate
di test non hanno speso credito. I due campi più vecchi, thisMonth.documentsSucceeded e
documentsFailed, hanno sempre sommato le due cose; restano perché un client che li legge continui a
funzionare, sono deprecati e rimossi nella 2.0, e vanno sostituiti con
documents.production. testItems è la riserva di prova condivisa di oggi: used,
limit, remaining e resetsAt, la prossima mezzanotte UTC (la riserva si ricarica una
volta al giorno, in un momento fisso). La console mostra la stessa divisione: elenchi e totali dicono “1.240
documenti · 37 di test”, con un filtro Produzione / Test, e la dashboard dice quanto manca alla
ricarica.
Gli orari in questa risposta sono UTC: Orari e fusi orari.
Ogni organizzazione è prepagata: il saldo è un budget che scende man mano che lo spendi e a zero viene rifiutato
(QUOTA_EXCEEDED). Due campi sono deprecati e saranno rimossi nella 2.0, e restano solo
perché un client che li legge continui a funzionare: organisation.deliveryMode è sempre la costante
"prepaid", e limits.documentsPerMonth è sempre null perché non c'è più un tetto mensile
di documenti. Non basare decisioni su nessuno dei due.
Orari e fusi orari
Ogni orario in una risposta dell'API o in un webhook è UTC, ISO 8601, e non cambia mai con le tue
impostazioni, così un programma non deve mai indovinare un fuso. Quello che legge una persona — la tua console, le
email, il certificato di completamento e le esportazioni CSV — è scritto nella localizzazione della
tua organizzazione: il suo fuso orario e il suo modo di scrivere una data (per esempio it-IT,
Europe/Rome, 21/10/2026 14:30). La localizzazione della tua organizzazione la impostiamo noi da un elenco
fisso; scrivici per cambiarla. Ha sostituito le impostazioni separate Fuso orario e Formato data, che
non avevano effetto.
- Il certificato di completamento riporta l'ora locale e l'istante UTC; un'esportazione riporta lo scostamento su ogni orario.
- L'invito alla firma e ogni promemoria dicono per esteso giorno e ora entro cui firmare, nel fuso orario della tua organizzazione.
- La riga stampata sotto ogni firma sul documento (Firmato da …) dà l'ora nel fuso della tua organizzazione, con il fuso indicato, e un campo data porta il giorno della tua organizzazione.
- La riserva di prova giornaliera si ricarica alle 00:00 UTC; la pagina Limiti della console mostra quel momento nell'ora della tua organizzazione (le 02:00 in Italia d'estate, l'01:00 d'inverno) e quanto manca.
Cosa fa ciascuna delle tue chiavi
Dai a ogni integrazione la sua chiave, con un nome che dica chi la usa (billing-export, crm-sync). Ogni richiesta autenticata è contata sulla chiave che l'ha fatta, così la tabella delle chiavi nella tua console (Chiavi API) mostra, per chiave, quando è stata usata l'ultima volta, quante richieste ha fatto negli ultimi 30 giorni e quante di queste sono fallite. È così che trovi l'integrazione che va in errore, la chiave che nessuno usa più (revocala) e quella che sta per scadere.
Si conta la richiesta, il suo esito e il suo percorso, mai il contenuto: niente payload, niente indirizzo, niente
chiave. I contatori sono tenuti per chiave e per giorno, scritti circa ogni mezzo minuto, e non sono una fattura
— quanto è costata una chiamata è in output.credits e nella cronologia
del credito. Una richiesta fatta con una chiave sconosciuta, revocata o scaduta non appartiene a nessuna chiave e
non viene contata.
Tutto il ciclo di vita, in ordine
GET /api/v1/flow/{flowId}/events risponde alla terza domanda che un flusso
pone. GET /api/v1/flow/{flowId} dice dove si trova; questo dice come ci è arrivato.
{
"flowId": "...", "schema": 1, "count": 7,
"events": [
{ "sequence": 1, "type": "composition.started", "occurredAt": "...", "recipient": null },
{ "sequence": 2, "type": "composition.completed", "occurredAt": "...", "recipient": null },
{ "sequence": 3, "type": "envelope.created", "occurredAt": "...", "recipient": null },
{ "sequence": 4, "type": "envelope.sent", "occurredAt": "...", "recipient": null },
{ "sequence": 5, "type": "recipient.viewed", "occurredAt": "...", "recipient": "anna@example.com" },
{ "sequence": 6, "type": "recipient.signed", "occurredAt": "...", "recipient": "anna@example.com" },
{ "sequence": 7, "type": "envelope.completed", "occurredAt": "...", "recipient": null }
]
}
Tre soggetti — composition, envelope e recipient — e il secondo e il
terzo usano le stesse parole della callback, così envelope.completed significa una cosa sola ovunque lo
incontri.
sequence è una posizione, non un identificativo: ordina l'elenco e ti fa notare un buco.
È id che elimina i doppioni.
È disponibile mentre il flusso è ancora aperto, ed è questa la differenza con il pacchetto di prove — quella è la forma d'archivio degli stessi fatti ed esiste solo quando la questione è chiusa, cioè proprio nel momento in cui non serve più per capire un problema.
Eliminare i doppioni di una callback
Se ricevi callback webhook, il corpo porta un blocco event, così un nuovo tentativo non può essere
scambiato per una seconda firma:
{
"envelopeId": "...", "status": "completed",
"event": { "id": "evt_...", "type": "envelope.completed",
"occurredAt": "2026-09-22T18:20:00+00:00", "schema": 1 }
}
event.id è stabile tra un tentativo e l'altro: conserva gli id che hai già elaborato e
ignora quello che hai già visto. Un ricevitore che ci ha risposto e poi è andato in timeout riceverà un nuovo
tentativo, e quel tentativo è lo stesso evento, non uno nuovo.
event.occurredAt sta dentro il corpo firmato di proposito. L'HMAC dimostra che un corpo viene da noi;
non dimostra quando, quindi una richiesta catturata e ripetuta mesi dopo verifica ancora. Rifiuta tutto ciò
che è molto più vecchio della tua tolleranza.
Gli stessi valori sono ripetuti in X-Signer-Event-Id e X-Signer-Attempt per comodità —
ma gli header non sono coperti dalla firma. Elimina i doppioni sul corpo.
Sapere quando un firmatario viene sostituito
Per impostazione predefinita una callback dice solo come è finita una busta (envelope.completed,
declined, expired, cancelled), su onSuccess o onError. Per sapere
anche quando un firmatario viene sostituito (POST .../recipients/{id}/replace), indica un terzo
indirizzo nel callbackUrl della richiesta:
"callbackUrl": { "onSuccess": "https://you.example/ok",
"onError": "https://you.example/ko",
"onEvent": "https://you.example/event" }
Con un ambiente, onEvent è un percorso come gli altri due.
È un indirizzo a sé, e da attivare, di proposito: una chiamata su onSuccess o
onError ha sempre voluto dire “firmato” o “non firmato”, e a un'integrazione scritta per
quello non deve mai arrivare altro. Senza onEvent non si invia nulla, e onEvent non riceve mai
come è finita la busta. Il corpo è la busta come la mostra GET, il solito blocco event firmato
("type": "recipient.replaced", stesso id tra i tentativi), l'header
X-Signer-Event: event, e la sostituzione:
{ "envelopeId": "...", "status": "in_progress", "recipients": [ ... ],
"event": { "id": "evt_...", "type": "recipient.replaced", "occurredAt": "...", "schema": 1 },
"replaced": { "recipientId": "...",
"from": { "email": "anna@example.com", "fullName": "Anna Rossi" },
"to": { "email": "carla@example.com", "fullName": "Carla Bianchi" },
"reason": "Anna ha lasciato l'azienda" } }
Consegnata come ogni callback: firmata, ritentata, elencata nel registro dei webhook della console e ripetibile. È l'unico evento che arriva prima della fine, quindi non trattarlo come un esito; la busta è ancora aperta.
Il contratto per le macchine
Il documento OpenAPI è su /openapi.json — pubblico, senza chiave, senza
cookie. Non distribuiamo SDK per i linguaggi, di proposito: l'API è un POST con un corpo JSON, e cinque librerie
client da tenere allineate a un contratto pubblicato sarebbero cinque cose da far invecchiare. Generane una dalla
specifica se ti serve.
Descrive i tre prefissi dei prodotti e gli endpoint a livello di account. Le grafie più vecchie —
/api/v1/documents, /api/v1/templates, /api/v1/signatures — funzionano ancora e
seguono le stesse regole; semplicemente non sono pubblicizzate, così un client generato ha un metodo per chiamata e
non due.
Configurare una callback: Sapere quando qualcosa è finito · Ogni codice di errore: Esiti e codici di errore · Quanto costa un credito: Credito, prezzi e IVA.