Skip to main content

TurboQuote Ruby SDK

Agent Skill

Let an agent scaffold this for you

Install the TurboDocx Quickstart Skill and let Claude Code, Cursor, Copilot, Codex, or any agent that speaks the Agent Skills standard install the SDK, wire it into your app, and write a working TurboQuote integration end-to-end.

bash — turbodocx
$npx skills add TurboDocx/quickstart
# then, inside your agent:/turbodocx-sdk turboquote

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).


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. Quotes can be sent with an attached Deliverable document for a branded proposal experience.

Installation

gem install turbodocx-sdk

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

require "turbodocx_sdk"

TurboDocxSdk::TurboQuote.configure(
api_key: ENV["TURBODOCX_API_KEY"],
org_id: ENV["TURBODOCX_ORG_ID"] # optional — auto-reads env var
)
No sender email required

Unlike TurboSign, TurboQuote.configure does not require 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 itself from TURBODOCX_API_KEY and TURBODOCX_ORG_ID on first method invocation.

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

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

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

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 Hashes with string keys.

Quotes

list_quotes

page = TurboDocxSdk::TurboQuote.list_quotes(
"limit" => 20,
"offset" => 0,
"query" => "acme",
"statuses" => ["draft", "sent"] # repeated-key filter
)
# page["results"], page["totalRecords"], page["stats"]

create_quote

quote = TurboDocxSdk::TurboQuote.create_quote(
"name" => "Q3 Proposal",
"companyId" => "company-uuid", # required
"contactId" => "contact-uuid", # required
"currency" => "USD",
"validUntil" => "2026-09-30",
"notes" => "Includes implementation services",
"taxRate" => "8.5"
)
# returns Quote hash

get_quote

Returns the quote with statusInfo merged in when the quote is in a terminal state.

quote = TurboDocxSdk::TurboQuote.get_quote("quote-uuid")
# quote["status"], quote["grandTotal"], quote["statusInfo"]

update_quote

All fields are optional — pass only what changes. Send an explicit nil to clear a nullable field.

updated = TurboDocxSdk::TurboQuote.update_quote("quote-uuid",
"name" => "Q3 Proposal — Revised",
"validUntil" => "2026-10-15",
"taxRate" => nil # clears the field
)

delete_quote

result = TurboDocxSdk::TurboQuote.delete_quote("quote-uuid")
# result["message"]

duplicate_quote

new_quote = TurboDocxSdk::TurboQuote.duplicate_quote("quote-uuid")
# returns new Quote in draft status

download_quote_pdf

Returns raw PDF bytes.

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.

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.

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:

FieldTypeAllowed / rangeDefault
prefixstring≤ 12 characters"Q"
yearTokenenumnone | two | fourfour
monthTokenenumoff | twooff
separatorstring≤ 4 characters"-"
padWidthinteger012 (0 = no padding)5
suffixstring≤ 12 characters""
startNumberinteger010000000001
resetCadenceenumnever | yearly | monthlyyearly

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

send_quote

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.

result = TurboDocxSdk::TurboQuote.send_quote_with_deliverable("quote-uuid",
"deliverableId" => "deliverable-uuid",
"ccEmails" => ["manager@example.com"]
)
# result["quote"], result["message"], result["documentId"]

decline_quote

quote = TurboDocxSdk::TurboQuote.decline_quote("quote-uuid",
"reason" => "Customer selected a competitor"
)

void_quote

quote = TurboDocxSdk::TurboQuote.void_quote("quote-uuid",
"reason" => "Terms expired before acceptance"
)

handle_expired_quote

quote = TurboDocxSdk::TurboQuote.handle_expired_quote("quote-uuid",
"action" => "void", # "void" or "decline"
"reason" => "Quote expired",
"newValidUntil" => "2026-12-31" # optional — extend before re-sending
)

Price Books (on a Quote)

apply_price_book

Apply a price book to an existing quote; updates matching line item prices.

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

quote = TurboDocxSdk::TurboQuote.remove_price_book("quote-uuid")

Line Items

list_line_items

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. Returns an array of created line item hashes.

items = TurboDocxSdk::TurboQuote.add_line_items("quote-uuid", [
{
"productId" => "product-uuid",
"productName" => "Platform License",
"quantity" => 2,
"unitPrice" => "199.00",
"billingFrequency" => "annual",
"discountPercent" => "10.0"
}
])

add_bundle_line_items

items = TurboDocxSdk::TurboQuote.add_bundle_line_items("quote-uuid", [
{
"bundleId" => "bundle-uuid",
"quantity" => 1
}
])

update_line_item

item = TurboDocxSdk::TurboQuote.update_line_item("quote-uuid", "item-uuid",
"quantity" => 5,
"unitPrice" => "179.00"
)

remove_line_item

result = TurboDocxSdk::TurboQuote.remove_line_item("quote-uuid", "item-uuid")
# result["message"]

Products

