Send from a template
Match signers to named roles, prefill fields your app already knows, and send. The shortest path to a signature.
All documentation
The shortest path to a signature, and the one most integrations need. Someone builds the document once in the web app - the layout, the fields, the named roles - and your code supplies only what changes: who is signing and what goes in the blanks.
Templates are authored in the web app. There is no endpoint that creates one, by design: placing fields is a visual job.
Find the template and read its roles
Only published templates are returned. Read the template once during development, hard-code the id, and you never need to list again.
curl https://api.documentesign.com/v1/templates/tpl_4c8a... \
-H "Authorization: Bearer $ESIGN_API_KEY"{
"id": "tpl_4c8a...",
"name": "Mutual NDA",
"description": null,
"status": "PUBLISHED",
"roles": [
{ "role": "Client", "type": "SIGNER", "order": 0 },
{ "role": "Counsel", "type": "CC", "order": 1 }
],
"fields": [
{ "field_key": "contract_value", "type": "TEXT",
"role": "Client", "required": true, "page": 2 }
],
"created_at": "2026-08-02T09:14:00.000Z"
}Those two lists are the contract between you and the template. roles tells you who must be supplied, and fields[].field_key tells you what you are allowed to prefill.
Send it
curl https://api.documentesign.com/v1/templates/tpl_4c8a.../send \
-H "Authorization: Bearer $ESIGN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "NDA - Acme Inc",
"recipients": [
{ "role": "Client", "name": "Sam Rivera", "email": "sam@example.com" }
],
"field_values": {
"contract_value": "48,000 USD"
},
"send": true
}'| Field | Type | Notes |
|---|---|---|
recipients | object[]required | 1 to 20. Each needs role, name and email; phone is optional. |
recipients[].role | stringrequired | Must match a role on the template. Matched case-insensitively, so "client" finds "Client". |
title | string | Defaults to the template name. |
field_values | object | Keyed by field_key. Values up to 2000 chars. |
send | boolean | Defaults to true here. That is the opposite of POST /documents, so pass it explicitly if you want a draft. |
400 missing_roles with the offending names in missing_roles. A role that does not exist on the template returns 400 unknown_role and hands you known_roles so you can see what it expected. Both errors tell you how to fix them - read the body rather than guessing.How prefill behaves
Three rules, and they are easy to get wrong from the outside:
- Unknown keys are ignored, silently. A typo in a
field_keyis not an error - the field simply stays empty. If a value is not showing up, check the spelling againstGET /templates/{id}first. - An empty string is not a value. Passing
""leaves the field signer-fillable rather than locking in a blank. - A supplied value is fixed. The field becomes prefilled content the signer reads but cannot edit. Use it for figures your system owns - the price, the account number, the start date - not for things the signer should be correcting.
- Only data-entry fields accept a value. Text, textarea, number, phone, checkbox, dropdown and radio, plus custom date and email fields. Anything else is refused - see below.
400 unprefillable_field_typenaming the offending keys. It has to: those fields are drawn by the signer, so a text value would lock the field, skip the signer's prompt, and render nothing in the final PDF. The same applies to name, title and company, which are filled from the signer's identity, and to standard date and email fields, which the server stamps at signing time. Use their custom variants if you need to supply your own value.A value that does not fit its field - text in a number, an option that is not on the dropdown - returns 400 invalid_prefill_value. Both errors list every offending key at once, so an integration is fixed in one pass rather than one round trip per mistake.
What comes back
{
"id": "doc_7a1b...",
"status": "in_progress",
"title": "NDA - Acme Inc",
"mode": "LIVE",
"recipients": [
{ "id": "rcp_3d9e...", "name": "Sam Rivera",
"email": "sam@example.com", "role": "Client", "status": "sent" }
],
"created_at": "2026-09-01T10:00:00.000Z"
}This is a normal document from here on. Store the id, then track it with webhooks.
Send and embed in one call
If the signer is about to sign inside your app, you can skip the separate embed-link call. Pass embed and the links come back with the document.
curl https://api.documentesign.com/v1/templates/tpl_4c8a.../send \
-H "Authorization: Bearer $ESIGN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipients": [
{ "role": "Client", "name": "Sam Rivera", "email": "sam@example.com" }
],
"embed": { "expires_in": 900 }
}'{
"id": "doc_7a1b...",
"status": "in_progress",
"recipients": [ { "id": "rcp_3d9e...", "role": "Client", "status": "sent" } ],
"embed_links": [
{ "recipient_id": "rcp_3d9e...",
"url": "https://app.documentesign.com/sign/embed/eyJ...",
"expires_at": "2026-09-01T10:15:00.000Z" }
]
}One entry per signer; CC recipients are skipped because they have nothing to sign. It needs send: true, since a draft has no in-progress document, and your origin must be on the embed allowlist - otherwise you get 409 embedding_not_configured, exactly as the standalone call would. The full flow is on embedded signing.
POST /documents caps at 10. The limits are genuinely different, not a documentation slip.