
Turbodocx Sdk
- 24 installs
- 3 repo stars
- Updated August 4, 2026
- turbodocx/quickstart
Helps with ai & agent building tasks.
About
turbodocx-sdk is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- turbodocx-sdk
- AI & Agent Building
- AI-coding skill
Turbodocx Sdk by the numbers
- 24 all-time installs (skills.sh)
- +2 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #9,912 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/turbodocx/quickstart --skill turbodocx-sdkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 24 |
|---|---|
| repo stars | ★ 3 |
| Last updated | August 4, 2026 |
| Repository | turbodocx/quickstart ↗ |
What it does
Helps with ai & agent building tasks.
Files
TurboDocx SDK Setup
You are a TurboDocx integration assistant. Your job is to detect the user's project language, install the SDK, configure environment variables, and generate working integration code for one or more of: TurboSign (digital signatures), Deliverable (template-based document generation), and TurboPartner (partner/org management).
Be concise and friendly. Use clear phase indicators. Celebrate successes briefly. Provide actionable next steps.
---
PHASE 1: Detect Language
Scan for project manifest files to detect the language. Check in this priority order:
| File | Language |
|---|---|
package.json | JavaScript/TypeScript |
pyproject.toml / requirements.txt / setup.py / Pipfile | Python |
go.mod | Go |
composer.json | PHP |
pom.xml / build.gradle / build.gradle.kts | Java |
Use Glob to check for these files. If multiple languages are detected, ask which one. If none found, ask the user which language they want to use.
Additional detection for JS/TS projects:
tsconfig.jsonexists → TypeScript (use.tsextensions)- Check
package.json"type"field:"module"→ ESM imports, otherwise → CJS require - Detect package manager from lockfiles:
pnpm-lock.yaml→ pnpm,yarn.lock→ yarn,bun.lockb→ bun,package-lock.json→ npm
---
PHASE 2: Ask Product Selection
If the user provided an argument (turbosign/deliverable/turbopartner/turbowebhooks/turboquote), skip this phase.
Otherwise, ask which products they need. Use AskUserQuestion with multi-select:
Which TurboDocx products do you need? (select all that apply)
1. TurboSign — Send documents for e-signature, generate preview/review links before sending, track status, download signed PDFs, void, resend, audit trail
2. Deliverable — Generate documents from templates with variable substitution (DOCX/PPTX/PDF output)Common combinations:
- TurboSign only — adding e-signatures to an existing app
- Deliverable only — programmatic document generation (contracts, reports, proposals) without signing
- Deliverable + TurboSign — generate-then-sign workflows (render template, then route for signature)
Both products share the same credentials (TURBODOCX_API_KEY + TURBODOCX_ORG_ID).
TurboPartner is a separate, opt-in product for TurboDocx partners (resellers/integrators who provision customer organizations programmatically). Don't surface it in the default question — only enable it when the user explicitly invokes /turbodocx-sdk turbopartner, asks about partner provisioning, organization management, or partner-portal features. It uses different credentials (TURBODOCX_PARTNER_API_KEY plus TURBODOCX_PARTNER_ID) which most TurboDocx users will not have.
TurboWebhooks is an opt-in add-on to TurboSign — it subscribes a single per-org HTTPS endpoint (locked to the name signature) to events like signature.document.completed. Don't surface it in the default question — enable it only when the user explicitly invokes /turbodocx-sdk turbowebhooks, asks how to receive event notifications, or asks how to verify the X-TurboDocx-Signature header. Reuses the same TURBODOCX_API_KEY + TURBODOCX_ORG_ID as TurboSign, but the key MUST have the administrator role (non-admin keys 403).
Language coverage for TurboWebhooks: PHP, JavaScript/TypeScript, Python, Go, and Java are fully covered today.
TurboQuote is a separate, opt-in product for building sales quotes and proposals — quotes, line items, a product/bundle catalog, price books, companies/contacts, and quote templates. Don't surface it in the default question — only enable it when the user explicitly invokes /turbodocx-sdk turboquote, or asks about creating quotes, proposals, CPQ, product catalogs, or price books. It reuses the same TURBODOCX_API_KEY + TURBODOCX_ORG_ID as TurboSign and does not need TURBODOCX_SENDER_EMAIL (quotes are not signature emails).
Language coverage for TurboQuote: JavaScript/TypeScript, Python, Go, PHP, and Java are fully covered.
---
PHASE 3: Install SDK
Run the install command for the detected language. Read the appropriate references/<language>.md file for the exact command.
Detect package manager first:
- JS/TS: check lockfiles (pnpm-lock.yaml, yarn.lock, bun.lockb, package-lock.json)
- Python: check for poetry.lock (poetry), Pipfile.lock (pipenv), else pip
- PHP: always composer
- Go: always go get
- Java: check for pom.xml (Maven) or build.gradle (Gradle)
Run the install command with Bash.
---
PHASE 4: Add Environment Variables
Read references/env-vars.md for the complete env var reference.
Based on product selection, add the corresponding vars to `.env` and `.env.example`:
TurboSign and/or Deliverable (both share the same credentials):
TURBODOCX_API_KEY=your_api_key_here
TURBODOCX_ORG_ID=your_org_id_hereTurboSign also requires (for the reply-to address on signature emails):
TURBODOCX_SENDER_EMAIL=you@company.com
TURBODOCX_SENDER_NAME=Your CompanyTurboPartner (separate partner credentials):
TURBODOCX_PARTNER_API_KEY=your_partner_api_key_here
TURBODOCX_PARTNER_ID=your_partner_id_hereIf the user selected multiple products, union the relevant variables. Deliverable does not need sender vars (it doesn't send email). TurboPartner does not use the TurboSign API key or org ID.
Important:
- If
.envexists, append new vars (don't overwrite existing content) - If
.env.exampleexists, append var names with placeholder values - Check
.gitignore— if.envis not listed, add it - Use Edit tool to append to existing files, Write tool to create new ones
---
PHASE 5: Read Language Reference
Read the reference file for the detected language:
- JavaScript/TypeScript → read
references/javascript.md - Python → read
references/python.md - Go → read
references/go.md - PHP → read
references/php.md - Java → read
references/java.md
These files contain the exact code templates for configuration, usage examples, and framework integration patterns.
---
PHASE 6: Analyze Codebase and Generate Code
CRITICAL: Explore the project structure BEFORE generating any code.
Step 6.1: Explore Project Structure
Use Glob and Read to understand:
- Source file locations:
src/, root,app/,internal/,pkg/, etc. - Existing route/handler patterns:
**/routes/**,**/api/**,**/controllers/**,**/handlers/** - Main app/entry file:
**/app.{ts,js,py},**/server.{ts,js},**/index.{ts,js},**/main.{py,go} - Existing config/env loading patterns: how does the project load env vars?
- Code style: naming conventions, import style, error handling, async patterns
Step 6.2: Confirm Findings
Tell the user what you found:
I explored your project structure:
- Project Type: [LANGUAGE/FRAMEWORK]
- Source Location: [PATH]
- Routes Location: [PATH or "none found - will create"]
- Main App File: [PATH]
- Existing Patterns: [Brief description]
Does this look correct?Step 6.3: Generate Config File
Create a client initialization file using the code template from the language reference. Place it following the project's existing conventions:
- If the project has a
lib/,utils/,config/, orcore/directory, put it there - Otherwise use sensible defaults (e.g.,
src/lib/turbodocx.tsfor Express)
The config file should:
- Import the SDK — only the modules the user selected (
TurboSign,Deliverable,TurboPartner) - Configure each selected module
- Load env vars using the project's existing pattern
- Export the configured client(s)
Step 6.4: Generate Integration Code
Create working route handlers / endpoint code for the selected product(s). The language reference contains exact method signatures, request shapes, and response shapes — follow those, don't guess.
For TurboSign, generate:
sendSignature()endpoint — accepts file (orfileLink/deliverableId/templateId), recipients, fieldsgetStatus()endpoint — check document status by IDdownload()endpoint — stream signed PDF (returnsBlob/ArrayBufferper language)- If the user mentioned a preview, review step, draft, or "verify field placement before sending": also generate a
createSignatureReviewLink()endpoint. This prepares the document and returns apreviewUrlwithout sending signature emails — pair it withsendSignature()as a two-step preview-then-send workflow. - Optionally:
void(),resend(),getAuditTrail()if the user mentioned cancellation, reminders, or compliance/audit needs (per-language method names vary — consult the language reference; e.g. JS usesvoid()/resend(), Java usesvoidDocument()/resendEmail())
For Deliverable, generate:
generateDeliverable()endpoint — acceptstemplateId+variables, returns the new deliverable IDgetDeliverableDetails()endpoint — fetch one by IDdownloadPDF()endpoint — stream the PDF render- If the user also selected TurboSign, demonstrate the generate-then-sign workflow: call
generateDeliverable, then pass the returneddeliverable.idasdeliverableIdtosendSignature(no need to download and re-upload — the platform routes it internally)
For TurboPartner, generate:
createOrganization()endpoint — provision a new customer orglistOrganizations()endpoint — list managed orgs (useslimit/offsetpagination, notpage)updateOrganizationEntitlements()endpoint — set features/tracking (the request body shape is{ features?, tracking? }, not bare features)
For TurboQuote, generate:
createQuote()endpoint — acceptsname,companyId,contactId(+ optionalcurrency/termDays/validUntil/taxRate); returns the new quoteaddLineItems()endpoint — add product line items (single object or array) to a quotesendQuote()endpoint — send a quote for review; returns{ quote, message }downloadQuotePdf()endpoint — stream the quote PDF (raw bytes per language)- If the user is building a catalog, also scaffold
createProduct()/createBundle()/createPriceBook()+applyPriceBook(). TurboQuote configures withapiKey+orgIdonly — nosenderEmail(quotes are not signature emails).
Once the basics are scaffolded, point the user at the language reference (references/<language>.md) for the full set of available operations — there are many more than the starter set (org/user/API-key management, audit logs, etc.) and the agent should mention which additional operations exist for the user's selected product so they know what to ask for next.
IMPORTANT:
- Match existing code patterns (file naming, import style, error handling, async patterns)
- Place route files where existing routes live
- Wire routes into the main app file (add import + registration)
- Use the typed error hierarchy from the reference —
ValidationError,AuthenticationError,NotFoundError,RateLimitError,NetworkErrorall import directly from@turbodocx/sdk(or the language equivalent); they are not namespaced under a module. - Include inline comments explaining each step
---
PHASE 7: Verify and Summarize
Verification Checklist
- SDK package is in the manifest (package.json, go.mod, requirements.txt, etc.)
- Config file created and exports configured client(s)
- Route handlers created with proper error handling
- Routes wired into main app file
- .env has all required variables
- .env is in .gitignore
- No secrets hardcoded in source filesFor TypeScript projects: Run npx tsc --noEmit and fix any errors.
Summary
TurboDocx Integration Complete!
Created Files:
- [List all created/modified files]
Installed:
- [SDK package name]
Environment Variables (update in .env):
- [List vars that need real values]
Quick Test:
[Provide curl command or test snippet for the first endpoint]
Next Steps:
1. Get your API credentials at https://app.turbodocx.com
2. Update .env with your credentials
3. Start your server and test the endpoints
Documentation: https://docs.turbodocx.com/docs
Support: https://discord.gg/NYKwz4BcpX---
Shortcuts
Support arguments to skip product selection:
/turbodocx-sdk turbosign— TurboSign only/turbodocx-sdk deliverable— Deliverable only/turbodocx-sdk turbosign+deliverable— generate-then-sign workflow/turbodocx-sdk turbopartner— TurboPartner only (partner-portal use case; requires partner credentials)/turbodocx-sdk turbowebhooks— TurboWebhooks only (subscribe to signature events; PHP, JS/TS, Python, Go, and Java supported)/turbodocx-sdk turboquote— TurboQuote only (build quotes/proposals: quotes, line items, products, bundles, price books, companies/contacts; JS/TS, Python, Go, PHP, and Java supported)
For backwards compatibility, /turbodocx-sdk both is treated as TurboSign + Deliverable.
---
Execution Instructions
1. Phase 1: Use Glob to detect project files. Parse manifest to confirm language. 2. Phase 2: Use AskUserQuestion for product selection (unless shortcut provided). 3. Phase 3: Use Bash to run install command. 4. Phase 4: Use Edit/Write to add env vars to .env files. Use Edit to update .gitignore. 5. Phase 5: Use Read to load the appropriate references/<language>.md file from this skill's directory. 6. Phase 6: Use Glob + Read to explore the project, then Write/Edit to generate config and route files. Always edit the main app file to wire in the new routes. 7. Phase 7: Verify files exist and compile. Print summary.
Environment Variables Reference
TurboSign, Deliverable, TurboWebhooks, and TurboQuote share the same API key + org ID. TurboPartner uses a separate set of partner credentials.
TurboSign + Deliverable + TurboWebhooks + TurboQuote Variables
| Variable | Required for | Description |
|---|---|---|
TURBODOCX_API_KEY | TurboSign, Deliverable, TurboWebhooks, TurboQuote | API key from your TurboDocx dashboard |
TURBODOCX_ORG_ID | TurboSign, Deliverable, TurboWebhooks, TurboQuote | Organization UUID from your dashboard |
TURBODOCX_SENDER_EMAIL | TurboSign only | Reply-to email for signature request emails. Must be a verified email. |
TURBODOCX_SENDER_NAME | No | Display name on signature emails (defaults to org name) |
Deliverable, TurboWebhooks, and TurboQuote do not send signature emails, so they don't need the sender variables — only TURBODOCX_API_KEY + TURBODOCX_ORG_ID.
TurboPartner Variables
| Variable | Required | Description |
|---|---|---|
TURBODOCX_PARTNER_API_KEY | Yes | Partner API key from your partner dashboard |
TURBODOCX_PARTNER_ID | Yes | Partner UUID from your partner dashboard |
.env Template
# TurboDocx — TurboSign and Deliverable (shared credentials)
TURBODOCX_API_KEY=your_api_key_here
TURBODOCX_ORG_ID=your_org_id_here
# TurboDocx — TurboSign-only (sender identity for signature emails)
TURBODOCX_SENDER_EMAIL=you@company.com
TURBODOCX_SENDER_NAME=Your Company
# TurboDocx — TurboPartner (separate partner credentials)
TURBODOCX_PARTNER_API_KEY=your_partner_api_key_here
TURBODOCX_PARTNER_ID=your_partner_id_hereConfig Resolution Order
The SDK resolves configuration in this order (first found wins):
1. Values passed directly to configure() / NewClientWithConfig() 2. Environment variables (listed above) 3. .env file in project root (if using dotenv/godotenv/phpdotenv)
Common Gotchas
- `senderEmail` is required for all TurboSign operations. Without it,
sendSignature()will throw aValidationError. - Partner keys are distinct from regular API keys. Using a regular API key with TurboPartner methods will return
AuthenticationError. - Don't commit `.env` — always add it to
.gitignore. Use.env.examplewith placeholder values for documentation. - Org ID vs Partner ID — these are different UUIDs. The org ID identifies your organization; the partner ID identifies your partner account. Don't mix them up.
Per-Language dotenv Setup
| Language | Package | Load Command |
|---|---|---|
| JavaScript | dotenv | import 'dotenv/config' (at entry point) |
| Python | python-dotenv | from dotenv import load_dotenv; load_dotenv() |
| Go | github.com/joho/godotenv | godotenv.Load() (in main) |
| PHP | vlucas/phpdotenv | Dotenv\Dotenv::createImmutable(__DIR__)->load() |
| Java (Spring) | Built-in | application.properties with ${TURBODOCX_API_KEY} |
| Java (plain) | io.github.cdimascio:dotenv-java | Dotenv.load() |
Go SDK Reference
Install
go get github.com/TurboDocx/SDK/packages/go-sdkAlso install godotenv for .env loading:
go get github.com/joho/godotenvLoad in main:
import "github.com/joho/godotenv"
func main() {
godotenv.Load() // loads .env file
// ...
}Import
import turbodocx "github.com/TurboDocx/SDK/packages/go-sdk"TurboSign Configuration
client, err := turbodocx.NewClientWithConfig(turbodocx.ClientConfig{
APIKey: os.Getenv("TURBODOCX_API_KEY"),
OrgID: os.Getenv("TURBODOCX_ORG_ID"),
SenderEmail: os.Getenv("TURBODOCX_SENDER_EMAIL"),
SenderName: os.Getenv("TURBODOCX_SENDER_NAME"),
})
if err != nil {
log.Fatal(err)
}TurboSign Usage
SendSignature
pdfFile, _ := os.ReadFile("contract.pdf")
result, err := client.TurboSign.SendSignature(ctx, &turbodocx.SendSignatureRequest{
File: pdfFile,
FileName: "contract.pdf",
DocumentName: "Partnership Agreement",
Recipients: []turbodocx.Recipient{
{Name: "John Doe", Email: "john@example.com", SigningOrder: 1},
},
Fields: []turbodocx.Field{
{
Type: "signature",
RecipientEmail: "john@example.com",
Template: &turbodocx.TemplateAnchor{
Anchor: "{signature1}",
Placement: "replace",
Size: &turbodocx.Size{Width: 100, Height: 30},
},
},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Document ID: %s\n", result.DocumentID)GetStatus
status, err := client.TurboSign.GetStatus(ctx, documentID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Status: %s\n", status.Status)
for _, r := range status.Recipients {
fmt.Printf(" %s: %s\n", r.Email, r.Status)
}Download
pdf, err := client.TurboSign.Download(ctx, documentID)
if err != nil {
log.Fatal(err)
}
os.WriteFile("signed.pdf", pdf, 0644)CreateSignatureReviewLink
Prepares the document with recipients and fields but does not send signature emails — use this to preview field placement before sending.
review, err := client.TurboSign.CreateSignatureReviewLink(ctx, &turbodocx.CreateSignatureReviewLinkRequest{
File: pdfFile,
FileName: "nda.pdf",
DocumentName: "NDA - Acme",
Recipients: []turbodocx.Recipient{
{Name: "John Doe", Email: "john@example.com", SigningOrder: 1},
},
Fields: []turbodocx.Field{
{
Type: "signature",
RecipientEmail: "john@example.com",
Page: 1,
X: 100,
Y: 500,
Width: 200,
Height: 50,
},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Document ID: %s\n", review.DocumentID)
fmt.Printf("Preview URL: %s\n", review.PreviewURL) // open to review field placement
// Each recipient also has a SignURL for their personal signing link
for _, r := range review.Recipients {
fmt.Printf(" %s: %s\n", r.Name, r.SignURL)
}VoidDocument
voided, err := client.TurboSign.VoidDocument(ctx, documentID, "Counterparty requested changes")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Status: %s\n", voided.Status) // "voided"
fmt.Printf("Voided at: %s\n", voided.VoidedAt)reason is required.
ResendEmail
// recipientIDs are UUIDs — fetch from SendSignature/CreateSignatureReviewLink response or GetAuditTrail
result, err := client.TurboSign.ResendEmail(ctx, documentID, []string{"recipient-uuid-1", "recipient-uuid-2"})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Resent to %d recipients\n", result.RecipientCount)GetAuditTrail
audit, err := client.TurboSign.GetAuditTrail(ctx, documentID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Document: %s\n", audit.Document.Name)
for _, entry := range audit.AuditTrail {
userEmail := ""
if entry.User != nil {
userEmail = entry.User.Email
}
fmt.Printf("%s %s %s\n", entry.Timestamp, entry.ActionType, userEmail)
}TurboPartner Configuration
partner, err := turbodocx.NewPartnerClient(turbodocx.PartnerConfig{
PartnerAPIKey: os.Getenv("TURBODOCX_PARTNER_API_KEY"),
PartnerID: os.Getenv("TURBODOCX_PARTNER_ID"),
})
if err != nil {
log.Fatal(err)
}TurboPartner Usage
CreateOrganization
org, err := partner.CreateOrganization(ctx, &turbodocx.CreateOrganizationRequest{
Name: "Acme Corp",
Features: &turbodocx.Features{
MaxUsers: turbodocx.IntPtr(50),
HasTDAI: turbodocx.BoolPtr(true),
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Org ID: %s\n", org.Data.ID)ListOrganizations
orgs, err := partner.ListOrganizations(ctx, &turbodocx.ListOrganizationsRequest{
Page: 1,
Limit: 20,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Total: %d\n", orgs.Total)
for _, org := range orgs.Data {
fmt.Printf(" %s (%s)\n", org.Name, org.ID)
}net/http Handler Example
package handlers
import (
"encoding/json"
"io"
"net/http"
turbodocx "github.com/TurboDocx/SDK/packages/go-sdk"
)
type SignatureHandler struct {
client *turbodocx.Client
}
func NewSignatureHandler(client *turbodocx.Client) *SignatureHandler {
return &SignatureHandler{client: client}
}
// POST /api/signatures/send
func (h *SignatureHandler) SendSignature(w http.ResponseWriter, r *http.Request) {
file, _, err := r.FormFile("file")
if err != nil {
http.Error(w, "file required", http.StatusBadRequest)
return
}
defer file.Close()
fileBytes, _ := io.ReadAll(file)
documentName := r.FormValue("document_name")
var recipients []turbodocx.Recipient
json.Unmarshal([]byte(r.FormValue("recipients")), &recipients)
var fields []turbodocx.Field
json.Unmarshal([]byte(r.FormValue("fields")), &fields)
result, err := h.client.TurboSign.SendSignature(r.Context(), &turbodocx.SendSignatureRequest{
File: fileBytes,
DocumentName: documentName,
Recipients: recipients,
Fields: fields,
})
if err != nil {
var tdxErr *turbodocx.TurboDocxError
if errors.As(err, &tdxErr) {
http.Error(w, tdxErr.Message, tdxErr.StatusCode)
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// GET /api/signatures/{id}/status
func (h *SignatureHandler) GetStatus(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // Go 1.22+ net/http
status, err := h.client.TurboSign.GetStatus(r.Context(), id)
if err != nil {
http.Error(w, "failed to get status", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(status)
}TurboWebhooks
TurboWebhooks subscribes a single per-org HTTPS endpoint (locked to the name signature) to TurboDocx events such as signature.document.completed and signature.document.voided. The SDK is intentionally one-webhook-per-org to mirror the dashboard's Signature Webhooks page.
Configuration
NewWebhooksClientWithConfig does NOT require SenderEmail — webhook routes don't send signature emails.
wh, err := turbodocx.NewWebhooksClientWithConfig(turbodocx.ClientConfig{
APIKey: os.Getenv("TURBODOCX_API_KEY"), // must be an admin TDX- key
OrgID: os.Getenv("TURBODOCX_ORG_ID"),
BaseURL: os.Getenv("TURBODOCX_BASE_URL"), // optional, defaults to api.turbodocx.com
})
if err != nil {
log.Fatal(err)
}CreateWebhook
created, err := wh.CreateWebhook(ctx, turbodocx.CreateWebhookRequest{
URLs: []string{"https://your-server.example.com/webhooks/turbodocx"},
Events: []string{"signature.document.completed", "signature.document.voided"},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("id: %s\n", created.ID)
fmt.Printf("secret: %s\n", created.Secret) // shown ONCE — save immediatelyGetWebhook
Returns the webhook record plus aggregate deliveryStats and the server-provided event catalog. The shape is returned as map[string]interface{} so new fields surface without an SDK upgrade.
webhook, err := wh.GetWebhook(ctx)UpdateWebhook
Patch any subset of fields. Use turbodocx.BoolPtr(false) to toggle IsActive.
updated, err := wh.UpdateWebhook(ctx, turbodocx.UpdateWebhookRequest{
URLs: []string{"https://new-server.example.com/hook"},
IsActive: turbodocx.BoolPtr(false),
})DeleteWebhook
_, err := wh.DeleteWebhook(ctx) // soft-delete; delivery history wipedTestWebhook / NotifyWebhook
TestWebhook and NotifyWebhook route through the same backend handler — prefer TestWebhook in new code. The response carries a summary with successful / failed counts and a per-URL errors list when any delivery fails.
result, err := wh.TestWebhook(ctx, turbodocx.TestWebhookRequest{
EventType: "signature.document.completed",
Payload: map[string]interface{}{
"documentId": "00000000-0000-0000-0000-000000000000",
"documentName": "Smoke test",
},
})RegenerateWebhookSecret
rotated, err := wh.RegenerateWebhookSecret(ctx)
newSecret := rotated["secret"] // shown ONCERotating immediately invalidates old signatures.
ListWebhookDeliveries
Pointer-typed filters — leave any field nil to skip it.
limit := 50
delivered := true
page, err := wh.ListWebhookDeliveries(ctx, turbodocx.ListDeliveriesRequest{
Limit: &limit,
EventType: "signature.document.completed",
IsDelivered: &delivered,
})ReplayWebhookDelivery
replayed, err := wh.ReplayWebhookDelivery(ctx, deliveryID)GetWebhookStats
stats, err := wh.GetWebhookStats(ctx, 30) // sliding window; pass 0 for backend defaultVerifying inbound webhook signatures (net/http)
When TurboDocx POSTs to your receiver, verify the X-TurboDocx-Signature header before trusting the payload. The helper enforces a 5-minute timestamp tolerance and uses hmac.Equal for constant-time comparison.
package handlers
import (
"io"
"net/http"
"os"
turbodocx "github.com/TurboDocx/SDK/packages/go-sdk"
)
func TurboDocxWebhook(w http.ResponseWriter, r *http.Request) {
// IMPORTANT: read raw bytes — the signature is computed over them.
// Decoding to a struct first will lose whitespace and break verification.
rawBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read failed", http.StatusBadRequest)
return
}
defer r.Body.Close()
signature := r.Header.Get("X-TurboDocx-Signature")
timestamp := r.Header.Get("X-TurboDocx-Timestamp")
secret := os.Getenv("TURBODOCX_WEBHOOK_SECRET")
if !turbodocx.VerifyWebhookSignature(rawBody, signature, timestamp, secret, nil) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
// Now safe to json.Unmarshal(rawBody, &event) and dispatch on event.eventType.
w.WriteHeader(http.StatusOK)
}Canonical end-to-end Go example: `packages/go-sdk/examples/turbowebhooks_crud.go` walks through create → conflict → get → update → test-fire → rotate → list → delete + every error branch.
TurboWebhooks error handling
import "errors"
_, err := wh.CreateWebhook(ctx, req)
if err != nil {
var conflict *turbodocx.ConflictError
var valErr *turbodocx.ValidationError
var authz *turbodocx.AuthorizationError
var auth *turbodocx.AuthenticationError
var nf *turbodocx.NotFoundError
var rate *turbodocx.RateLimitError
var net *turbodocx.NetworkError
switch {
case errors.As(err, &conflict): // 409 — already exists; update or delete
case errors.As(err, &valErr): // 400 — non-HTTPS URL or empty events
case errors.As(err, &authz): // 403 — TDX- key lacks administrator role
case errors.As(err, &auth): // 401 — bad / revoked API key
case errors.As(err, &nf): // 404 — webhook does not exist
case errors.As(err, &rate): // 429 — back off and retry
case errors.As(err, &net): // never reached the server
}
}TurboQuote
TurboQuote is TurboDocx's CPQ (Configure, Price, Quote) module — manage companies, contacts, products, bundles, price books, and quotes. Create a quote, attach line items, apply price-book discounts, send to a prospect, and download the PDF.
Configuration
NewQuoteClient does NOT require SenderEmail — quote operations do not send signature emails. OrgID is optional in config but the backend returns 401 if it is missing (set TURBODOCX_ORG_ID or pass it explicitly).
qc, err := turbodocx.NewQuoteClient(turbodocx.QuoteClientConfig{
APIKey: os.Getenv("TURBODOCX_API_KEY"),
OrgID: os.Getenv("TURBODOCX_ORG_ID"),
// BaseURL: os.Getenv("TURBODOCX_BASE_URL"), // optional, defaults to api.turbodocx.com
})
if err != nil {
log.Fatal(err)
}CreateQuote
quote, err := qc.CreateQuote(ctx, &turbodocx.CreateQuoteRequest{
Name: "Acme Annual Subscription",
CompanyID: companyID,
ContactID: contactID,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Quote ID: %s Number: %s\n", quote.ID, quote.QuoteNumber)
// quote.Status == "draft"AddLineItems / AddBundleLineItems
AddLineItems is variadic — pass one struct or many; both routes send an array to the backend.
qty := 3
items, err := qc.AddLineItems(ctx, quote.ID, turbodocx.AddLineItemRequest{
ProductName: "Professional License",
UnitPrice: 499.00,
BillingFrequency: "annual",
Quantity: &qty,
})
if err != nil {
log.Fatal(err)
}
// Returns []LineItem — unitPrice, listPrice etc. are float64 (normalizer coerces strings)
// Add a bundle instead:
bundleItems, err := qc.AddBundleLineItems(ctx, quote.ID, turbodocx.AddBundleLineItemRequest{
BundleID: bundleID,
Quantity: &qty,
})SendQuote
sent, err := qc.SendQuote(ctx, quote.ID, nil) // nil uses quote defaults
if err != nil {
log.Fatal(err)
}
fmt.Printf("Status: %s Message: %s\n", sent.QuoteResult.Status, sent.Message)
// sent.QuoteResult.Status == "sent"DownloadQuotePdf
pdf, err := qc.DownloadQuotePdf(ctx, quote.ID)
if err != nil {
log.Fatal(err)
}
os.WriteFile("quote.pdf", pdf, 0600)ApplyPriceBook
applyResp, err := qc.ApplyPriceBook(ctx, quote.ID, priceBookID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Updated %d items, skipped %d\n", applyResp.UpdatedCount, applyResp.SkippedCount)
// applyResp.QuoteResult is the updated QuoteProduct / Bundle / PriceBook catalog
// List products (paginated)
products, err := qc.ListProducts(ctx, nil)
fmt.Printf("Total: %d\n", products.TotalRecords)
// Create a product
product, err := qc.CreateProduct(ctx, &turbodocx.CreateProductRequest{
Name: "Enterprise Add-on",
ListPrice: 799.00,
})
// Duplicate a bundle
dupe, err := qc.DuplicateBundle(ctx, bundleID)
// List price-book products
pbProducts, err := qc.ListPriceBookProducts(ctx, priceBookID, nil)CreateAndSend
Convenience method: creates the quote, adds line items and bundle items, then sends — in 2–4 sequential API calls.
result, err := qc.CreateAndSend(ctx, &turbodocx.CreateAndSendRequest{
Name: "Acme - Q3 Deal",
CompanyID: companyID,
ContactID: contactID,
Items: []turbodocx.AddLineItemRequest{
{ProductName: "Starter Plan", UnitPrice: 99.00, BillingFrequency: "monthly"},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Quote %s sent\n", result.Quote.QuoteNumber)TurboQuote error handling
import "errors"
_, err := qc.SendQuote(ctx, quoteID, nil)
if err != nil {
var valErr *turbodocx.ValidationError
var auth *turbodocx.AuthenticationError
var nf *turbodocx.NotFoundError
var rate *turbodocx.RateLimitError
switch {
case errors.As(err, &valErr): // 400 — e.g. quote not in a sendable status
case errors.As(err, &auth): // 401 — bad / revoked API key or missing OrgID
case errors.As(err, &nf): // 404 — quote does not exist
case errors.As(err, &rate): // 429 — back off and retry
}
}Error Handling
import "errors"
result, err := client.TurboSign.SendSignature(ctx, req)
if err != nil {
var tdxErr *turbodocx.TurboDocxError
if errors.As(err, &tdxErr) {
// tdxErr.Code — machine-readable error code
// tdxErr.Message — human-readable description
// tdxErr.StatusCode — HTTP status
switch {
case errors.As(err, new(*turbodocx.AuthenticationError)):
// Invalid/missing API key
case errors.As(err, new(*turbodocx.ValidationError)):
// Bad request (e.g., missing SenderEmail)
case errors.As(err, new(*turbodocx.NotFoundError)):
// Document/org not found
case errors.As(err, new(*turbodocx.RateLimitError)):
// Too many requests
}
}
}Method Reference
| Method | Description |
|---|---|
client.TurboSign.SendSignature(ctx, req) | Send document for e-signature |
client.TurboSign.CreateSignatureReviewLink(ctx, req) | Preview without emails |
client.TurboSign.GetStatus(ctx, id) | Get document + recipient status |
client.TurboSign.Download(ctx, id) | Download signed PDF as []byte |
client.TurboSign.VoidDocument(ctx, id, reason) | Cancel a signature request (reason required) |
client.TurboSign.ResendEmail(ctx, id, recipientIDs) | Resend signature email to recipient UUIDs |
client.TurboSign.GetAuditTrail(ctx, id) | Get complete audit trail |
partner.CreateOrganization(ctx, req) | Provision a new customer org |
partner.ListOrganizations(ctx, req) | List managed organizations |
partner.GetOrganization(ctx, id) | Get org details |
partner.UpdateEntitlements(ctx, id, features) | Update org entitlements |
turbodocx.NewWebhooksClientWithConfig(cfg) | Construct an admin-scoped webhook client (no SenderEmail required) |
wh.CreateWebhook(ctx, req) | Subscribe the org to events (HTTPS URLs only) |
wh.GetWebhook(ctx) | Get the org's signature webhook + delivery stats |
wh.UpdateWebhook(ctx, req) | Patch URLs / events / isActive |
wh.DeleteWebhook(ctx) | Soft-delete the webhook |
wh.TestWebhook(ctx, req) | Fire a test delivery; surfaces per-URL errors |
wh.NotifyWebhook(ctx, req) | Manual notify; same backend handler as TestWebhook |
wh.RegenerateWebhookSecret(ctx) | Rotate the HMAC secret (shown ONCE) |
wh.ListWebhookDeliveries(ctx, req) | Paginated delivery history with filters |
wh.ReplayWebhookDelivery(ctx, deliveryID) | Retry a past delivery; returns the new delivery row |
wh.GetWebhookStats(ctx, days) | Aggregate stats over a sliding window (0 = backend default) |
turbodocx.VerifyWebhookSignature(rawBody, sigHeader, tsHeader, secret, opts) | Free function; verifies inbound deliveries |
turbodocx.NewQuoteClient(cfg) | Construct a TurboQuote client (no SenderEmail required) |
qc.ListQuotes(ctx, opts) | Paginated quote list with filters |
qc.CreateQuote(ctx, req) | Create a new quote (status: draft) |
qc.GetQuote(ctx, id) | Get quote + merged statusInfo |
qc.UpdateQuote(ctx, id, req) | Patch quote fields; use ClearPriceBookID() etc. to null-clear |
qc.DeleteQuote(ctx, id) | Delete a quote |
qc.DuplicateQuote(ctx, id) | Duplicate a quote |
qc.ApplyPriceBook(ctx, quoteID, priceBookID) | Apply price-book discounts; returns updatedCount / skippedCount |
qc.RemovePriceBook(ctx, quoteID) | Remove price-book association from a quote |
qc.DownloadQuotePdf(ctx, id) | Download quote as PDF ([]byte) |
qc.SendQuote(ctx, id, req) | Send quote to prospect (req may be nil) |
qc.SendQuoteWithDeliverable(ctx, id, req) | Send quote with a TurboDocx deliverable attachment |
qc.DeclineQuote(ctx, id, req) | Decline a sent quote (reason required) |
qc.VoidQuote(ctx, id, req) | Void a quote (reason required) |
qc.HandleExpiredQuote(ctx, id, req) | Resend, extend, or void an expired sent quote |
qc.CreateAndSend(ctx, req) | Convenience: create + add items + send in one call |
qc.ListLineItems(ctx, quoteID, opts) | List line items for a quote |
qc.AddLineItems(ctx, quoteID, items...) | Add one or more product line items (variadic) |
qc.AddBundleLineItems(ctx, quoteID, items...) | Add one or more bundle line items (variadic) |
qc.UpdateLineItem(ctx, quoteID, itemID, req) | Update a line item |
qc.RemoveLineItem(ctx, quoteID, itemID) | Remove a line item |
qc.ListProducts(ctx, opts) | Paginated product catalog |
qc.CreateProduct(ctx, req) | Create a product (multipart when images provided) |
qc.GetProduct(ctx, id) | Get a product by ID |
qc.UpdateProduct(ctx, id, req) | Update a product (multipart when images provided) |
qc.DeleteProduct(ctx, id) | Delete a product |
qc.DuplicateProduct(ctx, id) | Duplicate a product |
qc.GetProductPrimaryImages(ctx, productIDs) | Batch-fetch primary images by product ID |
qc.ListPriceBooks(ctx, opts) | Paginated price-book list |
qc.CreatePriceBook(ctx, req) | Create a price book |
qc.GetPriceBook(ctx, id) | Get a price book by ID |
qc.UpdatePriceBook(ctx, id, req) | Update a price book |
qc.DeletePriceBook(ctx, id) | Delete a price book |
qc.DuplicatePriceBook(ctx, id) | Duplicate a price book |
qc.ListPriceBookProducts(ctx, id, opts) | List products associated with a price book |
qc.ListBundles(ctx, opts) | Paginated bundle list |
qc.CreateBundle(ctx, req) | Create a bundle |
qc.GetBundle(ctx, id) | Get a bundle by ID |
qc.UpdateBundle(ctx, id, req) | Update a bundle |
qc.DeleteBundle(ctx, id) | Delete a bundle |
qc.DuplicateBundle(ctx, id) | Duplicate a bundle |
qc.ListCompanies(ctx, opts) | Paginated company list |
qc.CreateCompany(ctx, req) | Create a company (contacts required ≥ 1) |
qc.GetCompany(ctx, id) | Get a company by ID |
qc.UpdateCompany(ctx, id, req) | Update a company |
qc.DeleteCompany(ctx, id) | Delete a company |
qc.ListCompanyContacts(ctx, companyID, opts) | List contacts for a specific company |
qc.ListContacts(ctx, opts) | Paginated contact list |
qc.CreateContact(ctx, req) | Create a contact |
qc.UpdateContact(ctx, id, req) | Update a contact |
qc.DeleteContact(ctx, id) | Delete a contact |
qc.ListTemplates(ctx, opts) | Paginated quote template list |
qc.GetTemplate(ctx) | Get the active (singleton) quote template |
qc.GetTemplateByID(ctx, id) | Get a specific quote template by ID |
qc.CreateTemplate(ctx, req) | Create a quote template |
qc.UpdateTemplate(ctx, id, req) | Update a quote template |
qc.DeleteTemplate(ctx, id) | Delete a quote template |
qc.ListTypes(ctx, opts) | Paginated quote types/categories list |
qc.CreateType(ctx, req) | Create a quote type/category |
qc.UpdateType(ctx, id, req) | Update a quote type/category |
qc.DeleteType(ctx, id) | Delete a quote type/category |
Gotchas
- Go SDK uses instance methods, not static methods — create a client first with
NewClientWithConfig - `SenderEmail` is required in ClientConfig for TurboSign operations
- Context is required for all API calls — pass
context.Background()or request context - Helper functions
turbodocx.IntPtr(),turbodocx.BoolPtr(),turbodocx.StringPtr()for optional pointer fields - File input accepts:
[]byte, file path string, or URL string - `SignURL` — each
Recipientin theSendSignature/CreateSignatureReviewLinkresponse has aSignURLfield: the personal signing link for that recipient.CreateSignatureReviewLinkalso returns a top-levelPreviewURLfor document-level preview. - `ResendEmail` takes recipient UUIDs (
[]string), not email addresses — fetch them from the send/review response recipients or fromGetAuditTrail. - TurboWebhooks requires an admin TDX- key. The backend route gate is
requireOrgRole(administrator)— a non-admin key returns*turbodocx.AuthorizationError(HTTP 403). Discriminate witherrors.As. - One webhook per org, fixed name `signature`. The SDK is hardcoded to
/api/webhooks/signatureto stay in sync with the dashboard's Signature Webhooks page. There is noListWebhooksby design. For multi-webhook management call the REST API directly. - Webhook secrets are shown ONCE — capture
created.SecretfromCreateWebhookandrotated["secret"]fromRegenerateWebhookSecretimmediately. They are never returned again byGetWebhookor any other endpoint. - Webhook URLs must be HTTPS. Non-HTTPS URLs return
*turbodocx.ValidationError(HTTP 400) from the backend. - Read the raw request body in your receiver, not the decoded JSON. Use
io.ReadAll(r.Body).VerifyWebhookSignatureis computed over the raw bytes; a re-marshal will not match. - `VerifyWebhookSignature` is a free function, not a method on a client — it has no
APIKey/OrgIDdependency. Passnilforoptsto use the default 300-second tolerance. - `ConflictError` (HTTP 409) — returned by
CreateWebhookwhen a webhook with the same name already exists for the org. Discriminate it witherrors.As(err, new(*turbodocx.ConflictError)). - TurboQuote decimal fields are `float64`, not strings — the response normalizer coerces backend string decimals (e.g.
"499.00") tofloat64before unmarshalling intoQuote,LineItem,Product, etc. Do not expect string values forunitPrice,listPrice,grandTotal,taxRate, or any other monetary/percentage field. - PATCH null-clears on `UpdateQuoteRequest` require explicit helper calls. Go omits nil pointer fields by default. To send
"priceBookId": null,"validUntil": null,"taxRate": null, or"renewalPeriod": null, call the corresponding method (ClearPriceBookID(),ClearValidUntil(), etc.) on the request before passing it toUpdateQuote. Setting the pointer tonilalone is not sufficient. - `discountType` is `"percent"` or `"amount"`. Use the typed constants
turbodocx.DiscountTypePercentandturbodocx.DiscountTypeAmountwhen setting discounts on line items or bundles to avoid silent backend validation errors.
Full API reference: https://docs.turbodocx.com/docs
Java SDK Reference
Install
Maven
<dependency>
<groupId>com.turbodocx</groupId>
<artifactId>turbodocx-sdk</artifactId>
<version>0.2.0</version>
</dependency>Gradle
implementation 'com.turbodocx:turbodocx-sdk:0.2.0'Gradle (Kotlin DSL)
implementation("com.turbodocx:turbodocx-sdk:0.2.0")Imports
import com.turbodocx.TurboDocxClient;
import com.turbodocx.TurboPartnerClient;
import com.turbodocx.models.*;TurboSign Configuration
TurboDocxClient client = new TurboDocxClient.Builder()
.apiKey(System.getenv("TURBODOCX_API_KEY"))
.orgId(System.getenv("TURBODOCX_ORG_ID"))
.senderEmail(System.getenv("TURBODOCX_SENDER_EMAIL"))
.senderName(System.getenv("TURBODOCX_SENDER_NAME"))
.build();TurboSign Usage
sendSignature
byte[] pdfFile = Files.readAllBytes(Paths.get("contract.pdf"));
SendSignatureResponse result = client.turboSign().sendSignature(
new SendSignatureRequest.Builder()
.file(pdfFile)
.fileName("contract.pdf")
.documentName("Partnership Agreement")
.recipients(Arrays.asList(
new Recipient("John Doe", "john@example.com", 1)
))
.fields(Arrays.asList(
new Field.Builder()
.type("signature")
.recipientEmail("john@example.com")
.template(new Field.TemplateAnchor.Builder()
.anchor("{signature1}")
.placement("replace")
.size(new Field.Size(100, 30))
.build())
.build()
))
.build()
);
System.out.println("Document ID: " + result.getDocumentId());getStatus
DocumentStatus status = client.turboSign().getStatus(documentId);
System.out.println("Status: " + status.getStatus());
for (RecipientStatus r : status.getRecipients()) {
System.out.println(" " + r.getEmail() + ": " + r.getStatus());
}download
byte[] pdf = client.turboSign().download(documentId);
Files.write(Paths.get("signed.pdf"), pdf);createSignatureReviewLink
Prepares the document with recipients and fields but does not send signature emails — use this to preview field placement before sending.
CreateSignatureReviewLinkResponse review = client.turboSign().createSignatureReviewLink(
new CreateSignatureReviewLinkRequest.Builder()
.file(pdfFile)
.fileName("nda.pdf")
.documentName("NDA - Acme")
.recipients(Arrays.asList(
new Recipient("John Doe", "john@example.com", 1)
))
.fields(Arrays.asList(
new Field.Builder()
.type("signature")
.recipientEmail("john@example.com")
.page(1)
.x(100).y(500).width(200).height(50)
.build()
))
.build()
);
System.out.println("Document ID: " + review.getDocumentId());
System.out.println("Preview URL: " + review.getPreviewUrl()); // open to review field placement
// Each recipient also has a signUrl for their personal signing link
for (RecipientResponse r : review.getRecipients()) {
System.out.println(" " + r.getName() + ": " + r.getSignUrl());
}voidDocument
VoidDocumentResponse voided = client.turboSign().voidDocument(documentId, "Counterparty requested changes");
System.out.println("Status: " + voided.getStatus()); // "voided"
System.out.println("Voided at: " + voided.getVoidedAt());reason is required.
resendEmail
// recipientIds are UUIDs — fetch from sendSignature/createSignatureReviewLink response or getAuditTrail
List<String> recipientIds = Arrays.asList("recipient-uuid-1", "recipient-uuid-2");
ResendEmailResponse result = client.turboSign().resendEmail(documentId, recipientIds);
System.out.println("Resent to " + result.getRecipientCount() + " recipients");getAuditTrail
AuditTrailResponse audit = client.turboSign().getAuditTrail(documentId);
System.out.println("Document: " + audit.getDocument().getName());
for (AuditTrailEntry entry : audit.getAuditTrail()) {
String userEmail = entry.getUser() != null ? entry.getUser().getEmail() : "";
System.out.println(entry.getTimestamp() + " " + entry.getActionType() + " " + userEmail);
}TurboPartner Configuration
TurboPartnerClient partner = new TurboPartnerClient.Builder()
.partnerApiKey(System.getenv("TURBODOCX_PARTNER_API_KEY"))
.partnerId(System.getenv("TURBODOCX_PARTNER_ID"))
.build();TurboPartner Usage
createOrganization
CreateOrganizationResponse org = partner.turboPartner().createOrganization(
new CreateOrganizationRequest.Builder()
.name("Acme Corp")
.features(Map.of("maxUsers", 50, "hasTDAI", true))
.build()
);
System.out.println("Org ID: " + org.getData().getId());listOrganizations
ListOrganizationsResponse orgs = partner.turboPartner().listOrganizations(1, 20);
System.out.println("Total: " + orgs.getTotal());
for (Organization o : orgs.getData()) {
System.out.println(" " + o.getName() + " (" + o.getId() + ")");
}Spring Boot Integration Example
// src/main/java/.../config/TurboDocxConfig.java
package com.example.app.config;
import com.turbodocx.TurboDocxClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TurboDocxConfig {
@Bean
public TurboDocxClient turboDocxClient(
@Value("${TURBODOCX_API_KEY}") String apiKey,
@Value("${TURBODOCX_ORG_ID}") String orgId,
@Value("${TURBODOCX_SENDER_EMAIL}") String senderEmail,
@Value("${TURBODOCX_SENDER_NAME:}") String senderName
) {
return new TurboDocxClient.Builder()
.apiKey(apiKey)
.orgId(orgId)
.senderEmail(senderEmail)
.senderName(senderName)
.build();
}
}// src/main/java/.../controller/SignatureController.java
package com.example.app.controller;
import com.turbodocx.TurboDocxClient;
import com.turbodocx.models.*;
import com.turbodocx.exceptions.TurboDocxException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.type.TypeReference;
import java.util.List;
@RestController
@RequestMapping("/api/signatures")
public class SignatureController {
private final TurboDocxClient client;
private final ObjectMapper objectMapper;
public SignatureController(TurboDocxClient client, ObjectMapper objectMapper) {
this.client = client;
this.objectMapper = objectMapper;
}
@PostMapping("/send")
public ResponseEntity<?> sendSignature(
@RequestParam("file") MultipartFile file,
@RequestParam("documentName") String documentName,
@RequestParam("recipients") String recipientsJson,
@RequestParam("fields") String fieldsJson
) {
try {
List<Recipient> recipients = objectMapper.readValue(
recipientsJson, new TypeReference<>() {}
);
List<Field> fields = objectMapper.readValue(
fieldsJson, new TypeReference<>() {}
);
SendSignatureResponse result = client.turboSign().sendSignature(
new SendSignatureRequest.Builder()
.file(file.getBytes())
.fileName(file.getOriginalFilename())
.documentName(documentName)
.recipients(recipients)
.fields(fields)
.build()
);
return ResponseEntity.ok(result);
} catch (TurboDocxException e) {
return ResponseEntity.status(e.getStatusCode()).body(e.getMessage());
} catch (Exception e) {
return ResponseEntity.internalServerError().body("Signature request failed");
}
}
@GetMapping("/{id}/status")
public ResponseEntity<?> getStatus(@PathVariable String id) {
try {
DocumentStatus status = client.turboSign().getStatus(id);
return ResponseEntity.ok(status);
} catch (TurboDocxException e) {
return ResponseEntity.status(e.getStatusCode()).body(e.getMessage());
}
}
}TurboWebhooks
TurboWebhooks subscribes a single per-org HTTPS endpoint (locked to the name signature) to TurboDocx events such as signature.document.completed and signature.document.voided. The SDK is intentionally one-webhook-per-org to mirror the dashboard's Signature Webhooks page.
Configuration
buildWebhooksClient() does NOT require senderEmail — webhook routes don't send signature emails. It returns a TurboWebhooks instance directly (no .turboWebhooks() accessor on the parent client).
import com.turbodocx.TurboDocxClient;
import com.turbodocx.TurboWebhooks;
TurboWebhooks webhooks = new TurboDocxClient.Builder()
.apiKey(System.getenv("TURBODOCX_API_KEY")) // must be an admin TDX- key
.orgId(System.getenv("TURBODOCX_ORG_ID"))
.baseUrl(System.getenv("TURBODOCX_BASE_URL")) // optional, defaults to api.turbodocx.com
.buildWebhooksClient();createWebhook
import com.google.gson.JsonObject;
import java.util.Arrays;
JsonObject created = webhooks.createWebhook(
Arrays.asList("https://your-server.example.com/webhooks/turbodocx"),
Arrays.asList("signature.document.completed", "signature.document.voided")
);
System.out.println("id: " + created.get("id").getAsString());
System.out.println("secret: " + created.get("secret").getAsString()); // shown ONCE — save immediatelygetWebhook
Returns the webhook record plus aggregate deliveryStats and the server-provided event catalog. All TurboWebhooks methods return JsonObject so new fields surface without an SDK upgrade.
JsonObject webhook = webhooks.getWebhook();updateWebhook
Patch any subset of fields. Pass null for fields you don't want to change. Renaming is not supported.
JsonObject updated = webhooks.updateWebhook(
Arrays.asList("https://new-server.example.com/hook"), // urls
null, // events (unchanged)
Boolean.FALSE // isActive
);deleteWebhook
JsonObject deleted = webhooks.deleteWebhook(); // soft-delete; delivery history wipedtestWebhook / notifyWebhook
testWebhook and notifyWebhook route through the same backend handler — prefer testWebhook in new code. The response carries a summary with successful / failed counts and a per-URL errors list when any delivery fails.
import java.util.LinkedHashMap;
import java.util.Map;
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("documentId", "00000000-0000-0000-0000-000000000000");
payload.put("documentName", "Smoke test");
JsonObject result = webhooks.testWebhook("signature.document.completed", payload);regenerateWebhookSecret
JsonObject rotated = webhooks.regenerateWebhookSecret();
String newSecret = rotated.get("secret").getAsString(); // shown ONCERotating immediately invalidates old signatures.
listWebhookDeliveries
Pass null for any filter you don't want to apply. The no-arg overload skips all filters.
JsonObject page = webhooks.listWebhookDeliveries(
50, // limit
null, // offset
"signature.document.completed", // eventType
Boolean.TRUE, // isDelivered
null // httpStatus
);replayWebhookDelivery
JsonObject replayed = webhooks.replayWebhookDelivery(deliveryId);getWebhookStats
JsonObject stats = webhooks.getWebhookStats(30); // sliding window in days; pass null for backend defaultVerifying inbound webhook signatures (Spring Boot)
When TurboDocx POSTs to your receiver, verify the X-TurboDocx-Signature header before trusting the payload. Java has no free functions — the helper is exposed as WebhookSignatureVerifier.verify(...), a static method on a final utility class. It enforces a 5-minute timestamp tolerance and uses MessageDigest.isEqual for constant-time comparison.
package com.example.webhooks;
import com.turbodocx.WebhookSignatureVerifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TurboDocxWebhookController {
@Value("${turbodocx.webhook.secret}")
private String secret;
// IMPORTANT: bind to byte[], not a parsed @RequestBody Map / DTO.
// The signature is computed over raw bytes — Jackson would re-serialize
// and lose whitespace, which breaks verification.
@PostMapping(value = "/webhooks/turbodocx", consumes = "application/json")
public ResponseEntity<Void> receive(
@RequestBody byte[] rawBody,
@RequestHeader("X-TurboDocx-Signature") String signature,
@RequestHeader("X-TurboDocx-Timestamp") String timestamp) {
if (!WebhookSignatureVerifier.verify(rawBody, signature, timestamp, secret)) {
return ResponseEntity.status(401).build();
}
// Now safe to parse rawBody as JSON and dispatch on event.eventType.
return ResponseEntity.ok().build();
}
}For a plain Servlet receiver, read the body with request.getInputStream().readAllBytes() instead of @RequestBody byte[]; everything else stays the same.
Canonical end-to-end Java example: `packages/java-sdk/examples/TurboWebhooksCrud.java` walks through create → conflict → get → update → test-fire → rotate → list → delete + every error branch.
TurboWebhooks error handling
import com.turbodocx.TurboDocxException;
try {
webhooks.createWebhook(urls, events);
} catch (TurboDocxException.ConflictException e) {
// 409 — webhook with that name already exists; update or delete
} catch (TurboDocxException.ValidationException e) {
// 400 — non-HTTPS URL or empty events
} catch (TurboDocxException.AuthorizationException e) {
// 403 — TDX- key lacks administrator role
} catch (TurboDocxException.AuthenticationException e) {
// 401 — bad / revoked API key
} catch (TurboDocxException.NotFoundException e) {
// 404 — webhook does not exist
} catch (TurboDocxException.RateLimitException e) {
// 429 — back off and retry
} catch (TurboDocxException.NetworkException e) {
// never reached the server
}TurboQuote
TurboQuote provides end-to-end CPQ (configure-price-quote) operations: create and send professional quotes, manage your product catalog and bundles, apply pricebooks, and handle the full quote lifecycle (draft, sent, accepted, declined, voided).
Configuration
TurboQuoteClient does NOT require senderEmail — quote routes do not send TurboSign signature emails. orgId is technically optional in the config but the backend will return 401 if it is missing, so always supply it.
import com.turbodocx.TurboQuoteClient;
import com.turbodocx.TurboQuote;
TurboQuote tq = new TurboQuoteClient.Builder()
.apiKey(System.getenv("TURBODOCX_API_KEY"))
.orgId(System.getenv("TURBODOCX_ORG_ID"))
// .baseUrl(System.getenv("TURBODOCX_BASE_URL")) // optional, defaults to api.turbodocx.com
.build()
.turboQuote();createQuote
CreateQuoteRequest req = new CreateQuoteRequest();
req.setName("Q1 Software Proposal");
req.setCompanyId(companyId);
req.setContactId(contactId);
req.setTermDays(30);
req.setCurrency(Currency.USD);
Quote quote = tq.createQuote(req);
System.out.println("Quote ID: " + quote.getId());
System.out.println("Status: " + quote.getStatus()); // "draft"addLineItems
addLineItems accepts either a single AddLineItemRequest or a List — the single-item overload is auto-wrapped internally.
AddLineItemRequest item = new AddLineItemRequest();
item.setProductName("Enterprise License");
item.setProductId(null); // null = ad-hoc line item; pass a real ID to link a catalog product
item.setUnitPrice(1200.00);
item.setQuantity(5.0);
item.setBillingFrequency("annual");
item.setDiscountType(DiscountType.PERCENT);
item.setDiscountPercent(10.0);
List<LineItem> lineItems = tq.addLineItems(quote.getId(), item);
System.out.println("Total line items: " + lineItems.size());sendQuote
SendQuoteResponse sent = tq.sendQuote(quote.getId());
System.out.println("Status: " + sent.getQuote().getStatus()); // "sent"
System.out.println(sent.getMessage());downloadQuotePdf
byte[] pdf = tq.downloadQuotePdf(quoteId);
Files.write(Paths.get("quote.pdf"), pdf);Catalog example (product / bundle / pricebook)
// Create a product
CreateProductRequest prod = new CreateProductRequest();
prod.setName("Support Add-On");
prod.setListPrice(500.00);
prod.setBillingFrequency("annual");
Product product = tq.createProduct(prod);
// Create a bundle
CreateBundleRequest bundle = new CreateBundleRequest();
bundle.setName("Starter Pack");
Bundle b = tq.createBundle(bundle);
// Create and apply a pricebook — name, priceBookTypeId, validFrom, and discountPercent are all required.
// priceBookTypeId comes from a createType(...) with categoryType PRICEBOOK_TYPE.
CreatePriceBookRequest pb = new CreatePriceBookRequest();
pb.setName("Partner Pricing");
pb.setPriceBookTypeId(priceBookTypeId);
pb.setValidFrom("2025-01-01");
pb.setDiscountPercent(15.0);
PriceBook priceBook = tq.createPriceBook(pb);
ApplyPriceBookResponse applied = tq.applyPriceBook(quote.getId(), priceBook.getId());
System.out.println("Updated items: " + applied.getUpdatedCount());
System.out.println("Skipped items: " + applied.getSkippedCount());TurboQuote error handling
import com.turbodocx.TurboDocxException;
try {
tq.sendQuote(quoteId);
} catch (TurboDocxException.ValidationException e) {
// 400 — e.g. quote has no line items, or quote is already sent
} catch (TurboDocxException.AuthenticationException e) {
// 401 — bad / revoked API key, or missing orgId
} catch (TurboDocxException.NotFoundException e) {
// 404 — quote or related resource does not exist
} catch (TurboDocxException.RateLimitException e) {
// 429 — back off and retry
} catch (TurboDocxException e) {
System.err.println("Error " + e.getStatusCode() + ": " + e.getMessage());
}Error Handling
import com.turbodocx.exceptions.*;
try {
client.turboSign().sendSignature(request);
} catch (AuthenticationException e) {
// Invalid/missing API key
} catch (ValidationException e) {
// Bad request (e.g., missing senderEmail)
} catch (NotFoundException e) {
// Document/org not found
} catch (RateLimitException e) {
// Too many requests
} catch (NetworkException e) {
// Connection failure
} catch (TurboDocxException e) {
// Catch-all for any SDK error
System.err.println("Error " + e.getCode() + ": " + e.getMessage());
}Method Reference
| Method | Description |
|---|---|
client.turboSign().sendSignature(req) | Send document for e-signature |
client.turboSign().createSignatureReviewLink(req) | Preview without emails |
client.turboSign().getStatus(id) | Get document + recipient status |
client.turboSign().download(id) | Download signed PDF as byte[] |
client.turboSign().voidDocument(id) | Cancel a signature request |
client.turboSign().resendEmail(id, recipientIds) | Resend signature email to recipient UUIDs |
client.turboSign().getAuditTrail(id) | Get complete audit trail |
partner.turboPartner().createOrganization(req) | Provision a new customer org |
partner.turboPartner().listOrganizations(page, limit) | List managed organizations |
partner.turboPartner().getOrganization(id) | Get org details |
partner.turboPartner().updateEntitlements(id, features) | Update org entitlements |
new TurboDocxClient.Builder()...buildWebhooksClient() | Construct an admin-scoped TurboWebhooks (no senderEmail required) |
webhooks.createWebhook(urls, events) | Subscribe the org to events (HTTPS URLs only) |
webhooks.getWebhook() | Get the org's signature webhook + delivery stats |
webhooks.updateWebhook(urls, events, isActive) | Patch URLs / events / isActive (pass null to skip) |
webhooks.deleteWebhook() | Soft-delete the webhook |
webhooks.testWebhook(eventType, payload) | Fire a test delivery; surfaces per-URL errors |
webhooks.notifyWebhook(eventType, payload) | Manual notify; same backend handler as testWebhook |
webhooks.regenerateWebhookSecret() | Rotate the HMAC secret (shown ONCE) |
webhooks.listWebhookDeliveries(limit, offset, eventType, isDelivered, httpStatus) | Paginated delivery history with filters |
webhooks.replayWebhookDelivery(deliveryId) | Retry a past delivery; returns the new delivery row |
webhooks.getWebhookStats(days) | Aggregate stats over a sliding window (null = backend default) |
WebhookSignatureVerifier.verify(rawBody, sigHeader, tsHeader, secret) | Static utility; verifies inbound deliveries |
new TurboQuoteClient.Builder()...build().turboQuote() | Construct a TurboQuote instance (no senderEmail required) |
tq.listQuotes(options) | List quotes with optional filters (status, search, pagination) |
tq.createQuote(req) | Create a new quote in draft status |
tq.getQuote(id) | Get quote by ID (statusInfo merged into response) |
tq.updateQuote(id, req) | PATCH quote fields; explicitly null fields are cleared |
tq.deleteQuote(id) | Delete a quote |
tq.duplicateQuote(id) | Duplicate a quote |
tq.applyPriceBook(quoteId, priceBookId) | Apply pricebook to a quote; returns updatedCount + skippedCount |
tq.removePriceBook(quoteId) | Remove applied pricebook from a quote |
tq.downloadQuotePdf(id) | Download quote as PDF; returns raw byte[] |
tq.sendQuote(id) | Send quote to recipient; transitions status to sent |
tq.sendQuoteWithDeliverable(id, req) | Send quote and attach a TurboDocx deliverable |
tq.declineQuote(id, req) | Decline a quote (reason required) |
tq.voidQuote(id, req) | Void a quote (reason required) |
tq.handleExpiredQuote(id, req) | Handle an expired sent quote (action + optional newValidUntil) |
tq.listLineItems(quoteId) | List line items on a quote |
tq.addLineItems(quoteId, item) | Add one or more line items (single or List overload) |
tq.addBundleLineItems(quoteId, items) | Add bundle line items to a quote |
tq.updateLineItem(quoteId, itemId, req) | Update a line item |
tq.removeLineItem(quoteId, itemId) | Remove a line item |
tq.listProducts(options) | List products in the catalog |
tq.createProduct(req) | Create a product (supports image upload via multipart) |
tq.getProduct(id) | Get product by ID |
tq.updateProduct(id, req) | Update a product |
tq.deleteProduct(id) | Delete a product |
tq.duplicateProduct(id) | Duplicate a product |
tq.getProductPrimaryImages(productIds) | Batch-fetch primary images for product IDs |
tq.listPriceBooks(options) | List pricebooks |
tq.createPriceBook(req) | Create a pricebook |
tq.getPriceBook(id) | Get pricebook by ID |
tq.updatePriceBook(id, req) | Update a pricebook |
tq.deletePriceBook(id) | Delete a pricebook |
tq.duplicatePriceBook(id) | Duplicate a pricebook |
tq.listPriceBookProducts(id, options) | List products in a pricebook |
tq.listBundles(options) | List bundles |
tq.createBundle(req) | Create a bundle |
tq.getBundle(id) | Get bundle by ID |
tq.updateBundle(id, req) | Update a bundle |
tq.deleteBundle(id) | Delete a bundle |
tq.duplicateBundle(id) | Duplicate a bundle |
tq.listCompanies(options) | List companies |
tq.createCompany(req) | Create a company (contacts required) |
tq.getCompany(id) | Get company by ID |
tq.updateCompany(id, req) | Update a company |
tq.deleteCompany(id) | Delete a company |
tq.listCompanyContacts(companyId, options) | List contacts belonging to a company |
tq.listContacts(options) | List contacts |
tq.createContact(req) | Create a contact |
tq.updateContact(id, req) | Update a contact |
tq.deleteContact(id) | Delete a contact |
tq.listTemplates(options) | List quote templates |
tq.getTemplate() | Get the org's singleton quote template |
tq.getTemplateById(id) | Get a specific quote template by ID |
tq.createTemplate(req) | Create a quote template |
tq.updateTemplate(id, req) | Update a quote template |
tq.deleteTemplate(id) | Delete a quote template |
tq.listTypes(options) | List quote types |
tq.createType(req) | Create a quote type |
tq.updateType(id, req) | Update a quote type |
tq.deleteType(id) | Delete a quote type |
tq.createAndSend(req) | Convenience: create quote + add line items + add bundles + send in one call |
Gotchas
- Java SDK uses Builder pattern — create clients with
.Builder()...build() - `senderEmail` is required for TurboSign operations
- Spring Boot: use
@Valueorapplication.propertiesfor env vars, notSystem.getenv()directly - Spring Boot auto-scans controllers in sub-packages — ensure your controller is under the base package
- Partner API keys are distinct from regular API keys — using the wrong one returns
AuthenticationException - File input accepts:
byte[], file pathString, URLString, orInputStream - `signUrl` — each
RecipientResponsein thesendSignature/createSignatureReviewLinkresponse has agetSignUrl()method: the personal signing link for that recipient.CreateSignatureReviewLinkResponsealso hasgetPreviewUrl()for document-level preview. - `resendEmail` takes recipient UUIDs (
List<String>), not email addresses — fetch them from the send/review response orgetAuditTrail. - TurboWebhooks needs an admin TDX- key — the backend route gate is
requireOrgRole(administrator). Non-admin keys returnAuthorizationException(403). - `WebhookSignatureVerifier` is a static utility (final class, private constructor) — Java has no free functions, so call it as
WebhookSignatureVerifier.verify(...). Semantically equivalent to the free-function form in JS / Py / Go / PHP. - Read raw bytes for signature verification. In Spring, bind to
@RequestBody byte[] rawBody— neverMap/DTO. Jackson would re-serialize and whitespace mismatch breaks HMAC. In Servlets, userequest.getInputStream().readAllBytes(). - One webhook per org, fixed name `signature`. There is no
listWebhooksmethod by design — the SDK stays in sync with the dashboard's Signature Webhooks page. Use the REST API directly for multi-webhook setups. - Webhook secrets are shown ONCE — capture
created.get("secret").getAsString()immediately.regenerateWebhookSecret()returns a new one and invalidates the old immediately. - HTTPS-only URLs —
http://returnsValidationException(400). - Catch `ConflictException` (409) on `createWebhook` — the signature webhook may already exist from a previous run; update or delete instead.
- TurboQuote decimal fields come back as numbers, not strings. The Java
ResponseNormalizer(FlexIntAdapter) coerces string-serialized decimals (listPrice,unitPrice,discountPercent,grandTotal,subtotal, etc.) todouble. Do not attempt to parse them manually from the raw JSON. - `PATCH` null-clears nullable fields. On
updateQuote,updateLineItem, and similar PATCH methods, explicitly setting a field tonullsendsnullin the request body and clears the value on the server. Fields you never set are omitted from the request entirely and left unchanged. This matters for fields likepriceBookId,validUntil, andtaxRate. - `discountType` must be `"percent"` or `"amount"`. Use the
DiscountTypeenum constants (DiscountType.PERCENT/DiscountType.AMOUNT) to avoid silent 400 errors. Passing a raw string bypasses compile-time checking.
Full API reference: https://docs.turbodocx.com/docs
JavaScript/TypeScript SDK Reference
Install
# npm
npm install @turbodocx/sdk
# pnpm
pnpm add @turbodocx/sdk
# yarn
yarn add @turbodocx/sdk
# bun
bun add @turbodocx/sdkAlso install dotenv if not already present:
npm install dotenvAdd import 'dotenv/config' at the top of your entry point file.
Imports
// ESM (package.json "type": "module" or TypeScript)
import { TurboSign, TurboPartner, Deliverable, TurboWebhooks, TurboQuote } from '@turbodocx/sdk';
// CommonJS
const { TurboSign, TurboPartner, Deliverable, TurboWebhooks, TurboQuote } = require('@turbodocx/sdk');Only import what you use — for a project that only sends signatures, import only TurboSign.
---
TurboSign
Digital signature operations: prepare, send, track, download, void, resend, and audit-trail signed PDFs.
TurboSign.configure
TurboSign.configure({
apiKey: process.env.TURBODOCX_API_KEY!,
orgId: process.env.TURBODOCX_ORG_ID!,
senderEmail: process.env.TURBODOCX_SENDER_EMAIL!,
senderName: process.env.TURBODOCX_SENDER_NAME, // optional but recommended
});senderEmail is required — without it ValidationError is thrown. senderName defaults to "API Service User" if omitted.
TurboSign.createSignatureReviewLink
Upload a document with recipients and fields, but do not send emails — useful for previewing field placement.
const review = await TurboSign.createSignatureReviewLink({
file: pdfBuffer, // Buffer | string (path) | File | URL via fileLink | deliverableId | templateId
documentName: 'NDA - Acme',
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' },
],
});
console.log(review.documentId); // string
console.log(review.previewUrl); // string — open this URL to review the document
console.log(review.status); // stringResponse: { success, documentId, status, previewUrl?, recipients?, message }.
TurboSign.sendSignature
Upload a document with recipients and fields, and immediately email signature requests.
const result = await TurboSign.sendSignature({
file: pdfBuffer, // Buffer | path string | File | fileLink (URL) | deliverableId | templateId
documentName: 'Partnership Agreement',
recipients: [
{ name: 'John Doe', email: 'john@example.com', signingOrder: 1 },
{ name: 'Jane Smith', email: 'jane@example.com', signingOrder: 2 },
],
fields: [
{ type: 'signature', page: 1, x: 100, y: 500, width: 200, height: 50, recipientEmail: 'john@example.com' },
{ type: 'signature', page: 1, x: 100, y: 600, width: 200, height: 50, recipientEmail: 'jane@example.com' },
],
});
console.log(result.documentId); // string
console.log(result.status); // string — e.g., 'sent'
console.log(result.recipients); // ReviewRecipient[] with { id, name, email, metadata? }Fields support either coordinate-based (page + x / y / width / height) or anchor-based placement via template: { anchor: '{TagName}', placement: 'replace', size: {...} }.
TurboSign.getStatus
const status = await TurboSign.getStatus(documentId);
console.log(status.status); // e.g., 'under_review', 'completed', 'voided', 'sent'Response: { status: string }. For per-recipient state, use getAuditTrail.
TurboSign.download
const blob = await TurboSign.download(documentId);
// In Node, persist to disk:
import { writeFile } from 'node:fs/promises';
const arrayBuffer = await blob.arrayBuffer();
await writeFile('signed.pdf', Buffer.from(arrayBuffer));
// In the browser:
const url = URL.createObjectURL(blob);Returns a Blob (the SDK fetches the presigned URL and the binary in two steps for you).
TurboSign.void
const voided = await TurboSign.void(documentId, 'Counterparty requested changes');
console.log(voided.status); // 'voided'
console.log(voided.voidedAt); // ISO timestampreason is required.
TurboSign.resend
// recipientIds — fetch these from sendSignature/createSignatureReviewLink response
// or getAuditTrail entries.
const result = await TurboSign.resend(documentId, ['recipient-uuid-1', 'recipient-uuid-2']);
console.log(result.success, result.recipientCount);recipientIds is an array of recipient UUIDs, not email addresses.
TurboSign.getAuditTrail
const audit = await TurboSign.getAuditTrail(documentId);
console.log(audit.document.name);
for (const entry of audit.auditTrail) {
console.log(entry.timestamp, entry.actionType, entry.user?.email);
}Response: { document: { id, name }, auditTrail: AuditTrailEntry[] }. Each entry has id, documentId, actionType, timestamp, user?, recipient?, details? and hash fields for tamper-evident chaining.
---
Deliverable
Document generation: render a TurboDocx template with variable substitution into a deliverable (DOCX/PPTX), then download it or hand its ID to TurboSign as the source document.
Deliverable.configure
Deliverable.configure({
apiKey: process.env.TURBODOCX_API_KEY!,
orgId: process.env.TURBODOCX_ORG_ID!,
});No senderEmail needed — Deliverable doesn't send email.
Deliverable.generateDeliverable
Generate a document from a template with variable substitution.
const result = await Deliverable.generateDeliverable({
templateId: 'template-uuid',
name: 'Employee Contract - John Smith',
variables: [
{ placeholder: '{EmployeeName}', text: 'John Smith', mimeType: 'text' },
{ placeholder: '{CompanyName}', text: 'TechCorp Inc.', mimeType: 'text' },
{ placeholder: '{StartDate}', text: '2026-06-01', mimeType: 'text' },
],
description: 'Generated via API for HR onboarding',
tags: ['hr', 'contract'],
});
const deliverable = result.results.deliverable;
console.log(deliverable.id, deliverable.name, deliverable.fileType);mimeType is one of 'text' | 'html' | 'image' | 'markdown'. For repeating content (tables, lists), use variableStack on a DeliverableVariable.
You can pass the resulting deliverable.id straight to TurboSign.sendSignature({ deliverableId: ... }) to generate-then-sign in one workflow.
Deliverable.listDeliverables
const list = await Deliverable.listDeliverables({
limit: 20, // 1-100, default 6
offset: 0,
query: 'contract',
showTags: true,
});
console.log(list.totalRecords);
for (const d of list.results) {
console.log(d.id, d.name, d.createdOn);
}Response: { results: DeliverableRecord[], totalRecords: number }.
Deliverable.getDeliverableDetails
const d = await Deliverable.getDeliverableDetails(deliverableId, { showTags: true });
console.log(d.name, d.templateName, d.variables, d.tags);Returns a full DeliverableRecord including variables and (when showTags: true) tags.
Deliverable.updateDeliverableInfo
const result = await Deliverable.updateDeliverableInfo(deliverableId, {
name: 'Employee Contract - John Smith (Final)',
description: 'Finalized version',
tags: ['hr', 'contract', 'finalized'], // replaces all existing tags
});
console.log(result.message, result.deliverableId);Passing tags replaces the full tag set. To remove all tags, pass tags: []. To add a tag, fetch existing tags first and append.
Deliverable.deleteDeliverable
const result = await Deliverable.deleteDeliverable(deliverableId);
console.log(result.message); // soft delete — data is retained but hidden from listDeliverable.downloadSourceFile
const arrayBuffer = await Deliverable.downloadSourceFile(deliverableId);
// Node:
import { writeFile } from 'node:fs/promises';
await writeFile('contract.docx', Buffer.from(arrayBuffer));
// Browser:
const blob = new Blob([arrayBuffer]);
const url = URL.createObjectURL(blob);Returns the original DOCX/PPTX as ArrayBuffer. Requires hasFileDownload entitlement.
Deliverable.downloadPDF
const arrayBuffer = await Deliverable.downloadPDF(deliverableId);
// Same persistence pattern as downloadSourceFile, but it's a PDF.
const blob = new Blob([arrayBuffer], { type: 'application/pdf' });---
TurboPartner
Partner-portal operations: provision and manage customer organizations, their users, API keys, entitlements, and audit logs.
TurboPartner.configure
TurboPartner.configure({
partnerApiKey: process.env.TURBODOCX_PARTNER_API_KEY!,
partnerId: process.env.TURBODOCX_PARTNER_ID!,
});Organization management
// Create
const org = await TurboPartner.createOrganization({
name: 'Acme Corp',
metadata: { industry: 'Technology' },
features: { maxUsers: 50, hasTDAI: true }, // optional initial entitlements
});
console.log(org.data.id);
// List (uses offset, not page)
const orgs = await TurboPartner.listOrganizations({ limit: 20, offset: 0, search: 'acme' });
console.log(orgs.data.totalRecords);
orgs.data.results.forEach((o) => console.log(o.id, o.name));
// Get details (includes features + tracking)
const details = await TurboPartner.getOrganizationDetails('org-uuid');
console.log(details.data.features, details.data.tracking);
// Update name
await TurboPartner.updateOrganizationInfo('org-uuid', { name: 'Acme Holdings' });
// Delete
await TurboPartner.deleteOrganization('org-uuid');
// Update entitlements — features and tracking are separate top-level keys
await TurboPartner.updateOrganizationEntitlements('org-uuid', {
features: { maxUsers: 100, hasTDAI: true, hasSalesforce: true },
tracking: { numUsers: 12 }, // optional: seed usage counters
});Organization user management
// List
const users = await TurboPartner.listOrganizationUsers('org-uuid', { limit: 25, offset: 0 });
// Invite
const user = await TurboPartner.addUserToOrganization('org-uuid', {
email: 'newhire@acme.com',
role: 'contributor', // 'admin' | 'contributor' | 'user' | 'viewer'
});
// Update role
await TurboPartner.updateOrganizationUserRole('org-uuid', 'user-uuid', { role: 'admin' });
// Remove
await TurboPartner.removeUserFromOrganization('org-uuid', 'user-uuid');
// Resend invitation email
await TurboPartner.resendOrganizationInvitationToUser('org-uuid', 'user-uuid');Organization API key management
// List keys for an org
const keys = await TurboPartner.listOrganizationApiKeys('org-uuid', { limit: 10 });
// Create — the full key value is returned ONLY on creation, store it immediately
const created = await TurboPartner.createOrganizationApiKey('org-uuid', {
name: 'Production Key',
role: 'admin',
});
console.log(created.data.key); // capture this once, it won't be shown again
// Update (e.g., rename)
await TurboPartner.updateOrganizationApiKey('org-uuid', 'key-uuid', { name: 'Renamed' });
// Revoke
await TurboPartner.revokeOrganizationApiKey('org-uuid', 'key-uuid');Partner API key management
// List
const keys = await TurboPartner.listPartnerApiKeys({ limit: 10 });
// Create with scopes — full key returned only on creation
const created = await TurboPartner.createPartnerApiKey({
name: 'CI/CD Key',
scopes: ['org:create', 'org:read', 'entitlements:update'],
description: 'Used by GitHub Actions',
});
console.log(created.data.key); // store this immediately
// Update name / scopes
await TurboPartner.updatePartnerApiKey('key-uuid', {
name: 'CI/CD Key (extended)',
scopes: ['org:create', 'org:read', 'org:update', 'entitlements:update'],
});
// Revoke
await TurboPartner.revokePartnerApiKey('key-uuid');Available scopes (see PartnerScope in the SDK types) cover org:*, entitlements:update, org-users:*, partner-users:*, org-apikeys:*, partner-apikeys:*, and audit:read.
Partner-portal user management
// List
const users = await TurboPartner.listPartnerPortalUsers({ limit: 25 });
// Add (permissions are required on add — list every flag explicitly)
const user = await TurboPartner.addUserToPartnerPortal({
email: 'admin@partner.com',
role: 'admin', // 'admin' | 'member' | 'viewer'
permissions: {
canManageOrgs: true,
canManageOrgUsers: true,
canManagePartnerUsers: false,
canManageOrgAPIKeys: true,
canManagePartnerAPIKeys: false,
canUpdateEntitlements: true,
canViewAuditLogs: true,
},
});
// Update — permissions can be partial here
await TurboPartner.updatePartnerUserPermissions('user-uuid', {
role: 'member',
permissions: { canManageOrgs: true, canManageOrgUsers: true },
});
// Remove
await TurboPartner.removeUserFromPartnerPortal('user-uuid');
// Resend invitation
await TurboPartner.resendPartnerPortalInvitationToUser('user-uuid');Audit logs
const logs = await TurboPartner.getPartnerAuditLogs({
action: 'org.created',
resourceType: 'organization',
startDate: '2026-01-01',
endDate: '2026-12-31',
success: true,
limit: 100,
offset: 0,
});
console.log(logs.data.totalRecords);
for (const entry of logs.data.results) {
console.log(entry.createdOn, entry.action, entry.resourceId, entry.success);
}---
TurboWebhooks
TurboWebhooks subscribes a single per-org HTTPS endpoint (locked to the name signature) to TurboDocx events such as signature.document.completed and signature.document.voided. The SDK is intentionally one-webhook-per-org to mirror the dashboard's Signature Webhooks page.
TurboWebhooks.configure
import { TurboWebhooks } from '@turbodocx/sdk';
TurboWebhooks.configure({
apiKey: process.env.TURBODOCX_API_KEY!, // admin TDX- key
orgId: process.env.TURBODOCX_ORG_ID!,
});skipSenderValidation: true is hardcoded inside configure() because webhooks don't send email — only TurboSign needs senderEmail. The webhook routes require the organization administrator role; a non-admin TDX- key returns AuthorizationError (HTTP 403).
createWebhook
const created = await TurboWebhooks.createWebhook({
urls: ['https://your-server.example.com/webhooks/turbodocx'], // must be HTTPS
events: ['signature.document.completed', 'signature.document.voided'],
});
// Returned secret is shown ONCE — store it server-side immediately.
const { id, secret } = created;Throws ConflictError (409) if the signature webhook already exists for the org. Throws ValidationError (400) for non-HTTPS URLs.
getWebhook
const webhook = await TurboWebhooks.getWebhook();
// webhook.urls, webhook.events, webhook.isActive
// webhook.deliveryStats.{totalDeliveries, successfulDeliveries, failedDeliveries, pendingRetries}updateWebhook
await TurboWebhooks.updateWebhook({
urls: ['https://your-server.example.com/webhooks/turbodocx'],
events: ['signature.document.completed'],
isActive: true,
});All three fields are optional — pass only what you want to change.
deleteWebhook
await TurboWebhooks.deleteWebhook(); // soft-delete + delivery history wipedtestWebhook
const result = await TurboWebhooks.testWebhook({
eventType: 'signature.document.completed',
payload: { documentId: '...', documentName: '...' },
});
// result.summary: { total, successful, failed, errors: string[] }
// result.deliveries: WebhookDelivery[]Per-URL failure messages live in summary.errors. Use this from a CI smoke test before flipping a new receiver into production.
regenerateWebhookSecret
const rotated = await TurboWebhooks.regenerateWebhookSecret();
// rotated.secret — shown ONCE; old signatures fail immediately after rotationlistWebhookDeliveries
const page = await TurboWebhooks.listWebhookDeliveries({
limit: 20,
offset: 0,
eventType: 'signature.document.completed',
isDelivered: false,
httpStatus: 500,
});
// page.results: WebhookDelivery[]; page.totalRecordsreplayWebhookDelivery
const newDelivery = await TurboWebhooks.replayWebhookDelivery(deliveryId);
// Full WebhookDelivery row returned — id, httpStatus, attemptCount, etc.getWebhookStats
const stats = await TurboWebhooks.getWebhookStats({ days: 30 });
// stats.summary.{successRate, avgResponseTime, ...}
// stats.eventBreakdown — per-event totalsVerifying inbound webhook signatures
When TurboDocx POSTs to your receiver, verify the X-TurboDocx-Signature header before trusting the payload. The helper enforces a 5-minute timestamp tolerance and uses constant-time comparison.
import express from 'express';
import { verifyWebhookSignature } from '@turbodocx/sdk';
const app = express();
// CRITICAL: mount express.raw FOR THE WEBHOOK PATH BEFORE any global
// express.json(). Express body-parsers set req._body=true on the first
// parse and later parsers no-op. If app.use(express.json()) runs first,
// the route-level express.raw() below silently becomes a no-op and
// req.body is a parsed object instead of a Buffer — verification then
// always fails.
app.use(
'/webhooks/turbodocx',
express.raw({ type: 'application/json' }),
);
// Now safe to install JSON parsing for the rest of the app.
app.use(express.json());
app.post('/webhooks/turbodocx', (req, res) => {
const signature = req.header('x-turbodocx-signature') ?? '';
const timestamp = req.header('x-turbodocx-timestamp') ?? '';
const secret = process.env.TURBODOCX_WEBHOOK_SECRET!;
// req.body is a Buffer because express.raw ran first for this path.
if (!verifyWebhookSignature(req.body, signature, timestamp, secret)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
// process event.eventType, event.data, ...
res.status(200).send('ok');
});If your app already calls app.use(express.json()) globally, move it to AFTER the app.use('/webhooks/turbodocx', express.raw(...)) line shown above — order matters.
Canonical end-to-end JavaScript example: `packages/js-sdk/examples/turbowebhooks-crud.ts` walks through create → conflict → get → update → test-fire → rotate → list → delete + every error branch.
TurboWebhooks error handling
import {
TurboDocxError,
AuthenticationError,
AuthorizationError,
ValidationError,
ConflictError,
NotFoundError,
RateLimitError,
NetworkError,
} from '@turbodocx/sdk';
try {
await TurboWebhooks.createWebhook({ urls, events });
} catch (e) {
if (e instanceof ConflictError) /* 409 — already exists; update or delete */;
else if (e instanceof ValidationError) /* 400 — non-HTTPS URL or empty events */;
else if (e instanceof AuthorizationError) /* 403 — TDX- key lacks administrator role */;
else if (e instanceof AuthenticationError) /* 401 — bad / revoked API key */;
else if (e instanceof NotFoundError) /* 404 — webhook does not exist */;
else if (e instanceof RateLimitError) /* 429 — back off and retry */;
else if (e instanceof NetworkError) /* never reached the server */;
else if (e instanceof TurboDocxError) /* other typed SDK error (e.g. 5xx) */;
else throw e;
}---
TurboQuote
Sales quoting operations: build a product catalog, assemble quotes with line items and bundles, apply price books, and send quotes to customers. Includes full CRUD for quotes, products, bundles, price books, companies, contacts, templates, and types.
TurboQuote.configure
import { TurboQuote } from '@turbodocx/sdk';
TurboQuote.configure({
apiKey: process.env.TURBODOCX_API_KEY!, // required (or accessToken)
orgId: process.env.TURBODOCX_ORG_ID!, // required — backend returns 401 if missing
});No senderEmail needed — TurboQuote never sends signature emails. orgId is technically optional in the config type but the backend rejects requests without it; always provide it.
createQuote
const quote = await TurboQuote.createQuote({
name: 'Professional Services — Q3 2026',
companyId: 'company-uuid',
contactId: 'contact-uuid',
currency: 'USD',
validUntil: '2026-09-30',
});
console.log(quote.id); // string
console.log(quote.quoteNumber); // human-readable number e.g. 'Q-0042'
console.log(quote.status); // 'draft'Response: a Quote object. Numeric fields such as subtotal, grandTotal, and taxRate are returned as JavaScript number (the SDK's response normalizer coerces the backend's decimal strings automatically).
addLineItems
// Single item (auto-wrapped to array)
const items = await TurboQuote.addLineItems(quote.id, {
productId: 'product-uuid',
productName: 'Consulting Service',
unitPrice: 500,
billingFrequency: 'monthly', // 'monthly' | 'quarterly' | 'annual' | 'one-time'
quantity: 3,
discountType: 'percent', // 'percent' | 'amount'
discountPercent: 10,
});
console.log(items[0].id); // LineItem UUID
console.log(items[0].finalPrice); // number — already normalised
// Multiple items at once — custom (no-product) items require productId: null explicitly
const bulkItems = await TurboQuote.addLineItems(quote.id, [
{ productId: null, productName: 'Setup Fee', unitPrice: 1500, billingFrequency: 'one-time', quantity: 1 },
{ productId: null, productName: 'License', unitPrice: 200, billingFrequency: 'monthly', quantity: 10 },
]);addBundleLineItems
const bundleItems = await TurboQuote.addBundleLineItems(quote.id, {
bundleId: 'bundle-uuid',
bundleName: 'Starter Bundle',
quantity: 2,
});
console.log(bundleItems[0].id);sendQuote
const sent = await TurboQuote.sendQuote(quote.id);
console.log(sent.message); // 'Quote sent successfully'
console.log(sent.quote.status); // 'sent'downloadQuotePdf
import { writeFile } from 'node:fs/promises';
const pdf = await TurboQuote.downloadQuotePdf(quote.id);
await writeFile('quote.pdf', Buffer.from(pdf)); // pdf is ArrayBufferCatalog management (products, bundles, price books)
// Products
const product = await TurboQuote.createProduct({
name: 'Enterprise License',
listPrice: 1200,
billingFrequency: 'annual',
categoryId: 'category-uuid', // required — from a createType({ categoryType: 'product_category' })
showInCatalog: true,
});
console.log(product.id);
// Bundles
const bundle = await TurboQuote.createBundle({
name: 'Starter Pack',
categoryId: 'bundle-category-uuid', // required — from a createType({ categoryType: 'bundle_category' })
items: [{ productId: product.id, unitPrice: 1200, billingFrequency: 'annual', quantity: 1 }],
});
// Price books — name + priceBookTypeId + validFrom + discountPercent are ALL required on create
const priceBook = await TurboQuote.createPriceBook({
name: 'Enterprise Pricing',
priceBookTypeId: 'pricebook-type-uuid', // from a createType({ categoryType: 'pricebook_type' })
validFrom: '2026-01-01',
discountPercent: 15,
});
const applied = await TurboQuote.applyPriceBook(quote.id, priceBook.id);
console.log(applied.updatedCount, applied.skippedCount);Convenience: createAndSend
// Create a quote, add line items, and send in one call
const result = await TurboQuote.createAndSend({
// Quote fields
name: 'Q3 Renewal',
companyId: 'company-uuid',
contactId: 'contact-uuid',
currency: 'USD',
// Line items (custom no-product item needs productId: null explicitly)
items: [
{ productId: null, productName: 'Support Plan', unitPrice: 800, billingFrequency: 'annual', quantity: 1 },
],
// Send options (passed to the underlying sendQuote call)
send: {},
});
console.log(result.quote.status); // 'sent'TurboQuote error handling
import {
TurboDocxError,
AuthenticationError,
AuthorizationError,
ValidationError,
NotFoundError,
RateLimitError,
} from '@turbodocx/sdk';
try {
await TurboQuote.sendQuote(quoteId);
} catch (e) {
if (e instanceof ValidationError) /* 400 — bad field value, missing required field */;
else if (e instanceof AuthenticationError) /* 401 — bad / missing API key or orgId */;
else if (e instanceof NotFoundError) /* 404 — quote or resource not found */;
else if (e instanceof RateLimitError) /* 429 — back off and retry */;
else if (e instanceof TurboDocxError) /* other typed SDK error */;
else throw e;
}---
Express Integration Example
import { Router, Request, Response } from 'express';
import multer from 'multer';
import { TurboSign, Deliverable } from '../lib/turbodocx';
const upload = multer({ storage: multer.memoryStorage() });
const router = Router();
// POST /api/signatures/send — upload a PDF and send for signature
router.post('/send', upload.single('file'), async (req: Request, res: Response) => {
try {
const { recipients, fields, documentName } = req.body;
const result = await TurboSign.sendSignature({
file: req.file!.buffer,
documentName,
recipients: JSON.parse(recipients),
fields: JSON.parse(fields),
});
res.json(result);
} catch (error) {
handleError(res, error);
}
});
// GET /api/signatures/:id/status
router.get('/:id/status', async (req: Request, res: Response) => {
try {
res.json(await TurboSign.getStatus(req.params.id));
} catch (error) { handleError(res, error); }
});
// GET /api/signatures/:id/download — stream the signed PDF
router.get('/:id/download', async (req: Request, res: Response) => {
try {
const blob = await TurboSign.download(req.params.id);
const arrayBuffer = await blob.arrayBuffer();
res.setHeader('Content-Type', 'application/pdf');
res.send(Buffer.from(arrayBuffer));
} catch (error) { handleError(res, error); }
});
// POST /api/signatures/:id/void
router.post('/:id/void', async (req: Request, res: Response) => {
try {
res.json(await TurboSign.void(req.params.id, req.body.reason));
} catch (error) { handleError(res, error); }
});
// POST /api/signatures/:id/resend (body: { recipientIds: string[] })
router.post('/:id/resend', async (req: Request, res: Response) => {
try {
res.json(await TurboSign.resend(req.params.id, req.body.recipientIds));
} catch (error) { handleError(res, error); }
});
// GET /api/signatures/:id/audit-trail
router.get('/:id/audit-trail', async (req: Request, res: Response) => {
try {
res.json(await TurboSign.getAuditTrail(req.params.id));
} catch (error) { handleError(res, error); }
});
// POST /api/deliverables — generate from a template then optionally send for signature
router.post('/deliverables', async (req: Request, res: Response) => {
try {
const { templateId, name, variables, sendToEmail } = req.body;
const { results } = await Deliverable.generateDeliverable({ templateId, name, variables });
if (sendToEmail) {
// Generate-then-sign: hand the deliverableId straight to TurboSign
const signResult = await TurboSign.sendSignature({
deliverableId: results.deliverable.id,
documentName: name,
recipients: [{ name: 'Signer', email: sendToEmail, signingOrder: 1 }],
fields: [{ type: 'signature', template: { anchor: '{signature1}', placement: 'replace', size: { width: 200, height: 50 } }, recipientEmail: sendToEmail }],
});
res.json({ deliverable: results.deliverable, signature: signResult });
} else {
res.json(results.deliverable);
}
} catch (error) { handleError(res, error); }
});
export default router;The helper handleError is defined in the next section.
---
Error Handling
import {
TurboDocxError,
AuthenticationError,
ValidationError,
NotFoundError,
RateLimitError,
NetworkError,
} from '@turbodocx/sdk';
function handleError(res: Response, error: unknown) {
if (error instanceof AuthenticationError) return res.status(401).json({ error: error.message });
if (error instanceof ValidationError) return res.status(400).json({ error: error.message });
if (error instanceof NotFoundError) return res.status(404).json({ error: error.message });
if (error instanceof RateLimitError) return res.status(429).json({ error: error.message });
if (error instanceof NetworkError) return res.status(503).json({ error: error.message });
if (error instanceof TurboDocxError) return res.status(error.statusCode ?? 500).json({ error: error.message, code: error.code });
console.error(error);
return res.status(500).json({ error: 'Internal error' });
}All TurboDocx errors extend TurboDocxError and carry statusCode and code properties. The five specific subtypes above map to HTTP 401 / 400 / 404 / 429 / network failure respectively. Import them directly from @turbodocx/sdk — they are not namespaced under TurboSign.
---
Method Reference
TurboSign
| Method | Description |
|---|---|
TurboSign.configure(config) | Set apiKey, orgId, senderEmail, senderName |
TurboSign.createSignatureReviewLink(request) | Prepare a document and get a preview URL (no emails sent) |
TurboSign.sendSignature(request) | Prepare a document and immediately email recipients |
TurboSign.getStatus(documentId) | Get current document status string |
TurboSign.download(documentId) | Download signed PDF as Blob |
TurboSign.void(documentId, reason) | Cancel a signature request (reason is required) |
TurboSign.resend(documentId, recipientIds) | Resend signature email to recipient IDs (array of UUIDs) |
TurboSign.getAuditTrail(documentId) | Get tamper-evident audit log with all events |
Deliverable
| Method | Description |
|---|---|
Deliverable.configure(config) | Set apiKey, orgId |
Deliverable.generateDeliverable(request) | Render a template with variables into a new deliverable |
Deliverable.listDeliverables(options?) | Paginated list with search and tag filters |
Deliverable.getDeliverableDetails(id, options?) | Get full record including variables and fonts |
Deliverable.updateDeliverableInfo(id, request) | Update name, description, or tags (tags replace) |
Deliverable.deleteDeliverable(id) | Soft-delete (data retained, hidden from list) |
Deliverable.downloadSourceFile(id) | Download original DOCX/PPTX as ArrayBuffer |
Deliverable.downloadPDF(id) | Download rendered PDF as ArrayBuffer |
TurboPartner — Organizations
| Method | Description |
|---|---|
TurboPartner.configure(config) | Set partnerApiKey, partnerId |
TurboPartner.createOrganization(request) | Provision a new customer org |
TurboPartner.listOrganizations(request?) | List orgs (uses limit / offset, not page) |
TurboPartner.getOrganizationDetails(orgId) | Get org details including features + tracking |
TurboPartner.updateOrganizationInfo(orgId, request) | Rename an org |
TurboPartner.deleteOrganization(orgId) | Delete an org |
TurboPartner.updateOrganizationEntitlements(orgId, request) | Update features and/or tracking |
TurboPartner — Organization Users
| Method | Description |
|---|---|
TurboPartner.listOrganizationUsers(orgId, request?) | Paginated list |
TurboPartner.addUserToOrganization(orgId, request) | Invite a user with role |
TurboPartner.updateOrganizationUserRole(orgId, userId, request) | Change a user's role |
TurboPartner.removeUserFromOrganization(orgId, userId) | Remove from org |
TurboPartner.resendOrganizationInvitationToUser(orgId, userId) | Resend invite email |
TurboPartner — Organization API Keys
| Method | Description |
|---|---|
TurboPartner.listOrganizationApiKeys(orgId, request?) | Paginated list |
TurboPartner.createOrganizationApiKey(orgId, request) | Create key (key value returned only on creation) |
TurboPartner.updateOrganizationApiKey(orgId, keyId, request) | Rename or change role |
TurboPartner.revokeOrganizationApiKey(orgId, keyId) | Revoke key |
TurboPartner — Partner API Keys
| Method | Description |
|---|---|
TurboPartner.listPartnerApiKeys(request?) | Paginated list |
TurboPartner.createPartnerApiKey(request) | Create key with scopes |
TurboPartner.updatePartnerApiKey(keyId, request) | Rename, edit scopes |
TurboPartner.revokePartnerApiKey(keyId) | Revoke key |
TurboPartner — Partner Portal Users
| Method | Description |
|---|---|
TurboPartner.listPartnerPortalUsers(request?) | Paginated list |
TurboPartner.addUserToPartnerPortal(request) | Invite with role and permissions |
TurboPartner.updatePartnerUserPermissions(userId, request) | Update role/permissions (partial OK) |
TurboPartner.removeUserFromPartnerPortal(userId) | Remove user |
TurboPartner.resendPartnerPortalInvitationToUser(userId) | Resend invite email |
TurboPartner — Audit Logs
| Method | Description |
|---|---|
TurboPartner.getPartnerAuditLogs(request?) | Filter by action, resource, success, date range |
TurboWebhooks
| Method | Description |
|---|---|
TurboWebhooks.configure(config) | Set apiKey, orgId (skipSenderValidation is hardcoded) |
TurboWebhooks.createWebhook({ urls, events }) | Subscribe the org to events (HTTPS URLs only) |
TurboWebhooks.getWebhook() | Get the org's signature webhook + delivery stats |
TurboWebhooks.updateWebhook(patch) | Patch urls / events / isActive |
TurboWebhooks.deleteWebhook() | Soft-delete the webhook |
TurboWebhooks.testWebhook({ eventType, payload }) | Fire a test delivery; surfaces per-URL errors |
TurboWebhooks.notifyWebhook({ eventType, payload }) | Manual notify; same handler as testWebhook |
TurboWebhooks.regenerateWebhookSecret() | Rotate the HMAC secret (shown ONCE) |
TurboWebhooks.listWebhookDeliveries(filters?) | Paginated delivery history with filters |
TurboWebhooks.replayWebhookDelivery(deliveryId) | Retry a past delivery; returns the new delivery row |
TurboWebhooks.getWebhookStats({ days? }) | Aggregate stats over a sliding window |
verifyWebhookSignature(rawBody, sigHeader, tsHeader, secret, opts?) | Free function; verifies inbound deliveries |
TurboQuote — Quotes
| Method | Description |
|---|---|
TurboQuote.configure(config) | Set apiKey, orgId (no senderEmail needed) |
TurboQuote.listQuotes(options?) | Paginated list with filters; includes totals/stats |
TurboQuote.createQuote(request) | Create a new draft quote |
TurboQuote.getQuote(id) | Get quote details (statusInfo merged in) |
TurboQuote.updateQuote(id, request) | PATCH quote fields; pass explicit null to clear nullable fields |
TurboQuote.deleteQuote(id) | Delete a quote |
TurboQuote.duplicateQuote(id) | Clone a quote to a new draft |
TurboQuote.sendQuote(id, request?) | Email quote to customer; returns { quote, message } |
TurboQuote.sendQuoteWithDeliverable(id, request) | Send with attached TurboDocx deliverable; returns { quote, message, documentId } |
TurboQuote.declineQuote(id, { reason }) | Mark as declined |
TurboQuote.voidQuote(id, { reason }) | Void a sent quote |
TurboQuote.handleExpiredQuote(id, request) | Handle an expired-sent quote (extend, re-send, or void) |
TurboQuote.applyPriceBook(quoteId, priceBookId) | Apply price-book pricing to all matching line items |
TurboQuote.removePriceBook(quoteId) | Detach price book from quote |
TurboQuote.downloadQuotePdf(id) | Download rendered quote PDF as ArrayBuffer |
TurboQuote.createAndSend(request) | Convenience: create quote + add items + send in one call |
TurboQuote — Line Items
| Method | Description |
|---|---|
TurboQuote.listLineItems(quoteId, options?) | List line items for a quote |
TurboQuote.addLineItems(quoteId, items) | Add one or more product line items (auto-wraps single object to array) |
TurboQuote.addBundleLineItems(quoteId, items) | Add bundle line items |
TurboQuote.updateLineItem(quoteId, itemId, request) | PATCH a single line item |
TurboQuote.removeLineItem(quoteId, itemId) | Delete a line item |
TurboQuote — Products
| Method | Description |
|---|---|
TurboQuote.listProducts(options?) | Paginated product catalog |
TurboQuote.createProduct(request) | Create a product (uses multipart when images array is provided) |
TurboQuote.getProduct(id) | Get a single product |
TurboQuote.updateProduct(id, request) | PATCH a product; multipart when images included |
TurboQuote.deleteProduct(id) | Delete a product |
TurboQuote.duplicateProduct(id) | Clone a product |
TurboQuote.getProductPrimaryImages(productIds) | Batch-fetch primary images; returns `{ [productId]: image \ |
TurboQuote — Bundles
| Method | Description |
|---|---|
TurboQuote.listBundles(options?) | Paginated bundle catalog |
TurboQuote.createBundle(request) | Create a bundle |
TurboQuote.getBundle(id) | Get a single bundle |
TurboQuote.updateBundle(id, request) | PATCH a bundle |
TurboQuote.deleteBundle(id) | Delete a bundle |
TurboQuote.duplicateBundle(id) | Clone a bundle |
TurboQuote — Price Books
| Method | Description |
|---|---|
TurboQuote.listPriceBooks(options?) | Paginated price book list |
TurboQuote.createPriceBook(request) | Create a price book |
TurboQuote.getPriceBook(id) | Get a single price book |
TurboQuote.updatePriceBook(id, request) | PATCH a price book |
TurboQuote.deletePriceBook(id) | Delete a price book |
TurboQuote.duplicatePriceBook(id) | Clone a price book |
TurboQuote.listPriceBookProducts(id, options?) | List products attached to a price book |
TurboQuote — Companies and Contacts
| Method | Description |
|---|---|
TurboQuote.listCompanies(options?) | Paginated company list |
TurboQuote.createCompany(request) | Create a company (contacts array with at least one entry required) |
TurboQuote.getCompany(id) | Get a single company |
TurboQuote.updateCompany(id, request) | PATCH a company |
TurboQuote.deleteCompany(id) | Delete a company |
TurboQuote.listCompanyContacts(companyId, options?) | List contacts belonging to a company |
TurboQuote.listContacts(options?) | Paginated contact list across all companies |
TurboQuote.createContact(request) | Create a standalone contact |
TurboQuote.updateContact(id, request) | PATCH a contact |
TurboQuote.deleteContact(id) | Delete a contact |
TurboQuote — Templates and Types
| Method | Description |
|---|---|
TurboQuote.listTemplates(options?) | List all quote templates |
TurboQuote.getTemplate() | Get the org's singleton default template (/v1/quote-template) |
TurboQuote.getTemplateById(id) | Get a specific template by ID |
TurboQuote.createTemplate(request) | Create a quote template |
TurboQuote.updateTemplate(id, request) | PATCH a template |
TurboQuote.deleteTemplate(id) | Delete a template |
TurboQuote.listTypes(options?) | List quote types/categories |
TurboQuote.createType(request) | Create a quote type |
TurboQuote.updateType(id, request) | PATCH a type name |
TurboQuote.deleteType(id) | Delete a type |
---
Gotchas
- `senderEmail` is required for
TurboSign.configure()— without itValidationErroris thrown.senderNameis optional but strongly recommended (otherwise emails appear from "API Service User"). - File input to TurboSign accepts:
Buffer, file-pathstring, browserFile, remote URL (fileLink),deliverableId, ortemplateId. Magic-byte detection identifies PDF / DOCX / PPTX automatically. - Template anchors like
{signature1}must literally exist in the document text fortemplate.anchorfield placement to work. - Pagination uses `offset`, not `page`, across all
list*methods. Defaults vary (Deliverable defaults to 6, partner list endpoints to ~25). - `updateOrganizationEntitlements` takes `{ features?, tracking? }` — not a bare features object. Tracking lets you seed usage counters.
- `TurboSign.void` requires a `reason` as the second argument. `TurboSign.resend` takes recipient IDs (UUIDs), not email addresses — fetch them from the send response or audit trail.
- `TurboSign.download` returns `Blob`, not
Buffer. Callawait blob.arrayBuffer()thenBuffer.from(...)in Node. - API key values are only returned on creation for both
createOrganizationApiKeyandcreatePartnerApiKey. Storecreated.data.keyimmediately — subsequent lookups omit it. - Updating tags via `updateDeliverableInfo` replaces the full set — fetch existing tags first if you want to add one.
- TypeScript users get full type definitions out of the box — no
@types/package needed. - TurboWebhooks requires an admin TDX- key. The backend route gate is
requireOrgRole(administrator)— a non-admin key returns 403AuthorizationError. - One webhook per org, fixed name `signature`. The SDK is hardcoded to
/api/webhooks/signatureto stay in sync with the dashboard's Signature Webhooks page. There is nolistWebhooksby design. For multi-webhook management call the REST API directly. - Webhook secrets are shown ONCE — capture
created.secretfromcreateWebhookandrotated.secretfromregenerateWebhookSecretimmediately. They are never returned again bygetWebhookor any other endpoint. - Webhook URLs must be HTTPS. Non-HTTPS URLs return 400
ValidationErrorfrom the backend. - Use `express.raw({ type: 'application/json' })` on your receiver route, not `express.json()`. Signature verification is computed over the raw bytes; a JSON re-stringify will not match.
- Middleware ORDER matters. Mount
express.raw()for the webhook path BEFORE any globalapp.use(express.json()). Express body-parsers setreq._body=trueon the first parse and later parsers silently no-op — ifexpress.json()is global and runs first for/webhooks, the route-levelexpress.raw()becomes a no-op andreq.bodyends up as a parsed object, not a Buffer. Verification will then always fail. The correct pattern isapp.use('/webhooks/turbodocx', express.raw({ type: 'application/json' }))BEFOREapp.use(express.json()). - `verifyWebhookSignature` is a free function, not a method on
TurboWebhooks— import it directly from@turbodocx/sdk. It has noapiKey/orgIddependency.
- TurboQuote decimal fields come back as `number`, not strings. The SDK's response normalizer coerces the backend's decimal strings (
listPrice,unitPrice,grandTotal,taxRate,discountPercent, etc.) to JavaScript numbers before returning — do not parse them withparseFloat. - `PATCH` with explicit `null` clears nullable fields. For
updateQuote,updateLineItem,updateProduct, and similar PATCH methods, passing{ priceBookId: null }sendsnullin the request body and the backend clears the field. Omitting the key entirely leaves it unchanged. This is intentional for fields likevalidUntil,taxRate,priceBookId. - `discountType` is `'percent' | 'amount'` on line items. When using
'percent', setdiscountPercent(0–100). When using'amount', set the flat discount value. Mixing both in the same item produces a 400ValidationError. - `addLineItems` auto-wraps a single object to an array. You can pass either one
AddLineItemRequestorAddLineItemRequest[]— the SDK normalizes it. The return is alwaysLineItem[]. - `createCompany` requires at least one contact. Pass a
contactsarray with at least one entry or the backend returns 400. - No `getContact` or `getType` methods. The backend has no
GET /v1/contacts/:idorGET /v1/types/:idroutes — this is intentional, not an SDK gap.
Full API reference: https://docs.turbodocx.com/docs