DoConure

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.statusSignifica
pendingI link sono partiti, qualcuno deve ancora firmare. Continua a interrogare.
succeededHanno firmato tutti. output.sign.signedDocumentUrl è valorizzato.
failedSi è 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.statusclosedSignifica
awaiting_signaturefalseI link sono partiti. Continua a interrogare.
signedtrueHanno firmato tutti. output.sign.signedDocumentUrl è valorizzato.
declinedtrueQualcuno ha rifiutato. Niente è firmato — vedi output.sign.recipients[].declineReason.
expiredtrueLa scadenza è passata con firme ancora mancanti.
cancelledtrueL'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 blocco event — id, type, occurredAt e 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.