diff --git a/docs/SDKs/deliverable-ruby.md b/docs/SDKs/deliverable-ruby.md deleted file mode 100644 index f208693f..00000000 --- a/docs/SDKs/deliverable-ruby.md +++ /dev/null @@ -1,535 +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: Deliverable Ruby SDK -sidebar_position: 13 -sidebar_label: "Deliverable: Ruby" -description: Official TurboDocx Deliverable Ruby SDK. Simple synchronous API for document generation from templates. -keywords: - - turbodocx deliverable ruby - - document generation ruby - - template api ruby - - deliverable sdk ruby - - rails document generation - - gem turbodocx ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -import QuickstartSkillNudge from '@site/src/components/QuickstartSkillNudge'; - -# Deliverable Ruby 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`. - -## Installation - - - - -```bash -gem install turbodocx-sdk -``` - - - - -Add to your `Gemfile`: - -```ruby -gem "turbodocx-sdk" -``` - -Then run: - -```bash -bundle install -``` - - - - -## Requirements - -- Ruby 2.7+ -- No runtime dependencies (uses the standard library's `net/http`) - ---- - -## Configuration - -```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 - # 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. -::: - -### Environment Variables - -```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. To get your credentials, follow the **[Get Your Credentials](/docs/SDKs#1-get-your-credentials)** steps from the SDKs main page. -::: - ---- - -## Quick Start - -### Generate a document from a template - -```ruby -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 JSON.pretty_generate(result) -``` - -### Download and manage deliverables - -```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") -``` - ---- - -## Variable Types - -The Deliverable module supports four variable types for template injection: - -### 1. Text Variables - -Inject plain text values into template placeholders: - -```ruby -variables = [ - { "placeholder" => "{CompanyName}", "text" => "Acme Corporation", "mimeType" => "text" }, - { "placeholder" => "{Date}", "text" => "2026-03-12", "mimeType" => "text" } -] -``` - -### 2. HTML Variables - -Inject rich HTML content with formatting: - -```ruby -variables = [ - { - "placeholder" => "{Summary}", - "text" => "

This is a formatted summary with rich text.

