` | Parsed variable objects with values |
-
-### Tag
-
-Tag object included when `show_tags` is enabled. Each tag is a `Hash` with:
-
-| Key | Type | Description |
-| ----------- | --------- | ------------------------------------ |
-| `id` | `String` | Tag unique identifier (UUID) |
-| `label` | `String` | Tag display name |
-| `isActive` | `Boolean` | Whether the tag is active |
-| `updatedOn` | `String` | ISO 8601 last update timestamp |
-| `createdOn` | `String` | ISO 8601 creation timestamp |
-| `createdBy` | `String` | User ID of the tag creator |
-| `orgId` | `String` | Organization ID |
-
----
-
-## Additional Documentation
-
-For detailed information about advanced configuration and API concepts, see:
-
-### Core API References
-
-- **[TurboDocx Templating](/docs/TurboDocx-Templating/How-to-Create-a-Template)** - How to create and configure document templates
-- **[Variable Reference](/docs/API/Deliverable-API#variable-object-structure)** - Complete guide to variable types, formatting, and advanced injection options
-- **[API Reference](/docs/API/Deliverable-API)** - Full REST API documentation for Deliverable endpoints
-
----
-
-## Resources
-
-- [GitHub Repository](https://github.com/TurboDocx/SDK/tree/main/packages/ruby-sdk)
-- [RubyGems Package](https://rubygems.org/gems/turbodocx-sdk)
-- [API Reference](/docs/API/Deliverable-API)
diff --git a/docs/SDKs/deliverable.md b/docs/SDKs/deliverable.md
index ae98810..e787f24 100644
--- a/docs/SDKs/deliverable.md
+++ b/docs/SDKs/deliverable.md
@@ -2,7 +2,7 @@
title: Deliverable SDK
sidebar_position: 8
sidebar_label: Deliverable
-description: 'Deliverable SDK for JavaScript, TypeScript, Python, PHP, Go and Java: generate, list, update and download documents from templates.'
+description: 'Deliverable SDK for JavaScript, TypeScript, Python, PHP, Go, Java and Ruby: generate, list, update and download documents from templates.'
keywords:
- turbodocx deliverable javascript
- turbodocx deliverable typescript
@@ -36,6 +36,10 @@ keywords:
- maven turbodocx
- gradle turbodocx
- java sdk
+- turbodocx deliverable ruby
+- document generation ruby
+- deliverable sdk ruby
+- gem turbodocx
---
import Tabs from '@theme/Tabs';
@@ -71,6 +75,11 @@ The official TurboDocx Deliverable SDK for Go applications. Generate documents f
The official TurboDocx Deliverable SDK for Java applications. Generate documents from templates with dynamic variable injection, download source files and PDFs, and manage deliverables programmatically with the Builder pattern, comprehensive error handling, and type-safe APIs. Available on Maven Central as `com.turbodocx:turbodocx-sdk`.
+
+
+
+The official TurboDocx Deliverable SDK for Ruby applications. Generate documents from templates with dynamic variable injection, download source files and PDFs, and manage deliverables programmatically with a simple synchronous API and comprehensive error handling. Available on RubyGems as `turbodocx-sdk`.
+
@@ -177,6 +186,34 @@ implementation 'com.turbodocx:turbodocx-sdk:0.7.0'
+
+
+
+
+
+
+```bash
+gem install turbodocx-sdk
+```
+
+
+
+
+Add to your `Gemfile`:
+
+```ruby
+gem "turbodocx-sdk"
+```
+
+Then run:
+
+```bash
+bundle install
+```
+
+
+
+
@@ -218,6 +255,12 @@ This SDK leverages PHP 8.1+ features including enums, named parameters, readonly
- OkHttp 4.x (included)
- Gson 2.x (included)
+
+
+
+- Ruby 2.7+
+- No runtime dependencies (uses the standard library's `net/http`)
+
@@ -412,6 +455,25 @@ public class Main {
Use `buildDeliverableClient()` when you only need document generation: it skips the `senderEmail` validation required by TurboSign.
:::
+
+
+
+```ruby
+require "turbodocx_sdk"
+
+# Configure globally (recommended)
+TurboDocxSdk::Deliverable.configure(
+ api_key: ENV["TURBODOCX_API_KEY"], # Required: Your TurboDocx API key
+ org_id: ENV["TURBODOCX_ORG_ID"] # Required: Your organization ID
+ # access_token: "...", # Optional: OAuth access token instead of an API key
+ # base_url: "https://api.turbodocx.com" # Optional: Override base URL
+)
+```
+
+:::tip No Sender Email Required
+Unlike TurboSign, the Deliverable module only requires `api_key` and `org_id`: no sender email or name is needed.
+:::
+
@@ -472,6 +534,19 @@ export TURBODOCX_ORG_ID=your_org_id_here
Both `apiKey` and `orgId` parameters are **required** for all API requests. To get your credentials, follow the **[Get Your Credentials](/docs/SDKs#1-get-your-credentials)** steps from the SDKs main page.
:::
+
+
+
+```bash
+# .env
+TURBODOCX_API_KEY=your_api_key_here
+TURBODOCX_ORG_ID=your_org_id_here
+```
+
+:::caution API Credentials Required
+Both `api_key` and `org_id` parameters are **required** for all API requests. If you skip `configure`, the SDK reads `TURBODOCX_API_KEY`, `TURBODOCX_ORG_ID` and `TURBODOCX_BASE_URL` from the environment. To get your credentials, follow the **[Get Your Credentials](/docs/SDKs#1-get-your-credentials)** steps from the SDKs main page.
+:::
+
@@ -693,6 +768,35 @@ public class Main {
}
```
+
+
+
+```ruby
+require "json"
+require "turbodocx_sdk"
+
+TurboDocxSdk::Deliverable.configure(
+ api_key: ENV["TURBODOCX_API_KEY"],
+ org_id: ENV["TURBODOCX_ORG_ID"]
+)
+
+# Generate a document from a template with variables
+# Request-hash keys are camelCase: the SDK forwards them to the API verbatim
+result = TurboDocxSdk::Deliverable.generate_deliverable(
+ "name" => "Q1 Report",
+ "templateId" => "your-template-id",
+ "variables" => [
+ { "placeholder" => "{CompanyName}", "text" => "Acme Corporation", "mimeType" => "text" },
+ { "placeholder" => "{Date}", "text" => "2026-03-12", "mimeType" => "text" }
+ ],
+ "description" => "Quarterly business report",
+ "tags" => ["reports", "quarterly"]
+)
+
+puts "Deliverable ID: #{result['results']['deliverable']['id']}"
+puts JSON.pretty_generate(result)
+```
+
@@ -988,6 +1092,44 @@ public class Main {
}
```
+
+
+
+```ruby
+require "turbodocx_sdk"
+
+TurboDocxSdk::Deliverable.configure(
+ api_key: ENV["TURBODOCX_API_KEY"],
+ org_id: ENV["TURBODOCX_ORG_ID"]
+)
+
+# List deliverables with pagination
+items = TurboDocxSdk::Deliverable.list_deliverables(limit: 10, show_tags: true)
+puts "Total: #{items['totalRecords']}"
+
+# Get deliverable details
+details = TurboDocxSdk::Deliverable.get_deliverable_details("deliverable-uuid")
+puts "Name: #{details['name']}"
+
+# Download source file (DOCX/PPTX)
+source_bytes = TurboDocxSdk::Deliverable.download_source_file("deliverable-uuid")
+File.binwrite("report.docx", source_bytes)
+
+# Download PDF
+pdf_bytes = TurboDocxSdk::Deliverable.download_pdf("deliverable-uuid")
+File.binwrite("report.pdf", pdf_bytes)
+
+# Update deliverable
+TurboDocxSdk::Deliverable.update_deliverable_info(
+ "deliverable-uuid",
+ "name" => "Q1 Report - Final",
+ "description" => "Final quarterly business report"
+)
+
+# Delete deliverable
+TurboDocxSdk::Deliverable.delete_deliverable("deliverable-uuid")
+```
+
@@ -1067,6 +1209,16 @@ var.setText("Acme Corporation");
var.setMimeType("text");
```
+
+
+
+```ruby
+variables = [
+ { "placeholder" => "{CompanyName}", "text" => "Acme Corporation", "mimeType" => "text" },
+ { "placeholder" => "{Date}", "text" => "2026-03-12", "mimeType" => "text" }
+]
+```
+
@@ -1136,6 +1288,19 @@ var.setText("This is a formatted summary with rich text<
var.setMimeType("html");
```
+
+
+
+```ruby
+variables = [
+ {
+ "placeholder" => "{Summary}",
+ "text" => "This is a formatted summary with rich text.
",
+ "mimeType" => "html"
+ }
+]
+```
+
@@ -1205,6 +1370,19 @@ var.setText("https://example.com/logo.png");
var.setMimeType("image");
```
+
+
+
+```ruby
+variables = [
+ {
+ "placeholder" => "{Logo}",
+ "text" => "https://example.com/logo.png",
+ "mimeType" => "image"
+ }
+]
+```
+
@@ -1294,6 +1472,23 @@ var.setMimeType("markdown");
For repeating content (e.g., table rows), use `setVariableStack()` instead of `setText()` to provide multiple values for the same placeholder. See the [Types section](#createdeliverablerequest) for details.
:::
+
+
+
+```ruby
+variables = [
+ {
+ "placeholder" => "{Notes}",
+ "text" => "## Key Points\n- First item\n- Second item\n\n**Important:** Review before submission.",
+ "mimeType" => "markdown"
+ }
+]
+```
+
+:::info Variable Stack
+For repeating content (e.g., table rows), use `variableStack` instead of `text` to provide multiple values for the same placeholder. See the [Types section](#createdeliverablerequest) for details.
+:::
+
@@ -1308,6 +1503,13 @@ For repeating content (e.g., table rows), use `setVariableStack()` instead of `s
The snippets below are partial and run inside an `async` function. They assume you have already called `Deliverable.configure(...)` and that `asyncio` and `json` are imported (`import asyncio, json`). For a complete runnable script, wrap the calls in `async def main(): ...` and run with `asyncio.run(main())`, as shown in [Quick Start](#quick-start).
:::
+
+
+
+:::note Runnable snippets
+The snippets below assume you have already added `require "turbodocx_sdk"` (plus `require "json"` if the snippet calls `JSON.pretty_generate`) and called `TurboDocxSdk::Deliverable.configure(...)`, as shown in [Quick Start](#quick-start). All methods are synchronous class methods on `TurboDocxSdk::Deliverable`.
+:::
+
@@ -1459,6 +1661,24 @@ DeliverableClient deliverable = new TurboDocxClient.Builder()
`DeliverableClient` also has a public constructor, `new DeliverableClient(HttpClient)`, if you already hold a configured `HttpClient` and want the deliverable module to reuse it. Most applications should use the builder instead.
:::
+
+
+
+Configure the SDK with your API credentials and organization settings.
+
+```ruby
+TurboDocxSdk::Deliverable.configure(
+ api_key: nil, # API key (or use access_token)
+ access_token: nil, # OAuth2 access token (alternative to api_key)
+ org_id: nil, # Required: Your organization ID
+ base_url: "https://api.turbodocx.com" # Optional: API base URL
+)
+```
+
+:::caution API Credentials Required
+All keyword arguments are optional. `api_key`, `org_id` and `base_url` fall back to the `TURBODOCX_API_KEY`, `TURBODOCX_ORG_ID` and `TURBODOCX_BASE_URL` environment variables. Either `api_key` or `access_token` must be provided for authentication (the SDK raises `TurboDocxSdk::AuthenticationError` otherwise), and `org_id` is **required** for all Deliverable operations. To get your credentials, follow the **[Get Your Credentials](/docs/SDKs#1-get-your-credentials)** steps from the SDKs main page.
+:::
+
@@ -1586,6 +1806,24 @@ CreateDeliverableResponse result = deliverable.generateDeliverable(request);
System.out.println("Result: " + gson.toJson(result));
```
+
+
+
+```ruby
+result = TurboDocxSdk::Deliverable.generate_deliverable(
+ "name" => "Q1 Report",
+ "templateId" => "your-template-id",
+ "variables" => [
+ { "placeholder" => "{CompanyName}", "text" => "Acme Corp", "mimeType" => "text" },
+ { "placeholder" => "{Date}", "text" => "2026-03-12", "mimeType" => "text" }
+ ],
+ "description" => "Quarterly business report",
+ "tags" => ["reports", "quarterly"]
+)
+
+puts JSON.pretty_generate(result)
+```
+
@@ -1690,6 +1928,20 @@ DeliverableListResponse list = deliverable.listDeliverables(request);
System.out.println("Result: " + gson.toJson(list));
```
+
+
+
+```ruby
+items = TurboDocxSdk::Deliverable.list_deliverables(
+ limit: 10,
+ offset: 0,
+ query: "report",
+ show_tags: true
+)
+
+puts JSON.pretty_generate(items)
+```
+
@@ -1768,6 +2020,15 @@ DeliverableRecord details = deliverable.getDeliverableDetails("deliverable-uuid"
System.out.println("Result: " + gson.toJson(details));
```
+
+
+
+```ruby
+details = TurboDocxSdk::Deliverable.get_deliverable_details("deliverable-uuid", show_tags: true)
+
+puts JSON.pretty_generate(details)
+```
+
@@ -1864,6 +2125,20 @@ UpdateDeliverableResponse result = deliverable.updateDeliverableInfo("deliverabl
System.out.println("Result: " + gson.toJson(result));
```
+
+
+
+```ruby
+result = TurboDocxSdk::Deliverable.update_deliverable_info(
+ "deliverable-uuid",
+ "name" => "Q1 Report - Final",
+ "description" => "Final quarterly business report",
+ "tags" => ["reports", "final"]
+)
+
+puts JSON.pretty_generate(result)
+```
+
@@ -1935,6 +2210,15 @@ DeleteDeliverableResponse result = deliverable.deleteDeliverable("deliverable-uu
System.out.println("Result: " + gson.toJson(result));
```
+
+
+
+```ruby
+result = TurboDocxSdk::Deliverable.delete_deliverable("deliverable-uuid")
+
+puts JSON.pretty_generate(result)
+```
+
@@ -2022,6 +2306,16 @@ byte[] sourceData = deliverable.downloadSourceFile("deliverable-uuid");
Files.write(Paths.get("report.docx"), sourceData);
```
+
+
+
+```ruby
+source_bytes = TurboDocxSdk::Deliverable.download_source_file("deliverable-uuid")
+
+# Save to file
+File.binwrite("report.docx", source_bytes)
+```
+
@@ -2109,6 +2403,16 @@ byte[] pdfData = deliverable.downloadPDF("deliverable-uuid");
Files.write(Paths.get("report.pdf"), pdfData);
```
+
+
+
+```ruby
+pdf_bytes = TurboDocxSdk::Deliverable.download_pdf("deliverable-uuid")
+
+# Save to file
+File.binwrite("report.pdf", pdf_bytes)
+```
+
@@ -2141,6 +2445,11 @@ Files.write(Paths.get("report.pdf"), pdfData);
`deliverable.generateDeliverable()` throws `TurboDocxException.NotFoundException` when `templateId` doesn't match a template in the org, and `TurboDocxException.ValidationException` when a variable in the request is missing a required field:
+
+
+
+`TurboDocxSdk::Deliverable.generate_deliverable` raises `TurboDocxSdk::NotFoundError` when `templateId` doesn't match a template in the org, and `TurboDocxSdk::ValidationError` for invalid request parameters, most commonly a variable hash missing `text` (required unless it sets `variableStack` or `isDisabled`) or specifying an unsupported `mimeType`. Both extend the base `TurboDocxSdk::TurboDocxError`:
+
@@ -2425,6 +2734,59 @@ try {
The full typed-exception table (`AuthenticationException`, `AuthorizationException`, `ConflictException`, `RateLimitException`, `NetworkException`, HTTP status mapping) and the `getMessage()`/`getStatusCode()`/`getCode()` methods shared by every exception are documented once in the [Java SDK's Error Handling reference](/docs/SDKs/turbosign?language=java#error-handling).
+
+
+
+```ruby
+require "turbodocx_sdk"
+
+begin
+ result = TurboDocxSdk::Deliverable.generate_deliverable(
+ "name" => "Q1 Report",
+ "templateId" => "your-template-id",
+ "variables" => [
+ { "placeholder" => "{CompanyName}", "text" => "Acme Corp", "mimeType" => "text" }
+ ]
+ )
+rescue TurboDocxSdk::AuthenticationError => e
+ puts "Authentication failed: #{e.message}"
+ # Check your API key and org ID
+rescue TurboDocxSdk::AuthorizationError => e
+ puts "Not authorized: #{e.message}"
+ # Authenticated, but lacks permission for this operation
+rescue TurboDocxSdk::ValidationError => e
+ puts "Validation error: #{e.message}"
+ # Check request parameters
+rescue TurboDocxSdk::NotFoundError => e
+ puts "Resource not found: #{e.message}"
+ # Template or deliverable doesn't exist
+rescue TurboDocxSdk::ConflictError => e
+ puts "Conflict: #{e.message}"
+ # Request conflicts with the current resource state
+rescue TurboDocxSdk::RateLimitError => e
+ puts "Rate limited: #{e.message}"
+ # Wait and retry
+rescue TurboDocxSdk::NetworkError => e
+ puts "Network error: #{e.message}"
+ # Check connectivity
+rescue TurboDocxSdk::TurboDocxError => e
+ puts "SDK error: #{e.message}, status_code=#{e.status_code}, code=#{e.code}"
+end
+```
+
+| Error Class | Status Code | Description |
+| --------------------- | ----------- | --------------------------------------------- |
+| `TurboDocxError` | varies | Base error class for all SDK errors |
+| `AuthenticationError` | 401 | Invalid or missing API credentials |
+| `AuthorizationError` | 403 | Authenticated but lacks required permissions |
+| `ValidationError` | 400 | Invalid request parameters |
+| `NotFoundError` | 404 | Deliverable or template not found |
+| `ConflictError` | 409 | Request conflicts with current resource state |
+| `RateLimitError` | 429 | Too many requests |
+| `NetworkError` | - | Network connectivity issues |
+
+Every error class lives under the `TurboDocxSdk` namespace and exposes `message`, `status_code` (`Integer` or `nil`) and `code` (machine-readable `String` or `nil`).
+
@@ -2446,6 +2808,11 @@ The SDK exports TypeScript types for full type safety. Import them directly from
The SDK uses Python type hints with `Dict[str, Any]` for flexible JSON-like structures.
+
+
+
+The SDK uses plain Ruby `Hash` objects for flexible JSON-like structures. Request-body keys stay **camelCase** (the SDK forwards them verbatim); only the method keyword arguments (`api_key:`, `show_tags:`, and so on) are snake_case.
+
@@ -2483,6 +2850,15 @@ import type {
from typing import Dict, List, Any, Optional
```
+
+
+
+The Ruby SDK has no type classes to import. Requests and responses are plain `Hash` objects, so `require "turbodocx_sdk"` is all you need:
+
+```ruby
+require "turbodocx_sdk"
+```
+
@@ -2509,6 +2885,18 @@ String values for variable content types:
| `"image"` | Image URL or base64 content |
| `"markdown"` | Markdown converted to text |
+
+
+
+String values for variable content types:
+
+| Value | Description |
+| ------------ | ----------------------------- |
+| `"text"` | Plain text injection |
+| `"html"` | Rich HTML content |
+| `"image"` | Image URL or base64 content |
+| `"markdown"` | Markdown converted to text |
+
@@ -2611,6 +2999,25 @@ Variable configuration for template injection:
\*Required unless `variableStack` is provided or `isDisabled` is true.
+
+
+
+Variable configuration for template injection:
+
+
+
+| Property | Type | Required | Description |
+| ------------------------ | ----------------- | -------- | ---------------------------------------------------- |
+| `placeholder` | `String` | Yes | Template placeholder (e.g., `{CompanyName}`) |
+| `text` | `String` | No\* | Value to inject |
+| `mimeType` | `String` | Yes | `"text"`, `"html"`, `"image"`, or `"markdown"` |
+| `isDisabled` | `Boolean` | No | Skip this variable during generation |
+| `subvariables` | `Array` | No | Nested sub-variables for HTML content |
+| `variableStack` | `Hash \| Array` | No | Multiple instances for repeating content |
+| `aiPrompt` | `String` | No | AI prompt for content generation (max 16,000 chars) |
+
+\*Required unless `variableStack` is provided or `isDisabled` is true.
+
@@ -2685,6 +3092,21 @@ Request configuration for `generateDeliverable`:
| `description` | `String` | No | Description (up to 65,535 characters) |
| `tags` | `List` | No | Tag strings to associate |
+
+
+
+Request hash for `generate_deliverable`:
+
+
+
+| Property | Type | Required | Description |
+| --------------- | --------------- | -------- | ------------------------------------------ |
+| `name` | `String` | Yes | Deliverable name (3-255 characters) |
+| `templateId` | `String` | Yes | Template ID to generate from |
+| `variables` | `Array` | Yes | Variables for template substitution |
+| `description` | `String` | No | Description (up to 65,535 characters) |
+| `tags` | `Array` | No | Tag strings to associate |
+
@@ -2749,6 +3171,19 @@ Request configuration for `updateDeliverableInfo`:
| `description` | `String` | No | Updated description |
| `tags` | `List` | No | Replace all tags (empty list to remove) |
+
+
+
+Request hash for `update_deliverable_info`:
+
+
+
+| Property | Type | Required | Description |
+| ------------- | --------------- | -------- | ---------------------------------------- |
+| `name` | `String` | No | Updated name (3-255 characters) |
+| `description` | `String` | No | Updated description |
+| `tags` | `Array` | No | Replace all tags (empty array to remove) |
+
@@ -2821,6 +3256,20 @@ Options for `listDeliverables`:
| `query` | `String` | No | Search query to filter by name |
| `showTags` | `Boolean` | No | Include tags in the response |
+
+
+
+Options for `list_deliverables` (sent as query parameters; `show_tags` is accepted as snake_case or camelCase and sent as `showTags`):
+
+
+
+| Property | Type | Required | Description |
+| --------------- | --------- | -------- | ------------------------------------ |
+| `limit` | `Integer` | No | Results per page (1-100, default 6) |
+| `offset` | `Integer` | No | Results to skip (default 0) |
+| `query` | `String` | No | Search query to filter by name |
+| `show_tags` | `Boolean` | No | Include tags in the response |
+
@@ -2950,6 +3399,30 @@ The deliverable object returned by both `listDeliverables` and `getDeliverableDe
| `variables` | `List` | Parsed variable objects with values |
| `tags` | `List` | Associated tags (when `showTags=true`) |
+
+
+
+The deliverable hash returned by `list_deliverables`:
+
+
+
+| Property | Type | Description |
+| ----------------- | --------- | ------------------------------------- |
+| `id` | `String` | Unique deliverable ID (UUID) |
+| `name` | `String` | Deliverable name |
+| `description` | `String` | Description text |
+| `templateId` | `String` | Source template ID |
+| `createdBy` | `String` | User ID of the creator |
+| `email` | `String` | Creator's email address |
+| `fileSize` | `Integer` | File size in bytes |
+| `fileType` | `String` | MIME type of the generated file |
+| `defaultFont` | `String` | Default font used |
+| `fonts` | `Array` | Fonts used in the document |
+| `isActive` | `Boolean` | Whether the deliverable is active |
+| `createdOn` | `String` | ISO 8601 creation timestamp |
+| `updatedOn` | `String` | ISO 8601 last update timestamp |
+| `tags` | `Array` | Associated tags (when `show_tags: true`) |
+
@@ -2981,6 +3454,19 @@ The deliverable record returned by `getDeliverableDetails`. Includes all fields
| `templateNotDeleted` | `bool` | Whether the source template still exists |
| `variables` | `array` | Parsed variable objects with values |
+
+
+
+The deliverable hash returned by `get_deliverable_details`. Includes all fields from [DeliverableRecord](#deliverablerecord) **except `fileSize`**, plus:
+
+
+
+| Property | Type | Description |
+| -------------------- | ------------- | ---------------------------------------- |
+| `templateName` | `String` | Source template name |
+| `templateNotDeleted` | `Boolean` | Whether the source template still exists |
+| `variables` | `Array` | Parsed variable objects with values |
+
@@ -3063,6 +3549,21 @@ Tag object included when `showTags` is enabled:
| `createdBy` | `String` | User ID of the tag creator |
| `orgId` | `String` | Organization ID |
+
+
+
+Tag object included when `show_tags` is enabled. Each tag is a `Hash` with:
+
+| Key | Type | Description |
+| ----------- | --------- | ------------------------------------ |
+| `id` | `String` | Tag unique identifier (UUID) |
+| `label` | `String` | Tag display name |
+| `isActive` | `Boolean` | Whether the tag is active |
+| `updatedOn` | `String` | ISO 8601 last update timestamp |
+| `createdOn` | `String` | ISO 8601 creation timestamp |
+| `createdBy` | `String` | User ID of the tag creator |
+| `orgId` | `String` | Organization ID |
+
@@ -3117,5 +3618,12 @@ For detailed information about advanced configuration and API concepts, see:
- [Maven Central](https://search.maven.org/artifact/com.turbodocx/turbodocx-sdk)
- [API Reference](/docs/API/Deliverable-API)
+
+
+
+- [GitHub Repository](https://github.com/TurboDocx/SDK/tree/main/packages/ruby-sdk)
+- [RubyGems Package](https://rubygems.org/gems/turbodocx-sdk)
+- [API Reference](/docs/API/Deliverable-API)
+
diff --git a/docs/SDKs/index.md b/docs/SDKs/index.md
index abff76a..cf3a5ab 100644
--- a/docs/SDKs/index.md
+++ b/docs/SDKs/index.md
@@ -2,7 +2,7 @@
title: SDKs Overview
sidebar_position: 1
sidebar_label: Overview
-description: Official TurboDocx SDKs for JavaScript, Python, PHP, Go, and Java. Get started with document generation, deliverables, digital signatures, and quoting.
+description: Official TurboDocx SDKs for JavaScript, Python, PHP, Go, Java, and Ruby. Get started with document generation, deliverables, digital signatures, and quoting.
keywords:
- turbodocx sdk
- turbosign sdk
@@ -11,6 +11,7 @@ keywords:
- php sdk
- go sdk
- java sdk
+ - ruby sdk
- document api
- esignature sdk
- api client library
@@ -55,6 +56,7 @@ Send documents for legally-binding eSignatures with full audit trails.
| **PHP** | `turbodocx/sdk` | `composer require turbodocx/sdk` | [Docs](/docs/SDKs/turbosign?language=php) [GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/php-sdk) |
| **Go** | `github.com/TurboDocx/SDK/packages/go-sdk` | `go get github.com/TurboDocx/SDK/packages/go-sdk` | [Docs](/docs/SDKs/turbosign?language=go) [GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/go-sdk) |
| **Java** | `com.turbodocx:turbodocx-sdk` | [Maven Central](https://search.maven.org/artifact/com.turbodocx/turbodocx-sdk) | [Docs](/docs/SDKs/turbosign?language=java) [GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/java-sdk) |
+| **Ruby** | `turbodocx-sdk` | `gem install turbodocx-sdk` | [Docs](/docs/SDKs/turbosign?language=ruby) [GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/ruby-sdk) |
## TurboWebhooks SDKs
@@ -67,6 +69,7 @@ Subscribe to all 7 TurboSign signature events (`sent`, `viewed`, `recipient_sign
| **Python** | `turbodocx-sdk` | `pip install turbodocx-sdk` | [Docs](/docs/SDKs/webhooks?language=python) [GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/py-sdk) |
| **Go** | `github.com/TurboDocx/SDK/packages/go-sdk` | `go get github.com/TurboDocx/SDK/packages/go-sdk` | [Docs](/docs/SDKs/webhooks?language=go) [GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/go-sdk) |
| **Java** | `com.turbodocx:turbodocx-sdk` | `mvn` / `gradle` (see [docs](/docs/SDKs/webhooks?language=java)) | [Docs](/docs/SDKs/webhooks?language=java) [GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/java-sdk) |
+| **Ruby** | `turbodocx-sdk` | `gem install turbodocx-sdk` | [Docs](/docs/SDKs/webhooks?language=ruby) [GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/ruby-sdk) |
For the conceptual overview (delivery retries, payload schema, dashboard configuration), see [TurboSign → Webhooks](/docs/TurboSign/Webhooks).
@@ -81,6 +84,7 @@ Generate documents from templates with dynamic variable injection, download sour
| **PHP** | `turbodocx/sdk` | `composer require turbodocx/sdk` | [Docs](/docs/SDKs/deliverable?language=php) [GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/php-sdk) |
| **Go** | `github.com/TurboDocx/SDK/packages/go-sdk` | `go get github.com/TurboDocx/SDK/packages/go-sdk` | [Docs](/docs/SDKs/deliverable?language=go) [GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/go-sdk) |
| **Java** | `com.turbodocx:turbodocx-sdk` | [Maven Central](https://search.maven.org/artifact/com.turbodocx/turbodocx-sdk) | [Docs](/docs/SDKs/deliverable?language=java) [GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/java-sdk) |
+| **Ruby** | `turbodocx-sdk` | `gem install turbodocx-sdk` | [Docs](/docs/SDKs/deliverable?language=ruby) [GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/ruby-sdk) |
## TurboQuote SDKs
@@ -93,6 +97,7 @@ Build sales quotes and proposals programmatically: quotes and line items, a prod
| **PHP** | `turbodocx/sdk` | `composer require turbodocx/sdk` | [Docs](/docs/SDKs/quote?language=php) [GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/php-sdk) |
| **Go** | `github.com/TurboDocx/SDK/packages/go-sdk` | `go get github.com/TurboDocx/SDK/packages/go-sdk` | [Docs](/docs/SDKs/quote?language=go) [GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/go-sdk) |
| **Java** | `com.turbodocx:turbodocx-sdk` | [Maven Central](https://search.maven.org/artifact/com.turbodocx/turbodocx-sdk) | [Docs](/docs/SDKs/quote?language=java) [GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/java-sdk) |
+| **Ruby** | `turbodocx-sdk` | `gem install turbodocx-sdk` | [Docs](/docs/SDKs/quote?language=ruby) [GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/ruby-sdk) |
:::tip Low-code or No-code?
Check out our [n8n community node](https://www.npmjs.com/package/@turbodocx/n8n-nodes-turbodocx) for workflow automation, or get [TurboDocx Writer](https://appsource.microsoft.com/en-us/product/office/WA200007397) for Microsoft Word.
@@ -203,6 +208,15 @@ go get github.com/TurboDocx/SDK/packages/go-sdk
```
+
+
+
+```bash
+gem install turbodocx-sdk
+# or
+bundle add turbodocx-sdk
+```
+
@@ -438,6 +452,33 @@ public class Main {
}
```
+
+
+
+```ruby
+require "turbodocx_sdk"
+
+# Configure with your API key
+TurboDocxSdk::TurboSign.configure(
+ api_key: ENV["TURBODOCX_API_KEY"],
+ org_id: ENV["TURBODOCX_ORG_ID"],
+ sender_email: ENV["TURBODOCX_SENDER_EMAIL"] # required for TurboSign
+)
+
+# Send a document for signature
+result = TurboDocxSdk::TurboSign.send_signature(
+ fileLink: "https://www.turbodocx.com/examples/turbodocx.pdf",
+ recipients: [
+ { name: "John Doe", email: "john@example.com", signingOrder: 1 }
+ ],
+ fields: [
+ { type: "signature", page: 1, x: 100, y: 500, width: 200, height: 50, recipientEmail: "john@example.com" }
+ ]
+)
+
+puts "Document sent! ID: #{result['documentId']}"
+```
+
@@ -662,6 +703,25 @@ try {
}
```
+
+
+
+```ruby
+begin
+ result = TurboDocxSdk::TurboSign.send_signature(request)
+rescue TurboDocxSdk::AuthenticationError => e
+ puts "Invalid API key: #{e.message}"
+rescue TurboDocxSdk::ValidationError => e
+ puts "Validation error: #{e.message}"
+ puts "Error code: #{e.code}"
+rescue TurboDocxSdk::RateLimitError => e
+ puts "Rate limited: #{e.message}"
+rescue TurboDocxSdk::TurboDocxError => e
+ puts "Error #{e.code}: #{e.message}"
+ puts "Status code: #{e.status_code}"
+end
+```
+
diff --git a/docs/SDKs/partner-ruby.md b/docs/SDKs/partner-ruby.md
index 27e3c19..69ad30e 100644
--- a/docs/SDKs/partner-ruby.md
+++ b/docs/SDKs/partner-ruby.md
@@ -1,7 +1,5 @@
---
-# Hidden from the published site: the turbodocx-sdk gem is not on RubyGems yet.
-# Remove `draft: true` when the gem is published (TurboDocx/SDK issue).
-draft: true
+unlisted: true
title: TurboPartner Ruby SDK
sidebar_position: 13
sidebar_label: "TurboPartner: Ruby"
@@ -924,6 +922,6 @@ It creates an organization with entitlements, adds a user, mints an org API key,
- [GitHub Repository](https://github.com/TurboDocx/SDK/tree/main/packages/ruby-sdk)
- [RubyGems Package](https://rubygems.org/gems/turbodocx-sdk)
-- [TurboSign Ruby SDK](/docs/SDKs/ruby) — For digital signature operations
-- [TurboQuote Ruby SDK](/docs/SDKs/quote-ruby) — For quotes and proposals
-- [SDKs Overview](/docs/SDKs/) — All TurboDocx SDKs
+- [TurboSign Ruby SDK](/docs/SDKs/turbosign?language=ruby): for digital signature operations
+- [TurboQuote Ruby SDK](/docs/SDKs/quote?language=ruby): for quotes and proposals
+- [SDKs Overview](/docs/SDKs/): all TurboDocx SDKs
diff --git a/docs/SDKs/quote-ruby.md b/docs/SDKs/quote-ruby.md
deleted file mode 100644
index a5d7427..0000000
--- a/docs/SDKs/quote-ruby.md
+++ /dev/null
@@ -1,985 +0,0 @@
----
-# Hidden from the published site: the turbodocx-sdk gem is not on RubyGems yet.
-# Remove `draft: true` when the gem is published (TurboDocx/SDK issue).
-draft: true
-title: TurboQuote Ruby SDK
-sidebar_position: 23
-sidebar_label: "TurboQuote: Ruby"
-description: Official TurboDocx TurboQuote SDK for Ruby. Create, manage, and send quotes/proposals with full CPQ capabilities — line items, products, bundles, price books, companies, contacts, and quote templates, all from plain Ruby.
-keywords:
- - turboquote ruby
- - quote sdk ruby
- - proposal sdk ruby
- - cpq ruby
- - turbodocx quote ruby
- - gem turbodocx
- - quote line items ruby
- - price book ruby
----
-
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-import QuickstartSkillNudge from '@site/src/components/QuickstartSkillNudge';
-
-# TurboQuote Ruby SDK
-
-
-
-The official TurboDocx TurboQuote SDK for Ruby applications. Build full CPQ (configure, price, quote) workflows: create quotes, add product and bundle line items, apply price books, send proposals to contacts, and download PDF exports — all from plain Ruby 2.7+. Distributed on RubyGems as `turbodocx-sdk` (same gem as TurboSign, TurboWebhooks, and Deliverable).
-
-
-
-:::info What is TurboQuote?
-TurboQuote is TurboDocx's quoting and proposal engine. It covers the full quote lifecycle — draft, send, accept/decline/void — with a product catalog, bundle groupings, price books, company/contact CRM, and customizable quote templates. A draft can also be marked declined directly, for a deal that dies before the quote is ever sent. Quotes can be sent with an attached Deliverable document for a branded proposal experience.
-:::
-
-## Installation
-
-
-
-
-```bash
-gem install turbodocx-sdk
-```
-
-
-
-
-```ruby
-# Gemfile
-gem "turbodocx-sdk"
-```
-
-```bash
-bundle install
-```
-
-
-
-
-## Requirements
-
-- Ruby 2.7 or higher
-- No runtime dependencies (uses the standard library's `net/http`)
-- A TurboDocx API key (`TDX-` prefix) — generate one in **Settings → API Keys**
-- All SDK methods are synchronous — no async wrapper needed
-
-## Configuration
-
-```ruby
-require "turbodocx_sdk"
-
-TurboDocxSdk::TurboQuote.configure(
- api_key: ENV["TURBODOCX_API_KEY"],
- org_id: ENV["TURBODOCX_ORG_ID"] # optional — auto-reads env var
-)
-```
-
-:::tip No sender email on the client — but set one on your quote template
-`TurboQuote.configure` does **not** take `sender_email` or `sender_name`. Quotes are not signature emails. Only `api_key` and, optionally, `org_id` are needed. If you skip the explicit call, the SDK lazily auto-configures from `TURBODOCX_API_KEY` and `TURBODOCX_ORG_ID` on first use.
-
-The quote's **"Prepared by"** sender comes from your **org quote template** instead. Because an API key has no mailbox of its own, every sender-resolving call — `create_quote`, `duplicate_quote`, `send_quote` / `send_quote_with_deliverable`, and `handle_expired_quote` — fails with a `ValidationError` (`400 SenderEmailRequired`) when the org's quote template has no sender email set. A companion `400 SenderNameRequired` is returned when no sender **name** resolves. Configure both **Sender Name** and **Sender Email** once (`TurboQuote.update_template`) and all of them resolve cleanly.
-:::
-
-:::note camelCase request keys
-Method names are snake_case, but the keys **inside** every request hash (`companyId`, `validUntil`, `unitPrice`, …) are **camelCase** — the SDK forwards them to the API verbatim and does not convert snake_case hash keys.
-:::
-
-### Environment Variables
-
-```bash
-TURBODOCX_API_KEY=your_api_key_here
-TURBODOCX_ORG_ID=your_org_id_here
-# optional — defaults to https://api.turbodocx.com
-TURBODOCX_BASE_URL=https://api.turbodocx.com
-```
-
-## Quick Start
-
-### Create a quote, add line items, send, and download the PDF
-
-```ruby
-require "turbodocx_sdk"
-
-TurboDocxSdk::TurboQuote.configure(
- api_key: ENV["TURBODOCX_API_KEY"],
- org_id: ENV["TURBODOCX_ORG_ID"]
-)
-
-# 1. Create a draft quote
-quote = TurboDocxSdk::TurboQuote.create_quote(
- "name" => "Acme Corp — Annual Plan Q3",
- "companyId" => "company-uuid",
- "contactId" => "contact-uuid",
- "validUntil" => "2026-09-30",
- "currency" => "USD"
-)
-quote_id = quote["id"]
-puts "Created quote #{quote_id}"
-
-# 2. Add product line items
-items = TurboDocxSdk::TurboQuote.add_line_items(quote_id, [
- {
- "productId" => "product-uuid-1",
- "productName" => "Platform License",
- "quantity" => 5,
- "unitPrice" => "199.00",
- "billingFrequency" => "annual"
- },
- {
- "productId" => "product-uuid-2",
- "productName" => "Onboarding Package",
- "quantity" => 1,
- "unitPrice" => "499.00",
- "billingFrequency" => "one-time"
- }
-])
-puts "Added #{items.length} line items"
-
-# 3. Apply a price book (optional)
-result = TurboDocxSdk::TurboQuote.apply_price_book(quote_id, "pricebook-uuid")
-puts "Price book applied: #{result['updatedCount']} items updated"
-
-# 4. Send the quote
-sent = TurboDocxSdk::TurboQuote.send_quote(quote_id,
- "ccEmails" => ["manager@acme.com"],
- "validUntil" => "2026-09-30"
-)
-puts "Sent: #{sent['message']}"
-
-# 5. Download the PDF
-pdf_bytes = TurboDocxSdk::TurboQuote.download_quote_pdf(quote_id)
-File.binwrite("acme-quote.pdf", pdf_bytes)
-puts "PDF saved (#{pdf_bytes.bytesize} bytes)"
-```
-
-### Convenience: create, add items, and send in one call
-
-```ruby
-result = TurboDocxSdk::TurboQuote.create_and_send(
- "name" => "Acme Corp — Quick Proposal",
- "companyId" => "company-uuid",
- "contactId" => "contact-uuid",
- "currency" => "USD",
- "items" => [
- { "productId" => "product-uuid-1", "productName" => "Platform License", "quantity" => 3, "unitPrice" => "299.00", "billingFrequency" => "monthly" }
- ],
- "bundleItems" => [
- { "bundleId" => "bundle-uuid-1", "bundleName" => "Starter Pack", "quantity" => 1 }
- ],
- "send" => {
- "ccEmails" => ["cc@example.com"],
- "validUntil" => "2026-09-30"
- }
-)
-puts "Quote sent: #{result['quote']['id']}"
-```
-
----
-
-## Method Reference
-
-All methods are class-level on `TurboDocxSdk::TurboQuote`; configure once, then call on the class. Responses are plain Ruby `Hash`es with string keys.
-
-### Quotes
-
-#### `list_quotes`
-
-```ruby
-page = TurboDocxSdk::TurboQuote.list_quotes(
- "limit" => 20,
- "offset" => 0,
- "query" => "acme",
- "statuses" => ["draft", "sent"] # repeated-key filter
-)
-# page["results"], page["totalRecords"], page["stats"]
-
-page["results"].each do |q|
- puts "#{q["name"]} — #{q["status"]}"
-end
-```
-
-#### `create_quote`
-
-```ruby
-# Fixed-term quote — termDays is -1 or 0–3650; omit it to get the default of 60.
-quote = TurboDocxSdk::TurboQuote.create_quote(
- "name" => "Q3 Proposal",
- "companyId" => "company-uuid", # required
- "contactId" => "contact-uuid", # required
- "currency" => "USD",
- "termDays" => 30, # fixed term — do NOT send renewalPeriod with this
- "validUntil" => "2026-09-30",
- "notes" => "Includes implementation services",
- "taxRate" => "8.5"
-)
-# returns Quote hash
-
-# Auto-renewal quote — termDays -1 REQUIRES renewalPeriod.
-subscription = TurboDocxSdk::TurboQuote.create_quote(
- "name" => "Annual Subscription",
- "companyId" => "company-uuid",
- "contactId" => "contact-uuid",
- "currency" => "USD",
- "termDays" => -1, # -1 = auto-renewal
- "renewalPeriod" => "annually" # "weekly" | "monthly" | "quarterly" | "annually"
-)
-```
-
-:::caution `termDays` and `renewalPeriod` are coupled
-`termDays` defaults to **60** when omitted. Valid values are `-1` (auto-renewal) or `0`–`3650` (`0` = one-time).
-
-`renewalPeriod` is **required** when `termDays` is `-1`, and must be **`nil` or absent** for every other `termDays` value — sending it alongside a fixed term returns a `400`. The same rule applies on `update_quote`.
-:::
-
-#### `get_quote`
-
-Returns the quote with `statusInfo` and `preparedBy` merged in when present. `preparedBy` is the resolved "Prepared by" identity shown on the quote PDF.
-
-```ruby
-quote = TurboDocxSdk::TurboQuote.get_quote("quote-uuid")
-# quote["status"], quote["grandTotal"], quote["statusInfo"]
-prepared = quote["preparedBy"] || {}
-puts prepared["name"] # e.g. "Acme Billing Integration" or the template sender
-puts prepared["email"] # may be nil for an API-created quote — render a placeholder
-```
-
-`preparedBy` is resolved server-side (org template first, then the quote's creator). **Prefer it over `creator`** for any customer-facing display — `creator` may be the internal API service account. For an API-created quote the resolved name is the **API key's name** (never a generic "API Service User"), and the email comes from the org quote template. `preparedBy` is returned by the **single-quote fetch only** — it is not present on create, duplicate, or list responses.
-
-#### `update_quote`
-
-All fields are optional — pass only what changes. Send an explicit `nil` to clear a nullable field.
-
-```ruby
-updated = TurboDocxSdk::TurboQuote.update_quote("quote-uuid",
- "name" => "Q3 Proposal — Revised",
- "validUntil" => "2026-10-15",
- "taxRate" => nil # clears the field
-)
-```
-
-:::info `name` is trimmed, and renaming is draft-only
-`name` is trimmed on both `create_quote` and `update_quote` — `" Acme "` is stored as `"Acme"` — and a name that is empty once trimmed returns a `400`. A quote can only be renamed while it is a **draft**; on any other status the update is rejected with `Cannot update quote that is not in draft status` (`templateId` is the only field exempt from that rule). Rename before you send, because **sending snapshots the quote's current name onto the TurboSign document** — that is the name signers see in the request email and on the signed PDF.
-:::
-
-#### `delete_quote`
-
-```ruby
-result = TurboDocxSdk::TurboQuote.delete_quote("quote-uuid")
-# result["message"]
-```
-
-#### `duplicate_quote`
-
-```ruby
-new_quote = TurboDocxSdk::TurboQuote.duplicate_quote("quote-uuid")
-# returns new Quote in draft status
-```
-
-The copy is named **`Copy of `**, truncated to the name column's 255-character limit. Rename it with `update_quote` before sending if that is not what you want signers to see.
-
-The copy is attributed to **whoever ran the duplicate**, not to the original quote's creator — duplicating with an API key produces a quote whose "Prepared by" resolves through that API key and your org quote template.
-
-#### `download_quote_pdf`
-
-Returns raw PDF bytes.
-
-```ruby
-pdf_bytes = TurboDocxSdk::TurboQuote.download_quote_pdf("quote-uuid")
-File.binwrite("quote.pdf", pdf_bytes)
-```
-
----
-
-### Quote Numbering Configuration
-
-Customize the per-org quote number format: prefix, year/month tokens, separator, zero-padding, suffix, starting number, and reset cadence. Both methods are **admin only**; a non-admin API key receives a `403`.
-
-#### `get_quote_number_config`
-
-Fetch the org's current quote numbering format and the current per-period issued floor.
-
-```ruby
-config = TurboDocxSdk::TurboQuote.get_quote_number_config
-puts config["format"]["prefix"] # e.g. "Q-"
-puts config["currentFloor"] # the current per-period issued floor
-```
-
-#### `update_quote_number_config`
-
-Update the numbering format. Pass the full format hash; all eight fields are required. Keys stay camelCase.
-
-```ruby
-config = TurboDocxSdk::TurboQuote.update_quote_number_config(
- "prefix" => "INV",
- "yearToken" => "none", # "none" | "two" | "four"
- "monthToken" => "off", # "off" | "two"
- "separator" => "-",
- "padWidth" => 4, # 0–12
- "suffix" => "",
- "startNumber" => 1000, # >= 0
- "resetCadence" => "never" # "never" | "yearly" | "monthly"
-)
-puts config["format"]["startNumber"] # 1000
-```
-
-The SDK also exposes constants for the enum values: `TurboDocxSdk::QuoteNumberYearToken`, `TurboDocxSdk::QuoteNumberMonthToken`, and `TurboDocxSdk::QuoteNumberResetCadence`.
-
-#### Field reference, defaults & validation
-
-All eight `format` keys are sent on every update. The API enforces these caps and allowed values — a violation returns `400`:
-
-| Field | Type | Allowed / range | Default |
-|-------|------|-----------------|---------|
-| `prefix` | string | ≤ 12 characters | `"Q"` |
-| `yearToken` | enum | `none` \| `two` \| `four` | `four` |
-| `monthToken` | enum | `off` \| `two` | `off` |
-| `separator` | string | ≤ 4 characters | `"-"` |
-| `padWidth` | integer | `0`–`12` (`0` = no padding) | `5` |
-| `suffix` | string | ≤ 12 characters | `""` |
-| `startNumber` | integer | `0`–`1000000000` | `1` |
-| `resetCadence` | enum | `never` \| `yearly` \| `monthly` | `yearly` |
-
-An org that has never configured numbering uses the **default format** above, which renders like `Q-2026-00001`.
-
-Beyond the per-field caps, the API rejects self-inconsistent formats with a `400`:
-
-- `resetCadence: "yearly"` requires a year token (`yearToken` other than `none`) — otherwise numbers repeat across years.
-- `resetCadence: "monthly"` requires **both** a year token and a month token (`monthToken: "two"`).
-- The rendered quote number must be ≤ 256 characters.
-
-`currentFloor` (returned by both methods) is read-only — the sequence the next quote will use for the current period — and is never sent on update.
-
----
-
-### Quote Status Transitions
-
-:::caution Send preconditions
-
-Both send methods share the same server-side checks. Each is rejected with **HTTP 400** and a
-specific error `code` before anything is created or emailed:
-
-| Condition | Code |
-| :--- | :--- |
-| Quote is not a draft | `QuoteNotSendable` |
-| No `validUntil` date set | `QuoteValidUntilRequired` |
-| `validUntil` is in the past | `QuoteExpired` |
-| No line items | `QuoteHasNoLineItems` |
-| Contact missing a name or email | `QuoteContactRequired` |
-| Company or contact deleted/deactivated | `QuoteCustomerInactive` |
-| No sender email resolvable (API-key callers) | `SenderEmailRequired` |
-
-A quote with **no line items cannot be sent** — add at least one product, bundle, or custom
-line item first. Likewise an **expired quote is rejected**; update `validUntil`, or use the
-handle-expired flow to void it and create a fresh draft.
-
-:::
-
-#### `send_quote`
-
-```ruby
-sent = TurboDocxSdk::TurboQuote.send_quote("quote-uuid",
- "ccEmails" => ["manager@example.com"],
- "validUntil" => "2026-09-30"
-)
-# sent["quote"], sent["message"]
-```
-
-#### `send_quote_with_deliverable`
-
-Attach a TurboDocx-generated document to the sent quote.
-
-```ruby
-result = TurboDocxSdk::TurboQuote.send_quote_with_deliverable("quote-uuid",
- "deliverableId" => "deliverable-uuid",
- "ccEmails" => ["manager@example.com"]
-)
-# result["quote"], result["message"], result["documentId"]
-```
-
-#### Reminders & expiration on a sent quote
-
-`send_quote` and `send_quote_with_deliverable` accept the same reminder/expiration schedule as a TurboSign send — the eight `remindersEnabled` / `reminderDelay` / `reminderInterval` / `maxReminders` / `expirationEnabled` / `expireAfter` / `expirationWarning` / `expirationWarningInterval` keys, each duration a `{ "value" =>, "unit" => }` hash whose `"unit"` is `"days"` or `"hours"`. Both features are off by default. `maxReminders` is `-1` unlimited, `0` none, **max `50`**; `expirationWarning` may be `0` to never warn.
-
-```ruby
-sent = TurboDocxSdk::TurboQuote.send_quote("quote-uuid",
- "validUntil" => "2026-09-30",
- "remindersEnabled" => true,
- "reminderDelay" => { "value" => 3, "unit" => "days" },
- "reminderInterval" => { "value" => 3, "unit" => "days" },
- "maxReminders" => 5,
- "expirationEnabled" => true,
- "expirationWarning" => { "value" => 2, "unit" => "days" },
- "expirationWarningInterval" => { "value" => 1, "unit" => "days" }
-)
-```
-
-:::warning Quote expiry is pinned to `validUntil`
-Unlike a plain TurboSign send, a quote's signing deadline is **hard-pinned to the quote's `validUntil` date**. When expiration is on, `expireAfter` is **ignored** — the deadline is always `validUntil`. `expirationEnabled` still toggles expiry on or off. The reminder and expiry-warning cadence still applies, but every reminder and warning must fall **inside** the `validUntil` window; a cadence that would outlive it is rejected with a `400`.
-:::
-
-#### `decline_quote`
-
-Declines a **sent** quote or a **draft**. `reason` (max 190 characters) is required once a quote has been sent. A draft is declined **without** a reason — a draft never reached the customer, and because the reason is stored on the linked signature document, a draft has nowhere to keep one, so anything passed is ignored.
-
-```ruby
-quote = TurboDocxSdk::TurboQuote.decline_quote("quote-uuid",
- "reason" => "Customer selected a competitor"
-)
-
-# A draft is declined with no reason — one passed here would not be recorded.
-closed_out = TurboDocxSdk::TurboQuote.decline_quote("draft-quote-uuid", {})
-```
-
-#### `void_quote`
-
-Voids a **sent** quote; a **draft cannot be voided**, since voiding an unsent quote is meaningless.
-
-```ruby
-quote = TurboDocxSdk::TurboQuote.void_quote("quote-uuid",
- "reason" => "Terms expired before acceptance"
-)
-```
-
-#### `handle_expired_quote`
-
-Handles a quote that has passed its `validUntil` date. The endpoint **closes out the original quote** — voiding or declining it depending on `action` — and then **creates a duplicate carrying `newValidUntil`** as its new validity date. The returned quote is the new duplicate; the original stays terminal.
-
-`action` (`"void"`, `"decline"` or `"renew"`) and `newValidUntil` (ISO date) are **required**. `reason` (max 190 characters) is required for `"void"` and `"decline"`, and optional for `"renew"` — a renewal closes nothing out, so there is nothing to give a reason for. Use `"renew"` when the quote's signature request has already expired on its own; use `"void"` or `"decline"` when the quote is merely past its `validUntil` and you are closing it yourself.
-
-```ruby
-quote = TurboDocxSdk::TurboQuote.handle_expired_quote("quote-uuid",
- "action" => "void", # required — "void", "decline" or "renew"
- "reason" => "Quote expired", # required for void/decline, optional for renew
- "newValidUntil" => "2026-12-31" # required — ISO date, carried onto the duplicate
-)
-```
-
-:::warning There is no `extend` or `resend` action
-`action` accepts **only** `"void"`, `"decline"` and `"renew"`. `"extend"` and `"resend"` do not exist in the API and return a `400`. Extending is what the endpoint already does — pass `newValidUntil` and it lands on the duplicate it creates.
-:::
-
-:::info The replacement draft keeps the original name
-Unlike `duplicate_quote`, the draft this endpoint creates is **not** prefixed with `Copy of ` — re-issuing the same deal keeps the original quote's name, so repeated renewals cannot compound into `Copy of Copy of …`. That matters because the next send snapshots the name onto the TurboSign document.
-:::
-
-:::note Terminal statuses
-`accepted`, `declined`, and `voided` are **terminal** — a quote in one of these states cannot be transitioned out of it, and any further status call returns a `400`. Check `quote["statusInfo"]` before attempting a transition, and use `duplicate_quote` when you need to revive a closed-out quote.
-:::
-
----
-
-### Price Books (on a Quote)
-
-#### `apply_price_book`
-
-Apply a price book to an existing quote; updates matching line item prices.
-
-```ruby
-result = TurboDocxSdk::TurboQuote.apply_price_book("quote-uuid", "pricebook-uuid")
-# result["quote"] — updated Quote
-# result["updatedCount"] — Integer, items that were re-priced
-# result["skippedCount"] — Integer, items that had no pricebook match
-# result["message"]
-```
-
-#### `remove_price_book`
-
-```ruby
-quote = TurboDocxSdk::TurboQuote.remove_price_book("quote-uuid")
-```
-
----
-
-### Line Items
-
-#### `list_line_items`
-
-```ruby
-page = TurboDocxSdk::TurboQuote.list_line_items("quote-uuid", "limit" => 50)
-# page["results"], page["totalRecords"]
-```
-
-#### `add_line_items`
-
-Accepts a single item hash **or** an array of up to **50** items. Returns an array of created line item hashes.
-
-`productId`, `productName`, `unitPrice`, and `billingFrequency` are all **required** on every row. `productId` is special: the key must be **present**, but its value may be `nil` for a custom (freeform) line item — omitting the key entirely returns a `400`. `quantity` is optional and defaults to `1`.
-
-```ruby
-items = TurboDocxSdk::TurboQuote.add_line_items("quote-uuid", [
- {
- "productId" => "product-uuid", # required key — nil for a custom line item
- "productName" => "Platform License",
- "quantity" => 2,
- "unitPrice" => "199.00",
- "billingFrequency" => "annual",
- "discountPercent" => "10.0"
- },
- {
- "productId" => nil, # custom (freeform) line item — key still required
- "productName" => "Implementation Credit",
- "unitPrice" => "-250.00",
- "billingFrequency" => "one-time"
- }
-])
-```
-
-:::note Array caps
-`add_line_items` accepts a single hash **or** an array of 1–**50** items. A reorder request accepts up to **200** items. Exceeding either cap returns a `400`.
-:::
-
-#### `add_bundle_line_items`
-
-`bundleId` and `bundleName` are both **required**; the server expands the bundle's child products for you. Accepts a single hash or an array of up to **50** items.
-
-```ruby
-items = TurboDocxSdk::TurboQuote.add_bundle_line_items("quote-uuid", [
- {
- "bundleId" => "bundle-uuid",
- "bundleName" => "Starter Bundle", # required
- "quantity" => 1
- }
-])
-```
-
-#### `update_line_item`
-
-```ruby
-item = TurboDocxSdk::TurboQuote.update_line_item("quote-uuid", "item-uuid",
- "quantity" => 5,
- "unitPrice" => "179.00"
-)
-```
-
-#### `remove_line_item`
-
-```ruby
-result = TurboDocxSdk::TurboQuote.remove_line_item("quote-uuid", "item-uuid")
-# result["message"]
-```
-
----
-
-### Products
-
-| Method | Signature | Returns |
-|---|---|---|
-| `list_products` | `(options = nil)` | paginated list |
-| `create_product` | `(request)` | `Product` |
-| `bulk_create_products` | `(rows)` | [bulk report](#bulk-create) |
-| `get_product` | `(id)` | `Product` |
-| `update_product` | `(id, request)` | `Product` |
-| `delete_product` | `(id)` | `{"message" => ...}` |
-| `duplicate_product` | `(id)` | `Product` |
-| `get_product_primary_images` | `(product_ids)` | `{id => image or nil}` |
-
-:::caution `categoryId` is required on create
-`create_product` **requires** `name`, `categoryId`, `listPrice`, and `billingFrequency`. `categoryId` must be the **UUID** of an existing type (`categoryType` `"product_category"`) — resolve or create it first with `list_types` / `create_type`. It is optional on `update_product`, so you only need to pass it when creating.
-:::
-
-```ruby
-# Resolve the product category first — create_product needs its UUID.
-page = TurboDocxSdk::TurboQuote.list_types("categoryType" => "product_category")
-category = page["results"].find { |t| t["name"] == "Software" }
-category ||= TurboDocxSdk::TurboQuote.create_type(
- "name" => "Software",
- "categoryType" => "product_category"
-)
-
-# Create a product with images (multipart upload auto-detected)
-product = TurboDocxSdk::TurboQuote.create_product(
- "name" => "Enterprise License",
- "categoryId" => category["id"], # required
- "listPrice" => "499.00",
- "cost" => "200.00",
- "billingFrequency" => "annual",
- "showInCatalog" => true,
- "images" => ["/path/to/product-photo.png"] # file path(s) or IO objects
-)
-
-# List products with filters
-page = TurboDocxSdk::TurboQuote.list_products(
- "limit" => 20,
- "query" => "enterprise",
- "showInCatalog" => true
-)
-
-# Bulk fetch primary images for a set of product IDs
-images = TurboDocxSdk::TurboQuote.get_product_primary_images(["id-1", "id-2", "id-3"])
-# images["id-1"] → ProductImage hash, or nil
-```
-
----
-
-### Bundles
-
-| Method | Signature | Returns |
-|---|---|---|
-| `list_bundles` | `(options = nil)` | paginated list |
-| `create_bundle` | `(request)` | `Bundle` |
-| `bulk_create_bundles` | `(rows)` | [bulk report](#bulk-create) |
-| `get_bundle` | `(id)` | `Bundle` |
-| `update_bundle` | `(id, request)` | `Bundle` |
-| `delete_bundle` | `(id)` | `{"message" => ...}` |
-| `duplicate_bundle` | `(id)` | `Bundle` |
-
-Each entry in `items` **requires** `productId`, `unitPrice`, and `billingFrequency`. `quantity` is optional and defaults to `1`.
-
-```ruby
-bundle = TurboDocxSdk::TurboQuote.create_bundle(
- "name" => "Starter Pack",
- "categoryId" => "bundle-category-uuid", # required
- "items" => [
- {
- "productId" => "product-uuid-1",
- "unitPrice" => "199.00",
- "billingFrequency" => "monthly",
- "quantity" => 1
- },
- {
- "productId" => "product-uuid-2",
- "unitPrice" => "49.00",
- "billingFrequency" => "monthly",
- "quantity" => 2
- }
- ],
- "bundleDiscountType" => "percent",
- "bundleDiscountAmount" => 5
-)
-```
-
----
-
-### Price Books
-
-| Method | Signature | Returns |
-|---|---|---|
-| `list_price_books` | `(options = nil)` | paginated list |
-| `create_price_book` | `(request)` | `PriceBook` |
-| `bulk_create_price_books` | `(rows)` | [bulk report](#bulk-create) |
-| `get_price_book` | `(id)` | `PriceBook` |
-| `update_price_book` | `(id, request)` | `PriceBook` |
-| `delete_price_book` | `(id)` | `{"message" => ...}` |
-| `duplicate_price_book` | `(id)` | `PriceBook` |
-| `list_price_book_products` | `(id, options = nil)` | paginated list |
-
-```ruby
-# name, priceBookTypeId, and validFrom are REQUIRED.
-# discountPercent is optional and defaults to 0 if not provided.
-# priceBookTypeId comes from a create_type with categoryType "pricebook_type".
-pricebook = TurboDocxSdk::TurboQuote.create_price_book(
- "name" => "Partner Tier A",
- "priceBookTypeId" => "pricebook-type-uuid",
- "validFrom" => "2026-01-01",
- "discountPercent" => 15,
- "isDefault" => false,
- "productPricing" => [
- { "productId" => "product-uuid-1", "discountType" => "percent", "discountPercent" => 25 },
- { "productId" => "product-uuid-2", "discountType" => "percent", "discountPercent" => 30 }
- ]
-)
-
-# List products attached to a price book
-page = TurboDocxSdk::TurboQuote.list_price_book_products("pricebook-uuid", "limit" => 50)
-```
-
----
-
-### Companies
-
-| Method | Signature | Returns |
-|---|---|---|
-| `list_companies` | `(options = nil)` | paginated list |
-| `create_company` | `(request)` | `Company` |
-| `bulk_create_companies` | `(rows)` | [bulk report](#bulk-create) |
-| `get_company` | `(id)` | `Company` |
-| `update_company` | `(id, request)` | `Company` |
-| `delete_company` | `(id)` | `{"message" => ...}` |
-| `list_company_contacts` | `(company_id, options = nil)` | paginated list |
-
-```ruby
-company = TurboDocxSdk::TurboQuote.create_company(
- "name" => "Acme Corporation",
- "phone" => "+1-555-0100",
- "city" => "San Francisco",
- "state" => "CA",
- "country" => "US",
- "contacts" => [ # at least one contact required; each needs name + email
- {
- "name" => "Jane Doe",
- "email" => "jane@acme.com",
- "title" => "VP of Engineering"
- }
- ]
-)
-
-# List contacts for a company
-contacts = TurboDocxSdk::TurboQuote.list_company_contacts(company["id"])
-```
-
----
-
-### Contacts
-
-| Method | Signature | Returns |
-|---|---|---|
-| `list_contacts` | `(options = nil)` | paginated list |
-| `create_contact` | `(request)` | `Contact` |
-| `bulk_create_contacts` | `(rows)` | [bulk report](#bulk-create) |
-| `update_contact` | `(id, request)` | `Contact` |
-| `delete_contact` | `(id)` | `{"message" => ...}` |
-
-:::note No `get_contact` by design
-The backend has no `GET /v1/contacts/:id` endpoint. Fetch individual contacts via `list_company_contacts` or `list_contacts` with a query filter.
-:::
-
-```ruby
-contact = TurboDocxSdk::TurboQuote.create_contact(
- "name" => "John Smith",
- "email" => "john@acme.com",
- "companyId" => "company-uuid"
-)
-```
-
----
-
-### Quote Templates
-
-| Method | Signature | Returns |
-|---|---|---|
-| `list_templates` | `(options = nil)` | paginated list |
-| `get_template` | `()` | singleton `QuoteTemplate` — auto-created if none exists |
-| `get_template_by_id` | `(id)` | `QuoteTemplate` |
-| `create_template` | `(request)` | `QuoteTemplate` — `400` if one already exists |
-| `update_template` | `(id, request)` | `QuoteTemplate` |
-| `delete_template` | `(id)` | `{"message" => ...}` — resets to org branding defaults |
-
-:::warning Templates are auto-provisioned — use `get_template` → `update_template`
-`get_template` **self-heals**: if the org has no template, the API creates one from your org branding and returns it. Every established org therefore already has a template, which means:
-
-- `create_template` returns **400 `TEMPLATE_ALREADY_EXISTS`** and is effectively unreachable. Do not build a get-then-create flow.
-- `delete_template` is really "reset to org branding defaults" — it soft-deletes, and the next `get_template` regenerates a fresh one.
-
-The correct flow is **`get_template` → `update_template`**.
-:::
-
-```ruby
-# 1. Get the org's quote template (created from org branding on first read)
-template = TurboDocxSdk::TurboQuote.get_template
-
-# 2. Brand it by updating the template you just fetched
-branded = TurboDocxSdk::TurboQuote.update_template(template["id"],
- "logoUrl" => "https://cdn.example.com/logo.png",
- "primaryColor" => "#0057b8",
- "senderName" => "TurboDocx Sales"
-)
-
-# Get a specific named template by ID
-other = TurboDocxSdk::TurboQuote.get_template_by_id("template-uuid")
-```
-
----
-
-### Types / Categories
-
-| Method | Signature | Returns |
-|---|---|---|
-| `list_types` | `(options = nil)` | paginated list |
-| `create_type` | `(request)` | `QuoteType` |
-| `bulk_create_types` | `(rows)` | [bulk report](#bulk-create) |
-| `update_type` | `(id, request)` | `QuoteType` |
-| `delete_type` | `(id)` | `{"message" => ...}` |
-
-:::note No `get_type` by design
-The backend has no `GET /v1/types/:id` endpoint. Use `list_types` to retrieve individual type records.
-:::
-
-```ruby
-quote_type = TurboDocxSdk::TurboQuote.create_type("name" => "New Business")
-
-TurboDocxSdk::TurboQuote.update_type(quote_type["id"], "name" => "New Logo Business")
-```
-
----
-
-### Bulk Create
-
-Six methods import many records in one request — ideal for CSV-style migrations:
-
-| Method | Endpoint | Row shape |
-|---|---|---|
-| `bulk_create_products` | `POST /v1/products/bulk` | same fields as `create_product` |
-| `bulk_create_price_books` | `POST /v1/pricebooks/bulk` | same fields as `create_price_book` |
-| `bulk_create_bundles` | `POST /v1/bundles/bulk` | same fields as `create_bundle` |
-| `bulk_create_companies` | `POST /v1/companies/bulk` | same fields as `create_company` — each row **requires a `contacts` array with at least one contact** |
-| `bulk_create_contacts` | `POST /v1/contacts/bulk` | same fields as `create_contact` — each row **requires a `companyId`** |
-| `bulk_create_types` | `POST /v1/types/bulk` | same fields as `create_type` |
-
-Each method takes an array of row hashes; the SDK wraps them in the `{ "rows" => [...] }` envelope the endpoint expects. Rows use the exact request shape of the corresponding single-create method (camelCase keys).
-
-:::caution Product rows require a real `categoryId`
-Every `bulk_create_products` row **requires** `name`, `categoryId`, `listPrice`, and `billingFrequency`. `categoryId` must be the **UUID** of an existing type (`categoryType` `"product_category"`) — there is no `categoryName` key on the bulk row schema, and the API rejects unknown keys, so passing one returns a `400`. Resolve or create the category first with `list_types` / `create_type`, then pass its `"id"`.
-:::
-
-```ruby
-# 1. Resolve the product category first — bulk rows need its UUID, not its name.
-page = TurboDocxSdk::TurboQuote.list_types("categoryType" => "product_category")
-category = page["results"].find { |t| t["name"] == "Software" }
-category ||= TurboDocxSdk::TurboQuote.create_type(
- "name" => "Software",
- "categoryType" => "product_category"
-)
-category_id = category["id"]
-
-# 2. Import, passing the resolved UUID on every row.
-report = TurboDocxSdk::TurboQuote.bulk_create_products([
- { "name" => "Starter License", "categoryId" => category_id, "listPrice" => "99.00", "billingFrequency" => "monthly" },
- { "name" => "Pro License", "categoryId" => category_id, "listPrice" => "299.00", "billingFrequency" => "monthly" },
- { "name" => "Enterprise License", "categoryId" => category_id, "listPrice" => "499.00", "billingFrequency" => "annual" }
-])
-
-puts "Imported #{report['imported']} rows"
-report["failed"].each do |failure|
- puts "Row #{failure['row']} failed: #{failure['reason']}" # "row" is 1-indexed
-end
-report["adjusted"].each do |adjustment|
- puts "Row #{adjustment['row']} adjusted: #{adjustment['reason']}"
-end
-```
-
-**Partial-success semantics** — every bulk method returns the same report hash instead of raising for bad rows:
-
-| Key | Type | Meaning |
-|---|---|---|
-| `imported` | Integer | Rows created successfully |
-| `failed` | Array of `{"row", "reason"}` | Rows the server rejected — `row` is the **1-indexed** position in your input array |
-| `adjusted` | Array of `{"row", "reason"}` | Rows the server imported after modifying them (e.g. a bundle item whose product wasn't found was dropped) |
-
-Rules and limits:
-
-- **A failed row does not raise** and does not roll back earlier rows — always check `report["failed"]` after the call.
-- **Max 500 rows per request.** Exceeding the cap fails the whole request with a `400` (`TurboDocxSdk::ValidationError`); an invalid envelope (e.g. non-array input) also raises `ValidationError`.
-- **Roles:** available to **administrator and contributor** API keys.
-- The SDK performs no client-side row validation — errors come back per-row in the report.
-
-```ruby
-# Companies need >= 1 contact per row; contacts need a companyId per row
-TurboDocxSdk::TurboQuote.bulk_create_companies([
- {
- "name" => "Acme Corporation",
- "contacts" => [{ "name" => "Jane Doe", "email" => "jane@acme.com" }]
- }
-])
-
-TurboDocxSdk::TurboQuote.bulk_create_contacts([
- { "name" => "John Smith", "email" => "john@acme.com", "companyId" => "company-uuid" }
-])
-```
-
----
-
-### Convenience
-
-#### `create_and_send`
-
-Orchestrates multiple API calls: create quote → add product items → add bundle items → send. Returns `{ "quote" => }`.
-
-```ruby
-result = TurboDocxSdk::TurboQuote.create_and_send(
- # CreateQuote fields
- "name" => "Year-End Renewal",
- "companyId" => "company-uuid",
- "contactId" => "contact-uuid",
- "currency" => "USD",
- # optional line items added before send
- "items" => [
- { "productId" => "product-uuid-1", "productName" => "Platform License", "quantity" => 10, "unitPrice" => "99.00", "billingFrequency" => "monthly" }
- ],
- # optional bundle items added before send
- "bundleItems" => [
- { "bundleId" => "bundle-uuid-1", "bundleName" => "Starter Pack", "quantity" => 1 }
- ],
- # send options (ccEmails, validUntil)
- "send" => {
- "ccEmails" => ["finance@example.com"],
- "validUntil" => "2026-12-31"
- }
-)
-puts result["quote"]["id"]
-```
-
----
-
-## Constants
-
-The gem exposes constants so you can avoid hard-coding string literals:
-
-```ruby
-TurboDocxSdk::QuoteStatus::DRAFT # "draft"
-TurboDocxSdk::QuoteStatus::SENT # "sent"
-TurboDocxSdk::BillingFrequency::MONTHLY # "monthly"
-TurboDocxSdk::BillingFrequency::ANNUAL # "annual"
-TurboDocxSdk::DiscountType::PERCENT # "percent"
-TurboDocxSdk::DiscountType::AMOUNT # "amount"
-TurboDocxSdk::Currency::USD # "USD"
-TurboDocxSdk::LineItemType::PRODUCT # "product"
-TurboDocxSdk::CategoryType::PRICEBOOK_TYPE # "pricebook_type"
-```
-
----
-
-## Error Handling
-
-```ruby
-require "turbodocx_sdk"
-
-begin
- quote = TurboDocxSdk::TurboQuote.create_quote(
- "name" => "Q3 Proposal",
- "companyId" => "company-uuid",
- "contactId" => "contact-uuid"
- )
-rescue TurboDocxSdk::ValidationError => e
- # 400 — missing required fields, invalid enum value, etc.
- puts "Validation failed: #{e.message}"
-rescue TurboDocxSdk::AuthenticationError
- # 401 — bad or revoked API key
-rescue TurboDocxSdk::AuthorizationError
- # 403 — key valid but lacks permission for this org/action
-rescue TurboDocxSdk::NotFoundError => e
- # 404 — company, contact, or product not found
- puts "Not found: #{e.message}"
-rescue TurboDocxSdk::RateLimitError
- # 429 — back off and retry
-rescue TurboDocxSdk::NetworkError
- # request never reached the server (DNS, refused, timeout)
-rescue TurboDocxSdk::TurboDocxError => e
- # catch-all for any other typed SDK error (raw 5xx, etc.)
- puts "Error #{e.status_code}: #{e.message}"
-end
-```
-
-### Error Code Reference
-
-| Status | Class | When |
-|---|---|---|
-| 400 | `ValidationError` | Invalid fields, missing required keys, bad enum value |
-| 401 | `AuthenticationError` | Missing or invalid API key |
-| 403 | `AuthorizationError` | Valid key without permission for this operation |
-| 404 | `NotFoundError` | Quote, product, company, or other resource not found |
-| 409 | `ConflictError` | Duplicate resource (e.g., company domain conflict) |
-| 429 | `RateLimitError` | Rate limit exceeded — back off |
-
----
-
-## See Also
-
-- [TurboQuote JavaScript / TypeScript SDK](/docs/SDKs/quote-javascript) — same API, JS idioms
-- [TurboQuote Python SDK](/docs/SDKs/quote-python) — same API, Python idioms
-- [TurboSign Ruby SDK](/docs/SDKs/ruby) — send documents for e-signature
-- [TurboWebhooks Ruby SDK](/docs/SDKs/webhooks-ruby) — receive real-time signature events
-- [Deliverable Ruby SDK](/docs/SDKs/deliverable-ruby) — generate documents from templates
-- [SDKs Overview](/docs/SDKs) — all SDKs across all languages
-- [turbodocx-sdk on RubyGems](https://rubygems.org/gems/turbodocx-sdk)
-- [TurboDocx SDK on GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/ruby-sdk)
diff --git a/docs/SDKs/quote.md b/docs/SDKs/quote.md
index 43ee63a..135b2c9 100644
--- a/docs/SDKs/quote.md
+++ b/docs/SDKs/quote.md
@@ -2,7 +2,7 @@
title: TurboQuote SDK
sidebar_position: 16
sidebar_label: TurboQuote
-description: 'TurboQuote SDK for JavaScript, TypeScript, Python, PHP, Go and Java: create quotes, add line items, apply price books, send and download PDFs.'
+description: 'TurboQuote SDK for JavaScript, TypeScript, Python, PHP, Go, Java and Ruby: create quotes, add line items, apply price books, send and download PDFs.'
keywords:
- turboquote javascript
- turboquote typescript
@@ -51,6 +51,12 @@ keywords:
- gradle turboquote
- line items java
- pricebook java
+- turboquote ruby
+- quote sdk ruby
+- cpq ruby
+- turbodocx quote ruby
+- gem turbodocx
+- price book ruby
---
import Tabs from '@theme/Tabs';
@@ -118,6 +124,17 @@ The official TurboDocx TurboQuote SDK for Java applications. Create and send sal
TurboQuote is TurboDocx's CPQ (Configure, Price, Quote) module. Build a product catalog, assemble quotes with line items, apply price book discounts, and send branded proposals to contacts — with optional TurboSign e-signature delivery via `sendQuoteWithDeliverable`. A `draft` can also be marked `declined` directly, for a deal that dies before the quote is ever sent. The client config takes no `senderEmail`; the quote's **"Prepared by"** sender comes from your org quote template (see the note below `createQuote`).
:::
+
+
+
+The official TurboDocx TurboQuote SDK for Ruby applications. Build full CPQ (configure, price, quote) workflows: create quotes, add product and bundle line items, apply price books, send proposals to contacts, and download PDF exports, all from plain Ruby 2.7+. Distributed on RubyGems as `turbodocx-sdk` (same gem as TurboSign, TurboWebhooks, and Deliverable).
+
+
+
+:::info What is TurboQuote?
+TurboQuote is TurboDocx's quoting and proposal engine. It covers the full quote lifecycle (draft, send, accept, decline, void) with a product catalog, bundle groupings, price books, company and contact CRM, and customizable quote templates. A draft can also be marked declined directly, for a deal that dies before the quote is ever sent. Quotes can be sent with an attached Deliverable document through `send_quote_with_deliverable` for a branded proposal experience.
+:::
+
@@ -237,6 +254,25 @@ import com.turbodocx.TurboDocxException;
import com.turbodocx.models.quote.*;
```
+
+
+
+```bash
+gem install turbodocx-sdk
+```
+
+Or add it to your `Gemfile` and run `bundle install`:
+
+```ruby
+gem "turbodocx-sdk"
+```
+
+Then require it:
+
+```ruby
+require "turbodocx_sdk"
+```
+
@@ -280,6 +316,14 @@ import com.turbodocx.models.quote.*;
- Gson 2.x (included transitively)
- A TurboDocx API key (`TDX-` prefix) — no administrator role required
+
+
+
+- Ruby 2.7 or higher
+- No runtime dependencies (the gem uses the standard library's `net/http`)
+- A TurboDocx API key (`TDX-` prefix), generated in **Settings → API Keys**
+- All SDK methods are synchronous, so no async wrapper is needed
+
@@ -424,6 +468,29 @@ TurboQuote tq = client.turboQuote();
The quote's **"Prepared by"** sender comes from your **org quote template** instead. Because an API key has no mailbox of its own, every sender-resolving call — `createQuote`, `duplicateQuote`, `sendQuote` / `sendQuoteWithDeliverable`, and `handleExpiredQuote` — fails with a `ValidationException` (`400 SenderEmailRequired`) when the org's quote template has no sender email set. A companion `400 SenderNameRequired` is returned when no sender **name** resolves. Configure both **Sender Name** and **Sender Email** once (`updateTemplate`) and all of them resolve cleanly.
+
+
+
+```ruby
+require "turbodocx_sdk"
+
+TurboDocxSdk::TurboQuote.configure(
+ api_key: ENV["TURBODOCX_API_KEY"],
+ org_id: ENV["TURBODOCX_ORG_ID"], # optional, auto-reads the env var
+ # access_token: "your-oauth-token", # optional, OAuth token instead of api_key
+)
+```
+
+:::tip No sender email on the client, but set one on your quote template
+`TurboQuote.configure` does **not** take `sender_email` or `sender_name`, because quotes are not signature emails. Only a credential is required (`api_key` or `access_token`); `org_id` falls back to `TURBODOCX_ORG_ID`. If you skip the explicit call, the SDK lazily configures itself from `TURBODOCX_API_KEY`, `TURBODOCX_ORG_ID` and `TURBODOCX_BASE_URL` on first use.
+
+The quote's **"Prepared by"** sender comes from your **org quote template** instead. Because an API key has no mailbox of its own, if the org's quote template has no sender email set, `create_quote`, `duplicate_quote`, `send_quote` / `send_quote_with_deliverable`, and `handle_expired_quote` still succeed: they fall back to a generic TurboDocx sender (`no-reply@turbodocx.com`) rather than rejecting the call. Configure both **Sender Name** and **Sender Email** once (`template = TurboDocxSdk::TurboQuote.get_template` then `TurboDocxSdk::TurboQuote.update_template(template["id"], "senderEmail" => ..., "senderName" => ...)`) so quotes show your own sender identity instead of the generic fallback.
+:::
+
+:::note camelCase request keys
+Method names are snake_case, but the keys **inside** every request hash (`companyId`, `validUntil`, `unitPrice`, and so on) are **camelCase**. The SDK forwards them to the API verbatim and does not convert snake_case hash keys.
+:::
+
@@ -493,6 +560,20 @@ TURBODOCX_BASE_URL=https://api.turbodocx.com
Both `apiKey` and `orgId` are required. To get your credentials, follow the [Get Your Credentials](/docs/SDKs#1-get-your-credentials) steps from the SDKs main page.
:::
+
+
+
+```bash
+TURBODOCX_API_KEY=your_api_key
+TURBODOCX_ORG_ID=your_org_id
+# optional, defaults to https://api.turbodocx.com
+TURBODOCX_BASE_URL=https://api.turbodocx.com
+```
+
+:::caution API Credentials Required
+Both `api_key` and `org_id` are needed for most API requests. To get your credentials, follow the [Get Your Credentials](/docs/SDKs#1-get-your-credentials) steps from the SDKs main page.
+:::
+
@@ -590,6 +671,47 @@ writeFileSync('quote.pdf', Buffer.from(pdf));
+
+
+
+The most common flow: create a quote for a company and contact, add a product line item, send it, then download the PDF.
+
+```ruby
+require "turbodocx_sdk"
+
+TurboDocxSdk::TurboQuote.configure(
+ api_key: ENV["TURBODOCX_API_KEY"],
+ org_id: ENV["TURBODOCX_ORG_ID"]
+)
+
+# 1. Create a draft quote
+quote = TurboDocxSdk::TurboQuote.create_quote(
+ "name" => "Acme Corp: Enterprise Plan",
+ "companyId" => "company-uuid",
+ "contactId" => "contact-uuid",
+ "currency" => "USD",
+ "termDays" => 30
+)
+
+# 2. Add a product line item
+TurboDocxSdk::TurboQuote.add_line_items(quote["id"],
+ "productId" => "product-uuid",
+ "productName" => "Enterprise Licence",
+ "unitPrice" => 1200,
+ "billingFrequency" => "annual",
+ "quantity" => 5
+)
+
+# 3. Send the quote
+sent = TurboDocxSdk::TurboQuote.send_quote(quote["id"], "validUntil" => "2026-07-31")
+puts sent["message"] # "Quote sent successfully"
+puts sent["quote"]["status"] # "sent"
+
+# 4. Download the PDF
+pdf = TurboDocxSdk::TurboQuote.download_quote_pdf(quote["id"])
+File.binwrite("quote.pdf", pdf)
+```
+
@@ -669,6 +791,59 @@ public class QuoteLifecycle {
}
```
+
+
+
+```ruby
+require "turbodocx_sdk"
+
+TurboDocxSdk::TurboQuote.configure(
+ api_key: ENV["TURBODOCX_API_KEY"],
+ org_id: ENV["TURBODOCX_ORG_ID"]
+)
+
+# Step 1: Create a company with an initial contact
+company = TurboDocxSdk::TurboQuote.create_company(
+ "name" => "Acme Corp",
+ "contacts" => [{ "name" => "Alice Buyer", "email" => "alice@example.com" }]
+)
+contacts = TurboDocxSdk::TurboQuote.list_company_contacts(company["id"])
+contact_id = contacts["results"][0]["id"]
+
+# Step 2: Create a quote
+quote = TurboDocxSdk::TurboQuote.create_quote(
+ "name" => "Q1 Software License",
+ "companyId" => company["id"],
+ "contactId" => contact_id,
+ "termDays" => 30,
+ "currency" => TurboDocxSdk::Currency::USD
+)
+puts "Quote: #{quote['id']} status=#{quote['status']}"
+
+# Step 3: Add a line item.
+# productId, productName, unitPrice and billingFrequency are all required.
+# productId may be nil (custom line item), but the key must always be present.
+line_items = TurboDocxSdk::TurboQuote.add_line_items(quote["id"],
+ "productId" => "product-uuid",
+ "productName" => "Enterprise Software License",
+ "unitPrice" => 1200.00,
+ "quantity" => 3,
+ "billingFrequency" => TurboDocxSdk::BillingFrequency::ANNUAL,
+ "discountType" => TurboDocxSdk::DiscountType::PERCENT,
+ "discountPercent" => 10
+)
+puts "Added #{line_items.length} line item(s)"
+
+# Step 4: Send the quote
+sent = TurboDocxSdk::TurboQuote.send_quote(quote["id"], "validUntil" => "2026-09-30")
+puts "Sent. Status: #{sent['quote']['status']}"
+
+# Step 5: Download the PDF
+pdf = TurboDocxSdk::TurboQuote.download_quote_pdf(quote["id"])
+File.binwrite("quote.pdf", pdf)
+puts "PDF saved (#{pdf.bytesize} bytes)"
+```
+
@@ -699,6 +874,30 @@ CreateAndSendResponse result = tq.createAndSend(req);
System.out.println("Quote created and sent: " + result.getQuote().getId());
```
+
+
+
+`create_and_send` is a convenience method that creates the quote, adds line items and bundle items, and sends it in one call (several API requests run in sequence under the hood).
+
+```ruby
+result = TurboDocxSdk::TurboQuote.create_and_send(
+ "name" => "Partner Proposal",
+ "companyId" => company_id,
+ "contactId" => contact_id,
+ "items" => [
+ {
+ "productId" => "product-uuid", # required key, nil for a custom line item
+ "productName" => "Starter Plan",
+ "unitPrice" => 499.00,
+ "quantity" => 1,
+ "billingFrequency" => "monthly" # required
+ }
+ ],
+ "send" => { "validUntil" => "2026-09-30" } # send options, such as ccEmails and validUntil
+)
+puts "Quote created and sent: #{result['quote']['id']}"
+```
+
@@ -722,6 +921,22 @@ SendQuoteWithDeliverableResponse sendResp = tq.sendQuoteWithDeliverable(quoteId,
System.out.println("Document ID: " + sendResp.getDocumentId());
```
+
+
+
+```ruby
+# Apply a price book to recalculate line item prices
+applied = TurboDocxSdk::TurboQuote.apply_price_book(quote_id, price_book_id)
+puts "Updated: #{applied['updatedCount']}, Skipped: #{applied['skippedCount']}"
+
+# Send with a TurboDocx deliverable attached
+sent = TurboDocxSdk::TurboQuote.send_quote_with_deliverable(quote_id,
+ "deliverableId" => "your-deliverable-id",
+ "mergePosition" => "end"
+)
+puts "Document ID: #{sent['documentId']}"
+```
+
@@ -920,6 +1135,68 @@ func main() {
}
```
+
+
+
+```ruby
+require "turbodocx_sdk"
+
+TurboDocxSdk::TurboQuote.configure(
+ api_key: ENV["TURBODOCX_API_KEY"],
+ org_id: ENV["TURBODOCX_ORG_ID"]
+)
+
+# 1. Create a draft quote
+quote = TurboDocxSdk::TurboQuote.create_quote(
+ "name" => "Acme Corp: Annual Plan Q3",
+ "companyId" => "company-uuid",
+ "contactId" => "contact-uuid",
+ "validUntil" => "2026-09-30",
+ "currency" => "USD"
+)
+quote_id = quote["id"]
+puts "Created quote #{quote_id}"
+
+# 2. Add product line items
+items = TurboDocxSdk::TurboQuote.add_line_items(quote_id, [
+ {
+ "productId" => "product-uuid-1",
+ "productName" => "Platform License",
+ "quantity" => 5,
+ "unitPrice" => "199.00",
+ "billingFrequency" => "annual"
+ },
+ {
+ "productId" => "product-uuid-2",
+ "productName" => "Onboarding Package",
+ "quantity" => 1,
+ "unitPrice" => "499.00",
+ "billingFrequency" => "one-time"
+ }
+])
+puts "Added #{items.length} line items"
+
+# 3. Apply a price book (optional)
+result = TurboDocxSdk::TurboQuote.apply_price_book(quote_id, "pricebook-uuid")
+puts "Price book applied: #{result['updatedCount']} items updated"
+
+# 4. Send the quote
+sent = TurboDocxSdk::TurboQuote.send_quote(quote_id,
+ "ccEmails" => ["manager@acme.com"],
+ "validUntil" => "2026-09-30"
+)
+puts "Sent: #{sent['message']}"
+
+# 5. Download the PDF
+pdf_bytes = TurboDocxSdk::TurboQuote.download_quote_pdf(quote_id)
+File.binwrite("acme-quote.pdf", pdf_bytes)
+puts "PDF saved (#{pdf_bytes.bytesize} bytes)"
+```
+
+:::caution Always Handle Errors
+The above example omits error handling for brevity. In production, wrap TurboQuote calls in `begin` / `rescue` blocks. See [Error Handling](#error-handling) for complete patterns.
+:::
+
@@ -1036,6 +1313,29 @@ if err != nil {
fmt.Printf("Quote %s sent\n", resp.Quote.QuoteNumber)
```
+
+
+
+```ruby
+result = TurboDocxSdk::TurboQuote.create_and_send(
+ "name" => "Acme Corp: Quick Proposal",
+ "companyId" => "company-uuid",
+ "contactId" => "contact-uuid",
+ "currency" => "USD",
+ "items" => [
+ { "productId" => "product-uuid-1", "productName" => "Platform License", "quantity" => 3, "unitPrice" => "299.00", "billingFrequency" => "monthly" }
+ ],
+ "bundleItems" => [
+ { "bundleId" => "bundle-uuid-1", "bundleName" => "Starter Pack", "quantity" => 1 }
+ ],
+ "send" => {
+ "ccEmails" => ["cc@example.com"],
+ "validUntil" => "2026-09-30"
+ }
+)
+puts "Quote sent: #{result['quote']['id']}"
+```
+
@@ -1070,6 +1370,13 @@ All methods are instance methods on `com.turbodocx.TurboQuote`. Obtain the insta
---
+
+
+
+All methods are class-level on `TurboDocxSdk::TurboQuote`; configure once, then call on the class. Responses are plain Ruby `Hash`es with string keys, and request hashes use camelCase keys.
+
+---
+
@@ -1187,6 +1494,25 @@ list.getResults().forEach(q ->
System.out.println(q.getId() + " " + q.getStatus()));
```
+
+
+
+```ruby
+page = TurboDocxSdk::TurboQuote.list_quotes(
+ "limit" => 20,
+ "offset" => 0,
+ "query" => "acme",
+ "statuses" => ["draft", "sent"] # repeated-key filter
+)
+# page["results"], page["totalRecords"], page["stats"]
+
+page["results"].each do |q|
+ puts "#{q['name']}: #{q['status']}"
+end
+```
+
+Other filters: `companyId`, `contactId`, and `currency`.
+
@@ -1368,6 +1694,40 @@ Quote subscription = tq.createQuote(subReq);
`renewalPeriod` is **required** when `termDays` is `-1`, and must be **null or unset** for every other `termDays` value — sending it alongside a fixed term returns a `400`. The same rule applies on `updateQuote`.
:::
+
+
+
+```ruby
+# Fixed-term quote: termDays is -1 or 0 to 3650; omit it to get the default of 60.
+quote = TurboDocxSdk::TurboQuote.create_quote(
+ "name" => "Q3 Proposal",
+ "companyId" => "company-uuid", # required
+ "contactId" => "contact-uuid", # required
+ "currency" => "USD",
+ "termDays" => 30, # fixed term, so do NOT send renewalPeriod with this
+ "validUntil" => "2026-09-30",
+ "notes" => "Includes implementation services",
+ "taxRate" => "8.5"
+)
+# returns the quote Hash
+
+# Auto-renewal quote: termDays -1 REQUIRES renewalPeriod.
+subscription = TurboDocxSdk::TurboQuote.create_quote(
+ "name" => "Annual Subscription",
+ "companyId" => "company-uuid",
+ "contactId" => "contact-uuid",
+ "currency" => "USD",
+ "termDays" => -1, # -1 = auto-renewal
+ "renewalPeriod" => "annually" # "weekly" | "monthly" | "quarterly" | "annually"
+)
+```
+
+:::caution `termDays` and `renewalPeriod` are coupled
+`termDays` defaults to **60** when omitted. Valid values are `-1` (auto-renewal) or `0` to `3650` (`0` = one-time).
+
+`renewalPeriod` is **required** when `termDays` is `-1`, and must be **`nil` or absent** for every other `termDays` value. Sending it alongside a fixed term returns a `400`. The same rule applies on `update_quote`.
+:::
+
@@ -1458,6 +1818,21 @@ if (quote.getPreparedBy() != null) {
`preparedBy` is resolved server-side (org template first, then the quote's creator). **Prefer it over `getCreator()`** for any customer-facing display — the creator may be the internal API service account. For an API-created quote the resolved name is the **API key's name** (never a generic "API Service User"), and the email comes from the org quote template. `preparedBy` is returned by the **single-quote fetch only** — it is not present on create, duplicate, or list responses.
+
+
+
+Returns the quote with `statusInfo` and `preparedBy` merged in when present. `preparedBy` is the resolved "Prepared by" identity shown on the quote PDF.
+
+```ruby
+quote = TurboDocxSdk::TurboQuote.get_quote("quote-uuid")
+# quote["status"], quote["grandTotal"], quote["statusInfo"]
+prepared = quote["preparedBy"] || {}
+puts prepared["name"] # e.g. "Acme Billing Integration" or the template sender
+puts prepared["email"] # may be nil for an API-created quote, so render a placeholder
+```
+
+`preparedBy` is resolved server-side (org template first, then the quote's creator). **Prefer it over `creator`** for any customer-facing display, because `creator` may be the internal API service account. For an API-created quote the resolved name is the **API key's name** (never a generic "API Service User"), and the email comes from the org quote template. `preparedBy` is returned by the **single-quote fetch only**; it is not present on create, duplicate, or list responses.
+
@@ -1554,6 +1929,23 @@ Quote updated = tq.updateQuote(quoteId, req);
`name` is trimmed on both `createQuote` and `updateQuote` — `" Acme "` is stored as `"Acme"` — and a name that is empty once trimmed returns a `400`. A quote can only be renamed while it is a **draft**; on any other status the update is rejected with `Cannot update quote that is not in draft status` (`templateId` is the only field exempt from that rule). Rename before you send, because **sending snapshots the quote's current name onto the TurboSign document** — that is the name signers see in the request email and on the signed PDF.
:::
+
+
+
+All fields are optional, so pass only what changes. Send an explicit `nil` to clear a nullable field.
+
+```ruby
+updated = TurboDocxSdk::TurboQuote.update_quote("quote-uuid",
+ "name" => "Q3 Proposal (Revised)",
+ "validUntil" => "2026-10-15",
+ "taxRate" => nil # clears the field
+)
+```
+
+:::info `name` is trimmed, and renaming is draft-only
+`name` is trimmed on both `create_quote` and `update_quote` (`" Acme "` is stored as `"Acme"`), and a name that is empty once trimmed returns a `400`. A quote can only be renamed while it is a **draft**; on any other status the update is rejected with `Cannot update quote that is not in draft status` (`templateId` is the only field exempt from that rule). Rename before you send, because **sending snapshots the quote's current name onto the TurboSign document**. That is the name signers see in the request email and on the signed PDF.
+:::
+
@@ -1608,6 +2000,14 @@ SuccessResponse resp = tq.deleteQuote(quoteId);
System.out.println(resp.getMessage());
```
+
+
+
+```ruby
+result = TurboDocxSdk::TurboQuote.delete_quote("quote-uuid")
+# result["message"]
+```
+
@@ -1673,6 +2073,18 @@ System.out.println("New quote: " + copy.getId());
The copy is named **`Copy of `**, truncated to the name column's 255-character limit. Rename it with `updateQuote` before sending if that is not what you want signers to see.
+
+
+
+```ruby
+new_quote = TurboDocxSdk::TurboQuote.duplicate_quote("quote-uuid")
+# returns the new quote in draft status
+```
+
+The copy is named **`Copy of `**, truncated to the name column's 255-character limit. Rename it with `update_quote` before sending if that is not what you want signers to see.
+
+The copy is attributed to **whoever ran the duplicate**, not to the original quote's creator. Duplicating with an API key produces a quote whose "Prepared by" resolves through that API key and your org quote template.
+
@@ -1746,6 +2158,19 @@ ApplyPriceBookResponse resp = tq.applyPriceBook(quoteId, priceBookId);
System.out.println("Updated " + resp.getUpdatedCount() + " items.");
```
+
+
+
+Apply a price book to an existing quote; updates matching line item prices.
+
+```ruby
+result = TurboDocxSdk::TurboQuote.apply_price_book("quote-uuid", "pricebook-uuid")
+# result["quote"] the updated quote
+# result["updatedCount"] Integer, items that were re-priced
+# result["skippedCount"] Integer, items that had no price book match
+# result["message"]
+```
+
@@ -1802,6 +2227,13 @@ Remove the applied price book from a quote, restoring original line item pricing
Quote quote = tq.removePriceBook(quoteId);
```
+
+
+
+```ruby
+quote = TurboDocxSdk::TurboQuote.remove_price_book("quote-uuid")
+```
+
@@ -1880,6 +2312,18 @@ Files.write(Paths.get("quote.pdf"), pdf);
---
+
+
+
+Returns the raw PDF bytes as a binary `String`.
+
+```ruby
+pdf_bytes = TurboDocxSdk::TurboQuote.download_quote_pdf("quote-uuid")
+File.binwrite("quote.pdf", pdf_bytes)
+```
+
+---
+
@@ -1953,6 +2397,17 @@ System.out.println(config.getFormat().getPrefix()); // e.g. "Q-"
System.out.println(config.getCurrentFloor()); // the current per-period issued floor
```
+
+
+
+Fetch the org's current quote numbering format and the current per-period issued floor.
+
+```ruby
+config = TurboDocxSdk::TurboQuote.get_quote_number_config
+puts config["format"]["prefix"] # e.g. "Q-"
+puts config["currentFloor"] # the current per-period issued floor
+```
+
@@ -2065,6 +2520,27 @@ QuoteNumberConfig config = tq.updateQuoteNumberConfig(format);
System.out.println(config.getFormat().getStartNumber()); // 1000
```
+
+
+
+Update the numbering format. Pass the full format hash; all eight fields are required. Keys stay camelCase.
+
+```ruby
+config = TurboDocxSdk::TurboQuote.update_quote_number_config(
+ "prefix" => "INV",
+ "yearToken" => "none", # "none" | "two" | "four"
+ "monthToken" => "off", # "off" | "two"
+ "separator" => "-",
+ "padWidth" => 4, # 0 to 12
+ "suffix" => "",
+ "startNumber" => 1000, # >= 0
+ "resetCadence" => "never" # "never" | "yearly" | "monthly"
+)
+puts config["format"]["startNumber"] # 1000
+```
+
+The SDK also exposes constants for the enum values: `TurboDocxSdk::QuoteNumberYearToken`, `TurboDocxSdk::QuoteNumberMonthToken`, and `TurboDocxSdk::QuoteNumberResetCadence`.
+
@@ -2208,6 +2684,34 @@ Beyond the per-field caps, the API rejects self-inconsistent formats with a `400
---
+
+
+
+All eight `format` keys are sent on every update. The API enforces these caps and allowed values; a violation returns `400`:
+
+| Field | Type | Allowed / range | Default |
+|-------|------|-----------------|---------|
+| `prefix` | string | ≤ 12 characters | `"Q"` |
+| `yearToken` | enum | `none` \| `two` \| `four` | `four` |
+| `monthToken` | enum | `off` \| `two` | `off` |
+| `separator` | string | ≤ 4 characters | `"-"` |
+| `padWidth` | integer | `0` to `12` (`0` = no padding) | `5` |
+| `suffix` | string | ≤ 12 characters | `""` |
+| `startNumber` | integer | `0` to `1000000000` | `1` |
+| `resetCadence` | enum | `never` \| `yearly` \| `monthly` | `yearly` |
+
+An org that has never configured numbering uses the **default format** above, which renders like `Q-2026-00001`.
+
+Beyond the per-field caps, the API rejects self-inconsistent formats with a `400`:
+
+- `resetCadence: "yearly"` requires a year token (`yearToken` other than `none`), otherwise numbers repeat across years.
+- `resetCadence: "monthly"` requires **both** a year token and a month token (`monthToken: "two"`).
+- The rendered quote number must be ≤ 256 characters.
+
+`currentFloor` (returned by both methods) is read-only. It is the sequence the next quote will use for the current period, and it is never sent on update.
+
+---
+
@@ -2334,6 +2838,29 @@ handle-expired flow to void it and create a fresh draft.
:::
+
+
+
+:::caution Send preconditions
+
+Both send methods share the same server-side checks. Each is rejected with **HTTP 400** (raised as `TurboDocxSdk::ValidationError`) and a
+specific error `code` before anything is created or emailed:
+
+| Condition | Code |
+| :--- | :--- |
+| Quote is not a draft | `QuoteNotSendable` |
+| No `validUntil` date set | `QuoteValidUntilRequired` |
+| `validUntil` is in the past | `QuoteExpired` |
+| No line items | `QuoteHasNoLineItems` |
+| Contact missing a name or email | `QuoteContactRequired` |
+| Company or contact deleted/deactivated | `QuoteCustomerInactive` |
+
+A quote with **no line items cannot be sent**, so add at least one product, bundle, or custom
+line item first. Likewise an **expired quote is rejected**; update `validUntil`, or use
+`handle_expired_quote` to close it out and create a fresh draft.
+
+:::
+
@@ -2419,6 +2946,17 @@ SendQuoteResponse resp = tq.sendQuote(quoteId);
System.out.println("Status: " + resp.getQuote().getStatus());
```
+
+
+
+```ruby
+sent = TurboDocxSdk::TurboQuote.send_quote("quote-uuid",
+ "ccEmails" => ["manager@example.com"],
+ "validUntil" => "2026-09-30"
+)
+# sent["quote"], sent["message"]
+```
+
@@ -2510,6 +3048,19 @@ SendQuoteWithDeliverableResponse resp = tq.sendQuoteWithDeliverable(quoteId, req
System.out.println("Document ID: " + resp.getDocumentId());
```
+
+
+
+Attach a TurboDocx-generated document to the sent quote.
+
+```ruby
+result = TurboDocxSdk::TurboQuote.send_quote_with_deliverable("quote-uuid",
+ "deliverableId" => "deliverable-uuid",
+ "ccEmails" => ["manager@example.com"]
+)
+# result["quote"], result["message"], result["documentId"]
+```
+
@@ -2624,11 +3175,41 @@ tq.sendQuoteWithDeliverable(quoteId, scheduledReq);
Each `Duration` is a `{value, unit}` pair (`unit` is `"hours"` or `"days"`, `value` a whole number from 1 up to 999 days / 23976 hours).
-
+
-
+Both `send_quote` and `send_quote_with_deliverable` accept the same reminder/expiration schedule
+as TurboSign. Because these are quote request-body fields, the keys are **camelCase** (like
+`ccEmails` / `validUntil` above), even though the method names are snake_case. Each duration is a
+plain `{ "value" => ..., "unit" => ... }` hash, sent flat on the JSON body:
+
+```ruby
+sent = TurboDocxSdk::TurboQuote.send_quote("quote-uuid",
+ "validUntil" => "2026-09-30",
+ "remindersEnabled" => true,
+ "reminderDelay" => { "value" => 3, "unit" => "days" }, # "days" or "hours"
+ "reminderInterval" => { "value" => 3, "unit" => "days" },
+ "maxReminders" => 5, # -1 unlimited, 0 none, max 50
+ "expirationEnabled" => true,
+ "expirationWarning" => { "value" => 1, "unit" => "days" }, # 0 = never warn
+ "expirationWarningInterval" => { "value" => 1, "unit" => "days" }
+)
+```
-#### `declineQuote` / `decline_quote` / DeclineQuote {#declinequote}
+:::warning Quote expiry is pinned to `validUntil`
+For a quote, the signing deadline is **hard-pinned to the quote's `validUntil` date**. Any
+`expireAfter` you pass is **ignored**. `expirationEnabled` still toggles expiry on or off, but
+when on, the document expires exactly at `validUntil`. The reminder and expiration-warning
+cadence still applies and **must fit inside the `validUntil` window**; a cadence that would
+outlive the quote is rejected with `400`. Duration values are `{value, unit}` (`"days"` or
+`"hours"`), min `1`, max **999 days / 23976 hours**.
+:::
+
+
+
+
+
+
+#### `declineQuote` / `decline_quote` / DeclineQuote {#declinequote}
@@ -2711,6 +3292,20 @@ Quote declined = tq.declineQuote(quoteId, req);
Quote closedOut = tq.declineQuote(draftQuoteId, new DeclineQuoteRequest());
```
+
+
+
+Declines a **sent** quote or a **draft**. `reason` (max 190 characters) is required once a quote has been sent. A draft is declined **without** a reason: a draft never reached the customer, and because the reason is stored on the linked signature document, a draft has nowhere to keep one, so anything passed is ignored.
+
+```ruby
+quote = TurboDocxSdk::TurboQuote.decline_quote("quote-uuid",
+ "reason" => "Customer selected a competitor"
+)
+
+# A draft is declined with no reason; one passed here would not be recorded.
+closed_out = TurboDocxSdk::TurboQuote.decline_quote("draft-quote-uuid", {})
+```
+
@@ -2780,6 +3375,17 @@ req.setReason("Replaced by new proposal");
Quote voided = tq.voidQuote(quoteId, req);
```
+
+
+
+Voids a **sent** quote; a **draft cannot be voided**, since voiding an unsent quote is meaningless.
+
+```ruby
+quote = TurboDocxSdk::TurboQuote.void_quote("quote-uuid",
+ "reason" => "Terms expired before acceptance"
+)
+```
+
@@ -2927,6 +3533,35 @@ Unlike `duplicateQuote`, the draft this endpoint creates is **not** prefixed wit
---
+
+
+
+Handles a quote that has passed its `validUntil` date. The endpoint **closes out the original quote** (voiding or declining it, depending on `action`) and then **creates a duplicate carrying `newValidUntil`** as its new validity date. The returned quote is the new duplicate; the original stays terminal.
+
+`action` (`"void"`, `"decline"` or `"renew"`) and `newValidUntil` (ISO date) are **required**. `reason` (max 190 characters) is required for `"void"` and `"decline"`, and optional for `"renew"`, since a renewal closes nothing out. Use `"renew"` when the quote's signature request has already expired on its own; use `"void"` or `"decline"` when the quote is merely past its `validUntil` and you are closing it yourself. The SDK forwards the hash verbatim, so the API's validation is what applies.
+
+```ruby
+quote = TurboDocxSdk::TurboQuote.handle_expired_quote("quote-uuid",
+ "action" => "void", # required: "void", "decline" or "renew"
+ "reason" => "Quote expired", # required for void/decline, optional for renew
+ "newValidUntil" => "2026-12-31" # required: ISO date carried onto the duplicate
+)
+```
+
+:::warning There is no `extend` or `resend` action
+`action` accepts **only** `"void"`, `"decline"` and `"renew"`. `"extend"` and `"resend"` do not exist in the API and return a `400`. Extending is what the endpoint already does: pass `newValidUntil` and it lands on the duplicate it creates.
+:::
+
+:::info The replacement draft keeps the original name
+Unlike `duplicate_quote`, the draft this endpoint creates is **not** prefixed with `Copy of `. Re-issuing the same deal keeps the original quote's name, so repeated renewals cannot compound into `Copy of Copy of …`. That matters because the next send snapshots the name onto the TurboSign document.
+:::
+
+:::note Terminal statuses
+`accepted`, `declined`, and `voided` are **terminal**: a quote in one of these states cannot be transitioned out of it, and any further status call returns a `400`. Check `quote["statusInfo"]` before attempting a transition, and use `duplicate_quote` when you need to revive a closed-out quote.
+:::
+
+---
+
@@ -2948,6 +3583,11 @@ Line items attach products or bundles to a quote, each with a price, quantity, b
Line items belong to a quote. Products are added individually; bundles use a separate endpoint.
+
+
+
+Line items attach products or bundles to a quote, each with a price, quantity, billing frequency, and optional discount. Product items go through `add_line_items` and bundles through `add_bundle_line_items`.
+
@@ -3015,6 +3655,16 @@ items.getResults().forEach(i ->
System.out.println(i.getProductName() + " x" + i.getQuantity()));
```
+
+
+
+```ruby
+page = TurboDocxSdk::TurboQuote.list_line_items("quote-uuid", "limit" => 50)
+# page["results"], page["totalRecords"]
+```
+
+Other filters: `offset`, `lineItemType`, `billingFrequency`, and `parentLineItemId`.
+
@@ -3182,6 +3832,37 @@ List added = tq.addLineItems(quoteId, Arrays.asList(item, custom));
`addLineItems` accepts a single request **or** a list of 1–**50** items. A reorder request accepts up to **200** items. Exceeding either cap returns a `400`.
:::
+
+
+
+Accepts a single item hash **or** an array of up to **50** items. Returns an array of the created line item hashes.
+
+`productId`, `productName`, `unitPrice`, and `billingFrequency` are all **required** on every row. `productId` is special: the key must be **present**, but its value may be `nil` for a custom (freeform) line item. Omitting the key entirely returns a `400`. `quantity` is optional and defaults to `1`.
+
+```ruby
+items = TurboDocxSdk::TurboQuote.add_line_items("quote-uuid", [
+ {
+ "productId" => "product-uuid", # required key, nil for a custom line item
+ "productName" => "Platform License",
+ "quantity" => 2,
+ "unitPrice" => "199.00",
+ "billingFrequency" => "annual",
+ "discountType" => "percent", # "percent" (default) or "amount"
+ "discountPercent" => 10
+ },
+ {
+ "productId" => nil, # custom (freeform) line item, key still required
+ "productName" => "Implementation Credit",
+ "unitPrice" => "-250.00",
+ "billingFrequency" => "one-time"
+ }
+])
+```
+
+:::note Array caps
+`add_line_items` accepts a single hash **or** an array of 1 to **50** items. A reorder request accepts up to **200** items. Exceeding either cap returns a `400`.
+:::
+
@@ -3271,6 +3952,21 @@ bundleItem.setQuantity(2.0);
List added = tq.addBundleLineItems(quoteId, bundleItem);
```
+
+
+
+`bundleId` and `bundleName` are both **required**; the server expands the bundle's child products for you, so do not send them. Accepts a single hash or an array of up to **50** items.
+
+```ruby
+items = TurboDocxSdk::TurboQuote.add_bundle_line_items("quote-uuid", [
+ {
+ "bundleId" => "bundle-uuid",
+ "bundleName" => "Starter Bundle", # required
+ "quantity" => 1
+ }
+])
+```
+
@@ -3342,6 +4038,18 @@ req.setDiscountPercent(15.0);
LineItem updated = tq.updateLineItem(quoteId, itemId, req);
```
+
+
+
+```ruby
+item = TurboDocxSdk::TurboQuote.update_line_item("quote-uuid", "item-uuid",
+ "quantity" => 5,
+ "unitPrice" => "179.00"
+)
+```
+
+Pass `"displayOrder" => nil` to clear a line item's sort order.
+
@@ -3402,6 +4110,16 @@ tq.removeLineItem(quoteId, itemId);
---
+
+
+
+```ruby
+result = TurboDocxSdk::TurboQuote.remove_line_item("quote-uuid", "item-uuid")
+# result["message"]
+```
+
+---
+
@@ -3603,6 +4321,57 @@ When `CreateProductRequest.getImages()` is non-empty, the SDK automatically swit
---
+
+
+
+| Method | Signature | Returns |
+|---|---|---|
+| `list_products` | `(options = nil)` | paginated list |
+| `create_product` | `(request)` | product `Hash` |
+| `get_product` | `(id)` | product `Hash` |
+| `update_product` | `(id, request)` | product `Hash` |
+| `delete_product` | `(id)` | `{"message" => ...}` |
+| `duplicate_product` | `(id)` | product `Hash` |
+| `get_product_primary_images` | `(product_ids)` | `{id => image or nil}` |
+
+:::caution `categoryId` is required on create
+`create_product` **requires** `name`, `categoryId`, `listPrice`, and `billingFrequency`. `categoryId` must be the **UUID** of an existing type (`categoryType` `"product_category"`), so resolve or create it first with `list_types` / `create_type`. It is optional on `update_product`, so you only need to pass it when creating.
+:::
+
+```ruby
+# Resolve the product category first; create_product needs its UUID.
+page = TurboDocxSdk::TurboQuote.list_types("categoryType" => "product_category")
+category = page["results"].find { |t| t["name"] == "Software" }
+category ||= TurboDocxSdk::TurboQuote.create_type(
+ "name" => "Software",
+ "categoryType" => "product_category"
+)
+
+# Create a product with images (multipart upload is used automatically)
+product = TurboDocxSdk::TurboQuote.create_product(
+ "name" => "Enterprise License",
+ "categoryId" => category["id"], # required
+ "listPrice" => "499.00",
+ "cost" => "200.00",
+ "billingFrequency" => "annual",
+ "showInCatalog" => true,
+ "images" => ["/path/to/product-photo.png"] # file path(s) or IO objects
+)
+
+# List products with filters
+page = TurboDocxSdk::TurboQuote.list_products(
+ "limit" => 20,
+ "query" => "enterprise",
+ "showInCatalog" => true
+)
+
+# Bulk fetch primary images for a set of product IDs
+images = TurboDocxSdk::TurboQuote.get_product_primary_images(["id-1", "id-2", "id-3"])
+# images["id-1"] is the image Hash, or nil
+```
+
+---
+
@@ -3620,6 +4389,16 @@ list, err := qc.ListProducts(ctx, &turbodocx.ListProductsOptions{
// list.CatalogValue float64
```
+
+
+
+```ruby
+page = TurboDocxSdk::TurboQuote.list_products("query" => "license")
+# page["results"]
+```
+
+Other filters: `limit`, `offset`, `categoryIds`, `billingFrequency`, `currency`, and `showInCatalog`.
+
@@ -3652,6 +4431,30 @@ product, err := qc.CreateProduct(ctx, &turbodocx.CreateProductRequest{
})
```
+
+
+
+```ruby
+product = TurboDocxSdk::TurboQuote.create_product(
+ "name" => "Starter License",
+ "listPrice" => 99.99,
+ "billingFrequency" => "monthly",
+ "categoryId" => "category-uuid"
+)
+```
+
+For products with images, add an `"images"` array of file paths or IO objects. The SDK then switches to a multipart upload and detects each image type from its bytes:
+
+```ruby
+product = TurboDocxSdk::TurboQuote.create_product(
+ "name" => "Starter License",
+ "listPrice" => 99.99,
+ "billingFrequency" => "monthly",
+ "categoryId" => "category-uuid",
+ "images" => ["/path/to/logo.png"]
+)
+```
+
@@ -3666,6 +4469,15 @@ result, err := qc.DeleteProduct(ctx, "product-uuid")
copy, err := qc.DuplicateProduct(ctx, "product-uuid")
```
+
+
+
+```ruby
+product = TurboDocxSdk::TurboQuote.get_product("product-uuid")
+result = TurboDocxSdk::TurboQuote.delete_product("product-uuid")
+copy = TurboDocxSdk::TurboQuote.duplicate_product("product-uuid")
+```
+
@@ -3683,6 +4495,18 @@ product, err := qc.UpdateProduct(ctx, "product-uuid", &turbodocx.UpdateProductRe
})
```
+
+
+
+PATCH semantics: pass only the keys you want to change, and an explicit `nil` to clear a nullable field such as `cost` or `sku`. `update_product` also accepts `"images"` for uploads.
+
+```ruby
+product = TurboDocxSdk::TurboQuote.update_product("product-uuid",
+ "listPrice" => 109.99,
+ "cost" => nil # clears the field
+)
+```
+
@@ -3702,6 +4526,19 @@ if img, ok := images["product-uuid-1"]; ok && img != nil {
---
+
+
+
+Returns a `Hash` keyed by product ID, with each value an image `Hash` or `nil`.
+
+```ruby
+images = TurboDocxSdk::TurboQuote.get_product_primary_images(["product-uuid-1", "product-uuid-2"])
+image = images["product-uuid-1"]
+puts image.inspect unless image.nil?
+```
+
+---
+
@@ -3931,6 +4768,45 @@ Bundle bundle = tq.createBundle(req);
---
+
+
+
+| Method | Signature | Returns |
+|---|---|---|
+| `list_bundles` | `(options = nil)` | paginated list |
+| `create_bundle` | `(request)` | bundle `Hash` |
+| `get_bundle` | `(id)` | bundle `Hash` |
+| `update_bundle` | `(id, request)` | bundle `Hash` |
+| `delete_bundle` | `(id)` | `{"message" => ...}` |
+| `duplicate_bundle` | `(id)` | bundle `Hash` |
+
+`categoryId` (a `bundle_category` type) is **required** on create. Each entry in `items` **requires** `productId`, `unitPrice`, and `billingFrequency`; `quantity` is optional and defaults to `1`.
+
+```ruby
+bundle = TurboDocxSdk::TurboQuote.create_bundle(
+ "name" => "Starter Pack",
+ "categoryId" => "bundle-category-uuid", # required
+ "items" => [
+ {
+ "productId" => "product-uuid-1",
+ "unitPrice" => "199.00",
+ "billingFrequency" => "monthly",
+ "quantity" => 1
+ },
+ {
+ "productId" => "product-uuid-2",
+ "unitPrice" => "49.00",
+ "billingFrequency" => "monthly",
+ "quantity" => 2
+ }
+ ],
+ "bundleDiscountType" => "percent", # "percent" reads bundleDiscountPercent
+ "bundleDiscountPercent" => 5 # "amount" reads bundleDiscountAmount
+)
+```
+
+---
+
@@ -4130,6 +5006,40 @@ System.out.println("Products: " + pbProducts.getTotalRecords());
---
+
+
+
+| Method | Signature | Returns |
+|---|---|---|
+| `list_price_books` | `(options = nil)` | paginated list |
+| `create_price_book` | `(request)` | price book `Hash` |
+| `get_price_book` | `(id)` | price book `Hash` |
+| `update_price_book` | `(id, request)` | price book `Hash` |
+| `delete_price_book` | `(id)` | `{"message" => ...}` |
+| `duplicate_price_book` | `(id)` | price book `Hash` |
+| `list_price_book_products` | `(id, options = nil)` | paginated list |
+
+```ruby
+# name, priceBookTypeId, and validFrom are REQUIRED.
+# discountPercent is optional; the SDK sends 0 when it is missing or nil.
+# priceBookTypeId comes from a create_type with categoryType "pricebook_type".
+pricebook = TurboDocxSdk::TurboQuote.create_price_book(
+ "name" => "Partner Tier A",
+ "priceBookTypeId" => "pricebook-type-uuid",
+ "validFrom" => "2026-01-01",
+ "discountPercent" => 15,
+ "productPricing" => [
+ { "productId" => "product-uuid-1", "discountType" => "percent", "discountPercent" => 25 },
+ { "productId" => "product-uuid-2", "discountType" => "amount", "discountAmount" => 50 }
+ ]
+)
+
+# List products attached to a price book
+page = TurboDocxSdk::TurboQuote.list_price_book_products("pricebook-uuid", "limit" => 50)
+```
+
+---
+
@@ -4303,6 +5213,40 @@ String contactId = contacts.getResults().get(0).getId();
---
+
+
+
+| Method | Signature | Returns |
+|---|---|---|
+| `list_companies` | `(options = nil)` | paginated list |
+| `create_company` | `(request)` | company `Hash` |
+| `get_company` | `(id)` | company `Hash` |
+| `update_company` | `(id, request)` | company `Hash` |
+| `delete_company` | `(id)` | `{"message" => ...}` |
+| `list_company_contacts` | `(company_id, options = nil)` | paginated list |
+
+```ruby
+company = TurboDocxSdk::TurboQuote.create_company(
+ "name" => "Acme Corporation",
+ "phone" => "+1-555-0100",
+ "city" => "San Francisco",
+ "state" => "CA",
+ "country" => "US",
+ "contacts" => [ # at least one contact required; each needs name + email
+ {
+ "name" => "Jane Doe",
+ "email" => "jane@acme.com",
+ "title" => "VP of Engineering"
+ }
+ ]
+)
+
+# List contacts for a company
+contacts = TurboDocxSdk::TurboQuote.list_company_contacts(company["id"])
+```
+
+---
+
@@ -4445,6 +5389,30 @@ Contact contact = tq.createContact(req);
---
+
+
+
+| Method | Signature | Returns |
+|---|---|---|
+| `list_contacts` | `(options = nil)` | paginated list |
+| `create_contact` | `(request)` | contact `Hash` |
+| `update_contact` | `(id, request)` | contact `Hash` |
+| `delete_contact` | `(id)` | `{"message" => ...}` |
+
+:::note No `get_contact` by design
+The backend has no `GET /v1/contacts/:id` endpoint. Fetch individual contacts via `list_company_contacts` or `list_contacts` with a query filter.
+:::
+
+```ruby
+contact = TurboDocxSdk::TurboQuote.create_contact(
+ "name" => "John Smith",
+ "email" => "john@acme.com",
+ "companyId" => "company-uuid"
+)
+```
+
+---
+
@@ -4637,6 +5605,44 @@ QuoteTemplateListResponse templates = tq.listTemplates();
---
+
+
+
+| Method | Signature | Returns |
+|---|---|---|
+| `list_templates` | `(options = nil)` | paginated list |
+| `get_template` | `()` | the org's template `Hash` (auto-created if none exists) |
+| `get_template_by_id` | `(id)` | template `Hash` |
+| `create_template` | `(request)` | template `Hash` (`400` if one already exists) |
+| `update_template` | `(id, request)` | template `Hash` |
+| `delete_template` | `(id)` | `{"message" => ...}`, resets to org branding defaults |
+
+:::warning Templates are auto-provisioned, so use `get_template` then `update_template`
+`get_template` **self-heals**: if the org has no template, the API creates one from your org branding and returns it. Every established org therefore already has a template, which means:
+
+- `create_template` returns **400 `TEMPLATE_ALREADY_EXISTS`** and is effectively unreachable. Do not build a get-then-create flow.
+- `delete_template` is really "reset to org branding defaults". It soft-deletes, and the next `get_template` regenerates a fresh one.
+
+The correct flow is **`get_template`, then `update_template`**.
+:::
+
+```ruby
+# 1. Get the org's quote template (created from org branding on first read)
+template = TurboDocxSdk::TurboQuote.get_template
+
+# 2. Brand it by updating the template you just fetched
+branded = TurboDocxSdk::TurboQuote.update_template(template["id"],
+ "logoUrl" => "https://cdn.example.com/logo.png",
+ "primaryColor" => "#0057b8",
+ "senderName" => "TurboDocx Sales"
+)
+
+# Fetch a specific named template by ID
+other = TurboDocxSdk::TurboQuote.get_template_by_id("template-uuid")
+```
+
+---
+
@@ -4652,6 +5658,16 @@ tmpl, err := qc.GetTemplate(ctx)
fmt.Printf("Primary color: %s\n", tmpl.PrimaryColor)
```
+
+
+
+Returns the active (default) quote template for the org, creating one from org branding if none exists. Use this for the most common case.
+
+```ruby
+template = TurboDocxSdk::TurboQuote.get_template
+puts "Primary color: #{template['primaryColor']}"
+```
+
@@ -4666,6 +5682,15 @@ Retrieves a specific template by ID when you have multiple templates.
tmpl, err := qc.GetTemplateByID(ctx, "template-uuid")
```
+
+
+
+Retrieves a specific template by ID when you have multiple templates.
+
+```ruby
+template = TurboDocxSdk::TurboQuote.get_template_by_id("template-uuid")
+```
+
@@ -4700,6 +5725,32 @@ result, err := qc.DeleteTemplate(ctx, tmpl.ID)
---
+
+
+
+Brand the org's template by fetching it and updating it in place, never by creating one.
+
+```ruby
+page = TurboDocxSdk::TurboQuote.list_templates("limit" => 5)
+
+# Get the auto-provisioned template, then update it.
+template = TurboDocxSdk::TurboQuote.get_template
+
+template = TurboDocxSdk::TurboQuote.update_template(template["id"],
+ "logoUrl" => "https://cdn.example.com/logo.png",
+ "primaryColor" => "#0066CC",
+ "senderName" => "Sales Team"
+)
+
+# delete_template resets the org back to its branding defaults;
+# the next get_template call regenerates a fresh template.
+result = TurboDocxSdk::TurboQuote.delete_template(template["id"])
+```
+
+To clear a nullable template field (for example `logoUrl` or `senderName`), pass it as `nil` to `update_template`.
+
+---
+
@@ -4850,6 +5901,38 @@ There is no `getType(id)` method — the backend has no `GET /v1/types/:id` endp
---
+
+
+
+| Method | Signature | Returns |
+|---|---|---|
+| `list_types` | `(options = nil)` | paginated list |
+| `create_type` | `(request)` | type `Hash` |
+| `update_type` | `(id, request)` | type `Hash` |
+| `delete_type` | `(id)` | `{"message" => ...}` |
+
+:::note No `get_type` by design
+The backend has no `GET /v1/types/:id` endpoint. Use `list_types` to retrieve individual type records.
+:::
+
+`categoryType` is required on create and cannot be changed afterwards. It is one of `product_category`, `pricebook_type`, `company_industry`, or `bundle_category` (see `TurboDocxSdk::CategoryType`).
+
+```ruby
+quote_type = TurboDocxSdk::TurboQuote.create_type(
+ "name" => "Software",
+ "categoryType" => TurboDocxSdk::CategoryType::PRODUCT_CATEGORY
+)
+
+TurboDocxSdk::TurboQuote.update_type(quote_type["id"], "name" => "Software and Services")
+
+page = TurboDocxSdk::TurboQuote.list_types(
+ "categoryType" => "company_industry",
+ "includeUsage" => true
+)
+```
+
+---
+
@@ -5128,6 +6211,57 @@ Rows process sequentially with **partial success** — a failed row does not thr
Requests are capped at **500 rows** — anything above the cap returns a `400`. Available to admin and contributor API keys.
+
+
+
+Every create-family entity has a matching `bulk_create_*` method for seeding a catalog or migrating CRM data in one call. Each method sends `POST {resource}/bulk` with an array of row hashes using the **same shape as that entity's single-create request** (keys stay camelCase); the SDK wraps them in the `{ "rows" => [...] }` envelope for you. Company rows require a `contacts` array with at least one contact; contact rows require a `companyId`.
+
+Rows process sequentially with **partial success**: a failed row does not raise and does not roll back earlier rows. Every bulk method returns the same report `Hash`:
+
+- `imported`: count of rows created
+- `failed`: array of `{"row" => ..., "reason" => ...}` for rows that did not import; `row` is the **1-indexed** position in your request array
+- `adjusted`: array of `{"row" => ..., "reason" => ...}` for rows that imported *with* a server-side adjustment (e.g. a bundle item whose product wasn't found was dropped)
+
+Requests are capped at **500 rows**; anything above the cap fails the whole request with a `400` (`TurboDocxSdk::ValidationError`). Available to administrator and contributor API keys. The SDK does no client-side row validation.
+
+:::caution Product rows require a real `categoryId`
+Every `bulk_create_products` row **requires** `name`, `categoryId`, `listPrice`, and `billingFrequency`. `categoryId` must be the **UUID** of an existing type (`categoryType` `"product_category"`). There is no `categoryName` key on the bulk row schema, and the API rejects unknown keys, so passing one returns a `400`. Resolve or create the category first with `list_types` / `create_type`, then pass its `"id"`.
+:::
+
+```ruby
+# 1. Resolve the product category first; bulk rows need its UUID, not its name.
+page = TurboDocxSdk::TurboQuote.list_types("categoryType" => "product_category")
+category = page["results"].find { |t| t["name"] == "Software" }
+category ||= TurboDocxSdk::TurboQuote.create_type(
+ "name" => "Software",
+ "categoryType" => "product_category"
+)
+
+# 2. Import, passing the resolved UUID on every row.
+result = TurboDocxSdk::TurboQuote.bulk_create_products([
+ { "name" => "Enterprise License", "categoryId" => category["id"], "listPrice" => "499.00", "billingFrequency" => "annual" },
+ { "name" => "Onboarding Package", "categoryId" => category["id"], "listPrice" => "999.00", "billingFrequency" => "one-time" }
+])
+
+puts "Imported #{result['imported']} of 2 rows"
+result["failed"].each do |failure|
+ puts "Row #{failure['row']} failed: #{failure['reason']}"
+end
+result["adjusted"].each do |adjustment|
+ puts "Row #{adjustment['row']} imported with adjustment: #{adjustment['reason']}"
+end
+```
+
+The other five bulk methods follow the exact same pattern:
+
+| Method | Rows |
+|---|---|
+| `bulk_create_price_books` | array of `create_price_book` hashes |
+| `bulk_create_bundles` | array of `create_bundle` hashes |
+| `bulk_create_companies` | array of `create_company` hashes, each with `contacts` (min. 1) |
+| `bulk_create_contacts` | array of `create_contact` hashes, each with `companyId` |
+| `bulk_create_types` | array of `create_type` hashes |
+
@@ -5201,6 +6335,31 @@ The other five bulk methods follow the exact same pattern:
---
+
+
+
+```ruby
+TurboDocxSdk::TurboQuote.bulk_create_products(rows) # rows: Array of create_product hashes
+```
+
+Ruby uses the same call shown under [Bulk Imports](#bulk-imports) above. The other bulk methods (`bulk_create_price_books`, `bulk_create_bundles`, `bulk_create_companies`, `bulk_create_contacts`, `bulk_create_types`) take an array of rows in the matching single-create shape:
+
+```ruby
+# Companies need at least one contact per row; contacts need a companyId per row
+TurboDocxSdk::TurboQuote.bulk_create_companies([
+ {
+ "name" => "Acme Corporation",
+ "contacts" => [{ "name" => "Jane Doe", "email" => "jane@acme.com" }]
+ }
+])
+
+TurboDocxSdk::TurboQuote.bulk_create_contacts([
+ { "name" => "John Smith", "email" => "john@acme.com", "companyId" => "company-uuid" }
+])
+```
+
+---
+
@@ -5226,6 +6385,31 @@ The other five bulk methods follow the exact same pattern:
---
+
+
+
+The gem exposes each enum as a module of string constants under `TurboDocxSdk`, each with an `ALL` array of its values:
+
+| Module | Values |
+|---|---|
+| `QuoteStatus` | `draft`, `pending_approval`, `sent`, `accepted`, `declined`, `voided` |
+| `BillingFrequency` | `monthly`, `quarterly`, `annual`, `one-time` |
+| `LineItemType` | `product`, `bundle` |
+| `RenewalPeriod` | `weekly`, `monthly`, `quarterly`, `annually` |
+| `Currency` | `USD`, `EUR`, `GBP`, `CAD`, `INR`, `AUD` |
+| `CategoryType` | `product_category`, `pricebook_type`, `company_industry`, `bundle_category` |
+| `BundleItemStatus` | `active`, `product_deleted`, `product_unavailable`, `currency_mismatch` |
+| `DiscountType` | `percent`, `amount` |
+| `QuoteNumberYearToken` | `none`, `two`, `four` |
+| `QuoteNumberMonthToken` | `off`, `two` |
+| `QuoteNumberResetCadence` | `never`, `yearly`, `monthly` |
+
+:::note Terminal statuses
+`accepted`, `declined`, and `voided` are **terminal**: a quote in one of these states cannot be transitioned out of it, and any further status call returns a `400`. Check `quote["statusInfo"]` before attempting a transition, and use `duplicate_quote` when you need to revive a closed-out quote.
+:::
+
+---
+
@@ -5259,6 +6443,23 @@ Types with `Clear*` helpers:
---
+
+
+
+Ruby needs no helpers here. The request hash is serialized as-is, so a key you set to `nil` is sent as JSON `null` (clearing that field), and a key you leave out is omitted from the payload entirely:
+
+```ruby
+quote = TurboDocxSdk::TurboQuote.update_quote("quote-uuid",
+ "taxRate" => nil, # sends "taxRate": null (removes tax from the quote)
+ "renewalPeriod" => nil # sends "renewalPeriod": null
+ # name left out, so it is not sent
+)
+```
+
+The same applies to every `update_*` method. Clearable fields include `priceBookId`, `validUntil`, `taxRate` and `renewalPeriod` on a quote, `cost` and `sku` on a product, `validTo` on a price book, and `logoUrl` or `senderName` on a quote template.
+
+---
+
@@ -5371,6 +6572,37 @@ System.out.println("Quote: " + result.getQuote().getId());
---
+
+
+
+Orchestrates multiple API calls: create quote, add product items, add bundle items, then send. Returns `{ "quote" => }`. The SDK does not modify the hash you pass in.
+
+```ruby
+result = TurboDocxSdk::TurboQuote.create_and_send(
+ # create_quote fields
+ "name" => "Year-End Renewal",
+ "companyId" => "company-uuid",
+ "contactId" => "contact-uuid",
+ "currency" => "USD",
+ # optional line items added before send
+ "items" => [
+ { "productId" => "product-uuid-1", "productName" => "Platform License", "quantity" => 10, "unitPrice" => "99.00", "billingFrequency" => "monthly" }
+ ],
+ # optional bundle items added before send
+ "bundleItems" => [
+ { "bundleId" => "bundle-uuid-1", "bundleName" => "Starter Pack", "quantity" => 1 }
+ ],
+ # send options (ccEmails, validUntil)
+ "send" => {
+ "ccEmails" => ["finance@example.com"],
+ "validUntil" => "2026-12-31"
+ }
+)
+puts result["quote"]["id"]
+```
+
+---
+
@@ -5436,6 +6668,11 @@ import type {
} from '@turbodocx/sdk';
```
+
+
+
+The Ruby gem ships no type definitions. Every method returns a plain `Hash` with string keys that mirror the API's camelCase field names (for example `quote["grandTotal"]`, `page["totalRecords"]`), and list methods return a `Hash` with `"results"` plus totals. `download_quote_pdf` returns a binary `String`. Boolean and decimal fields are normalized by the SDK before they reach you.
+
@@ -5459,6 +6696,25 @@ import type {
---
+
+
+
+Use the gem's constants instead of hard-coding string literals:
+
+```ruby
+TurboDocxSdk::QuoteStatus::DRAFT # "draft"
+TurboDocxSdk::QuoteStatus::SENT # "sent"
+TurboDocxSdk::BillingFrequency::MONTHLY # "monthly"
+TurboDocxSdk::BillingFrequency::ONE_TIME # "one-time"
+TurboDocxSdk::DiscountType::PERCENT # "percent"
+TurboDocxSdk::Currency::USD # "USD"
+TurboDocxSdk::RenewalPeriod::ANNUALLY # "annually"
+TurboDocxSdk::CategoryType::PRICEBOOK_TYPE # "pricebook_type"
+TurboDocxSdk::QuoteStatus::ALL # every quote status
+```
+
+---
+
@@ -5665,6 +6921,42 @@ try {
}
```
+
+
+
+```ruby
+require "turbodocx_sdk"
+
+begin
+ quote = TurboDocxSdk::TurboQuote.create_quote(
+ "name" => "Q3 Proposal",
+ "companyId" => "company-uuid",
+ "contactId" => "contact-uuid"
+ )
+rescue TurboDocxSdk::ValidationError => e
+ # 400: missing required fields, invalid enum value, etc.
+ puts "Validation failed: #{e.message} (#{e.code})"
+rescue TurboDocxSdk::AuthenticationError
+ # 401: bad or revoked API key
+rescue TurboDocxSdk::AuthorizationError
+ # 403: key valid but lacks permission for this org/action
+rescue TurboDocxSdk::NotFoundError => e
+ # 404: company, contact, or product not found
+ puts "Not found: #{e.message}"
+rescue TurboDocxSdk::ConflictError
+ # 409: conflicts with the current resource state
+rescue TurboDocxSdk::RateLimitError
+ # 429: back off and retry
+rescue TurboDocxSdk::NetworkError
+ # request never reached the server (DNS, refused, timeout)
+rescue TurboDocxSdk::TurboDocxError => e
+ # catch-all for any other SDK error (raw 5xx, etc.)
+ puts "Error #{e.status_code}: #{e.message}"
+end
+```
+
+Every error exposes `status_code` and `code`. `code` is always populated: it carries the API's specific code (such as `QuoteHasNoLineItems`) when there is one, and otherwise a class default such as `VALIDATION_ERROR` or `NOT_FOUND`.
+
@@ -5739,6 +7031,21 @@ try {
| 404 | `TurboDocxException.NotFoundException` | Quote, product, company, contact, etc. not found |
| 429 | `TurboDocxException.RateLimitException` | Rate limit exceeded — back off and retry |
+
+
+
+| Status | Class | When |
+|---|---|---|
+| 400 | `TurboDocxSdk::ValidationError` | Invalid fields, missing required keys, bad enum value |
+| 401 | `TurboDocxSdk::AuthenticationError` | Missing or invalid API key |
+| 403 | `TurboDocxSdk::AuthorizationError` | Valid key without permission for this operation |
+| 404 | `TurboDocxSdk::NotFoundError` | Quote, product, company, or other resource not found |
+| 409 | `TurboDocxSdk::ConflictError` | Conflicts with existing resource state |
+| 429 | `TurboDocxSdk::RateLimitError` | Rate limit exceeded, so back off |
+| none | `TurboDocxSdk::NetworkError` | The request never reached the server |
+
+All of them inherit from `TurboDocxSdk::TurboDocxError`.
+
@@ -5776,6 +7083,23 @@ Three fully runnable examples live in the SDK repo:
Run any example after exporting `TURBODOCX_API_KEY` and `TURBODOCX_ORG_ID`.
+
+
+
+Three runnable examples live in the SDK repo:
+
+- **[`turboquote_basic.rb`](https://github.com/TurboDocx/SDK/blob/main/packages/ruby-sdk/examples/turboquote_basic.rb)**: full quote lifecycle (create a company and contact, create a quote, add line items, download the PDF, send, clean up).
+- **[`turboquote_products.rb`](https://github.com/TurboDocx/SDK/blob/main/packages/ruby-sdk/examples/turboquote_products.rb)**: product and bundle catalog management.
+- **[`turboquote_pricebooks.rb`](https://github.com/TurboDocx/SDK/blob/main/packages/ruby-sdk/examples/turboquote_pricebooks.rb)**: price book CRUD, `apply_price_book`, and optional `send_quote_with_deliverable` (set the `DELIVERABLE_ID` env var).
+
+Run any example with:
+
+```bash
+export TURBODOCX_API_KEY=your_key
+export TURBODOCX_ORG_ID=your_org_id
+ruby examples/turboquote_basic.rb
+```
+
@@ -5787,7 +7111,7 @@ Run any example after exporting `TURBODOCX_API_KEY` and `TURBODOCX_ORG_ID`.
- [TurboSign JavaScript SDK](/docs/SDKs/turbosign?language=js) — send documents for e-signature
- [TurboWebhooks JavaScript SDK](/docs/SDKs/webhooks?language=js) — receive real-time signature events
- [Deliverable JavaScript SDK](/docs/SDKs/deliverable?language=js) — generate documents from templates
-- [SDKs Overview](/docs/SDKs) — all SDKs across all six languages
+- [SDKs Overview](/docs/SDKs) — all SDKs across all seven languages
- [@turbodocx/sdk on npm](https://www.npmjs.com/package/@turbodocx/sdk)
- [TurboDocx SDK on GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/js-sdk)
@@ -5809,7 +7133,7 @@ Run any example after exporting `TURBODOCX_API_KEY` and `TURBODOCX_ORG_ID`.
- [TurboSign PHP SDK](/docs/SDKs/turbosign?language=php) — sending documents for signature from PHP
- [TurboWebhooks PHP SDK](/docs/SDKs/webhooks?language=php) — receiving signature events in PHP
- [Deliverable PHP SDK](/docs/SDKs/deliverable?language=php) — document generation from PHP
-- [SDKs Overview](/docs/SDKs) — all SDKs across all six languages
+- [SDKs Overview](/docs/SDKs) — all SDKs across all seven languages
- [TurboDocx SDK on Packagist](https://packagist.org/packages/turbodocx/sdk)
- [TurboDocx SDK on GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/php-sdk)
@@ -5822,7 +7146,7 @@ Run any example after exporting `TURBODOCX_API_KEY` and `TURBODOCX_ORG_ID`.
- [TurboSign Go SDK](/docs/SDKs/turbosign?language=go) — sending documents for e-signature
- [Deliverable Go SDK](/docs/SDKs/deliverable?language=go) — generating documents from templates
- [TurboWebhooks Go SDK](/docs/SDKs/webhooks?language=go) — real-time event delivery
-- [SDKs Overview](/docs/SDKs) — all SDKs across all six languages
+- [SDKs Overview](/docs/SDKs) — all SDKs across all seven languages
@@ -5835,5 +7159,17 @@ Run any example after exporting `TURBODOCX_API_KEY` and `TURBODOCX_ORG_ID`.
- [SDKs Overview](/docs/SDKs) — all SDKs across all languages
- [TurboDocx SDK on GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/java-sdk)
+
+
+
+- [TurboQuote JavaScript / TypeScript SDK](/docs/SDKs/quote?language=js): same API, JS idioms
+- [TurboQuote Python SDK](/docs/SDKs/quote?language=python): same API, Python idioms
+- [TurboSign Ruby SDK](/docs/SDKs/turbosign?language=ruby): send documents for e-signature
+- [TurboWebhooks Ruby SDK](/docs/SDKs/webhooks?language=ruby): receive real-time signature events
+- [Deliverable Ruby SDK](/docs/SDKs/deliverable?language=ruby): generate documents from templates
+- [SDKs Overview](/docs/SDKs): all SDKs across all seven languages
+- [turbodocx-sdk on RubyGems](https://rubygems.org/gems/turbodocx-sdk)
+- [TurboDocx SDK on GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/ruby-sdk)
+
diff --git a/docs/SDKs/ruby.md b/docs/SDKs/ruby.md
index 1471405..1ceb572 100644
--- a/docs/SDKs/ruby.md
+++ b/docs/SDKs/ruby.md
@@ -1,11 +1,8 @@
---
-# Hidden from the published site: the turbodocx-sdk gem is not on RubyGems yet.
-# Remove `draft: true` when the gem is published (TurboDocx/SDK issue).
-draft: true
-title: TurboSign Ruby SDK
+title: TurboDocx Ruby SDK
sidebar_position: 7
-sidebar_label: "TurboSign: Ruby"
-description: Official TurboDocx Ruby SDK. Simple synchronous API for document generation and digital signatures from Ruby, Rails, and Sinatra.
+sidebar_label: Ruby
+description: Install and configure the TurboDocx Ruby SDK, then open the TurboSign, Deliverable, TurboWebhooks or TurboQuote guide.
keywords:
- turbodocx ruby
- turbosign ruby
@@ -20,11 +17,11 @@ import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import QuickstartSkillNudge from '@site/src/components/QuickstartSkillNudge';
-# TurboSign Ruby SDK
+# TurboDocx Ruby SDK
-The official TurboDocx SDK for Ruby applications. Build document generation and digital signature workflows with a simple synchronous API and comprehensive error handling. Available on RubyGems as `turbodocx-sdk`.
+The official TurboDocx SDK for Ruby applications. Build document generation and digital signature workflows from Ruby, Rails or Sinatra with a simple synchronous API and comprehensive error handling. Available on RubyGems as [`turbodocx-sdk`](https://rubygems.org/gems/turbodocx-sdk).
## Installation
@@ -38,18 +35,12 @@ gem install turbodocx-sdk
-Add to your `Gemfile`:
-
-```ruby
-gem "turbodocx-sdk"
-```
-
-Then run:
-
```bash
-bundle install
+bundle add turbodocx-sdk
```
+Or add `gem "turbodocx-sdk"` to your `Gemfile` and run `bundle install`.
+
@@ -62,690 +53,52 @@ bundle install
## Configuration
-```ruby
-require "turbodocx_sdk"
-
-# Configure globally (recommended)
-TurboDocxSdk::TurboSign.configure(
- api_key: ENV["TURBODOCX_API_KEY"], # Required: Your TurboDocx API key
- org_id: ENV["TURBODOCX_ORG_ID"], # Required: Your organization ID
- sender_email: "contracts@yourcompany.com", # Required: Reply-to address for signature emails
- sender_name: "Your Company" # Recommended: Sender name shown in emails
- # base_url: "https://api.turbodocx.com" # Optional: Override base URL
-)
-```
-
-:::tip Authentication
-Authenticate using `api_key`. API keys are recommended for server-side applications.
-:::
-
-### Environment Variables
-
-```bash
-# .env
-TURBODOCX_API_KEY=your_api_key_here
-TURBODOCX_ORG_ID=your_org_id_here
-TURBODOCX_SENDER_EMAIL=contracts@yourcompany.com
-TURBODOCX_SENDER_NAME=Your Company
-```
-
-:::warning API Credentials Required
-`api_key` and `org_id` are **required** for all API requests. TurboSign additionally **requires `sender_email`** (set it on `configure`, per call, or via the `TURBODOCX_SENDER_EMAIL` environment variable) — `configure` raises a `TurboDocxSdk::ValidationError` without it. `sender_name` is optional but strongly recommended. To get your credentials, follow the **[Get Your Credentials](/docs/SDKs#1-get-your-credentials)** steps from the SDKs main page.
-:::
-
----
-
-## Quick Start
-
-### Send a Document for Signature
+Each product module is configured on its own. If you skip `configure`, the module reads the `TURBODOCX_*` environment variables below.
```ruby
require "turbodocx_sdk"
+# TurboSign (requires a sender email)
TurboDocxSdk::TurboSign.configure(
api_key: ENV["TURBODOCX_API_KEY"],
org_id: ENV["TURBODOCX_ORG_ID"],
- sender_email: "contracts@acme.com",
- sender_name: "Acme Corp"
+ sender_email: ENV["TURBODOCX_SENDER_EMAIL"], # Required for TurboSign (reply-to address)
+ sender_name: "Your Company" # Optional: sender name shown in emails
+ # base_url: "https://api.turbodocx.com" # Optional custom base URL
)
-result = TurboDocxSdk::TurboSign.send_signature(
- # Request-hash keys are camelCase — the SDK forwards them to the API verbatim
- recipients: [
- { name: "Alice Smith", email: "alice@example.com", signingOrder: 1 },
- { name: "Bob Johnson", email: "bob@example.com", signingOrder: 2 }
- ],
- fields: [
- # Alice's signature
- { type: "signature", page: 1, x: 100, y: 650, width: 200, height: 50, recipientEmail: "alice@example.com" },
- { type: "date", page: 1, x: 320, y: 650, width: 100, height: 30, recipientEmail: "alice@example.com" },
- # Bob's signature
- { type: "signature", page: 1, x: 100, y: 720, width: 200, height: 50, recipientEmail: "bob@example.com" },
- { type: "date", page: 1, x: 320, y: 720, width: 100, height: 30, recipientEmail: "bob@example.com" }
- ],
- fileLink: "https://www.turbodocx.com/examples/turbodocx.pdf",
- documentName: "Service Agreement",
- senderName: "Acme Corp",
- senderEmail: "contracts@acme.com"
+# Deliverable, TurboQuote and TurboWebhooks take the same credentials
+TurboDocxSdk::Deliverable.configure(
+ api_key: ENV["TURBODOCX_API_KEY"],
+ org_id: ENV["TURBODOCX_ORG_ID"]
)
-
-puts JSON.pretty_generate(result)
```
-### Using Template-Based Fields
-
-```ruby
-result = TurboDocxSdk::TurboSign.send_signature(
- recipients: [{ name: "Alice Smith", email: "alice@example.com", signingOrder: 1 }],
- fields: [
- {
- type: "signature",
- recipientEmail: "alice@example.com",
- template: {
- anchor: "{SIGNATURE_ALICE}",
- placement: "replace",
- size: { width: 200, height: 50 }
- }
- },
- {
- type: "date",
- recipientEmail: "alice@example.com",
- # Pins a fixed date in MM/DD/YYYY; omit to auto-fill the signing date
- defaultValue: "12/31/2026",
- template: {
- anchor: "{DATE_ALICE}",
- placement: "replace",
- size: { width: 100, height: 30 }
- }
- }
- ],
- fileLink: "https://www.turbodocx.com/examples/turbodocx.pdf"
-)
-
-puts JSON.pretty_generate(result)
-```
-
-:::info Template Anchors Required
-**Important:** The document file must contain the anchor text (e.g., `{SIGNATURE_ALICE}`, `{DATE_ALICE}`) that you reference in your fields. If the anchors don't exist in the document, the API will return an error.
-:::
-
----
-
-## File Input Methods
-
-TurboSign supports four different ways to provide document files:
-
-:::note camelCase request keys
-The keys inside the request hash (`fileLink`, `deliverableId`, `templateId`, `documentName`, `recipientEmail`, `signingOrder`, …) are **camelCase** — the SDK passes them to the API verbatim and does **not** convert snake_case hash keys. Only the method keyword arguments on `configure` (`api_key:`, `sender_email:`, …) are snake_case.
-:::
-
-### 1. File Upload (path or IO)
-
-Pass a local file path (`String`) or any IO object (e.g., `File.open`, `StringIO`) — the SDK uploads it as multipart form data and detects the file type from magic bytes.
-
-```ruby
-result = TurboDocxSdk::TurboSign.send_signature(
- file: "./contract.pdf", # local path — or an IO: File.open("./contract.pdf", "rb")
- recipients: [
- { name: "John Doe", email: "john@example.com", signingOrder: 1 }
- ],
- fields: [
- {
- type: "signature",
- page: 1,
- x: 100,
- y: 650,
- width: 200,
- height: 50,
- recipientEmail: "john@example.com"
- }
- ]
-)
-```
-
-### 2. File URL (fileLink)
-
-```ruby
-result = TurboDocxSdk::TurboSign.send_signature(
- fileLink: "https://www.turbodocx.com/examples/turbodocx.pdf",
- recipients: [
- { name: "John Doe", email: "john@example.com", signingOrder: 1 }
- ],
- fields: [
- {
- type: "signature",
- page: 1,
- x: 100,
- y: 650,
- width: 200,
- height: 50,
- recipientEmail: "john@example.com"
- }
- ]
-)
-```
-
-:::tip When to use fileLink
-Use `fileLink` when your documents are already hosted on cloud storage (S3, Google Cloud Storage, etc.). This is more efficient than downloading and re-uploading files.
-:::
-
-### 3. TurboDocx Deliverable ID
-
-```ruby
-# Use a previously generated TurboDocx document
-result = TurboDocxSdk::TurboSign.send_signature(
- deliverableId: "deliverable-uuid-from-turbodocx",
- recipients: [
- { name: "John Doe", email: "john@example.com", signingOrder: 1 }
- ],
- fields: [
- {
- type: "signature",
- page: 1,
- x: 100,
- y: 650,
- width: 200,
- height: 50,
- recipientEmail: "john@example.com"
- }
- ]
-)
-```
-
-:::info Integration with TurboDocx
-`deliverableId` references documents generated using TurboDocx's document generation API. This creates a seamless workflow: generate → sign.
-:::
-
-### 4. TurboDocx Template ID
-
-```ruby
-# Use a pre-configured TurboSign template
-result = TurboDocxSdk::TurboSign.send_signature(
- templateId: "template-uuid-from-turbodocx", # Template already contains anchors
- recipients: [
- { name: "Alice Smith", email: "alice@example.com", signingOrder: 1 }
- ],
- fields: [
- {
- type: "signature",
- recipientEmail: "alice@example.com",
- template: {
- anchor: "{SIGNATURE_ALICE}",
- placement: "replace",
- size: { width: 200, height: 50 }
- }
- }
- ]
-)
-```
-
-:::info Integration with TurboDocx
-`templateId` references pre-configured TurboSign templates created in the TurboDocx dashboard. These templates come with built-in anchors and field positioning, making it easy to reuse signature workflows across multiple documents.
-:::
-
-### 5. Send a template set up in TurboDocx
-
-Upload a PDF as a template in TurboDocx, add its signers as roles (for example "Client" and "Countersigner"), drag their fields onto the page and save the signature setup. Then send it by naming each recipient's `role`. The fields saved for that role come with it, so `fields` can be left out, and a recipient with a `role` needs no `signingOrder`.
-
-```ruby
-# The role keys come from the template (also shown under "Use via API" on the template page)
-setup = TurboDocxSdk::TurboSign.get_template_signature_setup("your-template-id")
-setup["roles"].each { |r| puts "#{r['key']} (saved signer: #{r['hasSavedSigner']})" }
-# client (saved signer: false)
-# countersigner (saved signer: true)
-
-TurboDocxSdk::TurboSign.send_signature(
- templateId: "your-template-id",
- recipients: [
- { role: "client", name: "Jane Doe", email: "jane@client.com" }
- # "countersigner" left out: the signer saved on the template is used
- ]
-)
-```
-
-- Roles sign in the order saved on the template; recipients without a `role` sign after them.
-- A role left out uses the template's saved signer. If it has none, the API returns a 400 naming the role.
-- An unknown role raises `TurboDocxSdk::ValidationError` listing the template's roles (`e.code == "UnknownSignerRole"`).
-- Any `fields` you pass are added to the template's (for example an extra witness signature).
-- `role` works the same way on `create_signature_review_link` and `create_embedded_signature`.
-
-See [Sending with signer roles](/docs/TurboSign/API-Signatures#sending-with-signer-roles) for every rule and error.
-
----
-
-## API Reference
-
-All methods are class-level: call `TurboDocxSdk::TurboSign.configure` once, then invoke any method directly on the class. Every method is synchronous and returns a plain `Hash` (or raw bytes for downloads).
-
-### Configure
-
-Configure the SDK with your API credentials and organization settings.
-
-```ruby
-TurboDocxSdk::TurboSign.configure(
- api_key: "your-api-key", # Required: Your TurboDocx API key
- org_id: "your-org-id", # Required: Your organization ID
- sender_email: "you@company.com", # Required: Reply-to address for signature emails
- sender_name: "Your Company", # Recommended: Sender name shown in emails
- base_url: "https://api.turbodocx.com" # Optional: API base URL
-)
-```
-
-### Prepare for review
-
-Upload a document for preview without sending signature request emails.
-
-```ruby
-result = TurboDocxSdk::TurboSign.create_signature_review_link(
- 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" }],
- fileLink: "https://www.turbodocx.com/examples/turbodocx.pdf",
- documentName: "Contract Draft"
-)
-
-puts result["documentId"]
-puts result["previewUrl"]
-```
-
-### Prepare for signing
-
-Upload a document and immediately send signature requests to all recipients.
-
-```ruby
-result = TurboDocxSdk::TurboSign.send_signature(
- recipients: [{ name: "Recipient Name", email: "recipient@example.com", signingOrder: 1 }],
- fields: [{ type: "signature", page: 1, x: 100, y: 500, width: 200, height: 50, recipientEmail: "recipient@example.com" }],
- fileLink: "https://www.turbodocx.com/examples/turbodocx.pdf",
- documentName: "Service Agreement",
- senderName: "Your Company",
- senderEmail: "sender@company.com"
-)
-
-puts result["documentId"]
-```
-
-### Reminders and expiration
-
-`send_signature` accepts an optional per-document schedule. Both reminders and expiration are **off by default**, so omitting these keys preserves the original send behavior. Keys are camelCase like every other send key, and each duration is a `{ value:, unit: }` hash whose `unit` is `"days"` or `"hours"`.
-
-```ruby
-result = TurboDocxSdk::TurboSign.send_signature(
- recipients: [{ name: "Recipient Name", email: "recipient@example.com", signingOrder: 1 }],
- fields: [{ type: "signature", page: 1, x: 100, y: 500, width: 200, height: 50, recipientEmail: "recipient@example.com" }],
- fileLink: "https://www.turbodocx.com/examples/turbodocx.pdf",
- documentName: "Service Agreement",
-
- # Reminders — chase signers who haven't signed yet
- remindersEnabled: true,
- reminderDelay: { value: 3, unit: "days" }, # time to the FIRST reminder
- reminderInterval: { value: 3, unit: "days" }, # gap between later reminders
- maxReminders: 5, # -1 unlimited, 0 none, max 50 (default 5)
-
- # Expiration — close the signing window
- expirationEnabled: true,
- expireAfter: { value: 14, unit: "days" }, # how long it stays signable
- expirationWarning: { value: 3, unit: "days" }, # lead time before warnings (0 = never warn)
- expirationWarningInterval: { value: 1, unit: "days" }
-)
-```
-
-| Field | Bounds |
-| --- | --- |
-| `maxReminders` | Cap per signer: `-1` unlimited, `0` none, **max `50`** (default `5`). |
-| any duration `value` | Whole number, **min 1**, **max 999 days (23976 hours)**. |
-| `expirationWarning` | May be `0` to never warn. |
-
-The resolved deadline is **frozen onto the document at send time** and is then readable via `get_status(...)["expiresAt"]`.
-
-### Get status
-
-Retrieve the current status of a document. The `status` reaches the terminal value `"expired"` once the signing window closes, and `expiresAt` carries the deadline (ISO 8601, `nil` when expiration is off). For per-signer detail, use [Get recipients](#get-recipients).
-
-```ruby
-result = TurboDocxSdk::TurboSign.get_status("document-uuid")
-
-puts result["status"] # "under_review" | "completed" | "voided" | "expired" | ...
-puts result["expiresAt"] # ISO 8601 deadline, or nil when expiration is off
-
-puts JSON.pretty_generate(result)
-```
-
-### Get recipients
-
-See who the document went to, who has signed, who you are still waiting on,
-and who sent it.
-
-```ruby
-result = TurboDocxSdk::TurboSign.get_recipients("document-uuid")
-
-summary = result["summary"]
-puts "#{summary['completed']}/#{summary['total']} signed, waiting on #{summary['waitingOn']}"
-
-result["recipients"].each do |r|
- puts "#{r['name']} <#{r['email']}>: #{r['effectiveStatus']} " \
- "(emailed #{r['delivery']['totalSent']}x)"
-end
-```
-
-:::tip Two status fields, and they differ on purpose
-
-`status` is the raw database value and is only ever `pending`, `viewed` or `completed`.
-`effectiveStatus` layers the document's outcome on top, adding `voided` and `expired` — that
-is the one to display.
-
-On a voided or expired document an unsigned signer still reads `pending` in `status`, so
-branching on it would show someone as "still to sign" when their signing link is already dead.
-A completed signature is never revoked: someone who signed before the document was voided
-still reads `completed`.
-
-`summary` counts by `effectiveStatus`, and `waitingOn` (pending + viewed) drops to zero once
-the document is terminal.
-
-:::
-
-Each recipient also carries a `delivery` block — `firstSentOn`, `lastSentOn`, `totalSent`,
-`reminderCount`, `lastRemindedAt`, `warningCount`, `lastWarningAt`. It counts the signature
-request, resends, reminders, expiry warnings and terminal notices; CC notifications are
-excluded, since a CC address is not a signer.
-
-:::warning `reminderCount` and `lastRemindedAt` do not mean what their names suggest
-
-`reminderCount` counts **automatic (scheduled) reminders only** — the counter `maxReminders`
-caps. A manual "remind now" is a standalone nudge that must not consume the cap budget, so it
-does **not** increment this, even though the email it sends *does* appear in `totalSent`.
-
-`lastRemindedAt` is a **cadence clock**, not a record of a reminder: the initial
-signature-request send, each scheduled reminder, each manual "remind now" and each expiry
-warning all stamp it. Only scheduled reminders bump `reminderCount`.
-
-So a freshly-sent document returns a non-null `lastRemindedAt` equal to the invitation
-timestamp alongside `reminderCount: 0` — nobody has been reminded. To answer "have we actually
-chased this person", read `totalSent`, not `reminderCount`.
-
-`warningCount` / `lastWarningAt` have no such caveat.
-
-:::
-
-### Download document
-
-Download the completed signed document as PDF bytes.
-
-```ruby
-pdf_bytes = TurboDocxSdk::TurboSign.download("document-uuid")
-
-# Save to file
-File.binwrite("signed-contract.pdf", pdf_bytes)
-```
-
-### Void
-
-Cancel/void a signature request.
-
-```ruby
-result = TurboDocxSdk::TurboSign.void_document("document-uuid", "Contract terms changed")
-```
-
-### Resend
-
-Resend signature request emails to specific recipients.
-
-```ruby
-result = TurboDocxSdk::TurboSign.resend_email("document-uuid", ["recipient-uuid-1", "recipient-uuid-2"])
-```
-
-### Send a reminder
-
-`send_reminder` is a standalone nudge, independent of the automatic reminder cadence above: it works even when reminders are disabled or the per-signer cap is spent, does **not** consume that cap, and only emails signers at the **current** signing order. (Distinct from **Resend** above, which re-sends the original invitation — this sends the reminder copy.) Omit `recipient_ids` to remind everyone eligible; do not pass an empty array, which the API rejects.
-
-```ruby
-result = TurboDocxSdk::TurboSign.send_reminder("document-uuid")
-
-result["results"].each do |r|
- # "sent", or a "skipped_*" reason (e.g. "skipped_wrong_order", "skipped_completed")
- puts "#{r['recipientId']}: #{r['status']}"
-end
-
-# Or limit the reminder to specific recipients:
-TurboDocxSdk::TurboSign.send_reminder("document-uuid", ["recipient-uuid-1"])
-```
-
-Calls `POST /turbosign/documents/:id/send-reminder`.
-
-### Get audit trail
-
-Retrieve the complete audit trail for a document, including all events and actions.
-
-```ruby
-result = TurboDocxSdk::TurboSign.get_audit_trail("document-uuid")
-
-result["auditTrail"].each do |entry|
- puts "#{entry['actionType']} at #{entry['timestamp']}"
-end
-```
-
----
-
-### Get template signature setup
-
-List a template's signer roles, in signing order, to [send it with signer roles](#5-send-a-template-set-up-in-turbodocx). Each role carries `key` (the value for a recipient's `role`), `label`, `order`, `hasSavedSigner`, `fieldCount`, and `defaultName` / `defaultEmail` when it has a saved signer.
-
-```ruby
-setup = TurboDocxSdk::TurboSign.get_template_signature_setup("template-uuid")
-
-setup["roles"].each do |role|
- puts "#{role['key']} saved signer: #{role['hasSavedSigner']}"
-end
-```
-
-Calls `GET /turbosign/templates/:templateId/signature-setup`. A template with no saved signature setup returns a 404 (`SignatureSetupNotFound`).
-
----
-
-## Error Handling
-
-The SDK provides typed error classes for different failure scenarios. All errors extend the base `TurboDocxSdk::TurboDocxError` class.
-
-### Error Classes
-
-| Error Class | Status Code | Description |
-| --------------------- | ----------- | ----------------------------------- |
-| `TurboDocxError` | varies | Base error class for all SDK errors |
-| `AuthenticationError` | 401 | Invalid or missing API credentials |
-| `AuthorizationError` | 403 | Authenticated but lacks required permissions |
-| `ValidationError` | 400 | Invalid request parameters |
-| `NotFoundError` | 404 | Document or resource not found |
-| `ConflictError` | 409 | Request conflicts with current resource state |
-| `RateLimitError` | 429 | Too many requests |
-| `NetworkError` | - | Network connectivity issues |
-
-### Handling Errors
-
-```ruby
-require "turbodocx_sdk"
-
-begin
- result = TurboDocxSdk::TurboSign.send_signature(
- recipients: [{ name: "John Doe", email: "john@example.com", signingOrder: 1 }],
- fields: [{
- type: "signature",
- page: 1,
- x: 100,
- y: 650,
- width: 200,
- height: 50,
- recipientEmail: "john@example.com"
- }],
- fileLink: "https://www.turbodocx.com/examples/turbodocx.pdf"
- )
-rescue TurboDocxSdk::AuthenticationError => e
- puts "Authentication failed: #{e.message}"
- # Check your API key and org ID
-rescue TurboDocxSdk::ValidationError => e
- puts "Validation error: #{e.message}"
- # Check request parameters
-rescue TurboDocxSdk::NotFoundError => e
- puts "Resource not found: #{e.message}"
- # Document or recipient doesn't exist
-rescue TurboDocxSdk::RateLimitError => e
- puts "Rate limited: #{e.message}"
- # Wait and retry
-rescue TurboDocxSdk::NetworkError => e
- puts "Network error: #{e.message}"
- # Check connectivity
-rescue TurboDocxSdk::TurboDocxError => e
- puts "SDK error: #{e.message}, status_code=#{e.status_code}, code=#{e.code}"
-end
-```
-
-### Error Properties
-
-All errors include these properties:
-
-| Property | Type | Description |
-| ------------- | ---------------- | ------------------------------------ |
-| `message` | `String` | Human-readable error description |
-| `status_code` | `Integer \| nil` | HTTP status code (if applicable) |
-| `code` | `String \| nil` | Machine-readable error code |
-
----
-
-## Ruby Request Hashes
-
-The SDK uses plain Ruby `Hash` objects for flexible JSON-like structures. Symbol and string keys are both accepted; the key **spelling stays camelCase** because the SDK forwards the hash to the API verbatim.
-
-### SignatureFieldType
-
-String values for field types:
-
-```ruby
-# Available field type values
-field_types = [
- "signature",
- "initial",
- "date",
- "text",
- "full_name",
- "title",
- "company",
- "first_name",
- "last_name",
- "email",
- "checkbox"
-]
-```
-
-### Recipient
-
-Recipient configuration for signature requests:
-
-
-
-| Property | Type | Required | Description |
-| -------------- | --------- | -------- | ------------------------- |
-| `name` | `String` | Yes | Recipient's full name |
-| `email` | `String` | Yes | Recipient's email address |
-| `signingOrder` | `Integer` | Conditional | Signing order (1-indexed). Not needed when `role` is set |
-| `role` | `String` | No | Template signer role to fill (for example `"client"`). Requires `templateId` |
-
-```ruby
-recipient = {
- name: "John Doe",
- email: "john@example.com",
- signingOrder: 1
-}
-```
-
-### Field
-
-Field configuration supporting both coordinate-based and template-based positioning:
-
-
-
-| Property | Type | Required | Description |
-| ----------------- | --------- | -------- | --------------------------------------------------- |
-| `type` | `String` | Yes | Field type (see SignatureFieldType) |
-| `recipientEmail` | `String` | Yes | Which recipient fills this field |
-| `page` | `Integer` | No\* | Page number (1-indexed) |
-| `x` | `Integer` | No\* | X coordinate in pixels |
-| `y` | `Integer` | No\* | Y coordinate in pixels |
-| `width` | `Integer` | No\* | Field width in pixels |
-| `height` | `Integer` | No\* | Field height in pixels |
-| `defaultValue` | `String` | No | Default value (checkbox: `"true"`/`"false"`; date: a fixed `MM/DD/YYYY`, omit to auto-fill the signing date) |
-| `isMultiline` | `Boolean` | No | Enable multiline text |
-| `isReadonly` | `Boolean` | No | Make field read-only (pre-filled) |
-| `required` | `Boolean` | No | Whether field is required |
-| `backgroundColor` | `String` | No | Background color (hex, rgb, or named) |
-| `template` | `Hash` | No | Template anchor configuration |
-
-\*Required when not using template anchors
-
-**Template Configuration:**
-
-| Property | Type | Required | Description |
-| --------------- | --------- | -------- | ---------------------------------------------------------------- |
-| `anchor` | `String` | Yes | Text anchor pattern like `{TagName}` |
-| `placement` | `String` | Yes | `"replace"` \| `"before"` \| `"after"` \| `"above"` \| `"below"` |
-| `size` | `Hash` | Yes | `{ width: Integer, height: Integer }` |
-| `offset` | `Hash` | No | `{ x: Integer, y: Integer }` |
-| `caseSensitive` | `Boolean` | No | Case sensitive search (default: false) |
-| `useRegex` | `Boolean` | No | Use regex for anchor/searchText (default: false) |
+### Environment Variables
-```ruby
-field = {
- type: "signature",
- page: 1,
- x: 100,
- y: 500,
- width: 200,
- height: 50,
- recipientEmail: "john@example.com"
-}
+```bash
+export TURBODOCX_API_KEY=your_api_key_here
+export TURBODOCX_ORG_ID=your_org_id_here
+export TURBODOCX_SENDER_EMAIL=you@example.com # Required for TurboSign (reply-to address)
+export TURBODOCX_SENDER_NAME="Your Company" # Optional
```
-### Request Parameters
-
-Request configuration for `create_signature_review_link` and `send_signature` methods:
-
-
-
-| Parameter | Type | Required | Description |
-| --------------------- | --------------- | ----------- | ------------------------------ |
-| `recipients` | `Array` | Yes | Recipients who will sign |
-| `fields` | `Array` | Conditional | Signature fields configuration. Optional when a recipient has a `role` |
-| `file` | `String \| IO` | Conditional | Local file path or an IO object (multipart upload) |
-| `fileLink` | `String` | Conditional | URL to document file |
-| `deliverableId` | `String` | Conditional | TurboDocx deliverable ID |
-| `templateId` | `String` | Conditional | TurboDocx template ID |
-| `documentName` | `String` | No | Document name |
-| `documentDescription` | `String` | No | Document description |
-| `senderName` | `String` | No | Sender name (overrides the configured value) |
-| `senderEmail` | `String` | No\*\* | Sender / reply-to email (overrides the configured value) |
-| `ccEmails` | `Array` | No | Array of CC email addresses |
-
-:::info File Source (Conditional)
-Exactly one file source is required: `file`, `fileLink`, `deliverableId`, or `templateId`.
-:::
-
-\*\* `senderEmail` is optional per call but **required at the SDK level** for TurboSign: it must be supplied via `configure`, the `TURBODOCX_SENDER_EMAIL` environment variable, or this per-call parameter, otherwise the SDK raises a `TurboDocxSdk::ValidationError`.
+To get your credentials, follow the **[Get Your Credentials](/docs/SDKs#1-get-your-credentials)** steps from the SDKs main page.
---
-## Additional Documentation
+## Product guides
-For detailed information about advanced configuration and API concepts, see:
+Each product guide opens on the Ruby tab:
-### Core API References
-
-- **[Request Body Reference](/docs/TurboSign/API-Signatures#request-body-multipartform-data)** - Complete request body parameters, file sources, and multipart/form-data structure
-- **[Recipients Reference](/docs/TurboSign/API-Signatures#recipients-reference)** - Recipient properties, signing order, metadata, and configuration options
-- **[Field Types Reference](/docs/TurboSign/API-Signatures#field-types-reference)** - All available field types (signature, date, text, checkbox, etc.) with properties and behaviors
-- **[Field Positioning Methods](/docs/TurboSign/API-Signatures#field-positioning-methods)** - Template-based vs coordinate-based positioning, anchor configuration, and best practices
-
----
+- [TurboSign](/docs/SDKs/turbosign?language=ruby): send documents for signature, track status, download signed PDFs and audit trails
+- [Deliverable](/docs/SDKs/deliverable?language=ruby): generate documents from templates
+- [TurboWebhooks](/docs/SDKs/webhooks?language=ruby): receive real-time signature events
+- [TurboQuote](/docs/SDKs/quote?language=ruby): create, send and download quotes
## Resources
+- [RubyGems](https://rubygems.org/gems/turbodocx-sdk)
- [GitHub Repository](https://github.com/TurboDocx/SDK/tree/main/packages/ruby-sdk)
-- [RubyGems Package](https://rubygems.org/gems/turbodocx-sdk)
- [API Reference](/docs/TurboSign/API-Signatures)
- [Webhook Configuration](/docs/TurboSign/Webhooks)
diff --git a/docs/SDKs/turbosign.md b/docs/SDKs/turbosign.md
index 653f1a8..d2192e9 100644
--- a/docs/SDKs/turbosign.md
+++ b/docs/SDKs/turbosign.md
@@ -2,7 +2,7 @@
title: TurboSign SDK
sidebar_position: 2
sidebar_label: TurboSign
-description: 'TurboSign SDK for JavaScript, TypeScript, Python, PHP, Go and Java: send documents for signature, track status, download signed PDFs and audit trails.'
+description: 'TurboSign SDK for JavaScript, TypeScript, Python, PHP, Go, Java and Ruby: send documents for signature, track status, download signed PDFs and audit trails.'
keywords:
- turbodocx javascript
- turbodocx typescript
@@ -42,6 +42,12 @@ keywords:
- java sdk
- document api java
- esignature java
+- turbodocx ruby
+- turbosign ruby
+- ruby sdk
+- gem turbodocx
+- rails turbodocx
+- esignature ruby
---
import Tabs from '@theme/Tabs';
@@ -77,6 +83,11 @@ The official TurboDocx SDK for Go applications. Build document generation and di
The official TurboDocx SDK for Java applications. Build document generation and digital signature workflows with the Builder pattern, comprehensive error handling, and type-safe APIs. Available on Maven Central as `com.turbodocx:turbodocx-sdk`.
+
+
+
+The official TurboDocx SDK for Ruby applications. Build document generation and digital signature workflows with a simple synchronous API and comprehensive error handling. Available on RubyGems as `turbodocx-sdk`.
+
@@ -181,6 +192,34 @@ implementation 'com.turbodocx:turbodocx-sdk:0.7.0'
+
+
+
+
+
+
+```bash
+gem install turbodocx-sdk
+```
+
+
+
+
+Add to your `Gemfile`:
+
+```ruby
+gem "turbodocx-sdk"
+```
+
+Then run:
+
+```bash
+bundle install
+```
+
+
+
+
@@ -222,6 +261,12 @@ This SDK leverages PHP 8.1+ features including enums, named parameters, readonly
- OkHttp 4.x (included)
- Gson 2.x (included)
+
+
+
+- Ruby 2.7+
+- No runtime dependencies (the SDK uses the standard library's `net/http` and `json`)
+
@@ -403,6 +448,26 @@ public class Main {
}
```
+
+
+
+```ruby
+require "turbodocx_sdk"
+
+# Configure globally (recommended)
+TurboDocxSdk::TurboSign.configure(
+ api_key: ENV["TURBODOCX_API_KEY"], # Required: Your TurboDocx API key
+ org_id: ENV["TURBODOCX_ORG_ID"], # Required: Your organization ID
+ sender_email: "contracts@yourcompany.com", # Required: Reply-to address for signature emails
+ sender_name: "Your Company" # Recommended: Sender name shown in emails
+ # base_url: "https://api.turbodocx.com" # Optional: Override base URL
+)
+```
+
+:::tip Authentication
+Authenticate using `api_key`. API keys are recommended for server-side applications.
+:::
+
@@ -437,6 +502,25 @@ TurboDocxClient client = new TurboDocxClient.Builder()
.build();
```
+
+
+
+The Ruby SDK has no builder. Pass these keyword arguments to `TurboDocxSdk::TurboSign.configure`; an argument you leave out falls back to the environment variable listed, where there is one.
+
+| Keyword | Type | Required | Environment fallback | Description |
+| ---------------- | ----------------------------- | -------- | ------------------------- | --------------------------------------------- |
+| `api_key` | `String` | Yes\* | `TURBODOCX_API_KEY` | Organization API key |
+| `access_token` | `String` | Yes\* | - | Bearer access token (alternative to `api_key`) |
+| `org_id` | `String` | Yes | `TURBODOCX_ORG_ID` | Organization ID |
+| `sender_email` | `String` | Yes | `TURBODOCX_SENDER_EMAIL` | Reply-to address for signature request emails |
+| `sender_name` | `String` | No | `TURBODOCX_SENDER_NAME` | Display name used on signature request emails |
+| `base_url` | `String` | No | `TURBODOCX_BASE_URL` | API base URL (default `https://api.turbodocx.com`) |
+| `client_context` | `TurboDocxSdk::ClientContext` | No | - | Overrides the device and location details recorded on the audit trail |
+
+\*Provide either `api_key` or `access_token`.
+
+The SDK exposes no timeout settings: requests use the standard library's `Net::HTTP` defaults.
+
@@ -462,6 +546,11 @@ try (TurboDocxClient client = new TurboDocxClient.Builder()
Creating a client per request leaks OkHttp threads until they are closed. Prefer one long-lived client for the life of your application and call `client.close()` during shutdown.
:::
+
+
+
+There is nothing to close in Ruby. The SDK opens a new `Net::HTTP` connection for each request and keeps no connection pool or background threads, so configure it once at startup and call its class methods directly.
+
@@ -528,6 +617,21 @@ export TURBODOCX_SENDER_EMAIL=sender@yourcompany.com
Three parameters are **required** for TurboSign operations: `apiKey` (or `accessToken`), `orgId`, and `senderEmail`. The `senderEmail` is used as the reply-to address for signature request emails. To get your credentials, follow the **[Get Your Credentials](/docs/SDKs#1-get-your-credentials)** steps from the SDKs main page.
:::
+
+
+
+```bash
+# .env
+TURBODOCX_API_KEY=your_api_key_here
+TURBODOCX_ORG_ID=your_org_id_here
+TURBODOCX_SENDER_EMAIL=contracts@yourcompany.com
+TURBODOCX_SENDER_NAME=Your Company
+```
+
+:::warning API Credentials Required
+`api_key` and `org_id` are **required** for all API requests. TurboSign additionally **requires `sender_email`** (set it on `configure`, per call, or via the `TURBODOCX_SENDER_EMAIL` environment variable): `configure` raises a `TurboDocxSdk::ValidationError` without it. `sender_name` is optional but strongly recommended. To get your credentials, follow the **[Get Your Credentials](/docs/SDKs#1-get-your-credentials)** steps from the SDKs main page.
+:::
+
@@ -901,6 +1005,48 @@ public class Main {
}
```
+
+
+
+```ruby
+require "json"
+require "turbodocx_sdk"
+
+TurboDocxSdk::TurboSign.configure(
+ api_key: ENV["TURBODOCX_API_KEY"],
+ org_id: ENV["TURBODOCX_ORG_ID"],
+ sender_email: "contracts@acme.com",
+ sender_name: "Acme Corp"
+)
+
+result = TurboDocxSdk::TurboSign.send_signature(
+ # Request-hash keys are camelCase: the SDK forwards them to the API verbatim
+ recipients: [
+ { name: "Alice Smith", email: "alice@example.com", signingOrder: 1 },
+ { name: "Bob Johnson", email: "bob@example.com", signingOrder: 2 }
+ ],
+ fields: [
+ # Alice's signature
+ { type: "signature", page: 1, x: 100, y: 650, width: 200, height: 50, recipientEmail: "alice@example.com" },
+ # defaultValue pins a fixed date in MM/DD/YYYY; omit it to auto-fill the signing date
+ { type: "date", page: 1, x: 320, y: 650, width: 100, height: 30, recipientEmail: "alice@example.com", defaultValue: "12/31/2026" },
+ # Bob's signature
+ { type: "signature", page: 1, x: 100, y: 720, width: 200, height: 50, recipientEmail: "bob@example.com" },
+ { type: "date", page: 1, x: 320, y: 720, width: 100, height: 30, recipientEmail: "bob@example.com" }
+ ],
+ fileLink: "https://www.turbodocx.com/examples/turbodocx.pdf",
+ documentName: "Service Agreement",
+ senderName: "Acme Corp",
+ senderEmail: "contracts@acme.com"
+)
+
+puts "Result: #{JSON.pretty_generate(result)}"
+```
+
+:::warning Always Handle Errors
+The above example omits error handling for brevity. In production, wrap TurboSign calls in `begin`/`rescue`. See [Error Handling](#error-handling) for complete patterns.
+:::
+
@@ -1191,6 +1337,44 @@ SendSignatureResponse result = client.turboSign().sendSignature(
**Important:** The document file must contain the anchor text (e.g., `{SIGNATURE_ALICE}`, `{DATE_ALICE}`) that you reference in your fields. If the anchors don't exist in the document, the API will return an error.
:::
+
+
+
+```ruby
+result = TurboDocxSdk::TurboSign.send_signature(
+ recipients: [{ name: "Alice Smith", email: "alice@example.com", signingOrder: 1 }],
+ fields: [
+ {
+ type: "signature",
+ recipientEmail: "alice@example.com",
+ template: {
+ anchor: "{SIGNATURE_ALICE}",
+ placement: "replace",
+ size: { width: 200, height: 50 }
+ }
+ },
+ {
+ type: "date",
+ recipientEmail: "alice@example.com",
+ # Pins a fixed date in MM/DD/YYYY; omit to auto-fill the signing date
+ defaultValue: "12/31/2026",
+ template: {
+ anchor: "{DATE_ALICE}",
+ placement: "replace",
+ size: { width: 100, height: 30 }
+ }
+ }
+ ],
+ fileLink: "https://www.turbodocx.com/examples/turbodocx.pdf"
+)
+
+puts JSON.pretty_generate(result)
+```
+
+:::info Template Anchors Required
+**Important:** The document file must contain the anchor text (e.g., `{SIGNATURE_ALICE}`, `{DATE_ALICE}`) that you reference in your fields. If the anchors don't exist in the document, the API will return an error.
+:::
+
@@ -1227,6 +1411,15 @@ The SDK supports multiple ways to provide your document:
The SDK supports multiple ways to provide your document:
+
+
+
+TurboSign supports four different ways to provide document files:
+
+:::note camelCase request keys
+The keys inside the request hash (`fileLink`, `deliverableId`, `templateId`, `documentName`, `recipientEmail`, `signingOrder` and so on) are **camelCase**: the SDK passes them to the API verbatim and does **not** convert snake_case hash keys. Only the keyword arguments on `configure` (`api_key:`, `sender_email:` and so on) are snake_case.
+:::
+
@@ -1278,6 +1471,28 @@ SendSignatureResponse result = client.turboSign().sendSignature(
);
```
+
+
+
+Upload a document from bytes already in memory. Wrap the bytes in a `StringIO`: a plain `String` passed as `file` is read as a file path, not as content.
+
+```ruby
+require "stringio"
+
+pdf_bytes = File.binread("/path/to/document.pdf") # or bytes from S3, a database, an upload...
+
+result = TurboDocxSdk::TurboSign.send_signature(
+ file: StringIO.new(pdf_bytes),
+ fileName: "document.pdf", # Optional: otherwise "document.", detected from the content
+ 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" }
+ ]
+)
+```
+
@@ -1322,6 +1537,26 @@ $result = TurboSign::sendSignature(
);
```
+
+
+
+Pass any IO object opened in binary mode, such as a `File`. The SDK reads it, detects the file type from its content and uploads it as multipart form data.
+
+```ruby
+result = File.open("./contract.pdf", "rb") do |pdf|
+ TurboDocxSdk::TurboSign.send_signature(
+ file: pdf,
+ fileName: "contract.pdf", # Optional
+ 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" }
+ ]
+ )
+end
+```
+
@@ -1408,6 +1643,27 @@ SendSignatureResponse result = client.turboSign().sendSignature(
Use `fileLink` when your documents are already hosted on cloud storage (S3, Google Cloud Storage, etc.). This is more efficient than downloading and re-uploading files.
:::
+
+
+
+Provide a publicly accessible URL to your document:
+
+```ruby
+result = TurboDocxSdk::TurboSign.send_signature(
+ fileLink: "https://www.turbodocx.com/examples/turbodocx.pdf",
+ recipients: [
+ { name: "John Doe", email: "john@example.com", signingOrder: 1 }
+ ],
+ fields: [
+ { type: "signature", page: 1, x: 100, y: 500, width: 200, height: 50, recipientEmail: "john@example.com" }
+ ]
+)
+```
+
+:::tip When to use fileLink
+Use `fileLink` when your documents are already hosted on cloud storage (S3, Google Cloud Storage, etc.). This is more efficient than downloading and re-uploading files.
+:::
+
@@ -1439,6 +1695,11 @@ result = await TurboSign.send_signature(
)
```
+
+
+
+For bytes already in memory, wrap them in a `StringIO` as shown in [the in-memory upload example](#1-file-upload-byte).
+
@@ -1523,6 +1784,27 @@ await TurboSign.sendSignature({
:::
+
+
+
+Pass a local file path as a `String`. The SDK reads the file, uses its basename as the document filename and detects the file type from its content.
+
+```ruby
+result = TurboDocxSdk::TurboSign.send_signature(
+ file: "./contract.pdf",
+ recipients: [
+ { name: "John Doe", email: "john@example.com", signingOrder: 1 }
+ ],
+ fields: [
+ { type: "signature", page: 1, x: 100, y: 650, width: 200, height: 50, recipientEmail: "john@example.com" }
+ ]
+)
+```
+
+:::tip A String is always a path
+`file` accepts a path (`String`) or any IO object (`File`, `StringIO`). Raw bytes must be wrapped in a `StringIO`; see [the in-memory upload example](#1-file-upload-byte). Pass `fileName:` to control the stored document filename.
+:::
+
@@ -1613,6 +1895,11 @@ result = await TurboSign.send_signature(
Use `file_link` when your documents are already hosted on cloud storage (S3, Google Cloud Storage, etc.). This is more efficient than downloading and re-uploading files.
:::
+
+
+
+See [2. File URL](#2-file-url): pass the URL as `fileLink:`.
+
@@ -1786,6 +2073,28 @@ SendSignatureResponse result = client.turboSign().sendSignature(
`deliverableId` references documents generated using TurboDocx's document generation API. This creates a seamless workflow: generate → sign.
:::
+
+
+
+Use a document generated by TurboDocx document generation:
+
+```ruby
+# Use a previously generated TurboDocx document
+result = TurboDocxSdk::TurboSign.send_signature(
+ deliverableId: "deliverable-uuid-from-turbodocx",
+ 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" }
+ ]
+)
+```
+
+:::info Integration with TurboDocx
+`deliverableId` references documents generated using TurboDocx's document generation API. This creates a seamless workflow: generate → sign.
+:::
+
@@ -1961,6 +2270,34 @@ SendSignatureResponse result = client.turboSign().sendSignature(
`templateId` references pre-configured TurboSign templates created in the TurboDocx dashboard. These templates come with built-in anchors and field positioning, making it easy to reuse signature workflows across multiple documents.
:::
+
+
+
+```ruby
+# Use a pre-configured TurboSign template
+result = TurboDocxSdk::TurboSign.send_signature(
+ templateId: "template-uuid-from-turbodocx", # Template already contains anchors
+ recipients: [
+ { name: "Alice Smith", email: "alice@example.com", signingOrder: 1 }
+ ],
+ fields: [
+ {
+ type: "signature",
+ recipientEmail: "alice@example.com",
+ template: {
+ anchor: "{SIGNATURE_ALICE}",
+ placement: "replace",
+ size: { width: 200, height: 50 }
+ }
+ }
+ ]
+)
+```
+
+:::info Integration with TurboDocx
+`templateId` references pre-configured TurboSign templates created in the TurboDocx dashboard. These templates come with built-in anchors and field positioning, making it easy to reuse signature workflows across multiple documents.
+:::
+
@@ -2106,6 +2443,27 @@ SendSignatureResponse result = client.turboSign().sendSignature(
Use `new Recipient.Builder().role("client").name(...).email(...)` when you also need a phone or external id. `role` works the same way on `createSignatureReviewLink` and, through `new EmbeddedSignatureRecipient.Builder().role(...)`, on `createEmbeddedSignature`.
+
+
+
+```ruby
+# The role keys come from the template
+setup = TurboDocxSdk::TurboSign.get_template_signature_setup("your-template-id")
+setup["roles"].each { |r| puts "#{r['key']} (saved signer: #{r['hasSavedSigner']})" }
+# client (saved signer: false)
+# countersigner (saved signer: true)
+
+result = TurboDocxSdk::TurboSign.send_signature(
+ templateId: "your-template-id",
+ recipients: [
+ { role: "client", name: "Jane Doe", email: "jane@client.com" }
+ # "countersigner" left out: the signer saved on the template is used
+ ]
+)
+```
+
+`role` works the same way on `create_signature_review_link` and `create_embedded_signature`. An unknown role raises `TurboDocxSdk::ValidationError` with `e.code == "UnknownSignerRole"`.
+
@@ -2145,6 +2503,13 @@ asyncio.run(main())
The snippets below assume a configured `client` and an available `Gson` instance, e.g. `Gson gson = new GsonBuilder().setPrettyPrinting().create();` (as shown in the [Quick Start](#quick-start)).
:::
+
+
+
+:::note Class-level, synchronous calls
+All methods are class methods on `TurboDocxSdk::TurboSign`: call `configure` once, then call any method directly. Every call is synchronous and returns a plain `Hash` with camelCase string keys (or raw bytes for `download`). Snippets that print with `JSON.pretty_generate` need `require "json"`.
+:::
+
@@ -2276,6 +2641,23 @@ TurboDocxClient client = new TurboDocxClient.Builder()
.build();
```
+
+
+
+Configure the SDK with your API credentials and organization settings.
+
+```ruby
+TurboDocxSdk::TurboSign.configure(
+ api_key: "your-api-key", # Required: Your TurboDocx API key (or access_token:)
+ org_id: "your-org-id", # Required: Your organization ID
+ sender_email: "you@company.com", # Required: Reply-to address for signature emails
+ sender_name: "Your Company", # Recommended: Sender name shown in emails
+ base_url: "https://api.turbodocx.com" # Optional: API base URL
+)
+```
+
+`configure` raises `TurboDocxSdk::AuthenticationError` when no API key or access token is available, and `TurboDocxSdk::ValidationError` when no sender email is available.
+
@@ -2432,6 +2814,23 @@ CreateSignatureReviewLinkResponse result = client.turboSign().createSignatureRev
System.out.println("Result: " + gson.toJson(result));
```
+
+
+
+Upload a document for preview without sending signature request emails.
+
+```ruby
+result = TurboDocxSdk::TurboSign.create_signature_review_link(
+ 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" }],
+ fileLink: "https://www.turbodocx.com/examples/turbodocx.pdf",
+ documentName: "Contract Draft"
+)
+
+puts result["documentId"]
+puts result["previewUrl"]
+```
+
@@ -2594,6 +2993,24 @@ SendSignatureResponse result = client.turboSign().sendSignature(
System.out.println("Result: " + gson.toJson(result));
```
+
+
+
+Upload a document and immediately send signature requests to all recipients.
+
+```ruby
+result = TurboDocxSdk::TurboSign.send_signature(
+ recipients: [{ name: "Recipient Name", email: "recipient@example.com", signingOrder: 1 }],
+ fields: [{ type: "signature", page: 1, x: 100, y: 500, width: 200, height: 50, recipientEmail: "recipient@example.com" }],
+ fileLink: "https://www.turbodocx.com/examples/turbodocx.pdf",
+ documentName: "Service Agreement",
+ senderName: "Your Company",
+ senderEmail: "sender@company.com"
+)
+
+puts result["documentId"]
+```
+
@@ -2630,6 +3047,37 @@ Durations are `['value' => N, 'unit' => 'hours'|'days']` objects. `value` is a w
warnings. `maxReminders` accepts `-1` (unlimited), `0` (none), or any value up to `50`. The resulting
deadline is readable afterwards via `getStatus()->expiresAt`.
+
+
+
+`send_signature` (and `create_signature_review_link`) accept an optional per-document schedule. Both reminders and expiration are **off by default**, so omitting these keys preserves the original send behavior. Keys are camelCase like every other request key, and each duration is a `{ value:, unit: }` hash whose `unit` is `"days"` or `"hours"`. The resolved schedule is **frozen onto the document when it is sent**: changing your org defaults later never alters a document already out for signature.
+
+```ruby
+result = TurboDocxSdk::TurboSign.send_signature(
+ # ...recipients, fields, fileLink, documentName
+
+ # Reminders: chase signers who haven't signed yet
+ remindersEnabled: true,
+ reminderDelay: { value: 3, unit: "days" }, # time to the FIRST reminder
+ reminderInterval: { value: 3, unit: "days" }, # gap between later reminders
+ maxReminders: 5, # -1 unlimited, 0 none, max 50 (default 5)
+
+ # Expiration: close the signing window
+ expirationEnabled: true,
+ expireAfter: { value: 14, unit: "days" }, # how long it stays signable
+ expirationWarning: { value: 3, unit: "days" }, # lead time before warnings (0 = never warn)
+ expirationWarningInterval: { value: 1, unit: "days" }
+)
+```
+
+| Field | Bounds |
+| --- | --- |
+| `maxReminders` | Cap per signer: `-1` unlimited, `0` none, **max `50`** (default `5`). |
+| any duration `value` | Whole number, **min 1**, **max 999 days (23976 hours)**. |
+| `expirationWarning` | May be `0` to never warn. |
+
+`false` and `0` are sent as given, so `remindersEnabled: false` or `maxReminders: 0` override the org default rather than being dropped. The resolved deadline is readable afterwards via `get_status(...)["expiresAt"]`.
+
@@ -2657,6 +3105,26 @@ foreach ($result['results'] as $r) {
TurboSign::sendReminder('document-uuid', ['recipient-uuid-1', 'recipient-uuid-2']);
```
+
+
+
+`send_reminder` sends a standalone reminder to whoever's turn it is to sign (`POST /turbosign/documents/{documentId}/send-reminder`). It is independent of the automatic cadence: it works even when reminders are disabled or the per-signer cap is already spent, does **not** consume that cap, and only emails signers at the *current* signing order. Omit the second argument to remind everyone eligible. An empty array is treated the same as omitting it.
+
+```ruby
+# Remind everyone whose turn it is to sign
+result = TurboDocxSdk::TurboSign.send_reminder("document-uuid")
+
+result["results"].each do |r|
+ # "sent", or a "skipped_*" reason (e.g. "skipped_wrong_order", "skipped_completed")
+ puts "#{r['recipientId']}: #{r['status']}"
+end
+
+# Or limit to specific recipients (all-or-nothing): every id must be a current-order pending signer
+TurboDocxSdk::TurboSign.send_reminder("document-uuid", ["recipient-uuid-1", "recipient-uuid-2"])
+```
+
+This differs from **Resend**: resend re-sends the original invitation email, while `send_reminder` sends the reminder copy.
+
@@ -2749,6 +3217,11 @@ SendSignatureResponse result = client.turboSign().sendSignature(
Each `Duration` is a `{value, unit}` pair where `unit` is `"hours"` or `"days"` and `value` is a whole number from **1 up to 999 days (23976 hours)**. `maxReminders` accepts **-1 to 50** (`-1` unlimited, `0` none, default `5`) and caps only automatic reminders, never expiry warnings; `expirationWarning` may be `0` to disable warnings. Reminders and expiry warnings run as two independent clocks, coordinated so a signer never receives both at the same moment, and a reminder cadence that would outlive the expiry window is rejected with `400`.
+
+
+
+See [Reminders and expiration](#reminders-and-expiration) for the Ruby schedule keys (`remindersEnabled`, `reminderDelay`, `expireAfter` and the rest).
+
@@ -2819,6 +3292,11 @@ Durations are `{ value, unit }` objects: `unit` is `"hours"` or `"days"`, and `v
Reminders and expiry warnings run as **two independent clocks**, so a signer keeps getting reminders even after warnings begin; the two are coordinated so a reminder and a warning never land on the same tick. The API rejects a cadence that can't fit its window (for example a reminder interval that outlives `expireAfter`) with `400 InvalidSignatureSchedule`. See the [API validation rules](/docs/TurboSign/API-Signatures#reminders--expiration) for the full list.
+
+
+
+See [Reminders and expiration](#reminders-and-expiration) for the Ruby schedule keys and their bounds.
+
@@ -2927,6 +3405,13 @@ client.turboSign().sendReminder("document-uuid", Arrays.asList("recipient-uuid-1
---
+
+
+
+See [Send a reminder](#send-a-reminder) for `TurboDocxSdk::TurboSign.send_reminder`.
+
+---
+
@@ -3023,6 +3508,20 @@ System.out.println("Expires: " + status.getExpiresAt()); // ISO 8601, or null
System.out.println("Result: " + gson.toJson(status));
```
+
+
+
+Retrieve the document-level status. The `status` reaches the terminal value `"expired"` once the signing window closes, and `expiresAt` carries the deadline (ISO 8601, `nil` when expiration is off). For per-signer detail, use [Get recipients](#get-recipients).
+
+```ruby
+result = TurboDocxSdk::TurboSign.get_status("document-uuid")
+
+puts result["status"] # "under_review" | "completed" | "voided" | "expired" | ...
+puts result["expiresAt"] # ISO 8601 deadline, or nil when expiration is off
+```
+
+The same deadline is also returned on the document from `get_recipients` (`result["document"]["expiresAt"]`).
+
@@ -3139,6 +3638,24 @@ for (DocumentRecipientsResponse.RecipientSignatureStatus r : progress.getRecipie
}
```
+
+
+
+See who the document went to, who has signed, who you are still waiting on,
+and who sent it.
+
+```ruby
+result = TurboDocxSdk::TurboSign.get_recipients("document-uuid")
+
+summary = result["summary"]
+puts "#{summary['completed']}/#{summary['total']} signed, waiting on #{summary['waitingOn']}"
+
+result["recipients"].each do |r|
+ puts "#{r['name']} <#{r['email']}>: #{r['effectiveStatus']} " \
+ "(emailed #{r['delivery']['totalSent']}x)"
+end
+```
+
@@ -3278,6 +3795,18 @@ byte[] pdfData = client.turboSign().download("document-uuid");
Files.write(Paths.get("signed-contract.pdf"), pdfData);
```
+
+
+
+Download the completed signed document as PDF bytes.
+
+```ruby
+pdf_bytes = TurboDocxSdk::TurboSign.download("document-uuid")
+
+# Save to file
+File.binwrite("signed-contract.pdf", pdf_bytes)
+```
+
@@ -3347,6 +3876,18 @@ Cancel/void a signature request.
VoidDocumentResponse result = client.turboSign().voidDocument("document-uuid", "Contract terms changed");
```
+
+
+
+Cancel/void a signature request.
+
+```ruby
+result = TurboDocxSdk::TurboSign.void_document("document-uuid", "Contract terms changed")
+
+puts result["status"] # "voided"
+puts result["voidReason"]
+```
+
@@ -3429,6 +3970,17 @@ ResendEmailResponse result = client.turboSign().resendEmail(
);
```
+
+
+
+Resend signature request emails to specific recipients.
+
+```ruby
+result = TurboDocxSdk::TurboSign.resend_email("document-uuid", ["recipient-uuid-1", "recipient-uuid-2"])
+
+puts "Recipients notified: #{result['recipientCount']}"
+```
+
@@ -3520,6 +4072,19 @@ AuditTrailResponse auditTrail = client.turboSign().getAuditTrail("document-uuid"
System.out.println("Result: " + gson.toJson(auditTrail));
```
+
+
+
+Retrieve the complete audit trail for a document, including all events and actions.
+
+```ruby
+result = TurboDocxSdk::TurboSign.get_audit_trail("document-uuid")
+
+result["auditTrail"].each do |entry|
+ puts "#{entry['timestamp']} - #{entry['actionType']}"
+end
+```
+
@@ -3611,6 +4176,21 @@ for (TemplateSignatureRole role : setup.getRoles()) {
}
```
+
+
+
+List a template's signer roles, in signing order, to [send it with signer roles](#send-a-template-set-up-in-turbodocx). Each role carries `key` (the value for a recipient's `role`), `label`, `order`, `hasSavedSigner`, `fieldCount`, and `defaultName` / `defaultEmail` when it has a saved signer.
+
+```ruby
+setup = TurboDocxSdk::TurboSign.get_template_signature_setup("template-uuid")
+
+setup["roles"].each do |role|
+ puts "#{role['key']} saved signer: #{role['hasSavedSigner']}"
+end
+```
+
+A missing setup raises `TurboDocxSdk::NotFoundError` (`e.code == "SignatureSetupNotFound"`).
+
@@ -3639,6 +4219,25 @@ SignatureFieldType::COMPANY // Company name
SignatureFieldType::CHECKBOX // Checkbox field
```
+
+
+
+TurboSign supports 11 field types. In Ruby, pass the type as a plain string:
+
+```ruby
+"signature" # Signature field
+"initial" # Initial field
+"date" # Date stamp (auto-filled when signed)
+"text" # Free text input
+"full_name" # Full name (auto-filled from recipient)
+"first_name" # First name
+"last_name" # Last name
+"email" # Email address
+"title" # Job title
+"company" # Company name
+"checkbox" # Checkbox field
+```
+
@@ -3649,6 +4248,11 @@ SignatureFieldType::CHECKBOX // Checkbox field
TurboSign supports two ways to position fields:
+
+
+
+TurboSign supports two ways to position fields:
+
@@ -3669,6 +4273,21 @@ new Field(
)
```
+
+
+
+```ruby
+{
+ type: "signature",
+ recipientEmail: "john@example.com",
+ page: 1, # Page number (1-indexed)
+ x: 100, # X coordinate
+ y: 500, # Y coordinate
+ width: 200, # Width in pixels
+ height: 50 # Height in pixels
+}
+```
+
@@ -3700,6 +4319,29 @@ new Field(
- `FieldPlacement::ABOVE` - Place above the anchor
- `FieldPlacement::BELOW` - Place below the anchor
+
+
+
+```ruby
+{
+ type: "signature",
+ recipientEmail: "john@example.com",
+ template: {
+ anchor: "{signature1}", # Text to find in the PDF
+ placement: "replace", # How to place the field
+ size: { width: 100, height: 30 }
+ }
+}
+```
+
+**Placement Options:**
+
+- `"replace"` - Replace the anchor text
+- `"before"` - Place before the anchor
+- `"after"` - Place after the anchor
+- `"above"` - Place above the anchor
+- `"below"` - Place below the anchor
+
@@ -3750,6 +4392,44 @@ new Field(
)
```
+
+
+
+```ruby
+fields = [
+ # Checkbox (pre-checked, readonly)
+ {
+ type: "checkbox", recipientEmail: "john@example.com",
+ page: 1, x: 100, y: 600, width: 20, height: 20,
+ defaultValue: "true", # Pre-checked
+ isReadonly: true # Cannot be unchecked
+ },
+ # Multiline text field
+ {
+ type: "text", recipientEmail: "john@example.com",
+ page: 1, x: 100, y: 200, width: 400, height: 100,
+ isMultiline: true, # Allow multiple lines
+ required: true, # Field is required
+ backgroundColor: "#f0f0f0" # Background color
+ },
+ # Optional text field: the signer may leave it blank
+ {
+ type: "text", recipientEmail: "john@example.com",
+ page: 1, x: 100, y: 250, width: 300, height: 30,
+ required: false
+ },
+ # Readonly text (pre-filled, non-editable)
+ {
+ type: "text", recipientEmail: "john@example.com",
+ page: 1, x: 100, y: 300, width: 300, height: 30,
+ defaultValue: "This text is pre-filled",
+ isReadonly: true
+ }
+]
+```
+
+Fields are required by default. `required: false` makes a field optional for the signer; signature and initial fields are always required, and the API rejects `required: false` on them with a 400.
+
@@ -3805,6 +4485,42 @@ malformed rule returns `400 InvalidConditionalRule`; a well-formed rule whose
---
+
+
+
+The optional `metadata` hash builds IF/THEN relationships between fields. Put a `fieldKey` on a
+controlling checkbox, then point each dependent field's `controllingFieldKey` back at it.
+
+```ruby
+fields = [
+ # Controlling checkbox, carries a stable fieldKey
+ {
+ type: "checkbox", recipientEmail: "reviewer@company.com",
+ page: 1, x: 100, y: 400, width: 20, height: 20,
+ metadata: { fieldKey: "request_changes" }
+ },
+ # Dependent text field, hidden until the checkbox is checked
+ {
+ type: "text", recipientEmail: "reviewer@company.com",
+ page: 1, x: 130, y: 400, width: 300, height: 60,
+ metadata: {
+ conditional: {
+ controllingFieldKey: "request_changes", # = the checkbox's fieldKey
+ operator: "is_checked", # "is_checked" | "is_not_checked"
+ action: "show" # "show" | "unlock"
+ }
+ }
+ }
+]
+```
+
+Use `action: "unlock"` to keep a field visible but read-only until the box is checked. A
+malformed rule returns `400 InvalidConditionalRule`; a well-formed rule whose
+`controllingFieldKey` matches no checkbox **fails open** (the field stays visible/editable). See
+[Conditional (IF/THEN) Fields](/docs/TurboSign/Conditional-Fields).
+
+---
+
@@ -3873,6 +4589,11 @@ Every typed error embeds `TurboDocxError` by value, which promotes its `Message
Every typed exception is a nested static class of `TurboDocxException` (`TurboDocxException.ValidationException`, not a separate top-level import) and extends `RuntimeException`, so the compiler never forces a catch:
+
+
+
+Every error is a `StandardError` subclass that extends `TurboDocxSdk::TurboDocxError`; rescue the most specific class first, since `rescue TurboDocxSdk::TurboDocxError` also matches every subclass. Each error exposes `message`, `status_code` and `code`. `code` is the API's own code when the response includes one (for example `UnknownSignerRole`), and otherwise the class default listed below, so you can branch on `e.code` for the precise reason instead of just the HTTP category.
+
@@ -3906,6 +4627,22 @@ Every typed exception is a nested static class of `TurboDocxException` (`TurboDo
| `TurboDocxException.RateLimitException` | 429 | Too many requests |
| `TurboDocxException.NetworkException` | - | Network connectivity issues |
+
+
+
+| Error Class | Status Code | Default `code` | Description |
+| --------------------- | ----------- | ---------------------- | ----------------------------------- |
+| `TurboDocxError` | varies | `nil` unless the API sends one | Base error class for all SDK errors |
+| `AuthenticationError` | 401 | `AUTHENTICATION_ERROR` | Invalid or missing API credentials |
+| `AuthorizationError` | 403 | `AUTHORIZATION_ERROR` | Authenticated but lacks required permissions |
+| `ValidationError` | 400 | `VALIDATION_ERROR` | Invalid request parameters |
+| `NotFoundError` | 404 | `NOT_FOUND` | Document or resource not found |
+| `ConflictError` | 409 | `CONFLICT` | Request conflicts with current resource state |
+| `RateLimitError` | 429 | `RATE_LIMIT_EXCEEDED` | Too many requests |
+| `NetworkError` | - | `NETWORK_ERROR` | Network connectivity issues |
+
+All classes live under the `TurboDocxSdk` namespace (for example `TurboDocxSdk::ValidationError`).
+
@@ -3961,6 +4698,13 @@ All exceptions extend `TurboDocxException` and include:
---
+
+
+
+See [Error Types](#error-types) for the Ruby error classes, their status codes and default codes.
+
+---
+
@@ -4141,6 +4885,52 @@ async def send_with_error_handling():
asyncio.run(send_with_error_handling())
```
+
+
+
+```ruby
+require "turbodocx_sdk"
+
+begin
+ result = TurboDocxSdk::TurboSign.send_signature(
+ recipients: [{ name: "John Doe", email: "john@example.com", signingOrder: 1 }],
+ fields: [{
+ type: "signature",
+ page: 1,
+ x: 100,
+ y: 650,
+ width: 200,
+ height: 50,
+ recipientEmail: "john@example.com"
+ }],
+ fileLink: "https://www.turbodocx.com/examples/turbodocx.pdf"
+ )
+rescue TurboDocxSdk::AuthenticationError => e
+ puts "Authentication failed: #{e.message}"
+ # Check your API key and org ID
+rescue TurboDocxSdk::AuthorizationError => e
+ puts "Not authorized: #{e.message}"
+ # Authenticated, but lacks permission for this operation
+rescue TurboDocxSdk::ValidationError => e
+ puts "Validation error: #{e.message} (#{e.code})"
+ # Check request parameters
+rescue TurboDocxSdk::NotFoundError => e
+ puts "Resource not found: #{e.message}"
+ # Document or recipient doesn't exist
+rescue TurboDocxSdk::ConflictError => e
+ puts "Conflict: #{e.message}"
+ # Request conflicts with the current resource state
+rescue TurboDocxSdk::RateLimitError => e
+ puts "Rate limited: #{e.message}"
+ # Wait and retry
+rescue TurboDocxSdk::NetworkError => e
+ puts "Network error: #{e.message}"
+ # Check connectivity
+rescue TurboDocxSdk::TurboDocxError => e
+ puts "SDK error: #{e.message}, status_code=#{e.status_code}, code=#{e.code}"
+end
+```
+
@@ -4190,6 +4980,17 @@ All errors include these instance attributes:
| `getStatusCode()` | `int` | HTTP status code |
| `getCode()` | `String` | Machine-readable code; each of the 7 named subclasses falls back to its own default (e.g. `AuthenticationException`'s `AUTHENTICATION_ERROR`) whenever the API response carries none. The bare `TurboDocxException` thrown for an unmapped status (e.g. an unexpected 5xx) can return `null` |
+
+
+
+All errors include these readers:
+
+| Property | Type | Description |
+| ------------- | ---------------- | ------------------------------------ |
+| `message` | `String` | Human-readable error description |
+| `status_code` | `Integer \| nil` | HTTP status code (`nil` for `NetworkError`) |
+| `code` | `String \| nil` | Machine-readable error code; the API's code wins when present, otherwise the class default from [Error Types](#error-types) |
+
@@ -4276,6 +5077,13 @@ try {
---
+
+
+
+See [Handling Errors](#handling-errors) for a complete Ruby `begin`/`rescue` example.
+
+---
+
@@ -4295,6 +5103,11 @@ The SDK exports TypeScript types for full type safety. Import them directly from
The SDK uses Python type hints with `Dict[str, Any]` for flexible JSON-like structures.
+
+
+
+The SDK uses plain Ruby `Hash` objects for requests and responses. Symbol and string keys are both accepted on requests; the key **spelling stays camelCase** because the SDK forwards the hash to the API verbatim. Responses use camelCase string keys.
+
@@ -4338,6 +5151,11 @@ The `type` field accepts the following string values:
| `"title"` | Title field |
| `"company"` | Company field |
+
+
+
+See [Field Types](#field-types): the `type` key takes one of 11 plain strings. Note that the initials field is `"initial"`.
+
@@ -4396,6 +5214,11 @@ enum ConditionalAction: string {
}
```
+
+
+
+The Ruby SDK defines no TurboSign enums. Field types, placements, conditional operators and actions are plain strings, listed under [Field Types](#field-types), [Template Anchors](#2-template-anchors-dynamic) and [Conditional (IF/THEN) Fields](#conditional-ifthen-fields).
+
@@ -4451,6 +5274,11 @@ final class FieldConditional {
}
```
+
+
+
+The Ruby SDK has no request or model classes: recipients and fields are plain `Hash` objects. See [Recipient](#recipient) and [Field](#field) for their keys.
+
@@ -4484,6 +5312,13 @@ Exactly one file source is required: `file`, `fileLink`, `deliverableId`, or `te
---
+
+
+
+Requests are plain `Hash` objects passed to `send_signature` and `create_signature_review_link`. See [Request Parameters](#request-parameters) for every key.
+
+---
+
@@ -4511,6 +5346,13 @@ import type {
from typing import Dict, List, Any, Optional
```
+
+
+
+```ruby
+require "turbodocx_sdk" # loads TurboDocxSdk::TurboSign and the TurboDocxSdk error classes
+```
+
@@ -4558,6 +5400,11 @@ field_types = [
]
```
+
+
+
+See [Field Types](#field-types) for the 11 field type strings.
+
@@ -4617,6 +5464,28 @@ recipient: Dict[str, Any] = {
| `signingOrder` | `int` | Conditional | Order in which recipient should sign (1, 2, 3...). Not needed when `role` is set |
| `role` | `String` | No | Template signer role to fill (for example `"client"`), set with `Recipient.withRole(...)` or the builder. Requires `templateId`. See [Send a template set up in TurboDocx](#send-a-template-set-up-in-turbodocx) |
+
+
+
+Recipient configuration for signature requests:
+
+
+
+| Property | Type | Required | Description |
+| -------------- | --------- | ----------- | ------------------------- |
+| `name` | `String` | Yes | Recipient's full name |
+| `email` | `String` | Yes | Recipient's email address |
+| `signingOrder` | `Integer` | Conditional | Signing order (1-indexed). Not needed when `role` is set |
+| `role` | `String` | No | Template signer role to fill (for example `"client"`). Requires `templateId`. See [Send a template set up in TurboDocx](#send-a-template-set-up-in-turbodocx) |
+
+```ruby
+recipient = {
+ name: "John Doe",
+ email: "john@example.com",
+ signingOrder: 1
+}
+```
+
@@ -4838,6 +5707,44 @@ The coordinate-based constructor takes positional arguments in this order: `new
\*Required when not using template anchors
+
+
+
+Field configuration supporting both coordinate-based and template-based positioning:
+
+
+
+| Property | Type | Required | Description |
+| ----------------- | --------- | -------- | --------------------------------------------------- |
+| `type` | `String` | Yes | Field type (see [Field Types](#field-types)) |
+| `recipientEmail` | `String` | Yes | Which recipient fills this field |
+| `page` | `Integer` | No\* | Page number (1-indexed) |
+| `x` | `Integer` | No\* | X coordinate in pixels |
+| `y` | `Integer` | No\* | Y coordinate in pixels |
+| `width` | `Integer` | No\* | Field width in pixels |
+| `height` | `Integer` | No\* | Field height in pixels |
+| `defaultValue` | `String` | No | Default value (checkbox: `"true"`/`"false"`; date: a fixed `MM/DD/YYYY`, omit to auto-fill the signing date) |
+| `isMultiline` | `Boolean` | No | Enable multiline text |
+| `isReadonly` | `Boolean` | No | Make field read-only (pre-filled) |
+| `required` | `Boolean` | No | Whether the signer must complete the field. Default `true`; `false` makes it optional (not allowed on signature and initial fields) |
+| `backgroundColor` | `String` | No | Background color (hex, rgb, or named) |
+| `template` | `Hash` | No | Template anchor configuration, see [Template Configuration](#template-configuration) |
+| `metadata` | `Hash` | No | Conditional (IF/THEN) metadata, see [Metadata Configuration](#metadata-configuration-conditional-fields) |
+
+\*Required when not using template anchors
+
+```ruby
+field = {
+ type: "signature",
+ page: 1,
+ x: 100,
+ y: 500,
+ width: 200,
+ height: 50,
+ recipientEmail: "john@example.com"
+}
+```
+
@@ -4888,6 +5795,23 @@ A malformed rule returns `400 InvalidConditionalRule`; a well-formed rule whose
`ControllingFieldKey` matches no checkbox **fails open** (the field stays visible/editable). See
[Conditional (IF/THEN) Fields](/docs/TurboSign/Conditional-Fields).
+
+
+
+The optional `metadata` hash builds IF/THEN relationships between fields. Put a `fieldKey` on a
+controlling `checkbox`, then point each dependent field's `conditional.controllingFieldKey` back
+at it.
+
+| Property | Type | Required | Description |
+| --------------------------------- | -------- | -------- | ------------------------------------------------------------- |
+| `fieldKey` | `String` | No | Stable id on a **controlling checkbox** (`type: "checkbox"`). |
+| `conditional` | `Hash` | No | Rule on a **dependent field** (see below). |
+| `conditional.controllingFieldKey` | `String` | Yes | The controlling checkbox's `fieldKey`. Must be non-empty. |
+| `conditional.operator` | `String` | Yes | `"is_checked"` or `"is_not_checked"`. |
+| `conditional.action` | `String` | Yes | `"show"` (hidden until met) or `"unlock"` (locked until met). |
+
+See [Conditional (IF/THEN) Fields](#conditional-ifthen-fields) for a Ruby example.
+
@@ -4921,6 +5845,20 @@ When using `template` instead of coordinates:
| `caseSensitive` | `Boolean` | No | Case-sensitive anchor search |
| `useRegex` | `Boolean` | No | Use regex for anchor search |
+
+
+
+When using `template` instead of coordinates:
+
+| Property | Type | Required | Description |
+| --------------- | --------- | -------- | ---------------------------------------------------------------- |
+| `anchor` | `String` | Yes | Text anchor pattern like `{TagName}` |
+| `placement` | `String` | Yes | `"replace"` \| `"before"` \| `"after"` \| `"above"` \| `"below"` |
+| `size` | `Hash` | Yes | `{ width: Integer, height: Integer }` |
+| `offset` | `Hash` | No | `{ x: Integer, y: Integer }` |
+| `caseSensitive` | `Boolean` | No | Case sensitive search (default: false) |
+| `useRegex` | `Boolean` | No | Use regex for anchor/searchText (default: false) |
+
@@ -5021,6 +5959,44 @@ Exactly one file source is required: `file`, `fileLink`, `deliverableId`, or `te
---
+
+
+
+Request configuration for `create_signature_review_link` and `send_signature`. Keys are camelCase:
+
+
+
+| Parameter | Type | Required | Description |
+| --------------------- | --------------- | ----------- | ------------------------------ |
+| `recipients` | `Array` | Yes | Recipients who will sign |
+| `fields` | `Array` | Conditional | Signature fields configuration. Optional when a recipient has a `role` |
+| `file` | `String \| IO` | Conditional | Local file path, or an IO object such as `File` or `StringIO` (multipart upload) |
+| `fileName` | `String` | No | Document filename stored with an uploaded `file` |
+| `fileLink` | `String` | Conditional | URL to document file |
+| `deliverableId` | `String` | Conditional | TurboDocx deliverable ID |
+| `templateId` | `String` | Conditional | TurboDocx template ID |
+| `documentName` | `String` | No | Document name |
+| `documentDescription` | `String` | No | Document description |
+| `senderName` | `String` | No | Sender name (overrides the configured value) |
+| `senderEmail` | `String` | No\*\* | Sender / reply-to email (overrides the configured value) |
+| `ccEmails` | `Array` | No | Array of CC email addresses |
+| `remindersEnabled` | `Boolean` | No | Send reminder emails to signers who haven't signed. Off by default |
+| `reminderDelay` | `Hash` | No | `{ value: N, unit: "days" \| "hours" }`, time to the FIRST reminder |
+| `reminderInterval` | `Hash` | No | `{ value: N, unit: ... }`, gap between later reminders |
+| `maxReminders` | `Integer` | No | Cap per signer. `-1` unlimited, `0` none, max `50`. Default `5` |
+| `expirationEnabled` | `Boolean` | No | Close the signing window after `expireAfter`. Off by default |
+| `expireAfter` | `Hash` | No | `{ value: N, unit: ... }`, how long the document stays signable |
+| `expirationWarning` | `Hash` | No | `{ value: N, unit: ... }`, how far before expiry warnings start. `0` = never warn |
+| `expirationWarningInterval` | `Hash` | No | `{ value: N, unit: ... }`, gap between warnings once they start |
+
+:::info File Source (Conditional)
+Exactly one file source is required: `file`, `fileLink`, `deliverableId`, or `templateId`.
+:::
+
+\*\* `senderEmail` is optional per call but **required at the SDK level** for TurboSign: it must be supplied via `configure`, the `TURBODOCX_SENDER_EMAIL` environment variable, or this per-call key, otherwise the SDK raises a `TurboDocxSdk::ValidationError`.
+
+---
+
@@ -5074,6 +6050,13 @@ it falls back to a generic TurboDocx sender identity rather than returning `400
---
+
+
+
+See [Request Parameters](#request-parameters): Ruby takes the same request `Hash` for `create_signature_review_link` and `send_signature`.
+
+---
+
@@ -5131,5 +6114,13 @@ For detailed information about advanced configuration and API concepts, see:
- [API Reference](/docs/TurboSign/API-Signatures)
- [Webhook Configuration](/docs/TurboSign/Webhooks)
+
+
+
+- [GitHub Repository](https://github.com/TurboDocx/SDK/tree/main/packages/ruby-sdk)
+- [RubyGems Package](https://rubygems.org/gems/turbodocx-sdk)
+- [API Reference](/docs/TurboSign/API-Signatures)
+- [Webhook Configuration](/docs/TurboSign/Webhooks)
+
diff --git a/docs/SDKs/webhooks-ruby.md b/docs/SDKs/webhooks-ruby.md
deleted file mode 100644
index ac4ebd4..0000000
--- a/docs/SDKs/webhooks-ruby.md
+++ /dev/null
@@ -1,567 +0,0 @@
----
-# Hidden from the published site: the turbodocx-sdk gem is not on RubyGems yet.
-# Remove `draft: true` when the gem is published (TurboDocx/SDK issue).
-draft: true
-title: TurboWebhooks Ruby SDK
-sidebar_position: 20
-sidebar_label: "TurboWebhooks: Ruby"
-description: Official TurboDocx Webhooks SDK for Ruby. Subscribe to all seven TurboSign signature events with the WebhookEvent constants, verify inbound webhook signatures with HMAC-SHA256, and manage delivery history programmatically.
-keywords:
- - turbodocx webhooks
- - turbowebhooks ruby
- - webhook ruby
- - hmac signature verification ruby
- - signature webhook ruby
- - webhook receiver rails
- - rails webhook
- - sinatra webhook
- - webhook secret ruby
- - webhook events ruby
- - webhookevent constants ruby
- - signature lifecycle events
----
-
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-import QuickstartSkillNudge from '@site/src/components/QuickstartSkillNudge';
-
-# TurboWebhooks Ruby SDK
-
-
-
-The official TurboDocx Webhooks SDK for Ruby applications. Subscribe a single per-organization HTTPS endpoint to TurboDocx signature events, verify inbound signatures with HMAC-SHA256, replay delivery attempts, and rotate secrets — all from Ruby 2.7+. Available on RubyGems as `turbodocx-sdk` (same gem as TurboSign).
-
-
-
-:::info What is TurboWebhooks?
-TurboWebhooks lets your application receive real-time notifications across the whole signature lifecycle — sent, viewed, each recipient signing, partial progress, completed, voided, and finalization failures — instead of polling the API. Each organization has a single, named webhook (`signature`) that mirrors the **Signature Webhooks** page in the dashboard, so SDK-managed and UI-managed configuration stays in sync.
-
-For the full conceptual overview of how webhooks work in TurboSign (delivery retries, payload schema, dashboard UI), see [TurboSign → Webhooks](/docs/TurboSign/Webhooks).
-:::
-
-## Installation
-
-```bash
-gem install turbodocx-sdk
-```
-
-Or add it to your `Gemfile`:
-
-```ruby
-gem "turbodocx-sdk"
-```
-
-```bash
-bundle install
-```
-
-## Requirements
-
-- Ruby 2.7 or higher
-- An **administrator** TurboDocx API key (the webhook routes are gated on the administrator role — non-admin keys return HTTP 403)
-
-## Configuration
-
-```ruby
-require "turbodocx_sdk"
-
-TurboDocxSdk::TurboWebhooks.configure(
- api_key: ENV["TURBODOCX_API_KEY"],
- org_id: ENV["TURBODOCX_ORG_ID"]
-)
-```
-
-`configure` hardcodes `skip_sender_validation: true` internally — webhook management never sends signature emails, so no `sender_email` is required.
-
-If you skip `configure`, the first call falls back to the `TURBODOCX_API_KEY` and `TURBODOCX_ORG_ID` environment variables; if either is missing it raises a descriptive `TurboDocxSdk::TurboDocxError`.
-
-### Environment Variables
-
-```bash
-TURBODOCX_API_KEY=your_admin_api_key
-TURBODOCX_ORG_ID=your_org_id
-# optional — defaults to https://api.turbodocx.com
-TURBODOCX_BASE_URL=https://api.turbodocx.com
-# store the secret returned by create_webhook so your receiver can verify signatures
-TURBODOCX_WEBHOOK_SECRET=whsec_...
-```
-
-:::warning Administrator role required
-TurboWebhooks endpoints require the **administrator** role on the API key. A valid TDX- key without the role raises `TurboDocxSdk::AuthorizationError` (HTTP 403). Generate or rotate keys in the **Settings → API Keys** page.
-:::
-
-## Webhook Events
-
-TurboSign dispatches **seven** events. Subscribe to any subset — `events` requires at least one.
-
-| Event | Constant | Fires when |
-|---|---|---|
-| `signature.document.sent` | `TurboDocxSdk::WebhookEvent::SENT` | The document is dispatched to recipients |
-| `signature.document.viewed` | `TurboDocxSdk::WebhookEvent::VIEWED` | A recipient opens the document for the first time |
-| `signature.document.recipient_signed` | `TurboDocxSdk::WebhookEvent::RECIPIENT_SIGNED` | Any individual signer completes their signature — fires **once per signer**, including the last |
-| `signature.document.signed` | `TurboDocxSdk::WebhookEvent::SIGNED` | A signer signs and the document is **not yet complete** (document-level partial progress) |
-| `signature.document.completed` | `TurboDocxSdk::WebhookEvent::COMPLETED` | All recipients have signed and the signed PDF is finalized |
-| `signature.document.finalization_failed` | `TurboDocxSdk::WebhookEvent::FINALIZATION_FAILED` | The signed PDF fails to finalize (e.g. a KMS signing error); the document is **not** completed |
-| `signature.document.voided` | `TurboDocxSdk::WebhookEvent::VOIDED` | The document is voided or cancelled |
-
-:::danger `signed` does not mean "the document is done"
-`recipient_signed` is the **per-person** event: it fires once for **every** signer (including the last) and carries the signer's identity plus `is_final_signer` and `remaining_signers`.
-
-`signed` is a document-level **partial-progress** event. On each signature `recipient_signed` fires first, then exactly one of `signed` (signers still remain), `completed` (that was the final signature and finalization succeeded), or `finalization_failed` (final signature, finalization failed). Two consequences:
-
-- **`signed` never fires on the final signature.**
-- **A single-signer document never emits `signed` at all** — it emits `recipient_signed` (`is_final_signer: true`) then `completed`.
-
-To detect "the whole document is done", use `completed` (or `recipient_signed` with `is_final_signer: true`) — never `signed`.
-
-See [TurboSign → Webhooks](/docs/TurboSign/Webhooks) for the full payload schemas and the lifecycle diagram.
-:::
-
-### Event constants
-
-The events ship as frozen string constants under `TurboDocxSdk::WebhookEvent`, so a typo raises a `NameError` at load time instead of becoming a webhook that silently never fires.
-
-```ruby
-require "turbodocx_sdk"
-
-# Subscribe to a chosen subset
-TurboDocxSdk::TurboWebhooks.create_webhook(
- urls: ["https://your-server.example.com/webhooks/turbodocx"],
- events: [
- TurboDocxSdk::WebhookEvent::SENT,
- TurboDocxSdk::WebhookEvent::VIEWED,
- TurboDocxSdk::WebhookEvent::RECIPIENT_SIGNED,
- TurboDocxSdk::WebhookEvent::COMPLETED,
- TurboDocxSdk::WebhookEvent::FINALIZATION_FAILED,
- TurboDocxSdk::WebhookEvent::VOIDED
- ]
-)
-
-# ...or to everything TurboSign emits — ALL is the frozen list of all 7,
-# in lifecycle order.
-TurboDocxSdk::TurboWebhooks.create_webhook(
- urls: ["https://your-server.example.com/webhooks/turbodocx"],
- events: TurboDocxSdk::WebhookEvent::ALL
-)
-```
-
-:::note Raw strings still work
-Nothing was narrowed. `events` still accepts plain strings (`"signature.document.completed"`), so existing code keeps running and the backend can add new events without an SDK release. The constants exist for discoverability. `get_webhook` also returns `availableEvents` — the list the backend advertises at runtime.
-:::
-
-### Delivered payload shape
-
-Every delivery posts this envelope:
-
-```json
-{
- "event": "signature.document.completed",
- "event_id": "evt_a1b2c3…",
- "created_at": "2026-01-15T10:30:00.000Z",
- "version": "1.0",
- "data": { "documentId": "…", "documentName": "…" }
-}
-```
-
-:::warning Dispatch on `event`, not `eventType`
-
-The event name travels in the top-level **`event`** field. There is no `eventType` key on the
-wire — a receiver written against it reads `nil` and silently dispatches nothing, which
-looks exactly like "the webhook never fired".
-
-`eventType` **is** correct in two other places, which is where the confusion comes from: it is
-the request parameter name for `test_webhook` and `list_webhook_deliveries`, and it is the column name on
-the stored delivery-history rows those return. Those are not the delivered envelope.
-
-:::
-
-## Quick Start
-
-### 1. Create the signature webhook
-
-```ruby
-require "turbodocx_sdk"
-
-TurboDocxSdk::TurboWebhooks.configure(
- api_key: ENV["TURBODOCX_API_KEY"],
- org_id: ENV["TURBODOCX_ORG_ID"]
-)
-
-begin
- created = TurboDocxSdk::TurboWebhooks.create_webhook(
- urls: ["https://your-server.example.com/webhooks/turbodocx"],
- events: [
- TurboDocxSdk::WebhookEvent::SENT,
- TurboDocxSdk::WebhookEvent::VIEWED,
- TurboDocxSdk::WebhookEvent::RECIPIENT_SIGNED,
- TurboDocxSdk::WebhookEvent::COMPLETED,
- TurboDocxSdk::WebhookEvent::FINALIZATION_FAILED,
- TurboDocxSdk::WebhookEvent::VOIDED
- ]
- )
-
- # SAVE THIS SECRET — it is shown ONCE and cannot be retrieved later.
- File.write(".secret", created["secret"])
- puts "Created webhook id=#{created['id']}"
-rescue TurboDocxSdk::ConflictError
- # 409 — the signature webhook already exists for this org.
- # Use TurboDocxSdk::TurboWebhooks.update_webhook(...) or .delete_webhook instead.
- puts "Webhook already exists. Use update_webhook or delete_webhook."
-rescue TurboDocxSdk::ValidationError => e
- # 400 — most commonly a non-HTTPS URL or empty events array.
- puts "Validation failed: #{e.message}"
-end
-```
-
-:::warning HTTPS only
-TurboDocx rejects non-HTTPS webhook URLs with HTTP 400. For local development, expose your receiver via an HTTPS tunnel ([ngrok](https://ngrok.com), [cloudflared](https://github.com/cloudflare/cloudflared), etc.) and pass the tunnel URL to `create_webhook`.
-:::
-
-### 2. Verify inbound webhook signatures
-
-When TurboDocx POSTs to your receiver, every request carries an `X-TurboDocx-Signature` header. Verify it before trusting the payload — the helper enforces a 300-second timestamp tolerance and uses constant-time comparison.
-
-```ruby
-require "turbodocx_sdk"
-
-# In your Rack/Rails/Sinatra webhook handler:
-raw_body = request.body.read # raw bytes — do NOT JSON.parse first
-signature = request.get_header("HTTP_X_TURBODOCX_SIGNATURE") || ""
-timestamp = request.get_header("HTTP_X_TURBODOCX_TIMESTAMP") || ""
-secret = ENV["TURBODOCX_WEBHOOK_SECRET"]
-
-unless TurboDocxSdk.verify_webhook_signature(
- payload: raw_body,
- signature_header: signature,
- timestamp_header: timestamp,
- secret: secret
-)
- return [401, {}, ["invalid signature"]]
-end
-
-event = JSON.parse(raw_body)
-# process event["event"], event["data"], ... (NOT "eventType" — not on the wire)
-
-[200, {}, ["ok"]]
-```
-
-:::danger Use the raw request body
-The HMAC is computed over the **exact bytes** that left the TurboDocx server. Never `JSON.parse` and re-serialize before verifying — re-encoded JSON will not byte-match and verification will fail.
-:::
-
-The signature contract:
-
-| Field | Value |
-|---|---|
-| Header | `X-TurboDocx-Signature: sha256=` |
-| Timestamp header | `X-TurboDocx-Timestamp: ` |
-| Signed string | `.` |
-| Algorithm | HMAC-SHA256 |
-| Tolerance | 300 seconds (configurable) |
-| Comparison | constant-time |
-
-## Method Reference
-
-All methods are class-level; call `TurboDocxSdk::TurboWebhooks.configure` once, then invoke any method directly on the class.
-
-### create_webhook
-
-Subscribe the org to events. Returns `{ "id" => ..., "secret" => ... }` — the **secret is shown once**.
-
-```ruby
-created = TurboDocxSdk::TurboWebhooks.create_webhook(
- urls: ["https://your-server.example.com/webhooks/turbodocx"],
- events: [
- TurboDocxSdk::WebhookEvent::RECIPIENT_SIGNED,
- TurboDocxSdk::WebhookEvent::COMPLETED,
- TurboDocxSdk::WebhookEvent::FINALIZATION_FAILED,
- TurboDocxSdk::WebhookEvent::VOIDED
- ]
- # or subscribe to all 7: events: TurboDocxSdk::WebhookEvent::ALL
-)
-```
-
-`urls` accepts **1 to 10** HTTPS URLs. `events` requires **at least 1** of the [seven events](#webhook-events) — raw strings and `TurboDocxSdk::WebhookEvent::*` constants are interchangeable (the constants *are* strings). Both are required on create.
-
-| Raises | Why |
-|---|---|
-| `TurboDocxSdk::ConflictError` (409) | The signature webhook already exists for this org. |
-| `TurboDocxSdk::ValidationError` (400) | A URL is not HTTPS, `urls` is empty or has more than 10 entries, or `events` is empty. |
-| `TurboDocxSdk::AuthorizationError` (403) | API key lacks the administrator role. |
-
-### get_webhook
-
-Get the org's signature webhook plus delivery statistics.
-
-```ruby
-webhook = TurboDocxSdk::TurboWebhooks.get_webhook
-# webhook["urls"], webhook["events"], webhook["isActive"]
-# webhook["deliveryStats"]["totalDeliveries"]
-# webhook["deliveryStats"]["successfulDeliveries"]
-# webhook["deliveryStats"]["failedDeliveries"]
-# webhook["deliveryStats"]["pendingRetries"]
-# webhook["availableEvents"]
-```
-
-### update_webhook
-
-Patch one or more fields. All parameters are optional — pass only what changes; untouched fields are omitted from the request.
-
-```ruby
-TurboDocxSdk::TurboWebhooks.update_webhook(
- urls: ["https://your-server.example.com/webhooks/turbodocx"],
- events: ["signature.document.completed"],
- is_active: true
-)
-
-# Leaving urls and events alone: just don't pass those keyword arguments.
-TurboDocxSdk::TurboWebhooks.update_webhook(is_active: false)
-```
-
-:::danger Never send an empty array
-`urls` and `events` are optional on update, but optional does **not** relax their minimum length. `urls: []` or `events: []` is a **400** — the same as sending an empty array on create. To leave a field unchanged, **omit the keyword argument entirely**. `urls` still caps at 10 entries on update.
-:::
-
-### delete_webhook
-
-Soft-delete the webhook and its delivery history.
-
-```ruby
-TurboDocxSdk::TurboWebhooks.delete_webhook
-```
-
-### test_webhook
-
-Fire a synthetic delivery to every URL configured on the webhook. Useful for CI smoke tests before flipping a new receiver into production.
-
-```ruby
-result = TurboDocxSdk::TurboWebhooks.test_webhook(
- event_type: "signature.document.completed",
- payload: { "documentId" => "...", "documentName" => "..." }
-)
-
-puts "#{result['summary']['successful']}/#{result['summary']['total']} succeeded"
-result["summary"]["errors"].each do |err|
- puts " failure: #{err}" # per-URL failure messages
-end
-```
-
-### notify_webhook
-
-Send a manual notification to every configured URL. Routes through the same backend handler as `test_webhook` and returns the same shape; prefer `test_webhook` in new code.
-
-```ruby
-TurboDocxSdk::TurboWebhooks.notify_webhook(
- event_type: "signature.document.voided",
- payload: { "documentId" => "..." }
-)
-```
-
-### regenerate_webhook_secret
-
-Rotate the HMAC secret. The new secret is shown **once**; old signatures fail immediately after rotation.
-
-```ruby
-rotated = TurboDocxSdk::TurboWebhooks.regenerate_webhook_secret
-# rotated["secret"]
-# rotated["regeneratedAt"]
-```
-
-### list_webhook_deliveries
-
-Page through historical delivery attempts with filters.
-
-```ruby
-page = TurboDocxSdk::TurboWebhooks.list_webhook_deliveries(
- limit: 20,
- offset: 0,
- event_type: "signature.document.completed",
- is_delivered: false,
- http_status: 500
-)
-# page["results"]: array of delivery rows
-# page["totalRecords"]
-```
-
-### replay_webhook_delivery
-
-Manually retry a past delivery by ID. Returns a freshly-created delivery row.
-
-```ruby
-replay = TurboDocxSdk::TurboWebhooks.replay_webhook_delivery("delivery-uuid-here")
-# replay["id"], replay["httpStatus"], replay["attemptCount"], ...
-```
-
-### get_webhook_stats
-
-Aggregate delivery stats over a sliding window.
-
-```ruby
-stats = TurboDocxSdk::TurboWebhooks.get_webhook_stats(days: 30)
-# stats["summary"]["successRate"]
-# stats["summary"]["avgResponseTime"] (milliseconds)
-# stats["eventBreakdown"] (per-event totals)
-```
-
-### verify_webhook_signature (free function)
-
-Verify the `X-TurboDocx-Signature` header on an incoming request. It is a module-level function (`TurboDocxSdk.verify_webhook_signature`) and does **not** require `TurboWebhooks.configure` — receivers commonly run in a different process than the management code. It never raises on bad input; it always returns a boolean.
-
-```ruby
-ok = TurboDocxSdk.verify_webhook_signature(
- payload: raw_body,
- signature_header: signature_header,
- timestamp_header: timestamp_header,
- secret: webhook_secret,
- tolerance_seconds: 300 # default; pass 0 to disable the timestamp check (NOT recommended)
-)
-```
-
-A negative `tolerance_seconds` **fails closed** (rejects every signature) rather than silently disabling the replay-window check.
-
-## Rails Integration Example
-
-Configure TurboWebhooks once in an initializer, then add a controller for the receiver.
-
-```ruby
-# config/initializers/turbodocx.rb
-require "turbodocx_sdk"
-
-TurboDocxSdk::TurboWebhooks.configure(
- api_key: Rails.application.credentials.dig(:turbodocx, :api_key),
- org_id: Rails.application.credentials.dig(:turbodocx, :org_id)
-)
-```
-
-```ruby
-# app/controllers/webhooks_controller.rb
-class WebhooksController < ApplicationController
- # Webhook receivers are server-to-server — skip CSRF.
- skip_before_action :verify_authenticity_token
-
- def turbodocx
- raw_body = request.body.read
- signature = request.headers["X-TurboDocx-Signature"].to_s
- timestamp = request.headers["X-TurboDocx-Timestamp"].to_s
- secret = Rails.application.credentials.dig(:turbodocx, :webhook_secret)
-
- unless TurboDocxSdk.verify_webhook_signature(
- payload: raw_body, signature_header: signature, timestamp_header: timestamp, secret: secret
- )
- return head :unauthorized
- end
-
- event = JSON.parse(raw_body)
- case event["event"]
- # fires once per signer — event.dig("data", "is_final_signer") marks the last
- when TurboDocxSdk::WebhookEvent::RECIPIENT_SIGNED then on_recipient_signed(event["data"])
- # partial progress only — NEVER fires on the final signature
- when TurboDocxSdk::WebhookEvent::SIGNED then on_partial_progress(event["data"])
- # the document is done
- when TurboDocxSdk::WebhookEvent::COMPLETED then on_completed(event["data"])
- when TurboDocxSdk::WebhookEvent::FINALIZATION_FAILED then on_finalization_failed(event["data"])
- when TurboDocxSdk::WebhookEvent::VOIDED then on_voided(event["data"])
- end
-
- head :ok
- end
-end
-```
-
-```ruby
-# config/routes.rb
-post "/webhooks/turbodocx", to: "webhooks#turbodocx"
-```
-
-## Sinatra / Rack Integration Example
-
-```ruby
-require "sinatra"
-require "turbodocx_sdk"
-
-post "/webhooks/turbodocx" do
- raw_body = request.body.read
- signature = request.env["HTTP_X_TURBODOCX_SIGNATURE"].to_s
- timestamp = request.env["HTTP_X_TURBODOCX_TIMESTAMP"].to_s
-
- halt 401 unless TurboDocxSdk.verify_webhook_signature(
- payload: raw_body,
- signature_header: signature,
- timestamp_header: timestamp,
- secret: ENV["TURBODOCX_WEBHOOK_SECRET"]
- )
-
- event = JSON.parse(raw_body)
- # process event["event"], event["data"], ... (NOT "eventType" — not on the wire)
- status 200
- "ok"
-end
-```
-
-## Error Handling
-
-```ruby
-begin
- TurboDocxSdk::TurboWebhooks.create_webhook(urls: urls, events: events)
-rescue TurboDocxSdk::ConflictError
- # 409 — signature webhook already exists; update or delete it instead
-rescue TurboDocxSdk::ValidationError
- # 400 — non-HTTPS URL, empty events array, etc.
-rescue TurboDocxSdk::AuthorizationError
- # 403 — API key lacks the administrator role
-rescue TurboDocxSdk::AuthenticationError
- # 401 — bad or revoked API key
-rescue TurboDocxSdk::NotFoundError
- # 404 — read/update/delete against a webhook that doesn't exist
-rescue TurboDocxSdk::RateLimitError
- # 429 — back off and retry
-rescue TurboDocxSdk::NetworkError
- # request never reached the server (DNS, refused, timeout)
-rescue TurboDocxSdk::TurboDocxError => e
- # base class for any other typed SDK error (raw 5xx, etc.)
- puts "Error #{e.status_code}: #{e.message}"
-end
-```
-
-All errors inherit from `TurboDocxSdk::TurboDocxError` and carry `status_code` and `code` readers.
-
-### Common Error Codes
-
-| Status | Error | When |
-|---|---|---|
-| 400 | `ValidationError` | Non-HTTPS URL, empty `urls`/`events` array, more than 10 URLs, invalid body |
-| 401 | `AuthenticationError` | Missing or invalid API key |
-| 403 | `AuthorizationError` | Valid key without administrator role |
-| 404 | `NotFoundError` | Operating on a non-existent webhook |
-| 409 | `ConflictError` | Creating when the signature webhook already exists |
-| 429 | `RateLimitError` | Rate limit exceeded — back off |
-
-## Runnable End-to-End Example
-
-A complete, validated CRUD walkthrough lives in the SDK repo:
-
-**[`packages/ruby-sdk/examples/turbowebhooks_crud.rb`](https://github.com/TurboDocx/SDK/blob/main/packages/ruby-sdk/examples/turbowebhooks_crud.rb)**
-
-It exercises every CRUD step plus every error branch (400 / 401 / 403 / 404 / 409) against a live backend.
-
-## Gotchas
-
-- **One webhook per org.** Every method targets the fixed-name `signature` webhook. Creating it twice raises `ConflictError` (409). To manage multiple webhooks per org, call the REST API directly.
-- **Save the secret immediately.** `create_webhook` and `regenerate_webhook_secret` return the HMAC secret **once**. There is no endpoint to retrieve it later. If you lose it, rotate.
-- **Use the raw bytes for verification.** The HMAC is over the exact request body received. Never `JSON.parse` first.
-- **`verify_webhook_signature` uses keyword arguments** (`payload:`, `signature_header:`, `timestamp_header:`, `secret:`) — not positional args.
-- **`verify_webhook_signature` never raises.** Non-String headers, a missing secret, or a malformed timestamp all return `false` rather than raising; a negative `tolerance_seconds` fails closed.
-- **`update_webhook` only sends what you pass.** Omitted keyword arguments are excluded from the PATCH body, so you can flip `is_active` without resending `urls`/`events`.
-- **An empty array is not "no change".** `urls: []` and `events: []` are 400s on update, not no-ops — omitting the keyword is the only way to leave a field alone.
-- **`signature.document.signed` is partial progress, not completion.** It never fires on the final signature, and a single-signer document never emits it at all. Use `TurboDocxSdk::WebhookEvent::COMPLETED` to detect a finished document. See [Webhook Events](#webhook-events).
-- **`WebhookEvent::ALL` is frozen.** It's the canonical list of all 7 events — `dup` it before mutating.
-
-## See Also
-
-- [TurboSign → Webhooks](/docs/TurboSign/Webhooks) — concepts, dashboard UI, retry behavior
-- [TurboWebhooks JavaScript / TypeScript SDK](/docs/SDKs/webhooks-javascript) — same API, JS idioms
-- [TurboWebhooks Python SDK](/docs/SDKs/webhooks-python) — same API, Python idioms
-- [TurboSign Ruby SDK](/docs/SDKs/ruby) — sending documents for signature
-- [SDKs Overview](/docs/SDKs) — all SDKs across all supported languages
-- [TurboDocx SDK on RubyGems](https://rubygems.org/gems/turbodocx-sdk)
-- [TurboDocx SDK on GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/ruby-sdk)
diff --git a/docs/SDKs/webhooks.md b/docs/SDKs/webhooks.md
index 7a7efe3..63a5cff 100644
--- a/docs/SDKs/webhooks.md
+++ b/docs/SDKs/webhooks.md
@@ -2,7 +2,7 @@
title: TurboWebhooks SDK
sidebar_position: 15
sidebar_label: TurboWebhooks
-description: 'TurboWebhooks SDK for JavaScript, TypeScript, Python, PHP, Go and Java: subscribe to TurboSign events, verify HMAC signatures, replay deliveries.'
+description: 'TurboWebhooks SDK for JavaScript, TypeScript, Python, PHP, Go, Java and Ruby: subscribe to TurboSign events, verify HMAC signatures, replay deliveries.'
keywords:
- turbodocx webhooks
- turbowebhooks javascript
@@ -56,6 +56,12 @@ keywords:
- webhook secret java
- webhook events java
- webhookevent enum java
+- turbowebhooks ruby
+- webhook ruby
+- hmac signature verification ruby
+- signature webhook rails
+- webhook receiver sinatra
+- webhookevent constants ruby
---
import Tabs from '@theme/Tabs';
@@ -91,6 +97,11 @@ The official TurboDocx Webhooks SDK for Go applications (net/http, Gin, Echo, Ch
The official TurboDocx Webhooks SDK for Java applications (Spring Boot, Servlet, Jakarta EE, etc.). Subscribe a single per-organization HTTPS endpoint to TurboDocx signature events, verify inbound signatures with HMAC-SHA256, replay delivery attempts, and rotate secrets — all from Java 11+. Distributed as `com.turbodocx:turbodocx-sdk` on Maven Central (same artifact as TurboSign).
+
+
+
+The official TurboDocx Webhooks SDK for Ruby applications (Rails, Sinatra, plain Rack, background jobs, etc.). Subscribe a single per-organization HTTPS endpoint to TurboDocx signature events, verify inbound signatures with HMAC-SHA256, replay delivery attempts, and rotate secrets, all from Ruby 2.7+. Available on RubyGems as `turbodocx-sdk` (same gem as TurboSign).
+
@@ -218,6 +229,25 @@ import com.turbodocx.TurboDocxException;
import com.turbodocx.WebhookSignatureVerifier;
```
+
+
+
+```bash
+gem install turbodocx-sdk
+```
+
+Or add it to your `Gemfile` and run `bundle install`:
+
+```ruby
+gem "turbodocx-sdk"
+```
+
+Then require it:
+
+```ruby
+require "turbodocx_sdk"
+```
+
@@ -258,6 +288,14 @@ import com.turbodocx.WebhookSignatureVerifier;
- An **administrator** TurboDocx API key (the webhook routes are gated on the administrator role — non-admin keys return HTTP 403)
- All TurboWebhooks methods return `com.google.gson.JsonObject` for forward compatibility — new server fields surface without an SDK upgrade
+
+
+
+- Ruby 2.7 or higher
+- An **administrator** TurboDocx API key (the webhook routes are gated on the administrator role, so non-admin keys return HTTP 403)
+- No runtime gem dependencies; the SDK uses the Ruby standard library (`net/http`, `openssl`, `json`)
+- All TurboWebhooks methods are synchronous class methods that return plain Hashes with **string keys** (for example `created["secret"]`)
+
@@ -370,6 +408,20 @@ TurboWebhooks webhooks = new TurboDocxClient.Builder()
TurboWebhooks endpoints require the **administrator** role on the API key. A valid TDX- key without the role throws `TurboDocxException.AuthorizationException` (HTTP 403). Generate or rotate keys in the **Settings → API Keys** page.
:::
+
+
+
+```ruby
+require "turbodocx_sdk"
+
+TurboDocxSdk::TurboWebhooks.configure(
+ api_key: ENV["TURBODOCX_API_KEY"],
+ org_id: ENV["TURBODOCX_ORG_ID"]
+)
+```
+
+`configure` hardcodes `skip_sender_validation: true` internally because webhooks don't send email, so no `sender_email` is needed (only TurboSign needs it). `configure` also accepts `access_token:` and `base_url:`. If you skip the explicit call, the first method call configures itself from `TURBODOCX_API_KEY` and `TURBODOCX_ORG_ID`; if either is missing it raises a `TurboDocxSdk::TurboDocxError` naming both.
+
@@ -455,6 +507,22 @@ TURBODOCX_WEBHOOK_SECRET=whsec_...
TurboWebhooks endpoints require the **administrator** role on the API key. A valid TDX- key without the role throws `TurboDocxException.AuthorizationException` (HTTP 403). Generate or rotate keys in the **Settings → API Keys** page.
:::
+
+
+
+```bash
+TURBODOCX_API_KEY=your_admin_api_key
+TURBODOCX_ORG_ID=your_org_id
+# optional, defaults to https://api.turbodocx.com
+TURBODOCX_BASE_URL=https://api.turbodocx.com
+# store the secret returned by create_webhook so your receiver can verify signatures
+TURBODOCX_WEBHOOK_SECRET=whsec_...
+```
+
+:::warning Administrator role required
+TurboWebhooks endpoints require the **administrator** role on the API key. A valid TDX- key without the role raises `TurboDocxSdk::AuthorizationError` (HTTP 403). Generate or rotate keys in the **Settings → API Keys** page.
+:::
+
@@ -600,6 +668,34 @@ To detect "the whole document is done", use `completed` (or `recipient_signed` w
See [TurboSign → Webhooks](/docs/TurboSign/Webhooks) for the full payload schemas and the lifecycle diagram.
:::
+
+
+
+TurboSign dispatches **seven** events. Subscribe to any subset; `events` requires at least one.
+
+| Event | Constant | Fires when |
+|---|---|---|
+| `signature.document.sent` | `TurboDocxSdk::WebhookEvent::SENT` | The document is dispatched to recipients |
+| `signature.document.viewed` | `TurboDocxSdk::WebhookEvent::VIEWED` | A recipient opens the document for the first time |
+| `signature.document.recipient_signed` | `TurboDocxSdk::WebhookEvent::RECIPIENT_SIGNED` | Any individual signer completes their signature. Fires **once per signer**, including the last |
+| `signature.document.signed` | `TurboDocxSdk::WebhookEvent::SIGNED` | A signer signs and the document is **not yet complete** (document-level partial progress) |
+| `signature.document.completed` | `TurboDocxSdk::WebhookEvent::COMPLETED` | All recipients have signed and the signed PDF is finalized |
+| `signature.document.finalization_failed` | `TurboDocxSdk::WebhookEvent::FINALIZATION_FAILED` | The signed PDF fails to finalize (e.g. a KMS signing error); the document is **not** completed |
+| `signature.document.voided` | `TurboDocxSdk::WebhookEvent::VOIDED` | The document is voided or cancelled |
+
+:::danger `signed` does not mean "the document is done"
+`recipient_signed` is the **per-person** event: it fires once for **every** signer (including the last) and carries the signer's identity plus `is_final_signer` and `remaining_signers`.
+
+`signed` is a document-level **partial-progress** event. On each signature `recipient_signed` fires first, then exactly one of `signed` (signers still remain), `completed` (that was the final signature and finalization succeeded), or `finalization_failed` (final signature, finalization failed). Two consequences:
+
+- **`signed` never fires on the final signature.**
+- **A single-signer document never emits `signed` at all.** It emits `recipient_signed` (`is_final_signer: true`) then `completed`.
+
+To detect "the whole document is done", use `completed` (or `recipient_signed` with `is_final_signer: true`), never `signed`.
+
+See [TurboSign → Webhooks](/docs/TurboSign/Webhooks) for the full payload schemas and the lifecycle diagram.
+:::
+
@@ -812,6 +908,41 @@ webhooks.testWebhook(WebhookEvent.COMPLETED.getValue(), payload);
Nothing was narrowed. The events list is still `List`, so existing code keeps compiling and the backend can add new events without an SDK release. The enum exists for discoverability and type safety. `getWebhook()` also returns `availableEvents` — the list the backend advertises at runtime.
:::
+
+
+
+The events ship as frozen string constants in the `TurboDocxSdk::WebhookEvent` module, so a typo raises `NameError` when that line runs instead of becoming a webhook that silently never fires.
+
+```ruby
+require "turbodocx_sdk"
+
+# Subscribe to a chosen subset
+TurboDocxSdk::TurboWebhooks.create_webhook(
+ urls: ["https://your-server.example.com/webhooks/turbodocx"],
+ events: [
+ TurboDocxSdk::WebhookEvent::SENT,
+ TurboDocxSdk::WebhookEvent::VIEWED,
+ TurboDocxSdk::WebhookEvent::RECIPIENT_SIGNED,
+ TurboDocxSdk::WebhookEvent::COMPLETED,
+ TurboDocxSdk::WebhookEvent::FINALIZATION_FAILED,
+ TurboDocxSdk::WebhookEvent::VOIDED
+ ]
+)
+
+# ...or to everything TurboSign emits. ALL is a frozen array of all 7, in lifecycle order.
+TurboDocxSdk::TurboWebhooks.create_webhook(
+ urls: ["https://your-server.example.com/webhooks/turbodocx"],
+ events: TurboDocxSdk::WebhookEvent::ALL
+)
+
+# The constants are plain Strings, so they work anywhere a wire string does.
+TurboDocxSdk::WebhookEvent::COMPLETED # => "signature.document.completed"
+```
+
+:::note Raw strings still work
+Nothing was narrowed. `events` still accepts plain strings (`"signature.document.completed"`), so existing code keeps running and the backend can add new events without a gem release. The constants exist for discoverability. `get_webhook` also returns `availableEvents`, the list the backend advertises at runtime.
+:::
+
@@ -904,6 +1035,21 @@ the stored delivery-history rows those return. Those are not the delivered envel
:::
+
+
+
+:::warning Dispatch on `event`, not `eventType`
+
+The event name travels in the top-level **`event`** field. There is no `eventType` key on the
+wire, so a receiver written against it reads `nil` and silently dispatches nothing, which
+looks exactly like "the webhook never fired".
+
+`eventType` **is** correct in two other places, which is where the confusion comes from: it is
+the request field that `test_webhook` and `list_webhook_deliveries` send for their `event_type:` keyword, and it is the column name on
+the stored delivery-history rows those return. Those are not the delivered envelope.
+
+:::
+
@@ -1189,6 +1335,47 @@ public class CreateSignatureWebhook {
TurboDocx rejects non-HTTPS webhook URLs with HTTP 400. For local development, expose your receiver via an HTTPS tunnel ([ngrok](https://ngrok.com), [cloudflared](https://github.com/cloudflare/cloudflared), or [webhook.site](https://webhook.site)) and pass the tunnel URL to `createWebhook`.
:::
+
+
+
+```ruby
+require "turbodocx_sdk"
+
+TurboDocxSdk::TurboWebhooks.configure(
+ api_key: ENV["TURBODOCX_API_KEY"],
+ org_id: ENV["TURBODOCX_ORG_ID"]
+)
+
+begin
+ created = TurboDocxSdk::TurboWebhooks.create_webhook(
+ urls: ["https://your-server.example.com/webhooks/turbodocx"],
+ events: [
+ TurboDocxSdk::WebhookEvent::SENT,
+ TurboDocxSdk::WebhookEvent::VIEWED,
+ TurboDocxSdk::WebhookEvent::RECIPIENT_SIGNED,
+ TurboDocxSdk::WebhookEvent::COMPLETED,
+ TurboDocxSdk::WebhookEvent::FINALIZATION_FAILED,
+ TurboDocxSdk::WebhookEvent::VOIDED
+ ]
+ )
+
+ # SAVE THIS SECRET. It is shown ONCE and cannot be retrieved later.
+ File.write(".secret", created["secret"])
+ puts "Created webhook id=#{created['id']}"
+rescue TurboDocxSdk::ConflictError
+ # 409: the signature webhook already exists for this org.
+ # Use update_webhook or delete_webhook instead.
+ puts "Webhook already exists. Use update_webhook or delete_webhook."
+rescue TurboDocxSdk::ValidationError => e
+ # 400: most commonly a non-HTTPS URL or an empty events array.
+ puts "Validation failed: #{e.message}"
+end
+```
+
+:::warning HTTPS only
+TurboDocx rejects non-HTTPS webhook URLs with HTTP 400. For local development, expose your receiver via an HTTPS tunnel ([ngrok](https://ngrok.com), [cloudflared](https://github.com/cloudflare/cloudflared), or [webhook.site](https://webhook.site)) and pass the tunnel URL to `create_webhook`.
+:::
+
@@ -1657,6 +1844,55 @@ The signature contract:
| Tolerance | 300 seconds (configurable via the 6-arg `verify` overload) |
| Comparison | `MessageDigest.isEqual` (constant-time) |
+
+
+
+When TurboDocx POSTs to your receiver, every request carries an `X-TurboDocx-Signature` header. Verify it before trusting the payload. The helper enforces a 300-second timestamp tolerance and uses a constant-time comparison.
+
+```ruby
+# config.ru (plain Rack; the same calls work in Sinatra and Rails, see Framework Examples)
+require "json"
+require "turbodocx_sdk"
+
+run lambda { |env|
+ request = Rack::Request.new(env)
+
+ # IMPORTANT: read the raw bytes. The signature is computed over them,
+ # so never JSON.parse and re-serialize before verifying.
+ raw_body = request.body.read
+ signature = request.get_header("HTTP_X_TURBODOCX_SIGNATURE").to_s
+ timestamp = request.get_header("HTTP_X_TURBODOCX_TIMESTAMP").to_s
+
+ ok = TurboDocxSdk.verify_webhook_signature(
+ payload: raw_body,
+ signature_header: signature,
+ timestamp_header: timestamp,
+ secret: ENV["TURBODOCX_WEBHOOK_SECRET"]
+ )
+ next [401, {}, ["invalid signature"]] unless ok
+
+ event = JSON.parse(raw_body)
+ # process event["event"], event["data"], ... (NOT "eventType", which is not on the wire)
+
+ [200, {}, ["ok"]]
+}
+```
+
+:::danger Use the raw request body
+The HMAC is computed over the **exact bytes** that left the TurboDocx server. Never `JSON.parse` and re-serialize before verifying, because re-encoded JSON will not byte-match and verification will fail. In Rails use `request.raw_post`; in Sinatra or Rack, `request.body.rewind` then `request.body.read`.
+:::
+
+The signature contract:
+
+| Field | Value |
+|---|---|
+| Header | `X-TurboDocx-Signature: sha256=` |
+| Timestamp header | `X-TurboDocx-Timestamp: ` |
+| Signed string | `"#{timestamp}.#{raw_body}"` |
+| Algorithm | HMAC-SHA256 |
+| Tolerance | 300 seconds (configurable via `tolerance_seconds:`) |
+| Comparison | `Rack::Utils.secure_compare` when Rack is loaded, otherwise a built-in constant-time compare |
+
@@ -1687,6 +1923,11 @@ All methods are instance methods on `*turbodocx.WebhooksClient`. Construct once,
All methods are instance methods on `com.turbodocx.TurboWebhooks`. Construct once via `new TurboDocxClient.Builder()...buildWebhooksClient()` and reuse.
+
+
+
+All methods are class methods on `TurboDocxSdk::TurboWebhooks`; configure once, then call them on the class. They take keyword arguments (except `replay_webhook_delivery`, which takes the delivery ID positionally).
+
@@ -1841,6 +2082,33 @@ The URL list accepts **1 to 10** HTTPS URLs. The events list requires **at least
| `TurboDocxException.ValidationException` (400) | A URL is not HTTPS, the URL list is empty or has more than 10 entries, or the events list is empty. |
| `TurboDocxException.AuthorizationException` (403) | API key lacks the administrator role. |
+
+
+
+Subscribe the org to events. Returns a Hash with `"id"` and `"secret"`. The **secret is shown once**.
+
+```ruby
+created = TurboDocxSdk::TurboWebhooks.create_webhook(
+ urls: ["https://your-server.example.com/webhooks/turbodocx"],
+ events: [
+ TurboDocxSdk::WebhookEvent::RECIPIENT_SIGNED,
+ TurboDocxSdk::WebhookEvent::COMPLETED,
+ TurboDocxSdk::WebhookEvent::FINALIZATION_FAILED,
+ TurboDocxSdk::WebhookEvent::VOIDED
+ ]
+ # or subscribe to all 7: events: TurboDocxSdk::WebhookEvent::ALL
+)
+# created["id"], created["secret"]
+```
+
+`urls:` accepts **1 to 10** HTTPS URLs. `events:` requires **at least 1** of the [seven events](#webhook-events); raw strings and `TurboDocxSdk::WebhookEvent::*` constants are interchangeable (the constants are strings). Both keywords are required.
+
+| Raises | Why |
+|---|---|
+| `TurboDocxSdk::ConflictError` (409) | The signature webhook already exists for this org. |
+| `TurboDocxSdk::ValidationError` (400) | A URL is not HTTPS, `urls` is empty or has more than 10 entries, or `events` is empty. |
+| `TurboDocxSdk::AuthorizationError` (403) | API key lacks the administrator role. |
+
@@ -1912,6 +2180,21 @@ JsonObject webhook = webhooks.getWebhook();
// webhook.get("availableEvents")
```
+
+
+
+Get the org's signature webhook plus delivery statistics. Raises `TurboDocxSdk::NotFoundError` if it doesn't exist.
+
+```ruby
+webhook = TurboDocxSdk::TurboWebhooks.get_webhook
+# webhook["urls"], webhook["events"], webhook["isActive"]
+# webhook["deliveryStats"]["totalDeliveries"]
+# webhook["deliveryStats"]["successfulDeliveries"]
+# webhook["deliveryStats"]["failedDeliveries"]
+# webhook["deliveryStats"]["pendingRetries"]
+# webhook["availableEvents"]
+```
+
@@ -2027,6 +2310,26 @@ JsonObject deactivated = webhooks.updateWebhook(null, null, Boolean.FALSE);
`urls` and `events` are optional on update, but optional does **not** relax their minimum length. An empty list serializes to `urls: []` / `events: []`, which the API rejects with a **400** — exactly as it would on create. To leave a field unchanged, pass `null` so the key is omitted; never pass `Collections.emptyList()`. The URL list still caps at 10 entries on update.
:::
+
+
+
+Patch one or more fields. All keywords are optional; pass only what changes. Keywords you omit are left out of the request.
+
+```ruby
+TurboDocxSdk::TurboWebhooks.update_webhook(
+ urls: ["https://your-server.example.com/webhooks/turbodocx"],
+ events: [TurboDocxSdk::WebhookEvent::RECIPIENT_SIGNED, TurboDocxSdk::WebhookEvent::COMPLETED],
+ is_active: true
+)
+
+# Leaving urls and events alone: don't pass those keywords at all.
+TurboDocxSdk::TurboWebhooks.update_webhook(is_active: false)
+```
+
+:::danger Never pass an empty array or `nil`
+`urls:` and `events:` are optional on update, but optional does **not** relax their minimum length. `urls: []`, `events: []`, `urls: nil` and `events: nil` all raise `TurboDocxSdk::ValidationError` in the SDK before any request is sent (the API would reject an empty list with a 400 anyway). Unlike Java, `nil` does not mean "skip". To leave a field unchanged, **omit the keyword entirely**. `urls:` still caps at 10 entries on update.
+:::
+
@@ -2071,6 +2374,13 @@ _, err := wh.DeleteWebhook(ctx)
JsonObject deleted = webhooks.deleteWebhook();
```
+
+
+
+```ruby
+TurboDocxSdk::TurboWebhooks.delete_webhook
+```
+
@@ -2171,6 +2481,24 @@ if (summary.has("errors") && summary.get("errors").isJsonArray()) {
`notifyWebhook` is also exposed for symmetry with the backend surface — it routes through the same handler and returns the same shape. Prefer `testWebhook` in new code.
+
+
+
+```ruby
+result = TurboDocxSdk::TurboWebhooks.test_webhook(
+ event_type: TurboDocxSdk::WebhookEvent::COMPLETED,
+ payload: { "documentId" => "...", "documentName" => "..." }
+)
+
+summary = result["summary"]
+puts "#{summary['successful']}/#{summary['total']} succeeded"
+(summary["errors"] || []).each do |err|
+ puts " failure: #{err}" # per-URL failure messages
+end
+```
+
+`notify_webhook` is also exposed for symmetry with the backend surface. It routes through the same handler and returns the same shape. Prefer `test_webhook` in new code.
+
@@ -2227,6 +2555,20 @@ $result = TurboWebhooks::notifyWebhook(
`notifyWebhook` exists for cross-SDK parity. For smoke-testing a receiver, use [`testWebhook`](#testwebhook) — same behaviour, clearer intent.
:::
+
+
+
+Send a manual notification to every URL configured on the webhook. Routes through the same backend handler as `test_webhook` and returns the same shape (`"deliveries"` and `"summary"`). Exposed for symmetry with the backend surface; prefer `test_webhook` in new code.
+
+```ruby
+result = TurboDocxSdk::TurboWebhooks.notify_webhook(
+ event_type: TurboDocxSdk::WebhookEvent::COMPLETED,
+ payload: { "documentId" => "...", "documentName" => "..." }
+)
+
+puts "#{result['summary']['successful']}/#{result['summary']['total']} succeeded"
+```
+
@@ -2281,6 +2623,15 @@ String newSecret = rotated.get("secret").getAsString();
// rotated.get("regeneratedAt")
```
+
+
+
+```ruby
+rotated = TurboDocxSdk::TurboWebhooks.regenerate_webhook_secret
+# rotated["secret"]
+# rotated["regeneratedAt"]
+```
+
@@ -2375,6 +2726,23 @@ JsonObject page = webhooks.listWebhookDeliveries(
// page.get("totalRecords")
```
+
+
+
+Page through historical delivery attempts with filters. Every keyword is optional; leave one out (or pass `nil`) to skip that filter.
+
+```ruby
+page = TurboDocxSdk::TurboWebhooks.list_webhook_deliveries(
+ limit: 20,
+ offset: 0,
+ event_type: TurboDocxSdk::WebhookEvent::COMPLETED,
+ is_delivered: false,
+ http_status: 500
+)
+# page["results"]: array of delivery row Hashes
+# page["totalRecords"], page["limit"], page["offset"]
+```
+
@@ -2424,6 +2792,14 @@ JsonObject replayed = webhooks.replayWebhookDelivery("delivery-uuid-here");
// replayed.get("id"), replayed.get("httpStatus"), replayed.get("attemptCount"), ...
```
+
+
+
+```ruby
+replay = TurboDocxSdk::TurboWebhooks.replay_webhook_delivery("delivery-uuid-here")
+# replay["id"], replay["httpStatus"], replay["attemptCount"], ...
+```
+
@@ -2491,6 +2867,19 @@ JsonObject stats = webhooks.getWebhookStats(30);
// stats.get("eventBreakdown") (per-event totals)
```
+
+
+
+Aggregate delivery stats over a sliding window. `days:` accepts 1 to 365; omit it for the backend default (30 days).
+
+```ruby
+stats = TurboDocxSdk::TurboWebhooks.get_webhook_stats(days: 30)
+# stats["summary"]["successRate"]
+# stats["summary"]["avgResponseTime"] (milliseconds)
+# stats["eventBreakdown"] (per-event totals)
+# stats["period"], stats["webhook"]
+```
+
@@ -2583,6 +2972,23 @@ boolean ok = WebhookSignatureVerifier.verify(
A `String` body overload is provided for convenience (`verify(String rawBody, ...)`), and a 6-arg overload accepts a custom `toleranceSeconds` plus an optional `LongSupplier now` for testing. Pass `toleranceSeconds = 0` to disable the timestamp check entirely (NOT recommended in production).
+
+
+
+Verify the `X-TurboDocx-Signature` header on an incoming request. It is a module-level function, `TurboDocxSdk.verify_webhook_signature`, and does **not** require `TurboWebhooks.configure`, since receivers commonly run in a different process (or different deploy) than the management code.
+
+```ruby
+ok = TurboDocxSdk.verify_webhook_signature(
+ payload: raw_body, # String, the raw body as received
+ signature_header: signature_header, # value of X-TurboDocx-Signature
+ timestamp_header: timestamp_header, # value of X-TurboDocx-Timestamp
+ secret: webhook_secret, # the secret from create_webhook
+ tolerance_seconds: 300 # default; pass 0 to disable the timestamp check (NOT recommended)
+)
+```
+
+All arguments are keywords, not positional. The helper never raises: a missing or non-String header or secret, or a malformed timestamp, returns `false`. A negative, non-numeric, NaN or infinite `tolerance_seconds` **fails closed** (rejects every delivery) rather than silently disabling the replay-window check. An optional `now:` Proc returning Unix epoch seconds overrides the clock for deterministic tests.
+
@@ -2661,6 +3067,11 @@ class WebhookController extends Controller
Route::post('/webhooks/turbodocx', [WebhookController::class, 'handle']);
```
+
+
+
+Laravel is a PHP framework. For Rails and Sinatra receivers, see the Ruby tab under [Framework Examples](#framework-examples).
+
@@ -2928,6 +3339,104 @@ def turbodocx_webhook(request):
+
+
+
+
+
+
+Configure TurboWebhooks once in an initializer, then add a controller for the receiver.
+
+```ruby
+# config/initializers/turbodocx.rb
+require "turbodocx_sdk"
+
+TurboDocxSdk::TurboWebhooks.configure(
+ api_key: Rails.application.credentials.dig(:turbodocx, :api_key),
+ org_id: Rails.application.credentials.dig(:turbodocx, :org_id)
+)
+```
+
+```ruby
+# app/controllers/webhooks_controller.rb
+class WebhooksController < ApplicationController
+ # Webhook receivers are server-to-server, so skip CSRF protection.
+ skip_forgery_protection
+
+ def turbodocx
+ raw_body = request.raw_post # raw bytes, required for HMAC verification
+
+ ok = TurboDocxSdk.verify_webhook_signature(
+ payload: raw_body,
+ signature_header: request.headers["X-TurboDocx-Signature"].to_s,
+ timestamp_header: request.headers["X-TurboDocx-Timestamp"].to_s,
+ secret: Rails.application.credentials.dig(:turbodocx, :webhook_secret)
+ )
+ return head :unauthorized unless ok
+
+ event = JSON.parse(raw_body)
+ case event["event"]
+ # fires once per signer; event["data"]["is_final_signer"] marks the last
+ when TurboDocxSdk::WebhookEvent::RECIPIENT_SIGNED then on_recipient_signed(event["data"])
+ # partial progress only, NEVER fires on the final signature
+ when TurboDocxSdk::WebhookEvent::SIGNED then on_partial_progress(event["data"])
+ # the document is done
+ when TurboDocxSdk::WebhookEvent::COMPLETED then on_completed(event["data"])
+ when TurboDocxSdk::WebhookEvent::FINALIZATION_FAILED then on_finalization_failed(event["data"])
+ when TurboDocxSdk::WebhookEvent::VOIDED then on_voided(event["data"])
+ end
+
+ head :ok
+ end
+end
+```
+
+```ruby
+# config/routes.rb
+post "/webhooks/turbodocx", to: "webhooks#turbodocx"
+```
+
+
+
+
+```ruby
+# app.rb
+require "json"
+require "sinatra"
+require "turbodocx_sdk"
+
+post "/webhooks/turbodocx" do
+ request.body.rewind
+ raw_body = request.body.read # raw bytes, required for HMAC verification
+
+ ok = TurboDocxSdk.verify_webhook_signature(
+ payload: raw_body,
+ signature_header: request.env["HTTP_X_TURBODOCX_SIGNATURE"].to_s,
+ timestamp_header: request.env["HTTP_X_TURBODOCX_TIMESTAMP"].to_s,
+ secret: ENV["TURBODOCX_WEBHOOK_SECRET"]
+ )
+ halt 401, "Invalid signature" unless ok
+
+ event = JSON.parse(raw_body)
+ case event["event"]
+ when TurboDocxSdk::WebhookEvent::RECIPIENT_SIGNED
+ # fires once per signer; event["data"]["is_final_signer"] marks the last
+ when TurboDocxSdk::WebhookEvent::SIGNED
+ # partial progress only, NEVER fires on the final signature
+ when TurboDocxSdk::WebhookEvent::COMPLETED
+ # the document is done
+ when TurboDocxSdk::WebhookEvent::VOIDED
+ # ...
+ end
+
+ status 200
+ "ok"
+end
+```
+
+
+
+
@@ -3119,6 +3628,34 @@ try {
}
```
+
+
+
+```ruby
+begin
+ TurboDocxSdk::TurboWebhooks.create_webhook(urls: urls, events: events)
+rescue TurboDocxSdk::ConflictError
+ # 409: signature webhook already exists; update or delete it instead
+rescue TurboDocxSdk::ValidationError
+ # 400: non-HTTPS URL, empty events array, etc.
+rescue TurboDocxSdk::AuthorizationError
+ # 403: API key lacks the administrator role
+rescue TurboDocxSdk::AuthenticationError
+ # 401: bad or revoked API key
+rescue TurboDocxSdk::NotFoundError
+ # 404: operating on a non-existent webhook
+rescue TurboDocxSdk::RateLimitError
+ # 429: back off and retry
+rescue TurboDocxSdk::NetworkError
+ # request never reached the server (DNS, refused, timeout)
+rescue TurboDocxSdk::TurboDocxError => e
+ # catch-all for any other typed SDK error (raw 5xx, etc.)
+ puts "Error #{e.status_code}: #{e.message}"
+end
+```
+
+Every error inherits from `TurboDocxSdk::TurboDocxError` (a `StandardError`) and carries `status_code` and `code` readers. `NetworkError` has a `nil` `status_code`.
+
@@ -3184,6 +3721,18 @@ try {
| 409 | `TurboDocxException.ConflictException` | Creating when the signature webhook already exists |
| 429 | `TurboDocxException.RateLimitException` | Rate limit exceeded — back off |
+
+
+
+| Status | Class | When |
+|---|---|---|
+| 400 | `TurboDocxSdk::ValidationError` | Non-HTTPS URL, empty `urls`/`events` array, more than 10 URLs, invalid body. Also raised client-side by `update_webhook` for an empty or `nil` `urls:`/`events:` |
+| 401 | `TurboDocxSdk::AuthenticationError` | Missing or invalid API key |
+| 403 | `TurboDocxSdk::AuthorizationError` | Valid key without administrator role |
+| 404 | `TurboDocxSdk::NotFoundError` | Operating on a non-existent webhook |
+| 409 | `TurboDocxSdk::ConflictError` | Creating when the signature webhook already exists |
+| 429 | `TurboDocxSdk::RateLimitError` | Rate limit exceeded; back off |
+
@@ -3226,6 +3775,13 @@ It exercises every CRUD step plus every error branch (400 / 401 / 403 / 404 / 40
It exercises every CRUD step plus every error branch (400 / 401 / 403 / 404 / 409) against a live backend. Run it after exporting `TURBODOCX_API_KEY` and `TURBODOCX_ORG_ID`; override `TURBODOCX_RECEIVER_URL` to point at a real receiver (e.g. webhook.site, ngrok).
+
+
+
+**[`packages/ruby-sdk/examples/turbowebhooks_crud.rb`](https://github.com/TurboDocx/SDK/blob/main/packages/ruby-sdk/examples/turbowebhooks_crud.rb)**
+
+It exercises every CRUD step plus every error branch (400 / 401 / 403 / 404 / 409) against a live backend. Run with `ruby examples/turbowebhooks_crud.rb` after exporting `TURBODOCX_API_KEY` and `TURBODOCX_ORG_ID` (optionally `TURBODOCX_BASE_URL`). To point it at a real receiver (e.g. webhook.site, ngrok), edit the `RECEIVER_URL` constant at the top of the file.
+
@@ -3299,6 +3855,20 @@ It exercises every CRUD step plus every error branch (400 / 401 / 403 / 404 / 40
- **`createWebhook` takes wire strings, not enum constants.** Pass `WebhookEvent.COMPLETED.getValue()` — the events parameter is `List`. `WebhookEvent.allValues()` returns all 7 wire strings in one call (as an unmodifiable list; copy it if you need to mutate).
- **`signature.document.signed` is partial progress, not completion.** It never fires on the final signature, and a single-signer document never emits it at all. Use `WebhookEvent.COMPLETED` to detect a finished document. See [Webhook Events](#webhook-events).
+
+
+
+- **One webhook per org.** Every method targets the fixed-name `signature` webhook. Creating it twice raises `TurboDocxSdk::ConflictError` (409). To manage multiple webhooks per org, call the REST API directly.
+- **Save the secret immediately.** `create_webhook` and `regenerate_webhook_secret` return the HMAC secret **once**. There is no endpoint to retrieve it later. If you lose it, rotate.
+- **Use the raw bytes for verification.** The HMAC is over the exact request body received. Never `JSON.parse` first. In Rails, use `request.raw_post`; in Sinatra or Rack, `request.body.rewind` then `request.body.read`.
+- **`verify_webhook_signature` is a module function**, `TurboDocxSdk.verify_webhook_signature`, not a method on `TurboWebhooks`. It has no `api_key`/`org_id` dependency, and it takes keyword arguments (`payload:`, `signature_header:`, `timestamp_header:`, `secret:`), not positional ones.
+- **`verify_webhook_signature` never raises.** Bad input returns `false`, and a negative or non-finite `tolerance_seconds` fails closed.
+- **Responses are Hashes with string keys.** Read `created["secret"]`, not `created[:secret]`.
+- **`test_webhook` summary includes per-URL errors.** Read `result["summary"]["errors"]` (guard with `|| []`) to see exactly which receiver failed and why.
+- **An empty array (or `nil`) is not "no change".** On `update_webhook`, `urls: []`, `events: []` and `nil` for either raise `TurboDocxSdk::ValidationError` before the request is sent. Omit the keyword to leave the field alone.
+- **`signature.document.signed` is partial progress, not completion.** It never fires on the final signature, and a single-signer document never emits it at all. Use `TurboDocxSdk::WebhookEvent::COMPLETED` to detect a finished document. See [Webhook Events](#webhook-events).
+- **`WebhookEvent::ALL` is frozen.** It's the canonical list of all 7 events; `dup` it before mutating.
+
@@ -3310,7 +3880,7 @@ It exercises every CRUD step plus every error branch (400 / 401 / 403 / 404 / 40
- [TurboSign → Webhooks](/docs/TurboSign/Webhooks) — concepts, dashboard UI, retry behavior
- [TurboWebhooks PHP SDK](/docs/SDKs/webhooks?language=php) — same API, PHP idioms
- [TurboSign JavaScript SDK](/docs/SDKs/turbosign?language=js) — sending documents for signature
-- [SDKs Overview](/docs/SDKs) — all SDKs across all six languages
+- [SDKs Overview](/docs/SDKs) — all SDKs across all seven languages
- [@turbodocx/sdk on npm](https://www.npmjs.com/package/@turbodocx/sdk)
- [TurboDocx SDK on GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/js-sdk)
@@ -3321,7 +3891,7 @@ It exercises every CRUD step plus every error branch (400 / 401 / 403 / 404 / 40
- [TurboWebhooks JavaScript / TypeScript SDK](/docs/SDKs/webhooks?language=js) — same API, JS idioms
- [TurboWebhooks PHP SDK](/docs/SDKs/webhooks?language=php) — same API, PHP idioms
- [TurboSign Python SDK](/docs/SDKs/turbosign?language=python) — sending documents for signature
-- [SDKs Overview](/docs/SDKs) — all SDKs across all six languages
+- [SDKs Overview](/docs/SDKs) — all SDKs across all seven languages
- [turbodocx-sdk on PyPI](https://pypi.org/project/turbodocx-sdk/)
- [TurboDocx SDK on GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/py-sdk)
@@ -3330,7 +3900,7 @@ It exercises every CRUD step plus every error branch (400 / 401 / 403 / 404 / 40
- [TurboSign → Webhooks](/docs/TurboSign/Webhooks) — concepts, dashboard UI, retry behavior
- [TurboSign PHP SDK](/docs/SDKs/turbosign?language=php) — sending documents for signature
-- [SDKs Overview](/docs/SDKs) — all SDKs across all six languages
+- [SDKs Overview](/docs/SDKs) — all SDKs across all seven languages
- [TurboDocx SDK on Packagist](https://packagist.org/packages/turbodocx/sdk)
- [TurboDocx SDK on GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/php-sdk)
@@ -3342,7 +3912,7 @@ It exercises every CRUD step plus every error branch (400 / 401 / 403 / 404 / 40
- [TurboWebhooks Python SDK](/docs/SDKs/webhooks?language=python) — same API, Python idioms
- [TurboWebhooks PHP SDK](/docs/SDKs/webhooks?language=php) — same API, PHP idioms
- [TurboSign Go SDK](/docs/SDKs/turbosign?language=go) — sending documents for signature
-- [SDKs Overview](/docs/SDKs) — all SDKs across all six languages
+- [SDKs Overview](/docs/SDKs) — all SDKs across all seven languages
- [TurboDocx SDK on GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/go-sdk)
@@ -3354,8 +3924,20 @@ It exercises every CRUD step plus every error branch (400 / 401 / 403 / 404 / 40
- [TurboWebhooks Go SDK](/docs/SDKs/webhooks?language=go) — same API, Go idioms
- [TurboWebhooks PHP SDK](/docs/SDKs/webhooks?language=php) — same API, PHP idioms
- [TurboSign Java SDK](/docs/SDKs/turbosign?language=java) — sending documents for signature
-- [SDKs Overview](/docs/SDKs) — all SDKs across all six languages
+- [SDKs Overview](/docs/SDKs) — all SDKs across all seven languages
- [TurboDocx SDK on GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/java-sdk)
+
+
+
+- [TurboSign → Webhooks](/docs/TurboSign/Webhooks): concepts, dashboard UI, retry behavior
+- [TurboWebhooks JavaScript / TypeScript SDK](/docs/SDKs/webhooks?language=js): same API, JS idioms
+- [TurboWebhooks Python SDK](/docs/SDKs/webhooks?language=python): same API, Python idioms
+- [TurboWebhooks PHP SDK](/docs/SDKs/webhooks?language=php): same API, PHP idioms
+- [TurboSign Ruby SDK](/docs/SDKs/turbosign?language=ruby): sending documents for signature
+- [SDKs Overview](/docs/SDKs): all SDKs across all seven languages
+- [turbodocx-sdk on RubyGems](https://rubygems.org/gems/turbodocx-sdk)
+- [TurboDocx SDK on GitHub](https://github.com/TurboDocx/SDK/tree/main/packages/ruby-sdk)
+
diff --git a/docs/TurboSign/Webhooks.md b/docs/TurboSign/Webhooks.md
index b04334b..4c9b287 100644
--- a/docs/TurboSign/Webhooks.md
+++ b/docs/TurboSign/Webhooks.md
@@ -413,6 +413,7 @@ Our SDKs include built-in webhook verification. The snippets below show the mini
- [TurboWebhooks PHP SDK](/docs/SDKs/webhooks?language=php)
- [TurboWebhooks Go SDK](/docs/SDKs/webhooks?language=go)
- [TurboWebhooks Java SDK](/docs/SDKs/webhooks?language=java)
+- [TurboWebhooks Ruby SDK](/docs/SDKs/webhooks?language=ruby)
Here are examples for each language:
@@ -500,6 +501,34 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
}
```
+
+
+
+```ruby
+require "sinatra"
+require "json"
+require "turbodocx_sdk"
+
+post "/webhook" do
+ request.body.rewind
+ body = request.body.read
+
+ valid = TurboDocxSdk.verify_webhook_signature(
+ payload: body,
+ signature_header: request.env["HTTP_X_TURBODOCX_SIGNATURE"],
+ timestamp_header: request.env["HTTP_X_TURBODOCX_TIMESTAMP"],
+ secret: ENV["TURBODOCX_WEBHOOK_SECRET"]
+ )
+
+ halt 401, "Invalid signature" unless valid
+
+ event = JSON.parse(body)
+ # Process event...
+ content_type :json
+ { received: true }.to_json
+end
+```
+
diff --git a/sidebars.js b/sidebars.js
index 66c4b52..7f867d4 100644
--- a/sidebars.js
+++ b/sidebars.js
@@ -193,6 +193,11 @@ const sidebars = {
id: 'SDKs/java',
label: 'Java',
},
+ {
+ type: 'doc',
+ id: 'SDKs/ruby',
+ label: 'Ruby',
+ },
],
},
{