BunnyDoc
APIE-Signing API

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

FieldTypeRequiredNotes
template_idstring (uuid)YesThe template to send. Get it from GET /v1/templates.
recipientsarrayYes1–50 recipients, one per unlocked template role.
sender_emailstringNoSend as this user. Must be an active user in your company with an eSignature seat. Defaults to the API key's owner.
titlestringNoEnvelope title, up to 250 characters. Defaults to the template name.
email_subjectstringNoSubject 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_messagestringNoMessage 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 fieldTypeRequiredNotes
rolestringYesMust match an unlocked role from GET /v1/templates/{id}. Up to 25 characters.
namestringYesRecipient's full name, up to 120 characters.
emailstringYesRecipient's email, up to 150 characters.
contact_numberstringNoUp 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

Send from a template
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" }
    ]
  }'

Response

201 Created
{
  "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"
}
FieldTypeDescription
envelope_idstringThe new envelope's id.
statusstringAlways sent on success.
senderobjectThe resolved sender's email.
recipientsarrayThe recipients you supplied.
signing_urlsarray | nullPresent only if the envelope shares signing links with the sender; otherwise null. See below.
created_atstringISO 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.

PlaceholderBecomes
{{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
One request, personalised per recipient
{
  "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:

AllowedTags
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

Statuserror.codeWhen it happens
402quota_exceededYour company has no remaining API signature requests for this period. Nothing was delivered.
404template_not_foundNo template with that id exists in your company.
422unknown_signer_roleA recipient role doesn't match the template, or a locked role was supplied.
422sender_not_foundsender_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.)
422sender_fields_incompleteThe 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 422validation_failedThe body is malformed — missing field, duplicate role, over a length limit, an unknown {{placeholder}}, or a disguised link.
429rate_limitedMore than 100 sends in 15 minutes for this key.
5xxsend_failedCreated but not sent, so it was discarded. Nothing was delivered; retrying creates a fresh envelope rather than duplicating one.

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

Get signing links
curl https://api2.bunnydoc.com/v1/envelopes/018f9a0b-1c2d-7e3f-8a4b-5c6d7e8f9a0b/signing-links \
  -H "Authorization: Bearer <access_token>"

Response

200 OK
{
  "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

Statuserror.codeWhen it happens
403signing_links_disabledThis envelope doesn't have "share signing links with sender" enabled.
404envelope_not_foundNo 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.

On this page