E-Signing API
Send documents for signature straight from your own systems — exchange an API key for a token, create envelopes from templates, manage contacts, and retrieve signing links over a simple REST API.
The E-Signing API lets your application do what your team does in the app: turn a template into a signature request, send it, and follow it through to a signed, audit-trailed PDF — without anyone opening BunnyDoc.
It's a small, focused REST API. JSON in, JSON out, one bearer token, and the exact same permissions your team already has in the app.
Base URL
Every endpoint lives under https://api2.bunnydoc.com/v1. All paths on these pages are relative
to that base.
How authentication works
Access is a two-step flow, and it's the first thing you'll build. Your long-lived API key is sent once to get a short-lived access token; every other request carries only the token, so the key never travels again.
Create an API key
In the app, open Settings → API Keys (owner or admin only) and create a key. It looks like
bd_live_… and is shown once, at creation — copy it somewhere safe. You can't retrieve it
afterwards, only rotate it. See
Create and manage API keys for scopes, expiry and
rotation.
Exchange the key for an access token
Send your key to POST /v1/auth/token. You get back a
bearer token that's valid for one hour.
Call the API with the token
Send Authorization: Bearer <token> on every other request. Cache the token and reuse it until it
expires — don't mint a new one per call.
Keep your API key server-side
An API key acts on behalf of your whole company. Never ship it in a browser, a mobile app, or any client you don't control. If a key is exposed, revoke it from Settings → API Keys and mint a new one — revoking invalidates its outstanding tokens immediately, where rotating leaves them valid for the rest of their hour.
Build it in this order
A complete integration is five calls. Follow them in this order and you'll hit the gotchas in the cheapest possible place — a 404 on a template you can read, not a 422 on a send you meant to deliver.
Mint a key and exchange it for a token
Create and manage API keys →
POST /v1/auth/token. Cache the token for its full
hour.
Find the template you want to send
GET /v1/templates to locate it, then
GET /v1/templates/{id} to read its roles. This second
call is not optional in practice: it tells you which role names to fill and which are locked.
Decide who sends it
Omit sender_email to send as the key's owner, or call
GET /v1/users to let your UI pick a colleague. Sending as
someone else uses their permissions, not the key owner's.
Send it
POST /v1/envelopes/from-template. This creates
and sends in one call; if the send fails, nothing is left behind. Store the returned
envelope_id.
Subscribe to what happens next
POST /v1/webhooks — the
API has no envelope-status endpoint by design. Take the finished PDF from
envelope.document_ready.
Get an access token
The first call every integration makes — exchange your API key for a bearer token.
Send from a template
Create an envelope from one of your templates and send it, in a single call.
Endpoints at a glance
Each request is authorised by both the scope on your key and the permission of the user who owns it — a key can never do more than that person can in the app.
| Do this | Endpoint | Scope |
|---|---|---|
| Exchange an API key for a token | POST /v1/auth/token | — |
| List your templates | GET /v1/templates | templates:read |
| Get one template and its roles | GET /v1/templates/{id} | templates:read |
| List contacts | GET /v1/contacts | contacts:read |
| Create a contact | POST /v1/contacts | contacts:write |
| Create an envelope from a template and send it | POST /v1/envelopes/from-template | envelopes:send |
| Retrieve signing links for a sent envelope | GET /v1/envelopes/{id}/signing-links | envelopes:send |
| List company members | GET /v1/users | users:read |
| List the events you can subscribe to | GET /v1/webhooks/events | webhooks:read |
| List your webhook subscriptions | GET /v1/webhooks | webhooks:read |
| Subscribe an endpoint to events | POST /v1/webhooks | webhooks:write |
| Unsubscribe an endpoint | DELETE /v1/webhooks/{id} | webhooks:write |
Want to be notified when a document is signed, declined or voided instead of polling? Use
webhooks — and take the finished PDF straight from the
envelope.document_ready
payload.
Conventions
- JSON in, JSON out — send and expect
application/json. - Scoped to your company — every resource belongs to one company, and requests never cross company boundaries.
- Same permissions as the app — a key can only do what its owner's role allows. See Users & roles.
- Scopes narrow access — the scopes on a key (
templates:read,contacts:read,contacts:write,envelopes:send,webhooks:read,webhooks:write,users:read) can only ever restrict what its owner could already do, never extend it.
Pagination
List endpoints take page (from 1) and per_page (up to 100, default 25), and return a
pagination object:
{
"data": [ /* … */ ],
"pagination": { "page": 1, "per_page": 25, "total": 134, "total_pages": 6 }
}Quotas
Reads are free of charge against your plan. Two things are metered:
- Sending draws on a separate API signature requests allowance — one unit per envelope,
no matter how many recipients it has. This is not the same pool as envelopes sent from the app.
When it's exhausted, sends return
402 quota_exceededand nothing is delivered. - Creating a contact consumes your plan's ordinary contact allowance, returning
403 quota_exceededwhen full. You don't need to create contacts to send to people, so this is easy to avoid.
Check both under Settings → Billing → Resources.
Rate limits
Requests are throttled per API key (the token endpoint is per IP). Exceeding a limit returns
429 rate_limited with a standard Retry-After header, in seconds — back off for that long
and retry.
| Limit | Scope |
|---|---|
| 20 token exchanges / 15 min | per IP |
| 100 sends / 15 min | per API key |
| 1000 other requests / 15 min | per API key |
Counting per key (not per IP) means a shared egress address — an integration platform, a NAT — never makes one customer's traffic count against another's.
Errors
Every error has the same shape — branch on error.code, never on the message text. See the full
error reference.
{
"error": {
"code": "unknown_signer_role",
"message": "…",
"details": { },
"reference": "0190f3a2-7b6c-7e21-9a3f-2c9b1d4e5f60"
}
}reference appears on the errors we log server-side (5xx and 422). Keep it in your own logs —
quoting it to support locates the exact failure in one lookup.