Developers

ToroDocs API reference

Send documents for signature, track their status, retrieve the sealed PDF, and receive real-time events, all over a clean REST API.

Introduction

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.

  • Base URL: https://torodocs.com/api/v1
  • Version: the API is versioned in the path (/v1).
  • Core API (create and send envelopes, check status, download sealed PDFs): included on every plan, including Free. Your plan's monthly document allowance still applies.
  • Webhooks (the /webhooks endpoints and signed event delivery): Enterprise and the pay-as-you-go Developer plan.

Authentication

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.

Conventions

Response envelope

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": { … } } }

Pagination

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.

Rate limiting

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.

Idempotency

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.

The field key

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.

Signature, initials, and checkbox fields have 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".

Quickstart: send your first envelope

  1. Create a read-write API key on the Developers page.
  2. GET /templates/{id} to see each role's fields and their keys.
  3. POST /envelopes with a recipient per role, optionally prefilling fields by key.
  4. Redirect your user to the returned signing_url, or wait for the envelope.sent / envelope.completed webhooks.

GET /me

GET /me read

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.

GET /templates

GET /templates read

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 }
}

GET /templates/{id}

GET /templates/{id} read

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 } ] }
    ]
  }
}

GET /envelopes

GET /envelopes read

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 }
}

GET /envelopes/{id}

GET /envelopes/{id} read

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"
    }
  }
}

GET /envelopes/{id}/recipients

GET /envelopes/{id}/recipients read

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" }
  ]
}

GET /envelopes/{id}/download

GET /envelopes/{id}/download read

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

POST /envelopes

POST /envelopes read-write

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.

Webhook endpoints (API)

Available on Enterprise and the Developer plan.

GET /webhooks read-write
POST /webhooks read-write
DELETE /webhooks/{id} read-write
POST /webhooks/{id}/test read-write

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"
  }
}

Webhooks

Subscribe endpoints to events and ToroDocs will POST a signed JSON payload as documents move through their lifecycle. Event types:

EventFires when…
envelope.sentan envelope is sent
recipient.vieweda recipient opens the signing page (first view)
recipient.signeda recipient completes signing
envelope.completedall recipients have signed
envelope.sealedthe sealed PDF is ready (payload carries sealed.sha256 + download_url)
envelope.declineda recipient declines
envelope.expiredan envelope passes its expiry
envelope.voidedthe 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…

Verifying signatures

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

Errors return the { "error": { "code", "message" } } envelope (validation adds details).

HTTPcodeMeaning
401unauthenticatedMissing or invalid token.
403api_not_enabledTeam's plan does not include this part of the API (for example, webhooks on Free, Personal or Business).
403insufficient_scopeToken lacks the required ability (read/write).
404not_foundResource doesn't exist or isn't in your team.
405method_not_allowedWrong HTTP method for the path.
409not_readySealed PDF requested before the envelope is sealed.
409idempotency_conflictIdempotency-Key reused with a different body.
422validation_failedInvalid request data (see details).
422invalid_urlWebhook URL rejected (must be https; no private/reserved addresses).
429rate_limitedToo many requests. Retry after Retry-After.
500server_errorSomething went wrong on our side.

OpenAPI

Import the machine-readable spec into Postman or Insomnia: /docs/openapi.json.


Questions? Contact us or read our e-signature guides.