Skip to main content

TurboQuote Python 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 Python 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 async Python 3.9+. Distributed on PyPI as turbodocx-sdk (same package 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

pip install turbodocx-sdk

Requirements

  • Python 3.9 or higher
  • httpx (installed automatically as a dependency)
  • A TurboDocx API key (TDX- prefix) — generate one in Settings → API Keys
  • All SDK methods are async — call them from an async def (or wrap with asyncio.run(...) in synchronous contexts)

Configuration

import os
from turbodocx_sdk import TurboQuote

TurboQuote.configure(
api_key=os.environ["TURBODOCX_API_KEY"],
org_id=os.environ["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.

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

import asyncio
import os
from turbodocx_sdk import TurboQuote

TurboQuote.configure(
api_key=os.environ["TURBODOCX_API_KEY"],
org_id=os.environ["TURBODOCX_ORG_ID"],
)


async def full_quote_lifecycle():
# 1. Create a draft quote
quote = await 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"]
print(f"Created quote {quote_id}")

# 2. Add product line items
items = await 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",
},
])
print(f"Added {len(items)} line items")

# 3. Apply a price book (optional)
result = await TurboQuote.apply_price_book(quote_id, "pricebook-uuid")
print(f"Price book applied: {result['updatedCount']} items updated")

# 4. Send the quote
sent = await TurboQuote.send_quote(quote_id, {
"ccEmails": ["manager@acme.com"],
"validUntil": "2026-09-30",
})
print(f"Sent: {sent['message']}")

# 5. Download the PDF
pdf_bytes = await TurboQuote.download_quote_pdf(quote_id)
with open("acme-quote.pdf", "wb") as f:
f.write(pdf_bytes)
print(f"PDF saved ({len(pdf_bytes)} bytes)")


asyncio.run(full_quote_lifecycle())

Convenience: create, add items, and send in one call

result = await 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",
},
})
print(f"Quote sent: {result['quote']['id']}")

Method Reference

All methods are @classmethods on TurboQuote; configure once, then call on the class.

Quotes

list_quotes

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

create_quote

quote = await 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 dict

get_quote

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

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

update_quote

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

updated = await TurboQuote.update_quote("quote-uuid", {
"name": "Q3 Proposal — Revised",
"validUntil": "2026-10-15",
"taxRate": None, # clears the field
})

delete_quote

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

duplicate_quote

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

download_quote_pdf

Returns raw PDF bytes.

pdf_bytes = await TurboQuote.download_quote_pdf("quote-uuid")
with open("quote.pdf", "wb") as f:
f.write(pdf_bytes)

Quote Status Transitions

send_quote

sent = await 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 = await TurboQuote.send_quote_with_deliverable("quote-uuid", {
"deliverableId": "deliverable-uuid",
"ccEmails": ["manager@example.com"],
})
# result["quote"], result["message"], result["documentId"]

decline_quote

quote = await TurboQuote.decline_quote("quote-uuid", {
"reason": "Customer selected a competitor",
})

void_quote

quote = await TurboQuote.void_quote("quote-uuid", {
"reason": "Terms expired before acceptance",
})

handle_expired_quote

quote = await 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 = await TurboQuote.apply_price_book("quote-uuid", "pricebook-uuid")
# result["quote"] — updated Quote
# result["updatedCount"] — int, items that were re-priced
# result["skippedCount"] — int, items that had no pricebook match
# result["message"]

remove_price_book

quote = await TurboQuote.remove_price_book("quote-uuid")

Line Items

list_line_items

page = await TurboQuote.list_line_items("quote-uuid", {"limit": 50})
# page["results"], page["totalRecords"]

add_line_items

Accepts a single item dict or a list. Returns a list of created LineItem dicts.

items = await 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 = await TurboQuote.add_bundle_line_items("quote-uuid", [
{
"bundleId": "bundle-uuid",
"quantity": 1,
}
])

update_line_item

item = await TurboQuote.update_line_item("quote-uuid", "item-uuid", {
"quantity": 5,
"unitPrice": "179.00",
})

remove_line_item

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

Products

MethodSignatureReturns
list_products(options?)paginated list
create_product(request)Product
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 | None}
# Create a product with images (multipart upload auto-detected)
product = await 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 bytes
})

# List products with filters
page = await TurboQuote.list_products({
"limit": 20,
"query": "enterprise",
"showInCatalog": True,
})

# Bulk fetch primary images for a set of product IDs
images = await TurboQuote.get_product_primary_images(["id-1", "id-2", "id-3"])
# images["id-1"] → ProductImage dict, or None

Bundles

MethodSignatureReturns
list_bundles(options?)paginated list
create_bundle(request)Bundle
get_bundle(id)Bundle
update_bundle(id, request)Bundle
delete_bundle(id){"message": ...}
duplicate_bundle(id)Bundle
bundle = await TurboQuote.create_bundle({
"name": "Starter Pack",
"items": [
{"productId": "product-uuid-1", "quantity": 1},
{"productId": "product-uuid-2", "quantity": 2},
],
"discountPercent": "5.0",
})

Price Books

MethodSignatureReturns
list_price_books(options?)paginated list
create_price_book(request)PriceBook
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?)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 = await 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 = await TurboQuote.list_price_book_products("pricebook-uuid", {"limit": 50})

Companies

MethodSignatureReturns
list_companies(options?)paginated list
create_company(request)Company
get_company(id)Company
update_company(id, request)Company
delete_company(id){"message": ...}
list_company_contacts(company_id, options?)paginated list
company = await 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 = await TurboQuote.list_company_contacts(company["id"])

Contacts

MethodSignatureReturns
list_contacts(options?)paginated list
create_contact(request)Contact
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 = await TurboQuote.create_contact({
"firstName": "John",
"lastName": "Smith",
"email": "john@acme.com",
"companyId": "company-uuid",
})

Quote Templates

MethodSignatureReturns
list_templates(options?)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 = await TurboQuote.get_template()

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

Types / Categories

MethodSignatureReturns
list_types(options?)paginated list
create_type(request)QuoteType
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 = await TurboQuote.create_type({"name": "New Business"})

await TurboQuote.update_type(quote_type["id"], {"name": "New Logo Business"})

Convenience

create_and_send

Orchestrates multiple API calls: create quote → add product items → add bundle items → send. Returns {"quote": <sent Quote>}.

result = await 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",
},
})
print(result["quote"]["id"])

Error Handling

from turbodocx_sdk import (
TurboDocxError,
AuthenticationError,
AuthorizationError,
ValidationError,
NotFoundError,
ConflictError,
RateLimitError,
NetworkError,
)

try:
quote = await TurboQuote.create_quote({
"name": "Q3 Proposal",
"companyId": "company-uuid",
"contactId": "contact-uuid",
})
except ValidationError as e:
# 400 — missing required fields, invalid enum value, etc.
print(f"Validation failed: {e}")
except AuthenticationError:
# 401 — bad or revoked API key
pass
except AuthorizationError:
# 403 — key valid but lacks permission for this org/action
pass
except NotFoundError as e:
# 404 — company, contact, or product not found
print(f"Not found: {e}")
except RateLimitError:
# 429 — back off and retry
pass
except NetworkError:
# request never reached the server (DNS, refused, timeout)
pass
except TurboDocxError as e:
# catch-all for any other typed SDK error (raw 5xx, etc.)
print(f"Error {getattr(e, 'status_code', '?')}: {e}")

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