Comporre e firmare, in una chiamata
POST /api/v1/flow
Questo è Document Fly — la suite. Genera il documento con Composer Fly e lo manda subito in firma con Signer Fly, così non devi tenere uno stato tra due chiamate né gestire il caso in cui la prima ha funzionato e la seconda no.
Il corpo è quello della generazione più una chiave. data e settings sono esattamente
quello che prende Generare un documento; tutto ciò che riguarda la firma sta sotto
signature, nella stessa forma di
POST /api/v1/sign/envelopes senza documentId — quando
scrivi la richiesta il documento non esiste ancora:
POST /api/v1/flow
Authorization: Bearer lpdc_...
Idempotency-Key: order-4471
{
"data": { "customer": { "name": "Acme Srl" } },
"settings": { "templateId": "contract-standard-8bc99b" },
"signature": {
"subject": "Contratto di servizio 2026",
"message": "Leggi la clausola 4 prima di firmare.",
"level": "SES",
"hasSigningOrder": false,
"signers": {
"A": {
"email": "anna@acme.example",
"fullName": "Anna Rossi",
"validationMode": "email",
"fields": [
{ "kind": "signature", "page": 1,
"x": 0.1, "y": 0.78, "width": 0.32, "height": 0.09 }
]
}
}
}
}
Dove va la firma. Metti {{ signature.A }} nel template e ometti
fields: il firmatario A firma dove c'è il tag, e il tag viene tolto dal documento prima che sia
sigillato o conservato. Un tag nel template per un firmatario che non hai inviato, o (quando il template usa i tag)
un firmatario senza tag e senza fields, è rifiutato prima di comporre o addebitare qualunque cosa. Un
firmatario i cui tag sono tutti dentro una condizione risultata falsa non viene messo nella busta: non
riceve l'invito, è indicato negli avvisi (SIGNER_NOT_ON_ENVELOPE, 2134); se non resta nessuno la chiamata è
rifiutata (SIGNER_NOT_PLACED) con il documento addebitato, indicato in details.documentId, e la
firma restituita. Anche un tag scritto in un allegato caricato (signatureTags) si conosce solo quando il
documento esiste. La stessa email su più firmatari è una persona sola, che firma tutti i suoi punti con
un solo invito e si paga come una firma; le sue voci devono coincidere (nome, telefono, verifica, lingua, turno).
fields (frazioni della pagina a partire dall'angolo in alto a sinistra) restano l'alternativa e prevalgono
quando ci sono: Dove va la firma sulla pagina.
Manda una Idempotency-Key. È facoltativa, ma non trattarla come tale. Questa chiamata
addebita due volte e manda email a persone vere; un client che ritenta una richiesta andata in timeout — e i
client lo fanno — senza chiave produce un secondo documento e una seconda serie di email. Con la chiave, il
nuovo tentativo risponde 200 e ti restituisce il flusso che hai già creato invece di
201 e uno nuovo. La chiave vale per la tua organizzazione, quindi deve essere unica solo per te.
Cosa torna indietro
Quando la chiamata risponde il documento esiste già — la generazione è sincrona e dura molto meno di un secondo, quindi il link è un file e non una promessa. Quello che richiede tempo è la firma, e niente la aspetta.
201 Created
{
"settings": { "templateId": "contract-standard-8bc99b", "...": "come stabilito" },
"output": {
"status": "pending",
"flow": {
"flowId": "3f9c...",
"status": "awaiting_signature",
"closed": false,
"terminal": false,
"statusUrl": "/api/v1/flow/3f9c...",
"eventsUrl": "/api/v1/flow/3f9c.../events",
"createdAt": "2026-09-20T09:14:02Z",
"updatedAt": "2026-09-20T09:14:02Z",
"closedAt": null,
"closedReason": null
},
"compose": {
"documentId": "9b21...",
"downloadUrl": "/api/v1/compose/documents/9b21...",
"templateId": "contract-standard-8bc99b",
"templateVersion": 3,
"sha256": "cdc82a...",
"pageCount": 4,
"...": "la stessa sezione con cui risponde /api/v1/compose"
},
"sign": {
"envelopeId": "7a04...",
"status": "sent",
"level": "SES",
"expiresAt": "2026-09-29T20:32:43Z",
"seal": false,
"timestamp": false,
"signedDocumentUrl": null,
"recipients": [
{ "email": "anna@acme.example", "status": "notified" }
],
"...": "la stessa sezione con cui risponde /api/v1/sign"
},
"credits": { "used": 15, "left": 1232 },
"warnings": [],
"error": null
}
}
Una sezione per URL. Un flusso guida gli altri due prodotti, quindi le riempie tutte e tre:
output.flow è lo stato del flusso, output.compose è byte per byte la sezione con cui
risponde /api/v1/compose, e output.sign è quella di /api/v1/sign. Il codice
che hai scritto per uno di quei due endpoint legge un flusso senza cambiare.
Due nomi che ingannano, entrambi ereditati e non inventati qui: mandi signature.signers — in un
flusso firmano tutti — e la busta risponde recipients, perché Signer Fly avrà approvatori e lettori
che non firmano. E output.status riguarda la chiamata (pending finché il flusso non si
chiude); output.flow.status è il vocabolario di cinque parole del flusso.
Crediti, per tutto il flusso. output.credits.used è il documento e la firma (e
il sigillo e la marca temporale, pagati all'invio della busta), più ogni codice SMS quando parte, così un
controllo che trova il flusso chiuso riporta il costo definitivo, e output.credits.items lo elenca voce
per voce; un flusso isTest è used: 0 e porta anche testRemaining, gli elementi di
prova rimasti oggi. left è il tuo saldo dal registro al momento della risposta. Se la busta fallisce dopo
che il documento è stato composto, l'output.credits.used della risposta di errore è il costo del
documento — l'unico errore che non vale zero. Dettagli:
I crediti in ogni risposta.
Modifica incompatibile, 2026-09-20. Questa risposta prima era piatta: flowId,
document, signature e signedDocument alla radice. Non c'è ripiego — le chiavi
si sono spostate, non sono state duplicate. document.id è output.compose.documentId,
document.url è output.compose.downloadUrl, e signedDocument.url è
output.sign.signedDocumentUrl. Lo stesso rilascio ha spostato le risposte di /api/v1/sign sotto
output.sign e rinominato l'id della busta in envelopeId, così le tre sezioni
chiamano i loro id allo stesso modo: documentId, envelopeId, flowId.
seal e timestamp sono riportati, non ripetuti. Entrambi sono
filtrati dalle impostazioni dell'organizzazione, che sono un tetto e non un valore predefinito — quindi
quello che hai mandato e quello che succederà non sono la stessa domanda. Leggili dalla risposta. Quando
l'organizzazione ti scavalca la chiamata riesce lo stesso e output.warnings lo dice, con un codice stabile:
Avvisi. Quando il flusso è firmato, output.sign.padesProfile riporta
quello che il file porta davvero.
Un flusso non può comporre in modalità dataless. settings.dataless: true qui è
rifiutato con DATALESS_NOT_AVAILABLE, prima di comporre o addebitare: il documento deve sopravvivere per
essere mostrato a un firmatario, e le sue prove si conservano dieci anni. Componilo su
/api/v1/compose/documents/generate se non si può conservare nulla.
Se la tua organizzazione ha attivo Non conservare mai i dati che ci invii, un flusso gira
comunque. Quello che hai inviato continua a non essere registrato; il documento invece si conserva, e
warnings porta DOCUMENT_STORED_FOR_SIGNATURE, così nessuno deve dedurlo.
Seguirlo
GET /api/v1/flow/{flowId}
Interroga questo finché output.status non smette di essere "pending". Decidi su quello, non su
un elenco di stati scritto nel codice — serve a questo, e uno stato aggiunto più avanti non trasformerà la tua
integrazione in un ciclo che non finisce mai. Quando il flusso arriva a signed,
output.sign.signedDocumentUrl è il risultato finito: il documento, le firme e il certificato di
completamento in coda.
| output.status | Significa |
|---|---|
pending | I link sono partiti, qualcuno deve ancora firmare. Continua a interrogare. |
succeeded | Hanno firmato tutti. output.sign.signedDocumentUrl è valorizzato. |
failed | Si è chiuso senza firma: rifiutato, scaduto o annullato. output.flow.status dice quale. |
E le cinque parole del flusso, sotto output.flow.status, per quando devi distinguere i tre modi di
fallire:
| output.flow.status | closed | Significa |
|---|---|---|
awaiting_signature | false | I link sono partiti. Continua a interrogare. |
signed | true | Hanno firmato tutti. output.sign.signedDocumentUrl è valorizzato. |
declined | true | Qualcuno ha rifiutato. Niente è firmato — vedi output.sign.recipients[].declineReason. |
expired | true | La scadenza è passata con firme ancora mancanti. |
cancelled | true | L'hai ritirato tu. |
Puoi essere richiamato invece di interrogare. Imposta signature.callbackUrl.onSuccess e
onError e facciamo una POST quando il flusso arriva a uno stato finale — completed al
primo, declined, expired e cancelled al secondo. Il corpo ha la stessa forma di
quello che restituisce l'interrogazione, così un solo gestore serve per entrambi.
Avvisato quando un firmatario viene sostituito. Un terzo indirizzo facoltativo,
signature.callbackUrl.onEvent, riceve l'evento recipient.replaced, firmato e ritentato come gli
altri due, che continuano a dire solo come è finito il flusso:
Sapere quando un firmatario viene sostituito.
Un ricevitore che vuole un token (Salesforce Apex REST, un API gateway): indica un ambiente
di callback e manda solo percorsi, "callbackUrl": {"env": "prod", "onSuccess": "/doconure/ok"}.
Aggiungiamo a ogni callback il token Bearer, la chiave API o il token OAuth2 dell'ambiente:
Come faccio ad autenticare le vostre callback presso il mio
sistema?
Tre cose da sapere prima di costruirci sopra:
- L'URL deve essere https e deve risolvere su un indirizzo pubblico. Un indirizzo privato o di loopback è rifiutato e non ritentato — quel rifiuto è una protezione contro l'uso di questo endpoint per entrare nella nostra rete, non una finezza di configurazione.
- Verifica la firma. Ogni richiesta porta
X-Signer-Signature: sha256=<hmac>, calcolata sull'esatto corpo grezzo con la chiave della tua organizzazione. Confrontala prima di fidarti del contenuto; chiunque può fare una POST al tuo URL. - Arriva entro circa due minuti, non all'istante. La consegna non avviene mai, di proposito,
dentro la richiesta che ha chiuso la busta, perché quella richiesta è un firmatario che aspetta su una pagina.
Un ricevitore spento viene ritentato con attese crescenti — otto tentativi in circa 44 ore, così un
endpoint spento per la notte lo viene comunque a sapere — e poi si rinuncia. Qualunque cosa succeda alla
callback,
GET /api/v1/flow/{flowId}risponde sempre la verità. - Elimina i doppioni su
event.id. Il corpo porta un bloccoevent—id,type,occurredAte un numero di schema — e l'id è lo stesso a ogni nuovo tentativo dello stesso evento. Un ricevitore che ci ha elaborato e poi è andato in timeout sarà richiamato; senza questo non può distinguerlo da una seconda firma.occurredAtè dentro il corpo firmato, così una richiesta catturata e ripetuta dopo si può rifiutare per età.
L'interrogazione continua a funzionare ed è ancora supportata. Se non puoi accettare una richiesta in entrata, per te non cambia nulla.
GET /api/v1/flow elenca i tuoi flussi, dal più recente; aggiungi
?kind=production o ?kind=test così le chiamate di prova e quelle reali non finiscono in un solo
elenco.
POST /api/v1/flow/{flowId}/cancel ritira la richiesta (la console la chiama
Revoca). Ogni link ancora aperto smette subito di funzionare, e ogni firmatario invitato che non aveva finito
viene avvisato della revoca. Un firmatario della busta di un flusso si sostituisce come qualunque altro:
Sostituire un firmatario. Il documento non viene toccato e resta
scaricabile — esiste ed è stato pagato — e né il documento né la firma sono rimborsati, perché il lavoro è
stato fatto e l'email è partita. Il sigillo e la marca temporale della busta, se li ha pagati all'invio,
sono restituiti al credito da cui venivano, e lo stesso succede quando la busta scade o è rifiutata, o
quando non si possono apporre (cosa torna indietro).
Quanto costa, e cosa rifiuta
Un flusso costa un documento più una firma. Sono riservati insieme, in un unico passo atomico prima
di generare qualunque cosa (due chiamate in gara per gli ultimi crediti non possono partire entrambe): se il
tuo saldo copre uno ma non entrambi, la chiamata è rifiutata con QUOTA_EXCEEDED e un
details.breakdown delle due tariffe, e non si produce né addebita nulla. Lo stesso vale per un livello di
firma non disponibile — vedi Livelli di firma: SES, AES, QES. Un flusso non ti
lascia mai con in mano un documento che non puoi inviare.