Official Ruby SDK for TurboDocx
The most developer-friendly DocuSign & PandaDoc alternative for e-signatures and document generation. Send documents for signature and automate document workflows programmatically.
Have an AI coding agent (Claude Code, Cursor, Copilot, Codex, Gemini CLI, OpenCode) install this SDK, configure your env, write working route handlers, and wire them into your app:
npx skills add TurboDocx/quickstartThen run /turbodocx-sdk inside your agent — or one of the focused shortcuts:
| Shortcut | What it scaffolds |
|---|---|
/turbodocx-sdk turbosign |
Send documents for e-signature, check status, download signed PDF |
/turbodocx-sdk deliverable |
Generate documents from templates with variable substitution |
/turbodocx-sdk turbopartner |
Provision and manage customer organizations (partner accounts) |
/turbodocx-sdk turbowebhooks |
Subscribe to signature.document.completed events + verify HMAC |
/turbodocx-sdk turboquote |
Build and send sales quotes with line items, bundles, and price books |
The skill auto-detects your framework (Rails, Sinatra, Rack, …) and follows your existing project conventions. Source: github.com/TurboDocx/quickstart.
A modern, developer-first alternative to legacy e-signature platforms:
| Looking for... | TurboDocx offers |
|---|---|
| DocuSign API alternative | Simple REST API, transparent pricing |
| PandaDoc alternative | Document generation + e-signatures in one SDK |
| HelloSign/Dropbox Sign alternative | Full API access, modern DX |
| Adobe Sign alternative | Quick integration, developer-friendly docs |
| SignNow alternative | Predictable costs, responsive support |
| Documint alternative | DOCX/PDF generation from templates |
| WebMerge alternative | Data-driven document automation |
Other platforms we compare to: SignRequest, SignEasy, Zoho Sign, Eversign, SignWell, Formstack Documents
| Package | Description |
|---|---|
| @turbodocx/html-to-docx | Convert HTML to DOCX - fastest JS library |
| @turbodocx/n8n-nodes-turbodocx | n8n community nodes for TurboDocx |
| TurboDocx Writer | Microsoft Word add-in |
- Production-Ready — Battle-tested, processing thousands of documents daily
- Zero runtime dependencies — Pure Ruby stdlib (
net/http,json,openssl); nothing else to install - Idiomatic Ruby — snake_case methods, keyword arguments, typed exception hierarchy
- Static class pattern —
configureonce, then call class methods; no instantiation required - Five modules — TurboSign (e-signatures), Deliverable (document generation), TurboPartner (partner portal), TurboWebhooks (signature webhook), TurboQuote (CPQ)
Add to your Gemfile:
gem "turbodocx-sdk"Then run:
bundle installOr install directly:
gem install turbodocx-sdkLet an AI coding agent set up this SDK for you with the TurboDocx Quickstart Agent Skill:
npx skills add TurboDocx/quickstartWorks with Claude Code, GitHub Copilot, Cursor, OpenCode, and other AI coding agents. The skill detects your language, installs the gem, and generates working integration code.
require "turbodocx_sdk"
# 1. Configure with your API key and sender information
TurboDocxSdk::TurboSign.configure(
api_key: ENV["TURBODOCX_API_KEY"],
org_id: ENV["TURBODOCX_ORG_ID"],
sender_email: ENV["TURBODOCX_SENDER_EMAIL"], # REQUIRED
sender_name: ENV["TURBODOCX_SENDER_NAME"] # OPTIONAL
)
# 2. Send a document for signature
result = TurboDocxSdk::TurboSign.send_signature(
# Request-hash keys are camelCase — the SDK forwards the hash to the API
# as-is and does NOT convert snake_case. (Only method keyword args like
# configure(api_key:, sender_email:) are snake_case.)
fileLink: "https://example.com/contract.pdf",
documentName: "Partnership Agreement",
recipients: [
{ name: "John Doe", email: "john@example.com", signingOrder: 1 }
],
fields: [
{
type: "signature",
recipientEmail: "john@example.com",
template: { anchor: "{signature1}", placement: "replace",
size: { width: 100, height: 30 } }
}
]
)
puts "Document ID: #{result['documentId']}"require "turbodocx_sdk"
TurboDocxSdk::TurboSign.configure(
api_key: "your-api-key", # REQUIRED
org_id: "your-org-id", # REQUIRED
sender_email: "you@company.com", # REQUIRED — reply-to for signature emails
sender_name: "Your Company" # OPTIONAL but strongly recommended
)Important: sender_email is REQUIRED. It is used as the reply-to address for signature request emails and recorded as the sender in the audit trail. An API key has no mailbox of its own, so the API rejects a send without it rather than mailing from an unmonitored address. sender_name is optional — it defaults to the name of your API key.
# .env
TURBODOCX_API_KEY=your-api-key
TURBODOCX_ORG_ID=your-org-id
TURBODOCX_SENDER_EMAIL=you@company.com
TURBODOCX_SENDER_NAME=Your Company NameConfigure the TurboSign module. sender_email is required.
Upload a document for review without sending signature emails. Returns a preview URL you can share manually. Accepts the same request hash as send_signature (file, fileLink, deliverableId, or templateId as document source).
result = TurboDocxSdk::TurboSign.create_signature_review_link(
fileLink: "https://example.com/contract.pdf",
recipients: [
{ name: "John Doe", email: "john@example.com", signingOrder: 1 }
],
fields: [
{ type: "signature", page: 1, x: 100, y: 500, width: 200, height: 50, recipientEmail: "john@example.com" }
],
documentName: "Service Agreement", # Optional
documentDescription: "Q4 Contract", # Optional
ccEmails: ["legal@acme.com"] # Optional
)
puts "Preview URL: #{result['previewUrl']}"
puts "Document ID: #{result['documentId']}"Upload a document and immediately send signature request emails. The request hash uses camelCase keys (fileLink, documentName, and inner recipients/fields keys like signingOrder/recipientEmail) — the SDK forwards them to the API unchanged and does not convert snake_case. Fields bind to a recipient via recipientEmail.
result = TurboDocxSdk::TurboSign.send_signature(
fileLink: "https://example.com/contract.pdf",
recipients: [
{ name: "Alice", email: "alice@example.com", signingOrder: 1 },
{ name: "Bob", email: "bob@example.com", signingOrder: 2 }
],
fields: [
{ type: "signature", page: 1, x: 100, y: 500, width: 200, height: 50, recipientEmail: "alice@example.com" },
{ type: "signature", page: 1, x: 100, y: 600, width: 200, height: 50, recipientEmail: "bob@example.com" }
]
)
# The created recipients come back on the send result — each carries id, name and email.
# Signing links are emailed to them; they are not returned here.
result["recipients"].each do |r|
puts "#{r['name']} <#{r['email']}> — #{r['id']}"
end
# For signing progress afterwards, use get_recipients.The document source can also be raw bytes or a local file path (file: File.binread("contract.pdf") or file: "contract.pdf" — the file type is detected from magic bytes), a TurboDocx deliverable (deliverableId:), or a TurboSign template (templateId:).
Send a standalone reminder to whoever's turn it is to sign. It is independent of the automatic reminder cadence — it works even when reminders are disabled or the cap is spent, does not consume that cap, and only emails signers at the CURRENT signing order. Omit the recipient ids to remind everyone eligible; do not pass an empty array, which the API rejects.
result = TurboDocxSdk::TurboSign.send_reminder("doc-uuid")
result["results"].each do |r|
# status is "sent", or a "skipped_*" reason (e.g. "skipped_wrong_order")
puts "#{r['recipientId']}: #{r['status']}"
endReminders and expiration can also be scheduled when you send. send_signature accepts optional
remindersEnabled / reminderDelay / reminderInterval / maxReminders and expirationEnabled
/ expireAfter / expirationWarning / expirationWarningInterval options — both features are
off by default, so omitting them preserves the original send behavior. These are camelCase like
every other send key, and each duration takes a { value:, unit: } shape. The deadline is frozen
onto the document at send time and is then readable via get_status(...)["expiresAt"].
TurboDocxSdk::TurboSign.send_signature(
fileLink: "https://example.com/contract.pdf",
remindersEnabled: true,
reminderDelay: { value: 3, unit: "days" },
expirationEnabled: true,
expireAfter: { value: 14, unit: "days" }
# ...recipients, fields, etc.
)Check the document-level status. For per-recipient detail, use get_recipients.
status = TurboDocxSdk::TurboSign.get_status("doc-uuid")
puts "Status: #{status['status']}" # "under_review" | "completed" | "voided" | "expired" | ...
# "expiresAt" is the signing-window deadline (ISO 8601), or nil when expiration is off.
puts "Expires: #{status['expiresAt'] || 'never'}"See who the document went to, who has signed, who you are still waiting on, and who sent it.
result = TurboDocxSdk::TurboSign.get_recipients("doc-uuid")
sender = result["document"]["sentBy"]
puts "Sent by #{sender['name']} <#{sender['email']}>"
summary = result["summary"]
puts "#{summary['completed']} of #{summary['total']} signed, waiting on #{summary['waitingOn']}"
result["recipients"].each do |r|
# "pending" | "viewed" | "completed" | "voided" | "expired"
puts "#{r['name']} <#{r['email']}>: #{r['effectiveStatus']}"
puts " signed #{r['signedOn']}" if r["signedOn"]
puts " emailed #{r['delivery']['totalSent']}x"
end
# Who are we chasing?
chasing = result["recipients"].select { |r| %w[pending viewed].include?(r["effectiveStatus"]) }Two status fields, and they differ on purpose:
| Field | Values | Use it for |
|---|---|---|
status |
pending, viewed, completed |
The raw database value |
effectiveStatus |
pending, viewed, completed, voided, expired |
Display |
The database has no per-recipient declined/voided/expired state, so on a voided or expired
document an unsigned signer still reads pending in status. effectiveStatus layers the
document's outcome on top — that's the one to show a user. A completed signature is never
revoked: someone who signed before the document was voided still reads completed.
summary counts by effectiveStatus, and waitingOn (pending + viewed) drops to zero once
the document is terminal.
delivery is that recipient's email history — firstSentOn, lastSentOn, totalSent,
reminderCount, lastRemindedAt, warningCount, lastWarningAt. It counts the signature
request, resends, reminders, expiry warnings and terminal notices. CC notifications are
excluded, since a CC address is not a signer.
Two delivery fields are easy to misread:
| Field | What it actually means |
|---|---|
reminderCount |
Automatic (scheduled) reminders only — the counter maxReminders caps. A manual "remind now" does not increment it (it must not consume the cap budget), though it does land in totalSent. So it can read 0 while reminder emails have genuinely been sent. |
lastRemindedAt |
When the reminder cadence clock was last reset — not necessarily when a reminder was sent. The initial signature-request send, each scheduled reminder, each manual "remind now" and each expiry warning all stamp it. A freshly-sent document therefore normally reads a non-null lastRemindedAt alongside reminderCount of 0. |
warningCount and lastWarningAt are touched only by an expiry warning.
Download the signed document as raw bytes.
pdf_bytes = TurboDocxSdk::TurboSign.download("doc-uuid")
File.binwrite("signed-contract.pdf", pdf_bytes)Cancel a signature request.
TurboDocxSdk::TurboSign.void_document("doc-uuid", "Contract terms changed")Resend signature emails to specific recipients.
TurboDocxSdk::TurboSign.resend_email("doc-uuid", ["recipient-uuid-1"])Get the complete audit trail for a document, including all events and timestamps.
audit = TurboDocxSdk::TurboSign.get_audit_trail("doc-uuid")
puts "Document: #{audit['document']['name']}"
audit["auditTrail"].each do |entry|
puts "#{entry['actionType']} at #{entry['timestamp']}"
puts " By: #{entry['user']['name']} (#{entry['user']['email']})" if entry["user"]
puts " Recipient: #{entry['recipient']['name']}" if entry["recipient"]
endThe audit trail includes a cryptographic hash chain for tamper-evidence verification.
The Deliverable module generates documents from TurboDocx templates with variable substitution, and manages/downloads the resulting deliverables.
require "turbodocx_sdk"
TurboDocxSdk::Deliverable.configure(
api_key: ENV["TURBODOCX_API_KEY"], # REQUIRED (or access_token)
org_id: ENV["TURBODOCX_ORG_ID"] # REQUIRED
# base_url: "https://api.turbodocx.com" # optional
)Deliverable does not require sender_email — it never sends signature emails.
Generate a new document from a template with variable substitution. Request-hash keys are camelCase (templateId).
result = TurboDocxSdk::Deliverable.generate_deliverable(
"templateId" => "template-uuid",
"name" => "Q3 Statement of Work",
"variables" => [
{ "placeholder" => "{ClientName}", "text" => "Acme Corp" },
{ "placeholder" => "{StartDate}", "text" => "2026-08-01" }
],
"description" => "SOW for Acme", # Optional
"tags" => ["sow", "acme"] # Optional
)
deliverable = result["results"]["deliverable"]
puts "Deliverable ID: #{deliverable['id']}"List deliverables with pagination, search, and tag filtering.
page = TurboDocxSdk::Deliverable.list_deliverables(limit: 25, offset: 0, query: "sow", show_tags: true)
puts "Total: #{page['totalRecords']}"
page["results"].each { |d| puts d["name"] }Get full details of a single deliverable, including variables, fonts, and template info.
deliverable = TurboDocxSdk::Deliverable.get_deliverable_details("deliverable-uuid", show_tags: true)
puts deliverable["name"]Update a deliverable's name, description, or tags. Note: when tags is provided, all existing tags are replaced.
TurboDocxSdk::Deliverable.update_deliverable_info("deliverable-uuid",
"name" => "Q3 SOW (final)",
"description" => "Signed-off version"
)Soft-delete a deliverable.
TurboDocxSdk::Deliverable.delete_deliverable("deliverable-uuid")Download the original source file (DOCX or PPTX) as raw bytes.
docx_bytes = TurboDocxSdk::Deliverable.download_source_file("deliverable-uuid")
File.binwrite("sow.docx", docx_bytes)Download the PDF version as raw bytes.
pdf_bytes = TurboDocxSdk::Deliverable.download_pdf("deliverable-uuid")
File.binwrite("sow.pdf", pdf_bytes)Deliverable pairs naturally with TurboSign — generate a document, then send it without downloading:
result = TurboDocxSdk::Deliverable.generate_deliverable(
"templateId" => "template-uuid",
"name" => "Consulting Agreement",
"variables" => [{ "placeholder" => "{ClientName}", "text" => "Acme Corp" }]
)
TurboDocxSdk::TurboSign.send_signature(
deliverableId: result["results"]["deliverable"]["id"],
recipients: [{ name: "John Doe", email: "john@example.com", signingOrder: 1 }],
fields: [
{ type: "signature", recipientEmail: "john@example.com",
template: { anchor: "{signature1}", placement: "replace", size: { width: 100, height: 30 } } }
]
)The TurboPartner module provides partner portal operations for managing organizations, users, API keys, and audit logs.
require "turbodocx_sdk"
TurboDocxSdk::TurboPartner.configure(
partner_api_key: ENV["TURBODOCX_PARTNER_API_KEY"], # Must start with TDXP-
partner_id: ENV["TURBODOCX_PARTNER_ID"] # UUID format
)Environment Variables:
TURBODOCX_PARTNER_API_KEY=TDXP-your-partner-api-key
TURBODOCX_PARTNER_ID=your-partner-uuid# Create an organization
org = TurboDocxSdk::TurboPartner.create_organization(
name: "Acme Corp",
features: { maxUsers: 50, hasTDAI: true }
)
puts "Org ID: #{org['data']['id']}"
# List organizations
orgs = TurboDocxSdk::TurboPartner.list_organizations(limit: 10, search: "acme")
# Get organization details (includes features + usage tracking)
details = TurboDocxSdk::TurboPartner.get_organization_details("org-uuid")
# Update organization
TurboDocxSdk::TurboPartner.update_organization_info("org-uuid", name: "New Name")
# Delete organization
TurboDocxSdk::TurboPartner.delete_organization("org-uuid")
# Update entitlements
TurboDocxSdk::TurboPartner.update_organization_entitlements("org-uuid",
features: { maxUsers: 100, hasTDAI: true }
)
# Read the org's TurboSign display preferences
# (returns only the partner-settable keys, with defaults applied)
prefs = TurboDocxSdk::TurboPartner.get_organization_preferences("org-uuid")["data"]["preferences"]
prefs["lockedFieldsBackground"] # => true by default
# Update them — pass only the keys you want to change; every other organization
# setting is preserved. Hash keys stay camelCase: they are the API contract, not
# Ruby names, and a snake_case key is dropped by the backend allowlist.
TurboDocxSdk::TurboPartner.update_organization_preferences("org-uuid",
"lockedFieldsBackground" => false
)Partner-settable display preferences
| Key | Default | Effect |
|---|---|---|
hideSignatureOutline |
false |
Hide the outline/label drawn around signed fields |
hideSignatureHash |
false |
Hide the verification hash printed on signed fields |
lockedFieldsBackground |
true |
Grey box behind locked fields (false = plain text) |
allowDownloadBeforeSigning |
false |
When enabled, a signer can download the unsigned PDF from the signing page before they sign it (for example, to review it with their legal team). Defaults to off. |
# List users
users = TurboDocxSdk::TurboPartner.list_organization_users("org-uuid")
# Add a user
TurboDocxSdk::TurboPartner.add_user_to_organization("org-uuid",
email: "user@example.com", role: "contributor"
)
# Update role
TurboDocxSdk::TurboPartner.update_organization_user_role("org-uuid", "user-uuid",
role: "admin"
)
# Remove user
TurboDocxSdk::TurboPartner.remove_user_from_organization("org-uuid", "user-uuid")
# Resend the org invitation email
TurboDocxSdk::TurboPartner.resend_organization_invitation_to_user("org-uuid", "user-uuid")# List keys
keys = TurboDocxSdk::TurboPartner.list_organization_api_keys("org-uuid")
# Create key (full value returned only on creation)
key = TurboDocxSdk::TurboPartner.create_organization_api_key("org-uuid",
name: "Production Key", role: "admin"
)
puts "Key: #{key['data']['key']}" # TDX-... (only shown once)
# Update key
TurboDocxSdk::TurboPartner.update_organization_api_key("org-uuid", "key-uuid", name: "Renamed Key")
# Revoke key
TurboDocxSdk::TurboPartner.revoke_organization_api_key("org-uuid", "key-uuid")# Create a scoped partner API key (full key value returned only on creation)
key = TurboDocxSdk::TurboPartner.create_partner_api_key(
name: "Read-Only Key",
scopes: ["org:read", "audit:read"]
)
# List / update / revoke partner API keys
TurboDocxSdk::TurboPartner.list_partner_api_keys(limit: 10)
TurboDocxSdk::TurboPartner.update_partner_api_key("key-uuid", name: "Renamed")
TurboDocxSdk::TurboPartner.revoke_partner_api_key("key-uuid")
# Add a user to the partner portal.
# `permissions` is all-or-nothing: every one of the seven keys must be present.
# A partial permissions hash is rejected with a 400.
TurboDocxSdk::TurboPartner.add_user_to_partner_portal(
email: "ops@company.com",
role: "member", # partner roles: "admin" | "member" | "viewer"
permissions: {
canManageOrgs: true,
canManageOrgUsers: true,
canManagePartnerUsers: false,
canManageOrgAPIKeys: false,
canManagePartnerAPIKeys: false,
canUpdateEntitlements: false,
canViewAuditLogs: true
}
)
# List / update / remove / re-invite partner portal users
TurboDocxSdk::TurboPartner.list_partner_portal_users(search: "ops")
# The `permissions` object is optional on update — but if you send it, send all
# seven keys. There is no partial permissions update.
TurboDocxSdk::TurboPartner.update_partner_user_permissions("user-uuid",
permissions: {
canManageOrgs: false,
canManageOrgUsers: true,
canManagePartnerUsers: false,
canManageOrgAPIKeys: false,
canManagePartnerAPIKeys: false,
canUpdateEntitlements: false,
canViewAuditLogs: true
}
)
TurboDocxSdk::TurboPartner.remove_user_from_partner_portal("user-uuid")
TurboDocxSdk::TurboPartner.resend_partner_portal_invitation_to_user("user-uuid")Partner permissions — all seven keys are required whenever permissions is sent (booleans):
| Key | Grants |
|---|---|
canManageOrgs |
Create / update / delete organizations |
canManageOrgUsers |
Manage users inside organizations |
canManagePartnerUsers |
Manage partner portal users |
canManageOrgAPIKeys |
Manage organization API keys |
canManagePartnerAPIKeys |
Manage partner API keys |
canUpdateEntitlements |
Update organization entitlements |
canViewAuditLogs |
Read the partner audit log |
Role enums are different for orgs and partners — do not mix them:
| Scope | Valid roles |
|---|---|
| Org users, org API keys | admin, contributor, user, viewer |
| Partner portal users | admin, member, viewer |
logs = TurboDocxSdk::TurboPartner.get_partner_audit_logs(
action: "org.created",
startDate: "2025-01-01",
endDate: "2025-12-31",
limit: 100
)
logs["data"]["results"].each do |entry|
puts "#{entry['action']} - #{entry['createdOn']}"
end| Category | Methods |
|---|---|
| Organizations | create_organization, list_organizations, get_organization_details, update_organization_info, delete_organization, update_organization_entitlements |
| Org display preferences | get_organization_preferences, update_organization_preferences |
| Org Users | add_user_to_organization, list_organization_users, update_organization_user_role, remove_user_from_organization, resend_organization_invitation_to_user |
| Org API Keys | create_organization_api_key, list_organization_api_keys, update_organization_api_key, revoke_organization_api_key |
| Partner API Keys | create_partner_api_key, list_partner_api_keys, update_partner_api_key, revoke_partner_api_key |
| Partner Users | add_user_to_partner_portal, list_partner_portal_users, update_partner_user_permissions, remove_user_from_partner_portal, resend_partner_portal_invitation_to_user |
| Audit Logs | get_partner_audit_logs |
The TurboWebhooks module manages your organization's signature webhook — a single subscription to TurboDocx signature events. It also pairs with the TurboDocxSdk.verify_webhook_signature helper for incoming webhook receivers.
One webhook per org. The SDK manages a single fixed-name webhook (
signature) per org so SDK-managed and UI-managed webhooks stay in sync — what you create here also appears in the dashboard's Signature Webhooks settings page. To manage multiple webhooks per org, call the REST API directly.Requires administrator role. All webhook routes require an admin TDX- API key.
Use the TurboDocxSdk::WebhookEvent constants instead of hand-writing the wire strings — a typo becomes a NameError rather than a webhook that silently never fires.
| Event | Constant | Fires when |
|---|---|---|
signature.document.sent |
WebhookEvent::SENT |
The document is dispatched to recipients |
signature.document.viewed |
WebhookEvent::VIEWED |
A recipient opens the document for the first time |
signature.document.recipient_signed |
WebhookEvent::RECIPIENT_SIGNED |
Any individual signer completes their signature — fires once per signer, and carries is_final_signer + remaining_signers |
signature.document.signed |
WebhookEvent::SIGNED |
A signer signs but the document is not yet complete (document-level partial progress) |
signature.document.completed |
WebhookEvent::COMPLETED |
All recipients have signed and the signed PDF is finalized |
signature.document.finalization_failed |
WebhookEvent::FINALIZATION_FAILED |
The signed PDF fails to finalize (e.g. a KMS signing error); the document is not completed |
signature.document.voided |
WebhookEvent::VOIDED |
The document is voided or cancelled |
On every signature, recipient_signed fires first, then exactly one document-level event:
Recipient signs
│
├─ signature.document.recipient_signed (always — one per signer)
│
└─ more signers remaining?
├─ yes → signature.document.signed (partial progress)
└─ no → signature.document.completed (finalized OK)
or signature.document.finalization_failed (finalization failed)
signednever fires on the final signature. To detect "the whole document is done", subscribe tocompleted(or torecipient_signedand checkis_final_signer: true) — notsigned.A single-signer document never emits
signedat all. It emitsrecipient_signed(withis_final_signer: true), thencompleted.
TurboDocxSdk::WebhookEvent::ALL is a frozen array of all 7 wire strings if you want to subscribe to everything. events: stays an array of plain strings, so the backend can add events without a gem release.
require "turbodocx_sdk"
TurboDocxSdk::TurboWebhooks.configure(
api_key: ENV["TURBODOCX_API_KEY"],
org_id: ENV["TURBODOCX_ORG_ID"]
# base_url: "https://api.turbodocx.com" # optional
)Unlike TurboSign, TurboWebhooks does NOT require sender_email — webhook routes don't send signature emails.
Events = TurboDocxSdk::WebhookEvent
created = TurboDocxSdk::TurboWebhooks.create_webhook(
urls: ["https://your-server.example.com/webhooks/turbodocx"], # HTTPS only, 1-10 urls
events: [
Events::SENT,
Events::VIEWED,
Events::RECIPIENT_SIGNED,
Events::COMPLETED,
Events::VOIDED
]
# ...or subscribe to everything: events: TurboDocxSdk::WebhookEvent::ALL
)
# `secret` is shown ONCE here. Store it securely; it cannot be retrieved later.
puts "Save this secret: #{created['secret']}"If the signature webhook already exists, create_webhook raises TurboDocxSdk::ConflictError (409). Either update the existing one with update_webhook or delete_webhook first.
webhook = TurboDocxSdk::TurboWebhooks.get_webhook
# webhook["deliveryStats"] and webhook["availableEvents"] are included
# Only the keywords you pass are sent; the rest are left untouched.
TurboDocxSdk::TurboWebhooks.update_webhook(is_active: false)
TurboDocxSdk::TurboWebhooks.update_webhook(
urls: ["https://new-endpoint.example.com/webhooks/turbodocx"],
events: ["signature.document.completed"]
)
TurboDocxSdk::TurboWebhooks.delete_webhook
urlsandeventscan never be empty. They staymin(1)on the backend even on update, sourls: []is a 400, not a "clear this field" instruction — and there is no way to remove every URL from a webhook.update_webhookraisesTurboDocxSdk::ValidationErrorbefore hitting the wire if you pass an empty ornilarray. To leave a field unchanged, just omit the keyword.urlsaccepts 1-10 entries.
tested = TurboDocxSdk::TurboWebhooks.test_webhook(
event_type: "signature.document.completed",
payload: { "documentId" => "doc-xyz", "status" => "completed" }
)
puts tested["summary"] # { "total" => ..., "successful" => ..., "failed" => ... }
deliveries = TurboDocxSdk::TurboWebhooks.list_webhook_deliveries(limit: 10)
replayed = TurboDocxSdk::TurboWebhooks.replay_webhook_delivery(deliveries["results"][0]["id"])list_webhook_deliveries accepts limit:, offset:, event_type:, is_delivered:, and http_status: filters. notify_webhook(event_type:, payload:) is also available and currently behaves identically to test_webhook — prefer test_webhook in new code.
rotated = TurboDocxSdk::TurboWebhooks.regenerate_webhook_secret
# rotated["secret"] is the new secret. Old signatures will fail immediately.stats = TurboDocxSdk::TurboWebhooks.get_webhook_stats(days: 30)
puts stats["summary"]["successRate"]
puts stats["eventBreakdown"]Webhook deliveries from TurboDocx are signed with HMAC-SHA256 over "#{timestamp}.#{raw_body}" using your webhook secret. Use the free module function TurboDocxSdk.verify_webhook_signature in your receiver:
# In your Rack/Rails webhook handler:
# CRITICAL: read the RAW body bytes. Do NOT parse-then-reserialize JSON first —
# the signature is over the exact bytes; re-serialization breaks verification.
raw_body = request.body.read
signature = request.get_header("HTTP_X_TURBODOCX_SIGNATURE") || ""
timestamp = request.get_header("HTTP_X_TURBODOCX_TIMESTAMP") || ""
secret = ENV["TURBODOCX_WEBHOOK_SECRET"]
unless TurboDocxSdk.verify_webhook_signature(
payload: raw_body,
signature_header: signature,
timestamp_header: timestamp,
secret: secret
)
return [401, {}, ["invalid signature"]]
end
event = JSON.parse(raw_body)
# ... process event ...
[200, {}, ["ok"]]By default the helper enforces a 300-second timestamp tolerance to prevent replay attacks. Override with tolerance_seconds: N (0 disables the check — not recommended in production). Comparison uses constant-time equality.
| Category | Methods |
|---|---|
| Configuration | configure |
| Webhook CRUD | create_webhook, get_webhook, update_webhook, delete_webhook |
| Testing | test_webhook, notify_webhook |
| Secret | regenerate_webhook_secret |
| Deliveries | list_webhook_deliveries, replay_webhook_delivery, get_webhook_stats |
| Receiver helper | TurboDocxSdk.verify_webhook_signature (free module function — no API key needed) |
The TurboQuote module provides end-to-end sales quoting: manage your product catalog (products, bundles, price books), companies and contacts, and assemble/send quotes with line items.
require "turbodocx_sdk"
TurboDocxSdk::TurboQuote.configure(
api_key: ENV["TURBODOCX_API_KEY"], # REQUIRED (or access_token)
org_id: ENV["TURBODOCX_ORG_ID"] # REQUIRED
# base_url: "https://api.turbodocx.com" # optional
)TurboQuote does not require sender_email — use api_key + org_id only.
| Group | Methods |
|---|---|
| Configuration | configure |
| Quotes | create_quote, get_quote, list_quotes, update_quote, delete_quote, duplicate_quote |
| Quote status transitions | send_quote, decline_quote, void_quote, handle_expired_quote |
| Quote extras | send_quote_with_deliverable, apply_price_book, remove_price_book, download_quote_pdf |
| Line items | add_line_items, add_bundle_line_items, list_line_items, update_line_item, remove_line_item |
| Products | create_product, bulk_create_products, get_product, list_products, update_product, delete_product, duplicate_product, get_product_primary_images |
| Bundles | create_bundle, bulk_create_bundles, get_bundle, list_bundles, update_bundle, delete_bundle, duplicate_bundle |
| Price books | create_price_book, bulk_create_price_books, get_price_book, list_price_books, update_price_book, delete_price_book, duplicate_price_book, list_price_book_products |
| Quote templates | get_template, get_template_by_id, list_templates, create_template, update_template, delete_template |
| Types / categories | create_type, bulk_create_types, list_types, update_type, delete_type |
| Companies | create_company, bulk_create_companies, get_company, list_companies, update_company, delete_company, list_company_contacts |
| Contacts | create_contact, bulk_create_contacts, list_contacts, update_contact, delete_contact |
| Quote number config (admin) | get_quote_number_config, update_quote_number_config |
| Convenience | create_and_send |
require "turbodocx_sdk"
TurboDocxSdk::TurboQuote.configure(
api_key: ENV["TURBODOCX_API_KEY"],
org_id: ENV["TURBODOCX_ORG_ID"]
)
# 1. Create the quote
quote = TurboDocxSdk::TurboQuote.create_quote(
"name" => "Professional Services Q3 2026",
"companyId" => "company-uuid",
"contactId" => "contact-uuid",
"currency" => "USD",
"termDays" => 90, # optional; defaults to 60, max 3650
"validUntil" => "2026-09-30"
)
puts "Quote number: #{quote['quoteNumber']}"
# 2. Add a product line item with a percent discount
# (single hash is auto-wrapped into an array; 1-50 items per call)
# productId, productName, unitPrice and billingFrequency are all REQUIRED.
# productId may be nil for an ad-hoc item, but the key must be present.
items = TurboDocxSdk::TurboQuote.add_line_items(quote["id"],
"productId" => "product-uuid",
"productName" => "Consulting Service",
"unitPrice" => 500,
"billingFrequency" => TurboDocxSdk::BillingFrequency::MONTHLY,
"quantity" => 3,
"discountType" => TurboDocxSdk::DiscountType::PERCENT,
"discountPercent" => 10
)
puts "Line item ID: #{items[0]['id']}"
# 3. Send the quote
sent = TurboDocxSdk::TurboQuote.send_quote(quote["id"])
puts sent["message"]send_quote and send_quote_with_deliverable accept the same eight optional
reminder/expiration keys as send_signature — remindersEnabled / reminderDelay /
reminderInterval / maxReminders and expirationEnabled / expireAfter /
expirationWarning / expirationWarningInterval. They're camelCase like every other send key,
each duration takes a { value:, unit: } shape, and they ride flat on the JSON body alongside
ccEmails / validUntil. Both features are off by default, so omitting them preserves the
original send behavior.
Quote expiry is pinned to validUntil. When expiration is on, expireAfter is ignored —
the quote already carries its own deadline — though expirationEnabled still toggles the
feature. The reminder and expiration-warning cadence applies (layered over your org defaults)
and must fit within validUntil, or the send is rejected.
TurboDocxSdk::TurboQuote.send_quote(quote["id"],
"ccEmails" => ["legal@acme.com"],
"validUntil" => "2026-09-30",
"remindersEnabled" => true,
"reminderDelay" => { "value" => 2, "unit" => "days" },
"reminderInterval" => { "value" => 3, "unit" => "days" },
"maxReminders" => 4,
"expirationEnabled" => true,
"expirationWarning" => { "value" => 2, "unit" => "days" }
)The quote's "Prepared by" name and email are resolved by the server, not by whoever downloads or sends the quote. Precedence: the org quote template's sender fields first, then the quote's creator.
A quote created with an API key has no mailbox of its own — so its sender email can only
come from the quote template. If your org's quote template has no sender email set,
create_quote (and duplicate_quote) raise ValidationError (400 SenderEmailRequired) for
an API-key caller. Set a sender email on the template once (via update_template) and every
subsequent create/duplicate/send resolves cleanly. Human (JWT) callers are never blocked — their
own email is the fallback.
get_quote returns the resolved identity under "preparedBy" — prefer it over "creator"
for any customer-facing display ("creator" may be the internal API service account). The
"email" key may be absent for an API-created quote:
quote = TurboDocxSdk::TurboQuote.get_quote(quote_id)
prepared = quote["preparedBy"] || {}
puts prepared["name"] # e.g. "Acme Billing Integration" or the template sender
puts prepared["email"] # may be nil — render a placeholdertermDays defaults to 60 and accepts up to 3650 (10 years). The special value -1 means auto-renewal, and it is the only case where renewalPeriod is allowed — in fact it is then required:
# Auto-renewing quote: termDays == -1 REQUIRES renewalPeriod
TurboDocxSdk::TurboQuote.create_quote(
"name" => "Managed Services (auto-renew)",
"companyId" => "company-uuid",
"contactId" => "contact-uuid",
"termDays" => -1,
"renewalPeriod" => TurboDocxSdk::RenewalPeriod::MONTHLY # weekly | monthly | quarterly | annually
)For any other termDays, renewalPeriod must be omitted (or nil) — sending it is a 400.
handle_expired_quote voids or declines an expired sent quote and creates a duplicate carrying the new validity date. All three keys are required, and action accepts only "void" or "decline":
replacement = TurboDocxSdk::TurboQuote.handle_expired_quote(quote["id"],
"action" => "void", # "void" or "decline" — nothing else is accepted
"reason" => "Pricing refreshed for Q4", # REQUIRED, max 190 chars
"newValidUntil" => "2026-12-31" # REQUIRED, ISO 8601
)
puts replacement["quoteNumber"] # the new duplicate quotepdf_bytes = TurboDocxSdk::TurboQuote.download_quote_pdf(quote["id"])
File.binwrite("quote.pdf", pdf_bytes)result = TurboDocxSdk::TurboQuote.create_and_send(
"name" => "Enterprise License",
"companyId" => "company-uuid",
"contactId" => "contact-uuid",
"items" => [
{ "productId" => "product-uuid", "productName" => "Platform License",
"unitPrice" => 1200, "quantity" => 5, "billingFrequency" => "annual" }
]
# "bundleItems" => [...], # optional bundle line items
# "send" => {...} # optional send options
)
puts result["quote"]["status"] # "sent"Six catalog resources support bulk creation: bulk_create_products, bulk_create_bundles, bulk_create_price_books, bulk_create_companies, bulk_create_contacts, and bulk_create_types. Each takes an array of row hashes (same field shapes as the corresponding single create_* method); the SDK wraps them in the { "rows" => [...] } envelope the API expects.
# Every product row requires name, categoryId, listPrice and billingFrequency.
# `categoryId` is a TurboQuote type UUID — there is no `categoryName` shorthand;
# resolve or create the category first (`list_types` / `create_type`) and pass its ID.
category = TurboDocxSdk::TurboQuote.create_type(
"name" => "Plans",
"categoryType" => TurboDocxSdk::CategoryType::PRODUCT_CATEGORY
)
report = TurboDocxSdk::TurboQuote.bulk_create_products([
{ "name" => "Basic Plan", "categoryId" => category["id"], "listPrice" => 10, "billingFrequency" => "monthly" },
{ "name" => "Premium Plan", "categoryId" => category["id"], "listPrice" => 100, "billingFrequency" => "monthly" }
])
puts "Imported: #{report['imported']}"
report["failed"].each do |f|
puts "Row #{f['row']} failed: #{f['reason']}" # "row" is 1-indexed
end
report["adjusted"].each do |a|
puts "Row #{a['row']} adjusted: #{a['reason']}" # server tweaked the row (e.g. defaults)
endBulk-create semantics:
- Partial success — a failed row does NOT raise and does NOT roll back earlier rows; it is reported in
"failed"with a 1-indexed"row"and a"reason". Server-adjusted rows appear in"adjusted". - 500-row cap per request — the API returns a 400 (
ValidationError) above the cap. The SDK does not validate rows or the cap client-side. - Roles — available to administrator and contributor API keys.
Read and update the org-wide quote-number format. Both methods return { "format" => {...}, "currentFloor" => Integer }.
config = TurboDocxSdk::TurboQuote.get_quote_number_config
puts config["format"]["prefix"]
puts config["currentFloor"]
# update_quote_number_config takes the FULL format hash — all keys required, camelCase
updated = TurboDocxSdk::TurboQuote.update_quote_number_config(
"prefix" => "Q",
"yearToken" => TurboDocxSdk::QuoteNumberYearToken::FOUR, # "none" | "two" | "four"
"monthToken" => TurboDocxSdk::QuoteNumberMonthToken::TWO, # "off" | "two"
"separator" => "-",
"padWidth" => 4, # 0-12
"suffix" => "",
"startNumber" => 1, # >= 0
"resetCadence" => TurboDocxSdk::QuoteNumberResetCadence::YEARLY # "never" | "yearly" | "monthly"
)Non-admin callers receive TurboDocxSdk::AuthorizationError.
| Type | Description |
|---|---|
signature |
Signature field (draw or type) |
initials |
Initials field |
text |
Free-form text input |
date |
Date stamp |
checkbox |
Checkbox / agreement |
full_name |
Full name |
first_name |
First name |
last_name |
Last name |
email |
Email address |
title |
Job title |
company |
Company name |
The checkbox type doubles as the controlling field for conditional logic (see below).
Any field hash may carry an optional "metadata" key that drives conditional logic. Set
metadata.fieldKey on a controlling checkbox to give it a stable id, then set
metadata.conditional on a dependent field that references that id. Field hashes are passed
through verbatim, so the keys stay camelCase:
"fields" => [
# Controlling checkbox — carries the fieldKey dependents reference
{
"type" => "checkbox",
"recipientEmail" => "john@example.com",
"template" => { "anchor" => "{request_changes}", "placement" => "replace", "size" => { "width" => 20, "height" => 20 } },
"metadata" => { "fieldKey" => "request_changes" }
},
# Dependent text field — hidden until the checkbox is checked ("If checked, explain")
{
"type" => "text",
"recipientEmail" => "john@example.com",
"isMultiline" => true,
"template" => { "anchor" => "{change_details}", "placement" => "replace", "size" => { "width" => 200, "height" => 50 } },
"metadata" => {
"conditional" => {
"controllingFieldKey" => "request_changes", # must equal the checkbox's fieldKey
"operator" => "is_checked", # "is_checked" | "is_not_checked"
"action" => "show" # "show" (hidden until met) | "unlock" (read-only until met)
}
}
}
]metadata key |
Set on | Meaning |
|---|---|---|
fieldKey |
controlling checkbox |
Stable client id (≤100 chars) that dependents reference |
conditional.controllingFieldKey |
dependent field | Must equal the controlling checkbox's fieldKey |
conditional.operator |
dependent field | "is_checked" or "is_not_checked" |
conditional.action |
dependent field | "show" (hidden until met) or "unlock" (visible but read-only until met) |
The SDK exposes idiomatic constants to avoid hard-coding string literals:
TurboDocxSdk::QuoteStatus::DRAFT # "draft"
TurboDocxSdk::QuoteStatus::SENT # "sent"
TurboDocxSdk::BillingFrequency::MONTHLY # "monthly"
TurboDocxSdk::BillingFrequency::ANNUAL # "annual"
TurboDocxSdk::DiscountType::PERCENT # "percent"
TurboDocxSdk::DiscountType::AMOUNT # "amount"
TurboDocxSdk::Currency::USD # "USD"
TurboDocxSdk::LineItemType::PRODUCT # "product"
TurboDocxSdk::CategoryType::PRODUCT_CATEGORY # "product_category"
# Quote number configuration
TurboDocxSdk::QuoteNumberYearToken::FOUR # "four"
TurboDocxSdk::QuoteNumberMonthToken::TWO # "two"
TurboDocxSdk::QuoteNumberResetCadence::NEVER # "never"
# Partner API key permission scopes
TurboDocxSdk::PartnerScope::ORG_READ # "org:read"
TurboDocxSdk::PartnerScope::AUDIT_READ # "audit:read"Each constants module also exposes an ALL array of its valid values (e.g. TurboDocxSdk::DiscountType::ALL).
For complete, working examples, see the examples/ directory:
turbosign_basic.rb— Send a document for signature, poll status, download the signed PDFturbowebhooks_crud.rb— Create/get/update/delete the signature webhook + verify HMAC signaturesturboquote_basic.rb— Full quote lifecycle: create company, quote, line items, download PDF, sendturboquote_products.rb— Products and bundles catalog managementturboquote_pricebooks.rb— Price books with per-product overrides, apply to quote
result = TurboDocxSdk::TurboSign.send_signature(
fileLink: "https://example.com/contract.pdf",
recipients: [
{ name: "Employee", email: "employee@company.com", signingOrder: 1 },
{ name: "Manager", email: "manager@company.com", signingOrder: 2 },
{ name: "HR", email: "hr@company.com", signingOrder: 3 }
],
fields: [
# Employee signs first
{ type: "signature", recipientEmail: "employee@company.com", page: 1, x: 100, y: 400, width: 200, height: 50 },
{ type: "date", recipientEmail: "employee@company.com", page: 1, x: 320, y: 400, width: 100, height: 30 },
# Manager signs second
{ type: "signature", recipientEmail: "manager@company.com", page: 1, x: 100, y: 500, width: 200, height: 50 },
# HR signs last
{ type: "signature", recipientEmail: "hr@company.com", page: 1, x: 100, y: 600, width: 200, height: 50 }
]
)def wait_for_completion(document_id, max_attempts: 60)
max_attempts.times do
status = TurboDocxSdk::TurboSign.get_status(document_id)
return TurboDocxSdk::TurboSign.download(document_id) if status["status"] == "completed"
raise "Document was voided" if status["status"] == "voided"
sleep 30
end
raise "Timeout waiting for signatures"
end# config/initializers/turbodocx.rb
require "turbodocx_sdk"
TurboDocxSdk::TurboSign.configure(
api_key: ENV["TURBODOCX_API_KEY"],
org_id: ENV["TURBODOCX_ORG_ID"],
sender_email: ENV["TURBODOCX_SENDER_EMAIL"]
)
# app/controllers/contracts_controller.rb
class ContractsController < ApplicationController
def create
result = TurboDocxSdk::TurboSign.send_signature(
fileLink: params[:pdf_url],
recipients: params[:recipients],
fields: params[:fields]
)
render json: { success: true, document_id: result["documentId"] }
rescue TurboDocxSdk::TurboDocxError => e
render json: { error: e.message }, status: e.status_code || 500
end
endrequire "sinatra"
require "json"
require "turbodocx_sdk"
TurboDocxSdk::TurboSign.configure(
api_key: ENV["TURBODOCX_API_KEY"],
org_id: ENV["TURBODOCX_ORG_ID"],
sender_email: ENV["TURBODOCX_SENDER_EMAIL"]
)
post "/api/send-contract" do
body = JSON.parse(request.body.read)
result = TurboDocxSdk::TurboSign.send_signature(
fileLink: body["pdf_url"],
recipients: body["recipients"],
fields: body["fields"]
)
json_response = { success: true, document_id: result["documentId"] }
content_type :json
json_response.to_json
endrequire "turbodocx_sdk"
begin
TurboDocxSdk::TurboSign.get_status("invalid-id")
rescue TurboDocxSdk::NotFoundError => e
puts "Not found: #{e.message}"
rescue TurboDocxSdk::AuthenticationError => e
puts "Auth failed (#{e.status_code}): #{e.message}"
rescue TurboDocxSdk::ValidationError => e
puts "Validation error: #{e.message}"
rescue TurboDocxSdk::RateLimitError
puts "Rate limited — slow down requests"
rescue TurboDocxSdk::TurboDocxError => e
puts "TurboDocx error (#{e.status_code}): #{e.message}"
end| Class | HTTP Status |
|---|---|
TurboDocxSdk::AuthenticationError |
401 |
TurboDocxSdk::AuthorizationError |
403 |
TurboDocxSdk::ValidationError |
400 |
TurboDocxSdk::NotFoundError |
404 |
TurboDocxSdk::ConflictError |
409 |
TurboDocxSdk::RateLimitError |
429 |
TurboDocxSdk::NetworkError |
(no status) |
TurboDocxSdk::TurboDocxError |
base class |
code is always populated — an API-supplied code when there is one, otherwise the error
class's default (VALIDATION_ERROR, AUTHENTICATION_ERROR, AUTHORIZATION_ERROR,
NOT_FOUND, CONFLICT, RATE_LIMIT_EXCEEDED, NETWORK_ERROR). Branch on it without a null
check.
The API also returns more specific codes, passed through unchanged:
| Code | Status | Meaning |
|---|---|---|
SenderEmailRequired |
400 | No sender email resolvable. TurboSign: set senderEmail on the request. TurboQuote: configure one on the org quote template (Quote Settings). |
SenderNameRequired |
400 | No sender name resolvable — the API key has no usable name. |
QuoteHasNoLineItems |
400 | The quote has no line items. Add at least one before sending. |
QuoteExpired |
400 | The quote is past its validUntil date. |
QuoteValidUntilRequired |
400 | The quote has no validUntil date set. |
QuoteNotSendable |
400 | Only draft quotes can be sent. |
QuoteContactRequired |
400 | The quote's contact is missing a name or email. |
QuoteCustomerInactive |
400 | The quote's company or contact was deleted or deactivated. |
Error messages carry the actionable reason, not a generic envelope — multiple field errors
are joined with "; ", e.g. "name" is not allowed to be empty; "companyId" must be a valid GUID.
- Ruby 2.7+
- Zero runtime dependencies — the SDK uses only the Ruby standard library (
net/http,json,openssl)
| Package | Description |
|---|---|
| @turbodocx/sdk (JavaScript) | JavaScript/TypeScript SDK |
| turbodocx-sdk (Python) | Python SDK |
| turbodocx (Go) | Go SDK |
| @turbodocx/n8n-nodes-turbodocx | n8n community nodes |
MIT — see LICENSE

