Send from a template
Create an envelope from one of your templates and send it in a single call, then retrieve the signing links.
This is the endpoint most integrations are built around: take one of your templates, assign real people to its roles, and send it for signature — all in one call. There is no draft step on the API.
Create an envelope from a template and send it
Requires the envelopes:send scope. Creating and sending happen together: if the send fails for any
reason, the envelope is deleted and an error is returned, so you never leave a half-created draft
behind. Each successful send consumes one of your plan's API signature requests.
Endpoint
POST https://api2.bunnydoc.com/v1/envelopes/from-template
Body
| Field | Type | Required | Notes |
|---|---|---|---|
template_id | string (uuid) | Yes | The template to send. Get it from GET /v1/templates. |
recipients | array | Yes | 1–50 recipients, one per unlocked template role. |
sender_email | string | No | Send as this user. Must be an active user in your company with an eSignature seat. Defaults to the API key's owner. |
title | string | No | Envelope title, up to 250 characters. Defaults to the template name. |
email_subject | string | No | Subject line of the invitation (and reminder) emails. Up to 300 characters. Supports placeholders. Defaults to the template's subject, or New Document for Signature - {title}. |
email_message | string | No | Message body shown to recipients. Up to 10 000 characters. Plain text or limited HTML, and supports placeholders. Defaults to the template's message, or your active brand's default message. |
Each recipient maps a real person to a role on the template:
| Recipient field | Type | Required | Notes |
|---|---|---|---|
role | string | Yes | Must match an unlocked role from GET /v1/templates/{id}. Up to 25 characters. |
name | string | Yes | Recipient's full name, up to 120 characters. |
email | string | Yes | Recipient's email, up to 150 characters. |
contact_number | string | No | Up to 20 characters. |
Match roles exactly, and skip locked ones
Each role must be an unlocked role name from the template, and each role may appear once. A role
that doesn't exist, or a locked role, is rejected with unknown_signer_role. Always call
GET /v1/templates/{id} first to read the valid roles.
Don't put the sender in recipients
recipients is for the people receiving the document. The sender is whoever sender_email
resolves to, and their part of the template — the Me role — is added and completed for you. You
never pass a Me recipient; if you do, its name and email are ignored in favour of the real sender.
Example request
curl -X POST https://api2.bunnydoc.com/v1/envelopes/from-template \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"template_id": "018f2c3d-4e5f-7a8b-9c0d-1e2f3a4b5c6d",
"title": "Mutual NDA — Acme Corp",
"sender_email": "sales@yourco.com",
"email_subject": "Please sign: Mutual NDA",
"email_message": "Hi Jane — please review and sign our mutual NDA. Thanks!",
"recipients": [
{ "role": "Client", "name": "Jane Doe", "email": "jane@acme.com" }
]
}'POST /v1/envelopes/from-template HTTP/1.1
Host: api2.bunnydoc.com
Authorization: Bearer <access_token>
Content-Type: application/json
{
"template_id": "018f2c3d-4e5f-7a8b-9c0d-1e2f3a4b5c6d",
"title": "Mutual NDA — Acme Corp",
"sender_email": "sales@yourco.com",
"email_subject": "Please sign: Mutual NDA",
"email_message": "Hi Jane — please review and sign our mutual NDA. Thanks!",
"recipients": [
{ "role": "Client", "name": "Jane Doe", "email": "jane@acme.com" }
]
}Response
{
"envelope_id": "018f9a0b-1c2d-7e3f-8a4b-5c6d7e8f9a0b",
"status": "sent",
"title": "Mutual NDA — Acme Corp",
"sender": { "email": "sales@yourco.com" },
"recipients": [
{ "role": "Client", "name": "Jane Doe", "email": "jane@acme.com" }
],
"signing_urls": [
{
"recipient_id": "018f9a0b-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"name": "Jane Doe",
"email": "jane@acme.com",
"recipient_type": "need-to-sign",
"signing_url": "https://sign.bunnydoc.com/s/eyJhbGciOi…"
}
],
"created_at": "2026-07-28T10:20:00.000Z"
}| Field | Type | Description |
|---|---|---|
envelope_id | string | The new envelope's id. |
status | string | Always sent on success. |
sender | object | The resolved sender's email. |
recipients | array | The recipients you supplied. |
signing_urls | array | null | Present only if the envelope shares signing links with the sender; otherwise null. See below. |
created_at | string | ISO 8601 timestamp. |
Where do expiry, reminders and fields come from?
Expiry, reminders, signing order, and field layout are inherited from the template. The API sets the recipients, the title, the sender, and — when you provide them — the email subject and message; the template (and your active brand default) supply the rest.
Customising the invitation email
email_subject and email_message control the email each recipient receives (and the reminders
that follow it — reminders reuse the subject, prefixed Reminder:). Omit them and the template's
own values are used.
Placeholders
Both fields support placeholders, filled in per recipient at send time. So one call can produce a personally addressed email for every signer.
| Placeholder | Becomes |
|---|---|
{{recipient_name}} | The recipient's full name |
{{recipient_first_name}} | The recipient's first name |
{{recipient_email}} | The recipient's email address |
{{sender_name}} | The sender's full name |
{{sender_first_name}} | The sender's first name |
{{sender_email}} | The sender's email address |
{{company_name}} | Your company name |
{{envelope_title}} | The envelope title |
{{expiry_date}} | The envelope's expiry date, formatted for the envelope's locale |
{
"template_id": "018f2c3d-4e5f-7a8b-9c0d-1e2f3a4b5c6d",
"email_subject": "{{recipient_first_name}}, please sign {{envelope_title}}",
"email_message": "<p>Hi {{recipient_first_name}},</p><p>Please review and sign. It expires on {{expiry_date}}.</p><p>Thanks,<br/>{{sender_name}}</p>",
"recipients": [
{ "role": "Client", "name": "Jane Doe", "email": "jane@acme.com" },
{ "role": "Witness", "name": "Sam Ortiz", "email": "sam@acme.com" }
]
}An unknown placeholder is rejected, not ignored
Anything in {{ … }} that isn't on the list above fails the request with validation_failed —
we won't send an email containing a literal {{customer_ref}}. Check your spelling against the
table; the names are exact and lowercase.
There is deliberately no placeholder for the signing link or the access code. The email already
carries the signing button, and an access code is a second factor that must reach the recipient
some other way. Use GET /v1/envelopes/{id}/signing-links
if your own system needs the URL.
Formatting in email_message
Send plain text (line breaks are preserved) or a small, safe subset of HTML:
| Allowed | Tags |
|---|---|
| Paragraphs and breaks | <p>, <br> |
| Emphasis | <strong>, <b>, <em>, <i>, <u>, <s> |
| Lists | <ul>, <ol>, <li> |
| Links | <a href> — https, http, mailto, tel only |
Everything else is stripped on the way in, including style, class, id, images, tables and
headings. The stored value is the cleaned one, so what you can save is exactly what the recipient
receives.
Links whose text disagrees with their target are rejected
<a href="https://evil.example">https://yourbank.example</a> fails with validation_failed. If
the link text looks like an address, it must be the address you're linking to. Use descriptive text
(<a href="https://acme.example/terms">our terms</a>) and this never bites.
The 10 000-character limit is counted after cleaning, so a little stray markup won't push you over.
Errors
| Status | error.code | When it happens |
|---|---|---|
402 | quota_exceeded | Your company has no remaining API signature requests for this period. Nothing was delivered. |
404 | template_not_found | No template with that id exists in your company. |
422 | unknown_signer_role | A recipient role doesn't match the template, or a locked role was supplied. |
422 | sender_not_found | sender_email isn't an active user with an eSignature seat in your company. (With no sender_email, the key's owner has lost their seat.) |
422 | sender_fields_incomplete | The template's own sender role has required fields with no value, so the document can't go out. Fix it on the template — see the error reference. |
400 422 | validation_failed | The body is malformed — missing field, duplicate role, over a length limit, an unknown {{placeholder}}, or a disguised link. |
429 | rate_limited | More than 100 sends in 15 minutes for this key. |
5xx | send_failed | Created but not sent, so it was discarded. Nothing was delivered; retrying creates a fresh envelope rather than duplicating one. |
Get signing links for a sent envelope
Signing links let the sender's system hand each recipient a direct link instead of waiting for the email. They're available any time after sending, and are also returned inline by the send call above.
Endpoint
GET https://api2.bunnydoc.com/v1/envelopes/{id}/signing-links
Example request
curl https://api2.bunnydoc.com/v1/envelopes/018f9a0b-1c2d-7e3f-8a4b-5c6d7e8f9a0b/signing-links \
-H "Authorization: Bearer <access_token>"GET /v1/envelopes/018f9a0b-1c2d-7e3f-8a4b-5c6d7e8f9a0b/signing-links HTTP/1.1
Host: api2.bunnydoc.com
Authorization: Bearer <access_token>Response
{
"signing_urls": [
{
"recipient_id": "018f9a0b-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"name": "Jane Doe",
"email": "jane@acme.com",
"recipient_type": "need-to-sign",
"signing_url": "https://sign.bunnydoc.com/s/eyJhbGciOi…"
}
]
}signing_url is null for recipients who don't sign (for example, a "receives a copy" recipient).
A signing URL is a bearer credential
Anyone holding a signing URL can sign as that recipient. Deliver them over secure channels and treat them like secrets — don't log them or put them in URLs you share.
Errors
| Status | error.code | When it happens |
|---|---|---|
403 | signing_links_disabled | This envelope doesn't have "share signing links with sender" enabled. |
404 | envelope_not_found | No envelope with that id exists in your company. |
Want status updates without polling?
The API is write-only for envelopes — there's no status endpoint by design. To know when a document is viewed, signed, or completed, subscribe to webhooks.