MethodSignatureReturns
list_products(options = nil)paginated list
create_product(request)Product
bulk_create_products(rows)bulk report
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}
# Create a product with images (multipart upload auto-detected)
product = TurboDocxSdk::TurboQuote.create_product(
"name" => "Enterprise License",
"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

MethodSignatureReturns
list_bundles(options = nil)paginated list
create_bundle(request)Bundle
bulk_create_bundles(rows)bulk report
get_bundle(id)Bundle
update_bundle(id, request)Bundle
delete_bundle(id){"message" => ...}
duplicate_bundle(id)Bundle
bundle = TurboDocxSdk::TurboQuote.create_bundle(
"name" => "Starter Pack",
"categoryId" => "bundle-category-uuid", # required
"items" => [
{ "productId" => "product-uuid-1", "quantity" => 1 },
{ "productId" => "product-uuid-2", "quantity" => 2 }
],
"bundleDiscountType" => "percent",
"bundleDiscountAmount" => 5
)

Price Books

MethodSignatureReturns
list_price_books(options = nil)paginated list
create_price_book(request)PriceBook
bulk_create_price_books(rows)bulk report
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
# 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

MethodSignatureReturns
list_companies(options = nil)paginated list
create_company(request)Company
bulk_create_companies(rows)bulk report
get_company(id)Company
update_company(id, request)Company
delete_company(id){"message" => ...}
list_company_contacts(company_id, options = nil)paginated list
company = TurboDocxSdk::TurboQuote.create_company(
"name" => "Acme Corporation",
"domain" => "acme.com",
"contacts" => [ # at least one contact required
{
"firstName" => "Jane",
"lastName" => "Doe",
"email" => "jane@acme.com"
}
]
)

# List contacts for a company
contacts = TurboDocxSdk::TurboQuote.list_company_contacts(company["id"])

Contacts

MethodSignatureReturns
list_contacts(options = nil)paginated list
create_contact(request)Contact
bulk_create_contacts(rows)bulk report
update_contact(id, request)Contact
delete_contact(id){"message" => ...}
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.

contact = TurboDocxSdk::TurboQuote.create_contact(
"firstName" => "John",
"lastName" => "Smith",
"email" => "john@acme.com",
"companyId" => "company-uuid"
)

Quote Templates

MethodSignatureReturns
list_templates(options = nil)paginated list
get_template()singleton QuoteTemplate
get_template_by_id(id)QuoteTemplate
create_template(request)QuoteTemplate
update_template(id, request)QuoteTemplate
delete_template(id){"message" => ...}
# Get the org's default quote template (singleton endpoint)
default_template = TurboDocxSdk::TurboQuote.get_template

# Get a specific template
template = TurboDocxSdk::TurboQuote.get_template_by_id("template-uuid")

Types / Categories

MethodSignatureReturns
list_types(options = nil)paginated list
create_type(request)QuoteType
bulk_create_types(rows)bulk report
update_type(id, request)QuoteType
delete_type(id){"message" => ...}
No get_type by design

The backend has no GET /v1/types/:id endpoint. Use list_types to retrieve individual type records.

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:

MethodEndpointRow shape
bulk_create_productsPOST /v1/products/bulksame fields as create_product
bulk_create_price_booksPOST /v1/pricebooks/bulksame fields as create_price_book
bulk_create_bundlesPOST /v1/bundles/bulksame fields as create_bundle
bulk_create_companiesPOST /v1/companies/bulksame fields as create_company — each row requires a contacts array with at least one contact
bulk_create_contactsPOST /v1/contacts/bulksame fields as create_contact — each row requires a companyId
bulk_create_typesPOST /v1/types/bulksame 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).

report = TurboDocxSdk::TurboQuote.bulk_create_products([
{ "name" => "Starter License", "listPrice" => "99.00", "billingFrequency" => "monthly" },
{ "name" => "Pro License", "listPrice" => "299.00", "billingFrequency" => "monthly" },
{ "name" => "Enterprise License", "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:

KeyTypeMeaning
importedIntegerRows created successfully
failedArray of {"row", "reason"}Rows the server rejected — row is the 1-indexed position in your input array
adjustedArray 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.
# Companies need >= 1 contact per row; contacts need a companyId per row
TurboDocxSdk::TurboQuote.bulk_create_companies([
{
"name" => "Acme Corporation",
"contacts" => [{ "firstName" => "Jane", "lastName" => "Doe", "email" => "jane@acme.com" }]
}
])

TurboDocxSdk::TurboQuote.bulk_create_contacts([
{ "firstName" => "John", "lastName" => "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" => <sent Quote> }.

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:

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

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

StatusClassWhen
400ValidationErrorInvalid fields, missing required keys, bad enum value
401AuthenticationErrorMissing or invalid API key
403AuthorizationErrorValid key without permission for this operation
404NotFoundErrorQuote, product, company, or other resource not found
409ConflictErrorDuplicate resource (e.g., company domain conflict)
429RateLimitErrorRate limit exceeded — back off

See Also