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": null, "remaining": null },
"token": { "name": "Zapier integration", "abilities": ["read", "write"] }
}
}
allowance/remaining are null on Enterprise (unlimited).
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.
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 is not on Enterprise. |
| 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 visit the help center.