DoConure

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.