", - "mimeType" => "html" - } -] -``` - -### 3. Image Variables - -Inject images by providing a URL or base64-encoded content: - -```ruby -variables = [ - { - "placeholder" => "{Logo}", - "text" => "https://example.com/logo.png", - "mimeType" => "image" - } -] -``` - -### 4. Markdown Variables - -Inject markdown content that gets converted to formatted text: - -```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. -::: - ---- - -## API Reference - -:::note Runnable snippets -The snippets below assume you have already called `TurboDocxSdk::Deliverable.configure(...)` and added `require "turbodocx_sdk"` (plus `require "json"` if the snippet calls `JSON.pretty_generate`), as shown in [Quick Start](#quick-start). All methods are synchronous. -::: - -### Configure - -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 and fall back to environment variables. Either `api_key` or `access_token` must be provided for authentication, and `org_id` is **required** for all Deliverable operations (enforced at runtime). To get your credentials, follow the **[Get Your Credentials](/docs/SDKs#1-get-your-credentials)** steps from the SDKs main page. -::: - -### Generate deliverable - -Generate a new document from a template with variable substitution. - -```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) -``` - -### List deliverables - -List deliverables with pagination, search, and filtering. - -```ruby -items = TurboDocxSdk::Deliverable.list_deliverables( - limit: 10, - offset: 0, - query: "report", - show_tags: true -) - -puts JSON.pretty_generate(items) -``` - -### Get deliverable details - -Retrieve the full details of a single deliverable, including variables and fonts. - -```ruby -details = TurboDocxSdk::Deliverable.get_deliverable_details("deliverable-uuid", show_tags: true) - -puts JSON.pretty_generate(details) -``` - -### Update deliverable info - -Update a deliverable's name, description, or tags. - -```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) -``` - -### Delete deliverable - -Soft-delete a deliverable. - -```ruby -result = TurboDocxSdk::Deliverable.delete_deliverable("deliverable-uuid") - -puts JSON.pretty_generate(result) -``` - -### Download source file - -Download the original source file (DOCX or PPTX). - -```ruby -source_bytes = TurboDocxSdk::Deliverable.download_source_file("deliverable-uuid") - -# Save to file -File.binwrite("report.docx", source_bytes) -``` - -### Download PDF - -Download the PDF version of a deliverable. - -```ruby -pdf_bytes = TurboDocxSdk::Deliverable.download_pdf("deliverable-uuid") - -# Save to file -File.binwrite("report.pdf", pdf_bytes) -``` - ---- - -## 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 | Deliverable or template 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::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 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. Request-body keys stay **camelCase** (the SDK forwards them verbatim); only the method keyword arguments (`api_key:`, `show_tags:`, …) are snake_case. - -### DeliverableVariable - -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. - -### CreateDeliverableRequest - -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 | - -### UpdateDeliverableRequest - -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) | - -### ListDeliverablesOptions - -Options hash for `list_deliverables` (these are 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 | - -### DeliverableRecord - -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`) | - -### DeliverableDetailRecord - -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 | - -### 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 ae988101..d8c2d4dd 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,18 @@ keywords: - maven turbodocx - gradle turbodocx - java sdk + +- turbodocx deliverable ruby + +- document generation ruby + +- template api ruby + +- deliverable sdk ruby + +- rails document generation + +- gem turbodocx --- import Tabs from '@theme/Tabs'; @@ -71,6 +83,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 +194,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 +263,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 +463,24 @@ 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 + # 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 +541,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. To get your credentials, follow the **[Get Your Credentials](/docs/SDKs#1-get-your-credentials)** steps from the SDKs main page. +::: + @@ -693,6 +775,33 @@ public class Main { } ``` + + + +```ruby +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 JSON.pretty_generate(result) +``` + @@ -988,6 +1097,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 +1214,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 +1293,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 +1375,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 +1477,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 +1508,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 called `TurboDocxSdk::Deliverable.configure(...)` and added `require "turbodocx_sdk"` (plus `require "json"` if the snippet calls `JSON.pretty_generate`), as shown in [Quick Start](#quick-start). All methods are synchronous. +::: + @@ -1459,6 +1666,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 and fall back to environment variables. Either `api_key` or `access_token` must be provided for authentication, and `org_id` is **required** for all Deliverable operations (enforced at runtime). To get your credentials, follow the **[Get Your Credentials](/docs/SDKs#1-get-your-credentials)** steps from the SDKs main page. +::: + @@ -1586,6 +1811,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 +1933,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 +2025,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 +2130,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 +2215,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 +2311,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 +2408,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 +2450,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 `NotFoundError` when `templateId` doesn't match a template in the org, and `ValidationError` for invalid request parameters, most commonly a variable hash missing `text` (required unless it sets `variableStack` or `isDisabled: true`) or specifying an unsupported `mimeType`. Both extend the base `TurboDocxSdk::TurboDocxError`: + @@ -2425,6 +2739,48 @@ 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 +``` + +The full typed-error table (`AuthenticationError`, `AuthorizationError`, `ConflictError`, `RateLimitError`, `NetworkError`, HTTP status mapping) and the `message`/`status_code`/`code` attributes shared by every error are documented once in the [Ruby SDK's Error Handling reference](/docs/SDKs/turbosign?language=ruby#error-handling). + @@ -2446,6 +2802,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:`, …) are snake_case. + @@ -2611,6 +2972,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 +3065,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 +3144,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 +3229,20 @@ Options for `listDeliverables`: | `query` | `String` | No | Search query to filter by name | | `showTags` | `Boolean` | No | Include tags in the response | + + + +Options hash for `list_deliverables` (these are 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 +3372,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 +3427,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 +3522,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 +3591,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 abff76ab..acd99a7a 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 add to your Gemfile: +# gem "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 +require "turbodocx_sdk" + +begin + result = TurboDocxSdk::TurboSign.send_signature( + # ... + ) +rescue TurboDocxSdk::ValidationError => e + puts "Validation error: #{e.message}" + # Handle validation error +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 27e3c193..40713b95 100644 --- a/docs/SDKs/partner-ruby.md +++ b/docs/SDKs/partner-ruby.md @@ -1,8 +1,6 @@ --- -# 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: TurboPartner Ruby SDK +unlisted: true sidebar_position: 13 sidebar_label: "TurboPartner: Ruby" description: Official TurboDocx Partner SDK for Ruby. Manage organizations, users, API keys, and entitlements programmatically from plain Ruby, Rails, or Sinatra. @@ -34,7 +32,7 @@ The official TurboDocx Partner SDK for Ruby applications. Build multi-tenant Saa
:::info What is TurboPartner? -TurboPartner is the partner management API for TurboDocx. It allows you to programmatically create and manage organizations, users, API keys, and feature entitlements — perfect for building white-label or multi-tenant applications on top of TurboDocx. +TurboPartner is the partner management API for TurboDocx. It allows you to programmatically create and manage organizations, users, API keys, and feature entitlements, perfect for building white-label or multi-tenant applications on top of TurboDocx. ::: ## TLDR @@ -71,7 +69,7 @@ key = TurboDocxSdk::TurboPartner.create_organization_api_key(org_id, "name" => "Production Key", "role" => "admin" ) -puts "API Key: #{key['data']['key']}" # Save this — only shown once! +puts "API Key: #{key['data']['key']}" # Save this, only shown once! ``` --- @@ -413,7 +411,7 @@ puts "Full Key: #{result['data']['key']}" # Only shown once! ``` :::caution Save Your API Key -The full API key is only returned once during creation. Store it securely — you won't be able to retrieve it again. +The full API key is only returned once during creation. Store it securely: you won't be able to retrieve it again. ::: ### `list_organization_api_keys(organization_id, request = nil)` @@ -525,11 +523,11 @@ result = TurboDocxSdk::TurboPartner.revoke_partner_api_key("partner-key-uuid-her ## Partner User Management :::danger Partner users use a different role enum -Partner portal users take `"admin"`, `"member"`, or `"viewer"`. **Organization** users and organization API keys take `"admin"`, `"contributor"`, `"user"`, or `"viewer"`. The two enums do not overlap beyond `admin`/`viewer` — `"member"` is rejected on an org call, and `"contributor"`/`"user"` are rejected on a partner call. See [Role Enums](#organization-user-roles). +Partner portal users take `"admin"`, `"member"`, or `"viewer"`. **Organization** users and organization API keys take `"admin"`, `"contributor"`, `"user"`, or `"viewer"`. The two enums do not overlap beyond `admin`/`viewer`: `"member"` is rejected on an org call, and `"contributor"`/`"user"` are rejected on a partner call. See [Role Enums](#organization-user-roles). ::: :::caution `permissions` is all-or-nothing -The `"permissions"` hash itself is optional on both `add_user_to_partner_portal` and `update_partner_user_permissions`, and the Ruby SDK forwards your hash verbatim without client-side validation. But if you send `"permissions"`, **all seven keys are required** — there is no partial permissions update. Omitting even one key raises `TurboDocxSdk::ValidationError` (400). Always send the complete hash: read the current values first and re-send them with your change applied. +The `"permissions"` hash itself is optional on both `add_user_to_partner_portal` and `update_partner_user_permissions`, and the Ruby SDK forwards your hash verbatim without client-side validation. But if you send `"permissions"`, **all seven keys are required**: there is no partial permissions update. Omitting even one key raises `TurboDocxSdk::ValidationError` (400). Always send the complete hash: read the current values first and re-send them with your change applied. ::: ### `add_user_to_partner_portal(request)` @@ -573,7 +571,7 @@ end ### `update_partner_user_permissions(user_id, request)` -Update a partner user's role and permissions. If you pass `"permissions"`, send **all seven keys** — a partial hash is a 400. +Update a partner user's role and permissions. If you pass `"permissions"`, send **all seven keys**: a partial hash is a 400. ```ruby result = TurboDocxSdk::TurboPartner.update_partner_user_permissions("partner-user-uuid-here", @@ -686,7 +684,7 @@ Pass features as a plain Hash with camelCase string keys. The SDK sends them str ### Tracking (Usage Counters) -Current consumption against the limits above. TurboDocx maintains these automatically, but `update_organization_entitlements` **accepts a `tracking` hash** — useful for seeding counters when migrating an existing customer: +Current consumption against the limits above. TurboDocx maintains these automatically, but `update_organization_entitlements` **accepts a `tracking` hash**, useful for seeding counters when migrating an existing customer: | Field | Type | Description | |-------|------|-------------| @@ -705,7 +703,7 @@ Every counter except `currentAICredits` floors at `0`. Only `currentAICredits` a ## Preferences Reference -TurboSign display preferences you can read and set per organization. Every key is a boolean and is validated strictly — the strings `"true"` / `"false"` are rejected with a 400, so pass real booleans. The API returns only these keys and never any of the organization's other settings. +TurboSign display preferences you can read and set per organization. Every key is a boolean and is validated strictly: the strings `"true"` / `"false"` are rejected with a 400, so pass real booleans. The API returns only these keys and never any of the organization's other settings. | Field | Type | Default | Description | |-------|------|---------|-------------| @@ -812,6 +810,8 @@ permissions = { ## Error Handling +`TurboDocxSdk::TurboPartner.create_organization` and the other partner calls raise `AuthorizationError` when the partner API key lacks the scope for the route, since partner keys are scoped separately from organization keys: + ```ruby begin result = TurboDocxSdk::TurboPartner.create_organization("name" => "Acme Corp") @@ -842,20 +842,7 @@ rescue TurboDocxSdk::TurboDocxError => e end ``` -All errors inherit from `TurboDocxSdk::TurboDocxError` and carry `status_code` and `code` readers. - -### Error Classes - -| Error Class | Status | When | -|---|---|---| -| `TurboDocxSdk::TurboDocxError` | varies | Base class for all SDK errors | -| `TurboDocxSdk::AuthenticationError` | 401 | Invalid or missing partner credentials | -| `TurboDocxSdk::AuthorizationError` | 403 | Valid credentials without permission for this operation | -| `TurboDocxSdk::ValidationError` | 400 | Invalid request parameters, or TurboPartner not configured | -| `TurboDocxSdk::NotFoundError` | 404 | Resource not found | -| `TurboDocxSdk::ConflictError` | 409 | Request conflicts with current resource state | -| `TurboDocxSdk::RateLimitError` | 429 | Too many requests — back off | -| `TurboDocxSdk::NetworkError` | — | Network connectivity issues | +The full typed-error table and HTTP status mapping is documented once in the [Ruby SDK's Error Handling reference](/docs/SDKs/turbosign?language=ruby#error-handling); partner calls use the same `AuthenticationError`/`AuthorizationError`/`ValidationError`/`NotFoundError`/`ConflictError`/`RateLimitError`/`NetworkError` types. --- @@ -925,5 +912,5 @@ 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 +- [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 a5d7427d..00000000 --- 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 43ee63ae..7dc5713b 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,22 @@ keywords: - gradle turboquote - line items java - pricebook java + +- 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'; @@ -118,6 +134,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/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. +::: +
@@ -237,6 +264,31 @@ import com.turbodocx.TurboDocxException; import com.turbodocx.models.quote.*; ``` + + + + + + +```bash +gem install turbodocx-sdk +``` + + + + +```ruby +# Gemfile +gem "turbodocx-sdk" +``` + +```bash +bundle install +``` + + + + @@ -280,6 +332,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 (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 + @@ -424,6 +484,28 @@ 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 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. +::: + @@ -493,6 +575,16 @@ 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_here +TURBODOCX_ORG_ID=your_org_id_here +# optional — defaults to https://api.turbodocx.com +TURBODOCX_BASE_URL=https://api.turbodocx.com +``` + @@ -920,6 +1012,64 @@ 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)" +``` + @@ -1036,6 +1186,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 +1243,11 @@ 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. + @@ -1187,6 +1365,23 @@ 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 +``` + @@ -1368,6 +1563,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–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`. +::: + @@ -1458,6 +1687,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 — 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. + @@ -1554,6 +1798,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 — 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 +1869,14 @@ SuccessResponse resp = tq.deleteQuote(quoteId); System.out.println(resp.getMessage()); ``` + + + +```ruby +result = TurboDocxSdk::TurboQuote.delete_quote("quote-uuid") +# result["message"] +``` + @@ -1673,6 +1942,16 @@ 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 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. + @@ -1746,6 +2025,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"] — updated Quote +# result["updatedCount"] — Integer, items that were re-priced +# result["skippedCount"] — Integer, items that had no pricebook match +# result["message"] +``` + @@ -1802,6 +2094,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 +2179,16 @@ Files.write(Paths.get("quote.pdf"), pdf); --- + + + +Returns raw PDF bytes. + +```ruby +pdf_bytes = TurboDocxSdk::TurboQuote.download_quote_pdf("quote-uuid") +File.binwrite("quote.pdf", pdf_bytes) +``` + @@ -1953,6 +2262,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 +2385,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–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 +2549,32 @@ 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`–`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. + @@ -2334,6 +2701,30 @@ 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** 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. + +::: + @@ -2419,6 +2810,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 +2912,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"] +``` + @@ -2623,6 +3038,28 @@ 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). + + + +`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`. +::: + @@ -2711,6 +3148,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 +3231,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 +3389,33 @@ 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"` — 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. +::: + @@ -3015,6 +3504,14 @@ 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"] +``` + @@ -3182,6 +3679,36 @@ 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 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`. +::: + @@ -3271,6 +3798,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. 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 +3884,16 @@ 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" +) +``` + @@ -3402,6 +3954,14 @@ tq.removeLineItem(quoteId, itemId); --- + + + +```ruby +result = TurboDocxSdk::TurboQuote.remove_line_item("quote-uuid", "item-uuid") +# result["message"] +``` + @@ -3603,6 +4163,56 @@ When `CreateProductRequest.getImages()` is non-empty, the SDK automatically swit --- + + + +| Method | Signature | Returns | +|---|---|---| +| `list_products` | `(options = nil)` | paginated list | +| `create_product` | `(request)` | `Product` | +| `bulk_create_products` | `(rows)` | [bulk report](#bulk-imports) | +| `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 +``` + @@ -3931,6 +4541,44 @@ Bundle bundle = tq.createBundle(req); --- + + + +| Method | Signature | Returns | +|---|---|---| +| `list_bundles` | `(options = nil)` | paginated list | +| `create_bundle` | `(request)` | `Bundle` | +| `bulk_create_bundles` | `(rows)` | [bulk report](#bulk-imports) | +| `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 +) +``` + @@ -4130,6 +4778,40 @@ System.out.println("Products: " + pbProducts.getTotalRecords()); --- + + + +| Method | Signature | Returns | +|---|---|---| +| `list_price_books` | `(options = nil)` | paginated list | +| `create_price_book` | `(request)` | `PriceBook` | +| `bulk_create_price_books` | `(rows)` | [bulk report](#bulk-imports) | +| `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) +``` + @@ -4303,6 +4985,39 @@ String contactId = contacts.getResults().get(0).getId(); --- + + + +| Method | Signature | Returns | +|---|---|---| +| `list_companies` | `(options = nil)` | paginated list | +| `create_company` | `(request)` | `Company` | +| `bulk_create_companies` | `(rows)` | [bulk report](#bulk-imports) | +| `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"]) +``` + @@ -4445,6 +5160,29 @@ Contact contact = tq.createContact(req); --- + + + +| Method | Signature | Returns | +|---|---|---| +| `list_contacts` | `(options = nil)` | paginated list | +| `create_contact` | `(request)` | `Contact` | +| `bulk_create_contacts` | `(rows)` | [bulk report](#bulk-imports) | +| `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" +) +``` + @@ -4637,6 +5375,42 @@ QuoteTemplateListResponse templates = tq.listTemplates(); --- + + + +| 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") +``` + @@ -4850,6 +5624,27 @@ 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)` | `QuoteType` | +| `bulk_create_types` | `(rows)` | [bulk report](#bulk-imports) | +| `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") +``` + @@ -5128,6 +5923,81 @@ 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. + + + +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" } +]) +``` + @@ -5226,6 +6096,23 @@ The other five bulk methods follow the exact same pattern: --- + + + +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" +``` + @@ -5371,6 +6258,35 @@ System.out.println("Quote: " + result.getQuote().getId()); --- + + + +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"] +``` + @@ -5665,6 +6581,38 @@ 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}" +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 +``` + @@ -5739,6 +6687,18 @@ 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 | `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 | + @@ -5776,6 +6736,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 +``` + @@ -5835,5 +6812,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 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 14714059..155435bd 100644 --- a/docs/SDKs/ruby.md +++ b/docs/SDKs/ruby.md @@ -1,26 +1,23 @@ --- -# 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 - - ruby sdk - - gem turbodocx - - rails turbodocx - - sinatra turbodocx - - document api ruby +- turbodocx ruby +- turbosign ruby +- ruby sdk +- gem turbodocx +- rails turbodocx +- sinatra turbodocx +- document api ruby --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import QuickstartSkillNudge from '@site/src/components/QuickstartSkillNudge'; -# TurboSign Ruby SDK +# TurboDocx Ruby SDK @@ -90,658 +87,19 @@ 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. +`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 +## Product guides -### Send a Document for Signature +Each product guide opens on the Ruby tab: -```ruby -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" }, - { 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" -) - -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) | - -```ruby -field = { - type: "signature", - page: 1, - x: 100, - y: 500, - width: 200, - height: 50, - recipientEmail: "john@example.com" -} -``` - -### 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`. - ---- - -## Additional Documentation - -For detailed information about advanced configuration and API concepts, see: - -### 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 diff --git a/docs/SDKs/turbosign.md b/docs/SDKs/turbosign.md index 653f1a8d..57bec31a 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,20 @@ keywords: - java sdk - document api java - esignature java + +- turbodocx ruby + +- turbosign ruby + +- ruby sdk + +- gem turbodocx + +- rails turbodocx + +- sinatra turbodocx + +- document api ruby --- import Tabs from '@theme/Tabs'; @@ -77,6 +91,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 +200,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 +269,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`) + @@ -403,6 +456,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. +::: + @@ -528,6 +601,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 +989,42 @@ public class Main { } ``` + + + +```ruby +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" }, + { 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" +) + +puts JSON.pretty_generate(result) +``` + @@ -1191,6 +1315,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 +1389,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`, …) 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. +::: + @@ -1526,6 +1697,36 @@ await TurboSign.sendSignature({ +### 1. File Upload (path or IO) {#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) / 2. File URL (file_link) {#2-file-url-filelink} @@ -1613,6 +1814,33 @@ 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. ::: + + + +```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. +::: + @@ -1786,6 +2014,34 @@ SendSignatureResponse result = client.turboSign().sendSignature( `deliverableId` references documents generated using TurboDocx's document generation API. This creates a seamless workflow: generate → sign. ::: + + + +```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. +::: + @@ -1961,6 +2217,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 +2390,35 @@ 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`. + + + +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. + @@ -2145,6 +2458,11 @@ 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)). ::: + + + +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). + @@ -2276,6 +2594,21 @@ 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 + 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 +) +``` + @@ -2432,6 +2765,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 +2944,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 +2998,40 @@ 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` 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"]`. + @@ -2657,6 +3059,25 @@ foreach ($result['results'] as $r) { TurboSign::sendReminder('document-uuid', ['recipient-uuid-1', 'recipient-uuid-2']); ``` + + + +`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`. + @@ -3023,6 +3444,24 @@ System.out.println("Expires: " + status.getExpiresAt()); // ISO 8601, or null System.out.println("Result: " + gson.toJson(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) +``` + +Once a document's deadline passes it moves to the terminal `expired` status and its signing +links stop working. The same `expiresAt` deadline is also returned on the document object from +`get_recipients` (`result["document"]["expiresAt"]`). + @@ -3139,6 +3578,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 +3735,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 +3816,15 @@ 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") +``` + @@ -3429,6 +3907,15 @@ 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"]) +``` + @@ -3520,6 +4007,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['actionType']} at #{entry['timestamp']}" +end +``` + @@ -3611,6 +4111,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 +``` + +Calls `GET /turbosign/templates/:templateId/signature-setup`. A template with no saved signature setup returns a 404 (`SignatureSetupNotFound`). + @@ -3873,6 +4388,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; rescue the most specific one first, since `rescue TurboDocxSdk::TurboDocxError` also matches every subclass below it. Each of the 7 named subclasses has its own default `code` (for example `"NOT_FOUND"`), so `e.code` is populated for those even when the API response itself carries none; the base `TurboDocxError` raised for an unmapped status (e.g. an unexpected 5xx) has no default, so `e.code` can be `nil` there. + @@ -3961,6 +4481,20 @@ All exceptions extend `TurboDocxException` and include: --- + + + +| 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 | + @@ -4141,6 +4675,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}" + # 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 +4770,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 expose these readers: + +| 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; the API's code wins when present, otherwise the class's default | + @@ -4295,6 +4886,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. Symbol and string keys are both accepted; the key **spelling stays camelCase** because the SDK forwards the hash to the API verbatim. + @@ -4558,6 +5154,28 @@ field_types = [ ] ``` + + + +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" +] +``` + @@ -4617,6 +5235,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` | + +```ruby +recipient = { + name: "John Doe", + email: "john@example.com", + signingOrder: 1 +} +``` + @@ -4838,6 +5478,97 @@ 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 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 | +| `metadata` | `Hash` | No | Conditional (IF/THEN) metadata, see below | + +\*Required when not using template anchors + +**Metadata Configuration (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"` \| `"is_not_checked"`. | +| `conditional.action` | `String` | Yes | `"show"` (hidden until met) \| `"unlock"` (locked until met). | + +```ruby +# Checkbox reveals a text field when checked +fields = [ + { + type: "checkbox", + recipientEmail: "reviewer@company.com", + page: 1, x: 100, y: 400, width: 20, height: 20, + metadata: { fieldKey: "request_changes" } + }, + { + type: "text", + recipientEmail: "reviewer@company.com", + page: 1, x: 130, y: 400, width: 300, height: 60, + metadata: { + conditional: { + controllingFieldKey: "request_changes", + operator: "is_checked", + action: "show" + } + } + } +] +``` + +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). + +**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) | + +```ruby +field = { + type: "signature", + page: 1, + x: 100, + y: 500, + width: 200, + height: 50, + recipientEmail: "john@example.com" +} +``` + @@ -5021,6 +5752,48 @@ Exactly one file source is required: `file`, `fileLink`, `deliverableId`, or `te --- + + + +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) | +| `fileName` | `String` | No | File name sent with `file` (a path upload uses the file's own name by default) | +| `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 Duration bounds +Each duration `{ "value", "unit" }` uses `"days"` or `"hours"`; `value` is a whole number from `1` +up to **999 days / 23976 hours**. Reminder and expiration are independent and both **off by +default**. See [Reminders and expiration](#reminders-and-expiration). +::: + +:::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`. + @@ -5131,5 +5904,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 ac4ebd4d..00000000 --- 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 7a7efe3b..30bbbaeb 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,26 @@ keywords: - webhook secret java - webhook events java - webhookevent enum java + +- 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 --- import Tabs from '@theme/Tabs'; @@ -91,6 +111,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. 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 +243,23 @@ import com.turbodocx.TurboDocxException; import com.turbodocx.WebhookSignatureVerifier; ``` + + + +```bash +gem install turbodocx-sdk +``` + +Or add it to your `Gemfile`: + +```ruby +gem "turbodocx-sdk" +``` + +```bash +bundle install +``` + @@ -258,6 +300,12 @@ 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 — non-admin keys return HTTP 403) + @@ -370,6 +418,22 @@ 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 — 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`. + @@ -455,6 +519,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 +680,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 +920,39 @@ 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 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. +::: + @@ -904,6 +1045,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 — 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. + +::: + @@ -1189,6 +1345,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 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`. +::: + @@ -1657,6 +1854,50 @@ 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 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 | + @@ -1687,6 +1928,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-level; call `TurboDocxSdk::TurboWebhooks.configure` once, then invoke any method directly on the class. + @@ -1841,6 +2087,32 @@ 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 `{ "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. | + @@ -1912,6 +2184,21 @@ JsonObject webhook = webhooks.getWebhook(); // webhook.get("availableEvents") ``` + + + +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"] +``` + @@ -2027,6 +2314,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 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. +::: + @@ -2071,6 +2378,13 @@ _, err := wh.DeleteWebhook(ctx) JsonObject deleted = webhooks.deleteWebhook(); ``` + + + +```ruby +TurboDocxSdk::TurboWebhooks.delete_webhook +``` + @@ -2171,6 +2485,21 @@ 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: "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 +``` + @@ -2227,6 +2556,18 @@ $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 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" => "..." } +) +``` + @@ -2281,6 +2622,15 @@ String newSecret = rotated.get("secret").getAsString(); // rotated.get("regeneratedAt") ``` + + + +```ruby +rotated = TurboDocxSdk::TurboWebhooks.regenerate_webhook_secret +# rotated["secret"] +# rotated["regeneratedAt"] +``` + @@ -2375,6 +2725,23 @@ JsonObject page = webhooks.listWebhookDeliveries( // page.get("totalRecords") ``` + + + +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"] +``` + @@ -2424,6 +2791,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 +2866,18 @@ JsonObject stats = webhooks.getWebhookStats(30); // stats.get("eventBreakdown") (per-event totals) ``` + + + +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) +``` + @@ -2583,6 +2970,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` — 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. + @@ -2928,6 +3332,93 @@ 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 — 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" +``` + + + + +```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 +``` + + + + @@ -3119,6 +3610,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 — 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. + @@ -3184,6 +3703,18 @@ try { | 409 | `TurboDocxException.ConflictException` | Creating when the signature webhook already exists | | 429 | `TurboDocxException.RateLimitException` | Rate limit exceeded — back off | + + + +| 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 | + @@ -3226,6 +3757,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. + @@ -3299,6 +3837,19 @@ 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 `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. + @@ -3357,5 +3908,16 @@ It exercises every CRUD step plus every error branch (400 / 401 / 403 / 404 / 40 - [SDKs Overview](/docs/SDKs) — all SDKs across all six 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 +- [TurboSign Ruby SDK](/docs/SDKs/turbosign?language=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/TurboSign/Webhooks.md b/docs/TurboSign/Webhooks.md index b04334b2..c99d9710 100644 --- a/docs/TurboSign/Webhooks.md +++ b/docs/TurboSign/Webhooks.md @@ -37,7 +37,7 @@ keywords: Webhooks enable your application to receive real-time notifications when important events occur in TurboSign. Instead of polling for changes, webhooks push event data to your specified endpoints immediately across the whole signature lifecycle — when a document is sent, viewed, signed by each recipient, completed, voided, or fails to finalize. -![Get It Signed button on TurboDocx homepage](/img/webhooks/webhook-schema.png) +![Diagram comparing webhooks, which push new data to your app, with APIs, which your app has to ask for data](/img/webhooks/webhook-schema.png) ## Overview @@ -46,7 +46,7 @@ TurboSign webhooks provide a robust and secure way to integrate document signatu ### Key Features - **Real-time Notifications**: Receive instant updates when documents are signed or voided -- **Multiple URLs**: Configure up to 5 webhook URLs per configuration +- **Multiple URLs**: Configure up to 10 webhook URLs per configuration - **Secure Authentication**: HMAC-SHA256 signature verification ensures webhook authenticity - **Reliable Delivery**: Automatic retry logic with up to 3 attempts per webhook - **Delivery History**: Track and replay webhook deliveries with detailed logs @@ -68,21 +68,21 @@ Webhooks can be configured from the E-Signature settings on the **Features and i ![Settings menu with Features and integrations highlighted](/img/webhooks/features-and-integrations-link.png) -3. **Open E-Signature Settings and go to the Webhooks tab** +3. **Open E-Signature Settings and select Webhooks** - Scroll down to the **Core Features** section, find the **Signatures** card, and click **"Configure E-Signature"** ![Features and integrations page with the Configure E-Signature button on the Signatures card highlighted](/img/webhooks/core-features-section.png) - - In the **E-Signature Settings** dialog that opens, select the **"Webhooks"** tab + - In the **E-Signature Settings** dialog that opens, select **Webhooks** in the left menu 4. **Add Webhook URLs** - Enter your webhook endpoint URL(s) - - You can add up to 5 different URLs + - You can add up to 10 different URLs - Each URL will receive all subscribed events - - URLs must use HTTPS for production environments + - Every URL must use HTTPS (plain `http://` is rejected, even for testing). To receive webhooks on your machine, expose your local server through an HTTPS tunnel such as ngrok or Cloudflare Tunnel -![E-Signature Settings Webhooks tab showing the Webhook URLs field and the Add Another URL button](/img/webhooks/signature-webhook-config.png) +![E-Signature Settings Webhooks section showing the Webhook URLs field and the Add Another URL button](/img/webhooks/signature-webhook-config.png) 5. **Select Events to Subscribe** - Choose which events should trigger webhooks: @@ -101,7 +101,7 @@ Webhooks can be configured from the E-Signature settings on the **Features and i - Your webhook secret key will be displayed (only shown once for new configurations) - **Important**: Copy and securely store your webhook secret - it won't be shown again -![E-Signature Settings Webhooks tab with the Save Configuration button highlighted](/img/webhooks/dill-signature-webhook-config.png) +![E-Signature Settings Webhooks section with the Save Configuration button highlighted](/img/webhooks/dill-signature-webhook-config.png) ![Webhook secret key displayed after saving the configuration](/img/webhooks/copy-webhook-secret.png) ### Managing Webhook Configuration @@ -113,10 +113,10 @@ The Delivery History tab shows all webhook delivery attempts with detailed infor - **Event Type**: The type of event that triggered the webhook - **HTTP Status**: Response status code from your endpoint - **Attempts**: Number of delivery attempts made -- **Timestamps**: When the webhook was created and last updated -- **Actions**: View details or replay failed deliveries +- **Timestamps**: When each delivery was created and last updated +- **Actions**: View details or replay any delivery -![Get It Signed button on TurboDocx homepage](/img/webhooks/delivery-history.png) +![Delivery History tab listing a webhook delivery with its event type, HTTP status, attempts, and the view and replay actions highlighted](/img/webhooks/delivery-history.png) #### Webhook Secret Management @@ -127,7 +127,7 @@ Your webhook secret is used to verify that webhooks are genuinely from TurboDocx - **Regeneration**: You can regenerate the secret at any time if compromised - **Display**: After initial viewing, only a masked version (first 3 + \*\*\* + last 3 characters) is shown -![Get It Signed button on TurboDocx homepage](/img/webhooks/regenerate-secret.png) +![Webhook Secret Key field showing the masked secret, with the regenerate button highlighted](/img/webhooks/regenerate-secret.png) ## Webhook Events @@ -394,7 +394,7 @@ Each webhook request includes these headers: | `X-TurboDocx-Signature` | HMAC signature for verification (format: `sha256=`) | | `X-TurboDocx-Timestamp` | Unix timestamp when the webhook was sent | | `X-TurboDocx-Event` | The event type that triggered this webhook | -| `X-TurboDocx-Delivery-Id` | Unique ID for this delivery attempt (for idempotency) | +| `X-TurboDocx-Delivery-Id` | Unique ID for this delivery attempt. A retry gets a new one, so dedupe on the payload's `event_id` instead. It is not the delivery `id` shown in Delivery History or returned by the API (use that `id` to replay) | ### Try it Now @@ -406,7 +406,7 @@ Each webhook request includes these headers: ### SDK Verification Examples -Our SDKs include built-in webhook verification. The snippets below show the minimum receiver code; for the full SDK reference (createWebhook, updateWebhook, testWebhook, listWebhookDeliveries, replayWebhookDelivery, rotateSecret, framework integration patterns, and gotchas), see the dedicated TurboWebhooks SDK pages: +Our SDKs include built-in webhook verification. The snippets below show the minimum receiver code; for the full SDK reference (createWebhook, updateWebhook, testWebhook, listWebhookDeliveries, replayWebhookDelivery, regenerateWebhookSecret, framework integration patterns, and gotchas), see the dedicated TurboWebhooks SDK pages: - [TurboWebhooks JavaScript / TypeScript SDK](/docs/SDKs/webhooks?language=js) - [TurboWebhooks Python SDK](/docs/SDKs/webhooks?language=python) @@ -423,25 +423,22 @@ import TabItem from '@theme/TabItem'; ```typescript -import { verifyWebhookSignature } from '@turbodocx/sdk'; import express from 'express'; +import { verifyWebhookSignature } from '@turbodocx/sdk'; const app = express(); +// Use express.raw: the signature is computed over the raw bytes. app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { - const isValid = verifyWebhookSignature({ - signature: req.headers['x-turbodocx-signature'], - timestamp: req.headers['x-turbodocx-timestamp'], - body: req.body, - secret: process.env.TURBODOCX_WEBHOOK_SECRET - }); - - if (!isValid) { + const signature = req.header('x-turbodocx-signature') ?? ''; + const timestamp = req.header('x-turbodocx-timestamp') ?? ''; + + if (!verifyWebhookSignature(req.body, signature, timestamp, process.env.TURBODOCX_WEBHOOK_SECRET!)) { return res.status(401).json({ error: 'Invalid signature' }); } - const event = JSON.parse(req.body.toString()); - // Process event... + const event = JSON.parse(req.body.toString('utf8')); + // Process event.event, event.event_id, event.data... res.status(200).json({ received: true }); }); ``` @@ -450,82 +447,151 @@ app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { ```python -from turbodocx import verify_webhook_signature -from fastapi import FastAPI, Request, HTTPException +import json +import os + +from fastapi import FastAPI, HTTPException, Request +from turbodocx_sdk import verify_webhook_signature app = FastAPI() @app.post("/webhook") async def handle_webhook(request: Request): - body = await request.body() - - is_valid = verify_webhook_signature( - signature=request.headers.get("x-turbodocx-signature"), - timestamp=request.headers.get("x-turbodocx-timestamp"), - body=body, - secret=os.environ["TURBODOCX_WEBHOOK_SECRET"] - ) + raw_body = await request.body() # raw bytes: the signature is computed over them + signature = request.headers.get("x-turbodocx-signature", "") + timestamp = request.headers.get("x-turbodocx-timestamp", "") - if not is_valid: + if not verify_webhook_signature(raw_body, signature, timestamp, os.environ["TURBODOCX_WEBHOOK_SECRET"]): raise HTTPException(status_code=401, detail="Invalid signature") - event = json.loads(body) - # Process event... + event = json.loads(raw_body) + # Process event["event"], event["event_id"], event["data"]... return {"received": True} ``` + + + +```php + ```go -func webhookHandler(w http.ResponseWriter, r *http.Request) { - body, _ := io.ReadAll(r.Body) +package main + +import ( + "encoding/json" + "io" + "log" + "net/http" + "os" - isValid := sdk.VerifyWebhookSignature( - r.Header.Get("X-TurboDocx-Signature"), - r.Header.Get("X-TurboDocx-Timestamp"), - body, - os.Getenv("TURBODOCX_WEBHOOK_SECRET"), - ) + turbodocx "github.com/TurboDocx/SDK/packages/go-sdk" +) + +func webhookHandler(w http.ResponseWriter, r *http.Request) { + rawBody, err := io.ReadAll(r.Body) // raw bytes: the signature is computed over them + if err != nil { + http.Error(w, "read failed", http.StatusBadRequest) + return + } - if !isValid { + signature := r.Header.Get("X-TurboDocx-Signature") + timestamp := r.Header.Get("X-TurboDocx-Timestamp") + if !turbodocx.VerifyWebhookSignature(rawBody, signature, timestamp, os.Getenv("TURBODOCX_WEBHOOK_SECRET"), nil) { http.Error(w, "Invalid signature", http.StatusUnauthorized) return } - var event sdk.WebhookEvent - json.Unmarshal(body, &event) - // Process event... + var event struct { + Event string `json:"event"` + EventID string `json:"event_id"` + Data json.RawMessage `json:"data"` + } + if err := json.Unmarshal(rawBody, &event); err != nil { + http.Error(w, "bad payload", http.StatusBadRequest) + return + } + // Process event.Event, event.EventID, event.Data... w.WriteHeader(http.StatusOK) } + +func main() { + http.HandleFunc("/webhook", webhookHandler) + log.Fatal(http.ListenAndServe(":8080", nil)) +} ``` - + -```csharp -[HttpPost] -public async Task HandleWebhook() -{ - using var reader = new StreamReader(Request.Body); - var body = await reader.ReadToEndAsync(); - - var isValid = WebhookVerifier.VerifySignature( - Request.Headers["X-TurboDocx-Signature"], - Request.Headers["X-TurboDocx-Timestamp"], - body, - _configuration["TurboDocx:WebhookSecret"] - ); - - if (!isValid) - return Unauthorized("Invalid signature"); - - var webhookEvent = JsonSerializer.Deserialize(body); - // Process event... - return Ok(new { Received = true }); +```java +import com.turbodocx.WebhookSignatureVerifier; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +public class WebhookController { + + // Bind the body to byte[]: the signature is computed over the raw bytes. + @PostMapping(value = "/webhook", consumes = "application/json") + public ResponseEntity handleWebhook( + @RequestBody byte[] rawBody, + @RequestHeader("X-TurboDocx-Signature") String signature, + @RequestHeader("X-TurboDocx-Timestamp") String timestamp) { + + if (!WebhookSignatureVerifier.verify(rawBody, signature, timestamp, System.getenv("TURBODOCX_WEBHOOK_SECRET"))) { + return ResponseEntity.status(401).build(); + } + + // Parse rawBody as JSON and process "event", "event_id", "data"... + return ResponseEntity.ok().build(); + } } ``` + + + +```ruby +require "json" +require "sinatra" +require "turbodocx_sdk" + +post "/webhook" do + raw_body = request.body.read # raw bytes: the signature is computed over them + + valid = 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.fetch("TURBODOCX_WEBHOOK_SECRET") + ) + halt 401, "Invalid signature" unless valid + + event = JSON.parse(raw_body) + # Process event["event"], event["event_id"], event["data"]... + status 200 +end +``` + @@ -534,11 +600,11 @@ public async Task HandleWebhook() ### Security Best Practices 1. **Always verify signatures**: Never process webhooks without verifying the signature -2. **Use HTTPS**: Always use HTTPS endpoints in production +2. **Use HTTPS**: Webhook URLs must be HTTPS; TurboDocx rejects `http://` URLs, so use an HTTPS tunnel when testing locally 3. **Store secrets securely**: Keep webhook secrets in environment variables or secure vaults 4. **Implement timestamp validation**: Reject webhooks with timestamps older than 5 minutes 5. **Use timing-safe comparison**: Prevent timing attacks when comparing signatures -6. **Handle retries idempotently**: Use the `X-TurboDocx-Delivery-Id` to prevent duplicate processing +6. **Handle retries idempotently**: Dedupe on the payload's `event_id`, which stays the same across retries (`X-TurboDocx-Delivery-Id` changes on every attempt). A manual replay resends the same payload, so it keeps the same `event_id` too: if you want a replay to be processed again, clear that event's record first 7. **Respond quickly**: Return 200 OK immediately and process webhooks asynchronously 8. **Log failures**: Keep logs of signature verification failures for security monitoring @@ -547,9 +613,9 @@ public async Task HandleWebhook() ### Delivery Behavior - **Timeout**: Each delivery attempt has a 10-second timeout -- **Success Criteria**: Only HTTP 2xx status codes are considered successful -- **Retry Logic**: Failed deliveries are automatically retried up to 3 times -- **Retry Schedule**: Exponential backoff between retry attempts +- **Success Criteria**: Only HTTP 2xx status codes are considered successful; redirects (3xx) are not followed and count as a failure +- **Retry Logic**: Failed deliveries are automatically retried up to 2 more times (3 attempts in total) +- **Retry Schedule**: Exponential backoff: the 2nd attempt about 1 minute after the 1st, the 3rd about 5 minutes after the 2nd - **Delivery Order**: Webhooks are delivered to all configured URLs in parallel ### Handling Failures @@ -558,7 +624,7 @@ When a webhook delivery fails: 1. **Automatic Retries**: The system will automatically retry failed deliveries 2. **Delivery History**: All attempts are logged in the delivery history -3. **Manual Replay**: You can manually replay failed deliveries from the UI +3. **Manual Replay**: You can manually replay any delivery from the UI 4. **Error Details**: Response status codes and error messages are captured @@ -584,6 +650,8 @@ You can test your webhook configuration before going live: 4. **Check the Delivery History** to verify successful delivery 5. **Verify your endpoint** received and processed the webhook correctly +To send a sample event without signing a document, call `testWebhook` from the [TurboWebhooks SDK](/docs/SDKs#turbowebhooks-sdks). It is delivered, signed and retried like a real event, and appears in the Delivery History. Its payload is sample data, not a real document: the IDs are placeholders, `event_id` starts with `evt_test_`, and a few `data` fields differ from real events, so don't look the document up or rely on exact field names from a test event. If your endpoint fails, the call returns only after all 3 attempts, about 6 minutes. + ### Development Tools For local development, consider using: @@ -627,7 +695,7 @@ For local development, consider using: - Ensure you're using the raw request body (not parsed JSON) - Verify the webhook secret matches exactly -- Check that header names are lowercase in your code +- Check that you read the `X-TurboDocx-Signature` and `X-TurboDocx-Timestamp` headers by name; header names are case-insensitive, so match your framework's lookup (Express lowercases them, Go's `Header.Get` and Spring accept any case) - Confirm timestamp validation isn't too strict #### Timeouts @@ -647,7 +715,7 @@ For local development, consider using: **Solutions**: -- Implement idempotency using `X-TurboDocx-Delivery-Id` +- Implement idempotency using the payload's `event_id` (the same on every retry; `X-TurboDocx-Delivery-Id` is new on each attempt) - Store processed event IDs temporarily - Use database constraints to prevent duplicates @@ -666,8 +734,8 @@ If you encounter issues not covered here: ```json { - "id": "webhook_abc123", - "orgId": "org_xyz789", + "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f", + "orgId": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d", "name": "signature", "urls": [ "https://api.example.com/webhooks/turbosign", @@ -684,30 +752,45 @@ If you encounter issues not covered here: ], "secretExists": true, "maskedSecret": "whs***f6a", - "isActive": true, + "isActive": 1, + "metadata": null, + "createdBy": "3c2b1a0f-9e8d-4c7b-a6f5-e4d3c2b1a0f9", "createdOn": "2024-01-15T09:00:00.000Z", - "updatedOn": "2024-01-15T09:00:00.000Z" + "updatedOn": "2024-01-15T09:00:00.000Z", + "deliveryStats": { + "totalDeliveries": 4, + "successfulDeliveries": 3, + "failedDeliveries": 1, + "pendingRetries": 0 + } } ``` +Fetching a single webhook also returns `availableEvents`, the list of events you can subscribe to. Boolean flags such as `isActive` and the delivery's `isDelivered` come back as `1` / `0`, so check them as truthy values rather than comparing with `true`. + ### Delivery Object ```json { - "id": "delivery_def456", - "webhookId": "webhook_abc123", + "id": "0ebddb5e-7c1a-4f2b-9d3e-5a6b7c8d9e0f", "eventType": "signature.document.completed", "url": "https://api.example.com/webhooks/turbosign", "httpStatus": 200, + "responseBody": "{\"received\":true}", "attemptCount": 1, "maxAttempts": 3, - "isDelivered": true, + "isDelivered": 1, "deliveredAt": "2024-01-15T10:30:05.000Z", + "nextRetryAt": null, + "errorMessage": null, + "status": "delivered", "createdOn": "2024-01-15T10:30:00.000Z", "updatedOn": "2024-01-15T10:30:05.000Z" } ``` +`status` is one of `delivered`, `retrying`, `failed` (all attempts used) or `pending`. + --- diff --git a/docs/TurboSign/embedded-signing/index.md b/docs/TurboSign/embedded-signing/index.md index ee070132..37574bff 100644 --- a/docs/TurboSign/embedded-signing/index.md +++ b/docs/TurboSign/embedded-signing/index.md @@ -103,10 +103,6 @@ Requesting a signing URL needs an **Administrator** or **Contributor** key. A ** You can allow `http://localhost` origins to test on your machine. Remove every `http://` origin before you go live; production origins must be `https://`. ::: -:::note Ruby -The Ruby tabs in these guides show the Ruby SDK API. The `turbodocx-sdk` gem is not on RubyGems yet; until it is, install it from the [TurboDocx SDK repository](https://github.com/TurboDocx/SDK/tree/main/packages/ruby-sdk). -::: - ## The flow, end to end 1. Your backend prepares the document with an embedded recipient and `sendEmail: false`, then stores the returned `documentId` on the customer record. diff --git a/sidebars.js b/sidebars.js index 66c4b52c..7f867d4a 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', + }, ], }, { diff --git a/src/theme/Root.js b/src/theme/Root.js index 54909bd6..78dd2a17 100644 --- a/src/theme/Root.js +++ b/src/theme/Root.js @@ -84,10 +84,10 @@ function scrollToHashTarget(hash) { // like /docs/SDKs/python#get-status points at a section that is no longer on the setup page, so once // the page has rendered and the anchor is still missing, hop to the same anchor on the TurboSign page // with that language's tab selected. Anchors that still exist on the setup page are left alone. -const MOVED_TURBOSIGN = { javascript: 'js', python: 'python', php: 'php', go: 'go', java: 'java' }; +const MOVED_TURBOSIGN = { javascript: 'js', python: 'python', php: 'php', go: 'go', java: 'java', ruby: 'ruby' }; function hopToMovedSection(pathname, hash) { - const m = pathname.match(/^\/docs\/SDKs\/(javascript|python|php|go|java)\/?$/); + const m = pathname.match(/^\/docs\/SDKs\/(javascript|python|php|go|java|ruby)\/?$/); if (!m) return null; let id; try { diff --git a/static/_redirects b/static/_redirects index a073df76..85902aaa 100644 --- a/static/_redirects +++ b/static/_redirects @@ -16,6 +16,9 @@ /docs/SDKs/deliverable-java /docs/SDKs/deliverable?language=java 301 /docs/SDKs/deliverable-java/ /docs/SDKs/deliverable?language=java 301 /docs/SDKs/deliverable-java.html /docs/SDKs/deliverable?language=java 301 +/docs/SDKs/deliverable-ruby /docs/SDKs/deliverable?language=ruby 301 +/docs/SDKs/deliverable-ruby/ /docs/SDKs/deliverable?language=ruby 301 +/docs/SDKs/deliverable-ruby.html /docs/SDKs/deliverable?language=ruby 301 /docs/SDKs/quote-javascript /docs/SDKs/quote?language=js 301 /docs/SDKs/quote-javascript/ /docs/SDKs/quote?language=js 301 /docs/SDKs/quote-javascript.html /docs/SDKs/quote?language=js 301 @@ -31,6 +34,9 @@ /docs/SDKs/quote-java /docs/SDKs/quote?language=java 301 /docs/SDKs/quote-java/ /docs/SDKs/quote?language=java 301 /docs/SDKs/quote-java.html /docs/SDKs/quote?language=java 301 +/docs/SDKs/quote-ruby /docs/SDKs/quote?language=ruby 301 +/docs/SDKs/quote-ruby/ /docs/SDKs/quote?language=ruby 301 +/docs/SDKs/quote-ruby.html /docs/SDKs/quote?language=ruby 301 /docs/SDKs/webhooks-javascript /docs/SDKs/webhooks?language=js 301 /docs/SDKs/webhooks-javascript/ /docs/SDKs/webhooks?language=js 301 /docs/SDKs/webhooks-javascript.html /docs/SDKs/webhooks?language=js 301 @@ -46,6 +52,9 @@ /docs/SDKs/webhooks-java /docs/SDKs/webhooks?language=java 301 /docs/SDKs/webhooks-java/ /docs/SDKs/webhooks?language=java 301 /docs/SDKs/webhooks-java.html /docs/SDKs/webhooks?language=java 301 +/docs/SDKs/webhooks-ruby /docs/SDKs/webhooks?language=ruby 301 +/docs/SDKs/webhooks-ruby/ /docs/SDKs/webhooks?language=ruby 301 +/docs/SDKs/webhooks-ruby.html /docs/SDKs/webhooks?language=ruby 301 # The TurboSign for Salesforce overview was merged into the Salesforce Integration page (CR #231). # Only the overview moved: the TurboSign for Salesforce guides below it keep their /turbosign-for-salesforce/... URLs, so these are exact matches, not a splat.