Compose and sign, in one call
POST /api/v1/flow
This is Document Fly — the suite. It generates the document with Composer Fly and immediately puts it out for signature with Signer Fly, so you do not have to hold state between two calls or handle the case where the first worked and the second did not.
The body is the generate body plus one key. data and
settings are exactly what
Generating a document takes; everything
about the signature is under signature, in the same shape as
POST /api/v1/sign/envelopes
minus documentId — the document does not exist yet when this is
written:
POST /api/v1/flow
Authorization: Bearer lpdc_...
Idempotency-Key: order-4471
{
"data": { "customer": { "name": "Acme Srl" } },
"settings": { "templateId": "contract-standard-8bc99b" },
"signature": {
"subject": "Service agreement 2026",
"message": "Please read clause 4 before signing.",
"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 }
]
}
}
}
}
Where the signature goes. Put
{{ signature.A }} in the template and leave out
fields: signer A signs where the tag is, and the tag is
removed from the document before it is sealed or stored. A tag in the template for
a signer you did not send, or (when the template uses tags) a signer with neither a
tag nor fields, is refused before anything is composed or charged. Two
cases are only known once the document exists, and are then refused with the
document charged and named in details.documentId: a tag inside a
condition that turned out false, and a tag typed in an uploaded annex
(signatureTags). fields (fractions of the page
from the top-left) remain the alternative and win when given:
Where the signature goes on the page.
Send an Idempotency-Key. It is optional and
you should not treat it as such. This call charges twice and emails real
people; a client that retries a request that timed out — and clients do —
will produce a second document and a second set of emails without one. With
one, the retry answers 200 and hands back the flow you
already made instead of 201 and a new one. The key is scoped
to your organisation, so it only ever has to be unique to you.
What comes back
The document already exists when this returns — generation is synchronous and takes well under a second, so the link is a file and not a promise. What takes time is the signature, and nothing waits for it.
201 Created
{
"settings": { "templateId": "contract-standard-8bc99b", "...": "as settled" },
"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,
"...": "the same section /api/v1/compose answers with"
},
"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" }
],
"...": "the same section /api/v1/sign answers with"
},
"credits": { "used": 15, "left": 1232 },
"warnings": [],
"error": null
}
}
One section per URL. A flow drives the other two
products, so it fills all three: output.flow is the flow's own
state, output.compose is byte for byte the section
/api/v1/compose answers with, and output.sign
is the one /api/v1/sign answers with. Code you wrote against
either of those endpoints reads a flow without changing.
Two names that catch people out, both inherited rather than invented
here: you send signature.signers — in a flow everybody signs —
and the envelope answers recipients, because Signer Fly will
grow approvers and viewers that do not sign. And output.status
is about the call (pending until the flow closes);
output.flow.status is the flow's own five-word vocabulary.
Credits, for the whole flow.
output.credits.used is the document and the signature
(and the seal and timestamp, paid when the envelope is sent), plus each SMS
code as it leaves, so a poll that finds the flow closed carries the definitive
cost, and output.credits.items lists it line by line; an isTest
flow is used: 0 and also carries testRemaining, the
test items left today. left is your balance from the
ledger at the moment of the answer. If the envelope fails after the document
was composed, the error answer's output.credits.used is the
document's cost — the one failure that is not zero. Details:
Credits in every answer.
Breaking change, 2026-09-20. This response used to be
flat: flowId, document, signature and
signedDocument at the root. There is no fallback — the keys
moved, they were not duplicated. document.id is
output.compose.documentId, document.url is
output.compose.downloadUrl, and
signedDocument.url is
output.sign.signedDocumentUrl. The same release moved
/api/v1/sign's answers under output.sign and
renamed the envelope's id to envelopeId, so that
the three sections name their ids the same way:
documentId, envelopeId, flowId.
seal and timestamp are echoed, not
repeated. Both are gated by the organisation's own
settings, which are a ceiling rather than a default — so what you sent and
what will happen are not the same question. Read them from the response.
When the organisation overrules you the call still succeeds and
output.warnings says so, with a stable code:
Warnings. Once the flow is
signed, output.sign.padesProfile reports what the file
actually carries.
A flow cannot compose dataless.
settings.dataless: true here is refused with
DATALESS_NOT_AVAILABLE, before anything is composed or charged:
the document has to survive in order to be shown to a signer, and its
evidence is kept for ten years. Compose it at
/api/v1/compose/documents/generate if nothing may be stored.
If your organisation has Never store the data you send us
switched on, a flow still runs. What you sent is still not recorded; the
document itself is kept, and warnings carries
DOCUMENT_STORED_FOR_SIGNATURE so that nobody has to infer it.
Following it
GET /api/v1/flow/{flowId}
Poll this until output.status stops being
"pending". Branch on that, not on a list of statuses you hardcode
— that is what it is for, and a status added later will not turn your
integration into a loop that never ends. When the flow reaches
signed, output.sign.signedDocumentUrl is the
finished article: the document, the marks, and the certificate of completion
appended.
| output.status | Means |
|---|---|
pending | Links are out, somebody has still to sign. Keep polling. |
succeeded | Everyone signed. output.sign.signedDocumentUrl is populated. |
failed | It closed without a signature: declined, expired or cancelled. output.flow.status says which. |
And the flow's own five words, under output.flow.status, for
when you need to tell the three ways of failing apart:
| output.flow.status | closed | Means |
|---|---|---|
awaiting_signature | false | Links are out. Keep polling. |
signed | true | Everyone signed. output.sign.signedDocumentUrl is populated. |
declined | true | Somebody refused. Nothing is signed — see output.sign.recipients[].declineReason. |
expired | true | The deadline passed with signatures outstanding. |
cancelled | true | You withdrew it. |
You can be called back instead of polling. Set
signature.callbackUrl.onSuccess and
onError and we POST to them when the flow reaches a terminal
state — completed to the first, declined,
expired and cancelled to the second. The body is
the same shape the poll returns, so one handler serves both.
Told when a signer is replaced. A third, optional address,
signature.callbackUrl.onEvent, receives the event
recipient.replaced, signed and retried like the other two, which
keep saying only how the flow ended:
Being told when a signer is
replaced.
A receiver that wants a token (Salesforce Apex REST,
an API gateway): name a callback environment and send only paths,
"callbackUrl": {"env": "prod", "onSuccess": "/doconure/ok"}. We
add the environment's Bearer token, API key or OAuth2 token to every callback:
How do I make our callbacks
authenticate at my system?
Three things worth knowing before you build on it:
- The URL must be https, and must resolve to a public address. A private or loopback address is refused and not retried — that refusal is a guard against this endpoint being used to reach inside our network, not a configuration nicety.
- Verify the signature. Every request carries
X-Signer-Signature: sha256=<hmac>, computed over the exact raw body with your tenant's own key. Compare it before trusting the payload; anyone can POST to your URL. - It arrives within about two minutes, not instantly.
Delivery deliberately never happens inside the request that closed the
envelope, because that request is a signer waiting on a page. A receiver
that is down is retried on a widening backoff — eight attempts over
roughly 44 hours, so an endpoint that is down overnight still hears
about it — and then given up on. Whatever happens to the callback,
GET /api/v1/flow/{flowId}still answers the truth. - Deduplicate on
event.id. The body carries aneventblock —id,type,occurredAtand a schema number — and the id is the same on every retry of the same event. A receiver that processed us and then timed out will be called again; without this it cannot tell that from a second signature.occurredAtis inside the signed body so a captured request replayed later can be rejected on age.
Polling still works and is still supported. If you cannot accept an inbound request, nothing changes for you.
GET /api/v1/flow lists your flows,
newest first; add ?kind=production or ?kind=test so test
calls and real ones do not end up in one list.
POST /api/v1/flow/{flowId}/cancel
withdraws the request (the console calls it Revoke). Every outstanding
link stops working immediately, and each signer who had been invited and had
not finished is told it was revoked. A signer of a flow's envelope is replaced
like any other:
Replacing a signer. The
document is untouched and stays fetchable — it exists and was paid for — and
neither the document nor the signature is refunded, because the work was done and the
mail went out. The envelope's seal and timestamp, if it paid them at
the send, are returned to the credit they came from, and the same
happens when the envelope expires or is declined, or when they cannot be applied
(what comes back).
What it costs, and what it refuses
A flow costs a document plus a signature. Both are reserved together, in one
atomic step before anything is generated (two calls racing for the
last credits cannot both start): if your balance covers one but
not both, the call is refused with QUOTA_EXCEEDED and a
details.breakdown of the two rates, and nothing is made or
charged. The same is true of an unavailable signature level — see
Signature levels: SES, AES, QES.
A flow never leaves you holding a document you cannot send.