Send documents for signature, track their status, retrieve the sealed PDF, and receive real-time events, all over a clean REST API.
The ToroDocs REST API lets you create and send envelopes from templates, poll their status, download the sealed PDF, and subscribe to webhook events, the same pipeline that powers the dashboard. All responses are JSON.
https://torodocs.com/api/v1/v1).Authenticate every request with a team API token in the Authorization header:
Authorization: Bearer <your-api-token>
Create a token on the Developers page (Dashboard → Developers). Tokens are minted on your team and act with your team's data and document allowance. A token is the whole team's access, so keep it secret. The plaintext token is shown once at creation.
Two scopes:
read: list & read templates, envelopes, recipients, status, and download sealed PDFs.read-write: everything read can do, plus create/send envelopes and manage webhooks.Success responses wrap the result in data (list endpoints add meta):
{ "data": { … } }
{ "data": [ … ], "meta": { "current_page": 1, "per_page": 25, "total": 3, "last_page": 1 } }
Errors use a consistent shape:
{ "error": { "code": "not_found", "message": "…", "details": { … } } }
List endpoints accept ?page and ?per_page (default 25, max 100; out of range → 422). The meta object carries current_page, per_page, total, and last_page.
Requests are limited to 120 per minute per token. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining; exceeding the limit returns 429 with a Retry-After header.
Send an Idempotency-Key header on POST /envelopes to make a retry safe. Replaying the same key with the same body returns the original envelope (no second send or charge). Reusing a key with a different body returns 409 idempotency_conflict.
Every template field has a stable, developer-facing name: its field key (key). Read a template's fields via GET /templates/{id} to discover the keys, then prefill values by key when you send (POST /envelopes). Prefilled values are locked for the signer and stamped onto the final sealed PDF.
key: null; they are the signer's action and can't be prefilled. Text/date/number/etc. fields carry a real key like "candidate_name".GET /templates/{id} to see each role's fields and their keys.POST /envelopes with a recipient per role, optionally prefilling fields by key.signing_url, or wait for the envelope.sent / envelope.completed webhooks.The authenticated team, its document usage, and the current token.
curl https://torodocs.com/api/v1/me \
-H "Authorization: Bearer $TORODOCS_TOKEN"
{
"data": {
"id": 42,
"name": "Acme Inc",
"plan": "enterprise",
"documents": { "used_this_period": 3, "allowance": 150, "remaining": 147 },
"token": { "name": "Zapier integration", "abilities": ["read", "write"] }
}
}
allowance is the team's monthly document allowance; on Business and Enterprise it is pooled per seat (e.g. 75 × 2 seats = 150 on Enterprise); remaining is what's left this period. Beyond the allowance, documents bill at $1.50 each.
Paginated list of your team's templates, newest first. Query: ?page, ?per_page.
curl "https://torodocs.com/api/v1/templates?per_page=25" \
-H "Authorization: Bearer $TORODOCS_TOKEN"
{
"data": [
{ "id": 10, "name": "Offer Letter", "description": "Employment offer",
"page_count": 3, "created_at": "2026-07-24T20:32:51.000000Z" }
],
"meta": { "current_page": 1, "per_page": 25, "total": 1, "last_page": 1 }
}
Full template with roles grouped by signer_slot; each role lists its fields with their key. This is the contract for sending: a recipient is required for every signer slot the template uses.
{
"data": {
"id": 10, "name": "Offer Letter", "description": "Employment offer",
"page_count": 3, "sequential": true,
"default_title": "Your offer", "default_message": null,
"created_at": "2026-07-24T20:32:51.000000Z",
"roles": [
{ "signer_slot": 1, "label": "Candidate", "default_email": null, "routing_order": 1,
"fields": [
{ "key": "candidate_name", "type": "text", "label": "Candidate Name", "required": true, "page": 1 },
{ "key": "start_date", "type": "date", "label": "Start Date", "required": true, "page": 1 },
{ "key": null, "type": "signature", "label": "Candidate Signature", "required": true, "page": 3 }
] },
{ "signer_slot": 2, "label": "HR", "default_email": "hr@example.com", "routing_order": 2,
"fields": [ { "key": null, "type": "signature", "label": "HR Signature", "required": true, "page": 3 } ] }
]
}
}
Paginated list, newest first. Filters: ?status= (draft, sent, in_progress, completed, voided, declined, expired) and ?template_id=.
curl "https://torodocs.com/api/v1/envelopes?status=completed" \
-H "Authorization: Bearer $TORODOCS_TOKEN"
{
"data": [
{ "id": 137, "title": "Please sign your offer", "status": "sent",
"recipient_count": 2, "is_sealed": false, "from_template": true, "template_id": 10,
"sent_at": "2026-07-24T20:00:00.000000Z", "completed_at": null,
"created_at": "2026-07-24T20:00:00.000000Z" }
],
"meta": { "current_page": 1, "per_page": 25, "total": 1, "last_page": 1 }
}
Full envelope with recipients, document, and (once completed) the sealed block (SHA-256 + download URL).
{
"data": {
"id": 138, "title": "Please sign your offer", "status": "completed",
"sequential": false, "recipient_count": 1, "is_sealed": true,
"from_template": true, "template_id": 10,
"sent_at": "2026-07-19T20:32:51.000000Z", "completed_at": "2026-07-21T20:32:51.000000Z",
"created_at": "2026-07-19T20:32:51.000000Z",
"document": { "title": "Offer Letter", "page_count": 3 },
"recipients": [
{ "id": 139, "name": "Jordan Lee", "email": "jordan@example.com",
"signer_slot": 1, "routing_order": 1, "status": "signed",
"signed_at": "2026-07-21T20:32:51.000000Z", "viewed_at": "2026-07-20T20:32:51.000000Z",
"signing_url": null }
],
"sealed": {
"sha256": "92e2f0976f47f01125d5ab20f0ed7cfe36c1de94faac6835dc65ef6887825ea1",
"sealed_at": "2026-07-21T20:32:51.000000Z", "bytes": 10951,
"download_url": "https://torodocs.com/api/v1/envelopes/138/download"
}
}
}
Recipient statuses for polling. Each recipient who can still act carries a live signing_url (for a sequential envelope, only the current signer's URL is live; everyone else is null).
{
"data": [
{ "id": 139, "name": "Jordan Lee", "email": "jordan@example.com",
"signer_slot": 1, "routing_order": 1, "status": "sent",
"signed_at": null, "viewed_at": null,
"signing_url": "https://torodocs.com/sign/AbCd…40-char-token" }
]
}
Streams the sealed PDF (application/pdf). Returns 409 not_ready until the envelope is completed & sealed.
curl -L "https://torodocs.com/api/v1/envelopes/138/download" \
-H "Authorization: Bearer $TORODOCS_TOKEN" -o signed.pdf
Create and send an envelope from a template: the main write endpoint. Provide one recipient per signer slot the template uses, and optionally prefill fields by key. Optional Idempotency-Key header.
Body: template_id (required), recipients[] (required; each signer_slot, name, email, optional routing_order and fields), plus optional title, message, sequential, expires_in_days (1–365), reminders{enabled,interval_days}.
curl -X POST https://torodocs.com/api/v1/envelopes \
-H "Authorization: Bearer $TORODOCS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-2481" \
-d '{
"template_id": 10,
"title": "Please sign your offer",
"sequential": true,
"expires_in_days": 14,
"reminders": { "enabled": true, "interval_days": 3 },
"recipients": [
{ "signer_slot": 1, "name": "Jordan Lee", "email": "jordan@example.com",
"routing_order": 1,
"fields": { "candidate_name": "Jordan Lee", "start_date": "2026-09-01" } },
{ "signer_slot": 2, "name": "HR Team", "email": "hr@acme.com", "routing_order": 2 }
]
}'
Returns 201 with the full envelope (same shape as GET /envelopes/{id}). Each recipient who can act carries a signing_url; for a sequential envelope only order-1 is live at send time.
Available on Enterprise and the Developer plan.
Manage endpoints programmatically. POST accepts url (https only) and optional events (defaults to ["*"]); the signing secret is returned once. /test queues a sample webhook.test delivery.
curl -X POST https://torodocs.com/api/v1/webhooks \
-H "Authorization: Bearer $TORODOCS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/webhooks/torodocs", "events": ["*"] }'
{
"data": {
"id": 7, "url": "https://example.com/webhooks/torodocs",
"events": ["*"], "active": true, "consecutive_failures": 0,
"last_success_at": null, "last_failure_at": null,
"created_at": "2026-07-24T22:48:30.000000Z",
"secret": "whsec_…store-this-now"
}
}
Subscribe endpoints to events and ToroDocs will POST a signed JSON payload as documents move through their lifecycle. Event types:
| Event | Fires when… |
|---|---|
envelope.sent | an envelope is sent |
recipient.viewed | a recipient opens the signing page (first view) |
recipient.signed | a recipient completes signing |
envelope.completed | all recipients have signed |
envelope.sealed | the sealed PDF is ready (payload carries sealed.sha256 + download_url) |
envelope.declined | a recipient declines |
envelope.expired | an envelope passes its expiry |
envelope.voided | the sender voids the envelope |
Payload: data.envelope matches GET /envelopes/{id}; recipient-scoped events also include data.recipient:
{
"id": "689dcf24-1167-4154-a4e4-6fd55a02a84b",
"type": "envelope.sealed",
"created_at": "2026-07-24T22:48:53.212200Z",
"data": {
"envelope": { "id": 138, "status": "completed", "is_sealed": true,
"sealed": { "sha256": "92e2f097…", "download_url": "https://torodocs.com/api/v1/envelopes/138/download", "bytes": 10951, "sealed_at": "…" },
"recipients": [ … ], "document": { … } }
}
}
Headers on each delivery:
Content-Type: application/json
X-ToroDocs-Event: envelope.sealed
X-ToroDocs-Delivery: 1837 # unique per delivery, use it to dedupe
X-ToroDocs-Signature: t=1784933335,v1=4648fe1c3bfced…
The X-ToroDocs-Signature header is t=<unix_ts>,v1=<hex>, where v1 = HMAC-SHA256(endpoint_secret, "<t>.<raw_request_body>"). Recompute it over the raw body and compare in constant time. Optionally reject if now − t exceeds a tolerance to guard against replay.
// Node.js (Express: use express.raw so you get the exact bytes)
const crypto = require('crypto');
function verify(secret, header, rawBody) {
const [tp, vp] = header.split(','); // "t=...", "v1=..."
const t = tp.split('=')[1], v1 = vp.split('=')[1];
const expected = crypto.createHmac('sha256', secret)
.update(`${t}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}
// PHP
function torodocs_verify(string $secret, string $header, string $rawBody): bool {
[$tp, $vp] = explode(',', $header);
$t = explode('=', $tp)[1];
$v1 = explode('=', $vp)[1];
$expected = hash_hmac('sha256', $t.'.'.$rawBody, $secret);
return hash_equals($expected, $v1);
}
Retries & reliability. A non-2xx or timeout is retried with exponential backoff (roughly 10s, 30s, 120s … up to ~30m over 6 attempts). After 15 consecutive failed deliveries an endpoint is auto-disabled (re-enable it on the Developers page). Deliveries may repeat on retry; dedupe using X-ToroDocs-Delivery.
Errors return the { "error": { "code", "message" } } envelope (validation adds details).
| HTTP | code | Meaning |
|---|---|---|
| 401 | unauthenticated | Missing or invalid token. |
| 403 | api_not_enabled | Team's plan does not include this part of the API (for example, webhooks on Free, Personal or Business). |
| 403 | insufficient_scope | Token lacks the required ability (read/write). |
| 404 | not_found | Resource doesn't exist or isn't in your team. |
| 405 | method_not_allowed | Wrong HTTP method for the path. |
| 409 | not_ready | Sealed PDF requested before the envelope is sealed. |
| 409 | idempotency_conflict | Idempotency-Key reused with a different body. |
| 422 | validation_failed | Invalid request data (see details). |
| 422 | invalid_url | Webhook URL rejected (must be https; no private/reserved addresses). |
| 429 | rate_limited | Too many requests. Retry after Retry-After. |
| 500 | server_error | Something went wrong on our side. |
Import the machine-readable spec into Postman or Insomnia: /docs/openapi.json.
Questions? Contact us or read our e-signature guides.