Signing on its own
If the document already exists — you generated it earlier, or you want to review it before anyone is asked to sign — the signature API is separate:
| Call | Does |
|---|---|
POST /api/v1/sign/files | Upload a PDF you already have and get a fileId. Charges nothing; see below. |
GET /api/v1/sign/files, GET /api/v1/sign/files/{fileId}, DELETE /api/v1/sign/files/{fileId} | List your PDFs, read one's facts (never its bytes), delete one nobody needs. |
POST /api/v1/sign/envelopes | Draft an envelope over a document you produced or a PDF you uploaded. Nothing sent, nothing charged — unless you add ?send=true, which drafts and sends in one call (and is then the billable moment). |
POST /api/v1/sign/envelopes/{id}/send | Issue the links and email them. This is the billable moment. |
GET /api/v1/sign/envelopes/{id} | Where it has got to, and what each signer has done. |
GET /api/v1/sign/envelopes/{id}/document | The signed PDF, once complete. |
GET /api/v1/sign/envelopes/{id}/evidence | A ZIP with the signed PDF, the source document and the full event trail as JSON — everything needed to defend the signature outside this system. Any closed envelope, not only a completed one. |
POST /api/v1/sign/envelopes/{id}/cancel | Revoke it (the console button says Revoke; the path stays /cancel). Every link dies, and each signer who had been asked is told it was revoked. |
POST /api/v1/sign/envelopes/{id}/recipients/{recipientId}/replace | Put somebody else in a signer's place (email, fullName, optional reason; phone when the signer confirms with a code by SMS): the fields and the place in the signing order go to them, the old link dies and its holder is told. On a draft it simply corrects the signer, telling nobody. A signature already given is never undone. Not a charge. |
POST /api/v1/sign/envelopes/{id}/recipients/{recipientId}/resend | Reissue one signer's link — the old one dies immediately. Not a second charge. |
Replacing a signer
When the wrong person was named, or somebody is no longer available, replace the signer instead
of leaving the request open. Send {"email": "...", "fullName": "...", "reason": "..."} to
.../recipients/{recipientId}/replace while the envelope is a draft or open
(sent or in_progress). On a draft nothing has been sent and nobody holds a
link, so the call simply corrects the signer's name, address (and phone): no message goes out, no
callback is sent, and the change is kept as one event on the envelope's trail and one line in the
audit log. A signer who confirms with a code by SMS (validationMode: "sms")
needs the new person's "phone" as well: the old number is never kept, and without it the
request is refused (INVALID_REQUEST_BODY, details.field: "phone"; a number the
code cannot reach is SMS_VALIDATION_UNAVAILABLE). A signer who has signed or declined, or
whose group was completed by somebody else (skipped), cannot be replaced. The slot is handed over
in place, so the signature fields and the signing order stay exactly where they were; the old link
stops working at once and its holder receives a message that the request was withdrawn; the new
person is invited when it is their turn. The answer's delegatedFrom names who held the
slot, with replacedBy set (it is empty when a signer passed their own turn), and the
certificate of completion says in words that the sender replaced them; its event trail lists each
event under the person who held the slot at the time. Somebody who opens a link that was replaced,
withdrawn or that expired before it was used is told which of the three it was, in the envelope's
language; the page shows nothing else and the link can do nothing. A signer who passes their own
turn on (when you allow it) is held to the same rules: on a signer who confirms with a code by SMS
the signing page asks for the new person's mobile number, and the old number is never kept. Use
cancel to end the whole request. When a request is cancelled or expires, every signer
who had been asked and had not finished is told; somebody who was never invited (a later group of an
ordered request) is not. Organisations can also have the mails their signers receive carry their
own logo, colours, footer and wording, and have each signer sent the signed document: the
first is under Branding (how), the second in
Settings, under Notifications.
The subject of the mails
Every mail we send that does not carry your branding has a subject that starts with
“DoConure - ”, so a signer recognises it in the inbox: the
invitation reads DoConure - followed by your subject. A branded mail keeps
your organisation's subject as you wrote it, without our name. In the text of every mail a time
is written in your organisation's locale, with its zone. The button sits in the middle of
the mail with its address written out underneath, for a mail program that does not show
buttons.
The mails your signers receive, in your look
Off until you switch it on (Branding → Brand). You set a header
colour, a button colour, a footer colour and a footer text (a contact line, a notice) in Italian
and in English, and the logo you already uploaded is shown at the top. Each mail carries the
footer text of its envelope's language; when that one is empty, the other. It is never HTML you write: our one template
applies those values, and the plain-text part of every mail is the same message with your footer
text in it. The logo travels inside the mail, so nothing is fetched when it is opened. The
greeting uses the signer's name as you sent it, whole. It covers the invitation, the reminder, the
revoked and expired notices and the signed copy.
Two things never change. The verification code mail is always ours: a code is not a place for branding. And a branded mail loses our own “DoConure by Limepeak” line, since the footer is yours, but keeps the automatic-message note and the link for reporting a document the person was not expecting. A test envelope's mail still opens with the red TEST warning.
The evidence bundle is meant to leave this system: hand it to a lawyer, an
auditor, or a customer's own archive without giving them access here. A
declined or cancelled envelope can produce one too — proving someone refused
to sign is still evidence. Signature evidence is kept for 10 years after an
envelope closes and the source document for 365 days after it is generated;
past either window the corresponding file is silently omitted rather than
failing the whole export, and evidence.json's own fields
(hashes, timestamps, closedReason) still answer what happened.
Sign a PDF you already have
Signer Fly works without Composer Fly. Upload the PDF once, then name its
fileId in any number of envelopes:
POST /api/v1/sign/files
Idempotency-Key: nda-2026-contract-v3
{ "fileName": "nda.pdf", "fileBase64": "JVBERi0xLjQK..." }
201 Created
{ "output": { "sign": { "file": {
"fileId": "6d1c...", "sha256": "ab12...", "pageCount": 3,
"sizeBytes": 48211, "created": true } } } }
POST /api/v1/sign/envelopes?send=true
{ "signers": { "A": { "email": "anna@acme.example", "fullName": "Anna Rossi",
"fields": [ { "kind": "signature", "page": 3,
"x": 0.1, "y": 0.78 } ] } },
"settings": { "documents": [ { "fileId": "6d1c..." } ],
"subject": "NDA" } }
- One file, many envelopes. The file is referenced, never
copied, and it never changes: a new version is a new upload and a new
fileId. Each envelope records the file'ssha256; add"sha256": "..."beside afileIdto be refused (SOURCE_FILE_CHANGED) if it is not the file you mean. - Identical bytes are one file. Uploading a PDF your
organisation already holds answers
200with the existingfileIdandcreated: false. Another organisation's file is never revealed: its id is a404. - One-shot.
{ "fileBase64": "...", "fileName": "nda.pdf" }insettings.documentsis the same upload inline: same validation, same storage, and the file is then on your account like any other. A flow's extradocumentstake the same three forms; the document a flow composes is still always first. - What is refused (
FILE_REJECTED, withdetails.reason): not a PDF, malformed, encrypted, more than 200 pages, over the size limit, or carrying JavaScript, a launch action, an attached file, XFA or rich media — the signer opens the file in their own browser, so nothing that runs is accepted. Nothing is rewritten: the certificate hashes the bytes you sent. - Deleting. A file an open envelope still uses cannot be
deleted (
STORED_FILE_IN_USE). One that only finished envelopes used can; its bytes are kept for their evidence bundle until their retention ends. Unused files expire like any stored file. - The certificate says whose it is. A source you uploaded was not produced by this service, so the certificate and the evidence manifest list it as uploaded by the customer, with its hash.
- The console has the same: Signatures → New signature request uploads or picks a PDF and sends it, with no API key.
Where the signer signs
The signing experience is hosted by DoConure at a secure public
URL: the signer follows a link to /sign/… on our
domain. That is the whole product today. There is no embedded or iframe signing
and no SDK to put the page inside your own site, and it is not
promised: it is a possible future evolution that has not been designed,
not a feature on a roadmap. (The page refuses to be framed on purpose.)
Test envelopes
settings.isTest: true (needs the isTest token
scope) makes a test envelope: it spends the shared daily test allowance and
never credit; every mail about it carries [TEST] in
the subject and opens with a red warning line (“TEST
— this document has no legal value”, in the envelope’s language,
italian or english); the signing page, the signed file (a red TEST band and
tiled text on every page, signature block and certificate) say TEST and that
it has no legal value; and it can never reach a paid third party. Asking for
an SMS code, a seal or a timestamp together with isTest is refused
with FEATURE_NOT_AVAILABLE_IN_TEST_MODE before anything is created.
Read it back as output.sign.isTest.
The request: signers and settings
The body is { signers, settings } — the same shape as
Generating a document's
{ data, settings }. signers is an object keyed by a
label you choose ("A", "B", …) — it identifies each
signer for you, and carries no ordering by itself:
POST /api/v1/sign/envelopes
Authorization: Bearer lpdc_...
{
"signers": {
"A": {
"email": "anna@acme.example",
"fullName": "Anna Rossi",
"validationMode": "sign",
"fields": [
{ "kind": "signature", "page": 1,
"x": 0.1, "y": 0.78, "width": 0.32, "height": 0.09 }
]
}
},
"settings": {
"documentId": "9b21...",
"subject": "Service agreement 2026",
"hasSigningOrder": false,
"expirationDate": null,
"reminderFrequencyHours": 0,
"cc": [], "ccNotifyOn": "none",
"bcc": [], "bccNotifyOn": "none",
"language": "en",
"callbackUrl": { "onSuccess": null, "onError": null }
}
}
| Setting | Default | Means |
|---|---|---|
subject | your organisation's default subject | What the request is about, up to 200 characters: the mail's subject, and named again in its text. Required, unless your organisation set a default in Settings. |
message | your organisation's default message, or none |
Your own words in the request mail, up to 2,000 characters, placed above the
signing button. Line breaks and **bold** are understood:
Writing the message. |
hasSigningOrder | false |
false: every signer is one group, notified together. true:
each signer's own order (a whole number, required only when
this is true) gates them — two signers sharing a number sign
as a group, and the next group is told only once this one is entirely
done, or once one of them has if the group's groupMode is
"OR". There is no separate "sequential" value: ordinary
sequential signing is just every signer given a different number. At
most 5 groups of at most 4 signers each — full reference and the OR
shape: Ask for signatures in a specific
order. |
expirationDate | your organisation's default (30 days) | A specific instant, strictly in the future, at most one year out. Left out, the envelope expires after your organisation's default expiry, 30 days unless you changed it in Settings. The invitation and every reminder say the day, time and time zone the link stops working, in your organisation's locale; the API answers in UTC. There is deliberately no "never" option — past that ceiling the evidence around who held a mailbox has decayed enough that re-sending is the honest move, not extending. |
reminderFrequencyHours | your organisation's default (48) |
0 turns reminders off. Above zero, whoever must currently sign is nudged every this many hours
from when they were notified. A reminder reissues that signer's
link, the same as a resend does, because the plaintext of an old link is
never kept anywhere to send again. |
cc / bcc | [] | Plain addresses that are told about the envelope but never asked to sign. Full reference: Cc and bcc on an envelope. |
language | your organisation's language |
"en" or "it". Selects which language every mail this
envelope sends is written in, and which of your two branded footer texts it
carries. |
callbackUrl.onSuccess / onError / onEvent | null |
Where we POST when the envelope closes — completed to
the first, declined/expired/cancelled
to the second. Must be https and resolve to a public
address. Each request carries
X-Signer-Signature: sha256=<hmac> over the raw body,
keyed with your tenant's own secret — verify it. The body also
carries an event block whose id is stable across
retries, so a repeated delivery can be ignored rather than processed twice
— see Tracing a call.
Left null, nothing is sent and polling works exactly as
before. With callbackUrl.env, the two values are
paths appended to that callback environment's base URL and
the callback carries its authentication (Bearer token, API key or OAuth2);
paths without env go to your default environment —
callback environments.
onEvent is optional and separate: it receives
recipient.replaced when a signer is replaced, signed and
retried like the other two, which keep saying only how the envelope ended
(Being told when a signer is
replaced). |
Writing the message
The message is plain text, the same in the API, in a flow's
signature and in the Default message of
Settings (used when a request does not send one). Every mail
goes out twice in one: an HTML part, and a plain-text part for the mail
programs that show only text. What you write becomes both:
| You write | HTML part | Text part |
|---|---|---|
**clause 4** | clause 4, in bold | clause 4, without the asterisks |
a line break (\n in the JSON) | a new line | a new line |
an empty line (\n\n) | a new paragraph | an empty line |
https://acme.example/terms |
the address, written out and clickable (a full stop right after it ends the sentence, not the link) | the address |
<b>, [text](url), *one* |
shown exactly as typed | shown exactly as typed |
- Bold is two asterisks on each side, on one line, at most
60 characters, with no space just inside the marks:
**by 30 June**is bold,** by 30 June **is printed as typed. It is the only formatting there is: no italics, colours or fonts, and no HTML. - A link is shown as its address; there is no way to give it
other words. Keep it inside a sentence (
The terms are at https://acme.example/terms.): a paragraph that is only an address is drawn as a button, and the mail already has one, the signing button. - Do not indent every line of a paragraph: an indented block is shown as a code, in a box.
"message": "Please read **clause 4** before signing.\n\nQuestions? Write to legal@acme.example."
The ** marks belong to mail messages only. In a document,
bold comes from the template itself:
Writing a template.
Each signer also takes a validationMode: "sign"
(default — a mark plus an explicit slide-to-confirm gesture, no code at all),
"email" (a one-time code emailed to the signer, the flow this
product started with), or "sms" (a one-time code by text
message — requires a "phone" field on that signer). An SMS code is
a service your organisation buys, each SMS the gateway accepts costs two
credits, and where it cannot be delivered the envelope is refused at creation
with SMS_VALIDATION_UNAVAILABLE:
How do I choose how a signer proves it's
them?
Each signer may also declare a signatureMeaning —
what their signature means, which 21 CFR Part 11
§11.50(c) asks a signed record to state alongside the name and the
time. One of "approved", "reviewed",
"authored", "verified" or
"responsibility_accepted"; free text is refused, because a
meaning nobody can compare against anything is not one. It is
per signer, not per envelope: on a real document one
person approves and another acknowledges.
Omit it and nothing is printed. That is deliberate rather than a
default worth setting: most documents are not signed under a regulated
procedure, and stamping “Approved” on one nobody said that
about would invent the very fact the rule exists to record. When it is
set it appears under the mark, on the certificate of completion, and in
the signatureMeaning field of every recipient the API and
the evidence bundle return.
Retrying safely
Send an Idempotency-Key on every
POST in this API — create, send, resend, replace, cancel. Send is
the one that matters most:
send is the billable moment, so a retry after a timeout
charges you again and emails every signer a second time — the
cost lands on your invoice and on your counterparty’s patience at
once.
POST /api/v1/sign/envelopes/7a04…/send
Authorization: Bearer lpdc_…
Idempotency-Key: contract-2026-114
The envelope’s own state is a second guard — sending an already-sent envelope is refused — but that refusal is an error, and a caller retrying a request it never got an answer to deserves the answer rather than a complaint. With a key it gets the answer.
Drafting takes one too. Nothing is charged for a draft, and a retried one still leaves a second envelope behind that somebody has to clean up by hand.
So does resending: a resend is not a second signature and costs nothing, but a signer who gets the same link twice thirty seconds apart is being confused for no reason. And so does replacing a signer: a retried replace that was not recognised would be refused, because the slot is no longer the old person's.
And cancelling, which is the case people do not expect. A cancel is harmless twice — and the second answer is a refusal, because the envelope is no longer cancellable. If you never saw the first answer you would read that refusal as the cancel not having happened, which is exactly backwards.
The full rules, the three outcome codes and the 24-hour window: Retrying safely.
The response
Every call in this API — create, send, resend, cancel, read — answers the
same envelope, with the envelope itself under output.sign:
{
"output": {
"status": "pending",
"compose": null,
"sign": {
"envelopeId": "7a04...",
"status": "sent",
"level": "SES",
"seal": false,
"timestamp": false,
"allowDelegation": false,
"expiresAt": "2027-09-20T20:32:43Z",
"documentId": "9b21...",
"sourceSha256": "cdc82a...",
"signedSha256": null,
"signedDocumentUrl": null,
"padesProfile": null,
"recipients": [
{ "id": "c81e...", "email": "anna@acme.example", "status": "notified",
"order": 0, "validationMode": "sign", "signedAt": null }
]
},
"flow": null,
"credits": { "used": 15, "left": 1232 },
"warnings": [],
"error": null
}
}
| Key | Meaning |
|---|---|
output.status | "pending" while the envelope can still change, "succeeded" once every signer has signed, "failed" if it closed without that — declined, cancelled or expired. Poll on this; output.sign.status is the seven-word vocabulary underneath it. |
output.sign.envelopeId | The id every other call in this API takes. Named to match output.compose.documentId and output.flow.flowId, so that a flow's answer — which carries all three — never leaves you wondering which id is which. |
seal, timestamp, allowDelegation, expiresAt | Your settings, settled. Each is gated or capped by the organisation's own configuration, so an accepted request does not imply the value you sent. That is why there is no separate settings echo here: this is it. |
output.credits | used is what this envelope has settled — the signature, the seal and the timestamp when you send it, then each SMS code as it leaves, so the read that finds it closed carries the definitive figure; items lists it line by line; left is your balance from the ledger now. Creating a draft charges nothing (used: 0). See Credits in every answer. |
output.warnings | Where a gate that overruled you says so, with a stable code. One array per call — see Warnings. |
recipients | What became of each person. You send them as signers, because in your request everybody signs; they come back as recipients, because this product will grow approvers and viewers that do not. |
GET /api/v1/sign/envelopes answers
{ "envelopes": [ … ], "count": n }, where each item is exactly
the object above — a collection is a collection of answers, so you parse one
shape rather than two. Narrow it with ?status=, ?limit= (up to 200)
and ?kind=production or ?kind=test: test and production
are never one list unless you ask for all (the default, as for any value
it does not know). The same kind works on GET /api/v1/flow.
Breaking change, 2026-09-20. These responses used to be
the envelope flat at the root, with id rather than
envelopeId and a warnings array inside the
envelope. The keys moved; they were not duplicated. The same release gave
/api/v1/compose and /api/v1/flow the identical
envelope, which is the point: the section is named after the URL
you called.
When the person you asked is not the right person
A signer can pass the request on from their own link. Theirs stops working immediately, the new person is sent one, and the certificate of completion names who handed it over — a document signed by somebody the sender never named is precisely the fact a dispute turns on, so it is stated in words rather than left in a table.
It is off unless the organisation turns it on, in Settings. With it on, a request may ask for less:
"settings": { "allowDelegation": false }
The organisation's setting is a ceiling, not a default.
If it forbids delegation, asking for it does not fail the call — the
envelope is created without it and the answer carries
DELEGATION_NOT_ENABLED. Read
allowDelegation back from the response rather than
assuming what you sent was used.
Once a slot has changed hands, the recipient in the response carries
delegatedFrom with the previous holder, when it happened and
why. A poller that only reads email would otherwise report
the wrong person as the signer.
Refused, each for its own reason: passing it back to the same address; passing it to somebody already signing this document, which would ask them to sign twice and, in an ordered envelope, make them wait for themselves; and more than three hops, because each one kills a link and sends an email.
Where a field's coordinates go: Where the signature goes on the page. What a signer sees, and the evidence trail: What the signer sees. Legal status of what this produces: Signature levels: SES, AES, QES.