Skip to main content

TurboQuote Java 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 Java applications. Create and send sales quotes, manage line items, products, bundles, and price books — all from Java 11+. Distributed as com.turbodocx:turbodocx-sdk on Maven Central (same artifact as TurboSign and TurboWebhooks).


What is TurboQuote?

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. The client config takes no senderEmail; the quote's "Prepared by" sender comes from your org quote template (see the note below createQuote).

Installation

<dependency>
<groupId>com.turbodocx</groupId>
<artifactId>turbodocx-sdk</artifactId>
<version>0.5.0</version>
</dependency>

Then import:

import com.turbodocx.TurboQuoteClient;
import com.turbodocx.TurboQuote;
import com.turbodocx.TurboDocxException;
import com.turbodocx.models.quote.*;

Requirements

  • Java 11 or higher
  • OkHttp 4.x (included transitively)
  • Gson 2.x (included transitively)
  • A TurboDocx API key (TDX- prefix) — no administrator role required

Configuration

import com.turbodocx.TurboQuoteClient;
import com.turbodocx.TurboQuote;

TurboQuoteClient client = new TurboQuoteClient.Builder()
.apiKey(System.getenv("TURBODOCX_API_KEY"))
.orgId(System.getenv("TURBODOCX_ORG_ID"))
.build();

TurboQuote tq = client.turboQuote();

TurboQuoteClient.Builder does not take senderEmail — a quote has no per-request sender field. Sender validation is not skipped, though: it moves to your org quote template (see below). orgId is required. Construct TurboQuoteClient once and reuse tq.

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.

Environment Variables

TURBODOCX_API_KEY=your_api_key
TURBODOCX_ORG_ID=your_org_id
# optional — defaults to https://api.turbodocx.com
TURBODOCX_BASE_URL=https://api.turbodocx.com
API Credentials Required

Both apiKey and orgId are required. To get your credentials, follow the Get Your Credentials steps from the SDKs main page.

Quick Start

1. Create a company, quote, and add line items

import com.turbodocx.TurboQuoteClient;
import com.turbodocx.TurboQuote;
import com.turbodocx.TurboDocxException;
import com.turbodocx.models.quote.*;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;

public class QuoteLifecycle {
public static void main(String[] args) throws Exception {
TurboQuoteClient client = new TurboQuoteClient.Builder()
.apiKey(System.getenv("TURBODOCX_API_KEY"))
.orgId(System.getenv("TURBODOCX_ORG_ID"))
.build();

TurboQuote tq = client.turboQuote();

// Step 1: Create a company with an initial contact
CreateCompanyContactInput contact = new CreateCompanyContactInput();
contact.setName("Alice Buyer");
contact.setEmail("alice@example.com");

CreateCompanyRequest companyReq = new CreateCompanyRequest();
companyReq.setName("Acme Corp");
companyReq.setContacts(Arrays.asList(contact));

Company company = tq.createCompany(companyReq);
ContactListResponse contacts = tq.listCompanyContacts(company.getId());
String contactId = contacts.getResults().get(0).getId();

// Step 2: Create a quote
CreateQuoteRequest quoteReq = new CreateQuoteRequest();
quoteReq.setName("Q1 Software License");
quoteReq.setCompanyId(company.getId());
quoteReq.setContactId(contactId);
quoteReq.setTermDays(30);
quoteReq.setCurrency(Currency.USD);

Quote quote = tq.createQuote(quoteReq);
System.out.println("Quote: " + quote.getId() + " status=" + quote.getStatus());

// Step 3: Add a line item.
// productId, productName, unitPrice and billingFrequency are all required.
// productId may be null (custom line item), but must always be set.
AddLineItemRequest item = new AddLineItemRequest();
item.setProductId("product-uuid");
item.setProductName("Enterprise Software License");
item.setUnitPrice(1200.00);
item.setQuantity(3.0);
item.setBillingFrequency("annual");
item.setDiscountType(DiscountType.PERCENT);
item.setDiscountPercent(10.0);

List<LineItem> lineItems = tq.addLineItems(quote.getId(), item);
System.out.println("Added " + lineItems.size() + " line item(s)");

// Step 4: Send the quote
SendQuoteResponse sent = tq.sendQuote(quote.getId());
System.out.println("Sent. Status: " + sent.getQuote().getStatus());

// Step 5: Download the PDF
byte[] pdf = tq.downloadQuotePdf(quote.getId());
Files.write(Paths.get("quote.pdf"), pdf);
System.out.println("PDF saved (" + pdf.length + " bytes)");
}
}

2. Create and send in one call

createAndSend is a convenience method that creates the quote, adds line items and bundle items, and sends it atomically.

CreateAndSendRequest req = new CreateAndSendRequest();
req.setName("Partner Proposal");
req.setCompanyId(companyId);
req.setContactId(contactId);

AddLineItemRequest item = new AddLineItemRequest();
item.setProductId("product-uuid"); // required — null for a custom line item
item.setProductName("Starter Plan");
item.setUnitPrice(499.00);
item.setQuantity(1.0);
item.setBillingFrequency("monthly"); // required
req.setItems(Arrays.asList(item));

// req.setSend(...) to configure send options, or omit to use defaults

CreateAndSendResponse result = tq.createAndSend(req);
System.out.println("Quote created and sent: " + result.getQuote().getId());

3. Apply a price book, then send with a deliverable

// Apply a price book to recalculate line item prices
ApplyPriceBookResponse applied = tq.applyPriceBook(quoteId, priceBookId);
System.out.println("Updated: " + applied.getUpdatedCount()
+ ", Skipped: " + applied.getSkippedCount());

// Send with a TurboDocx deliverable attached
SendQuoteWithDeliverableRequest sendReq = new SendQuoteWithDeliverableRequest();
sendReq.setDeliverableId("your-deliverable-id");
sendReq.setMergePosition("end");

SendQuoteWithDeliverableResponse sendResp = tq.sendQuoteWithDeliverable(quoteId, sendReq);
System.out.println("Document ID: " + sendResp.getDocumentId());

Method Reference

All methods are instance methods on com.turbodocx.TurboQuote. Obtain the instance via client.turboQuote() from a constructed TurboQuoteClient. All methods throw IOException and TurboDocxException subclasses.


Quotes — CRUD

listQuotes

QuoteListResponse listQuotes()
QuoteListResponse listQuotes(ListQuotesOptions options)

List quotes with optional pagination and filters. Returns a paginated response including stats (totals, counts by status).

ListQuotesOptions opts = new ListQuotesOptions();
opts.setLimit(20);
opts.setOffset(0);

QuoteListResponse list = tq.listQuotes(opts);
System.out.println("Total: " + list.getTotalRecords());
list.getResults().forEach(q ->
System.out.println(q.getId() + " " + q.getStatus()));

createQuote

Quote createQuote(CreateQuoteRequest request)

Create a new quote. Returns the created Quote.

// Fixed-term quote — termDays is -1 or 0–3650; leave it unset to get the default of 60.
CreateQuoteRequest req = new CreateQuoteRequest();
req.setName("Enterprise Proposal");
req.setCompanyId(companyId);
req.setContactId(contactId);
req.setCurrency(Currency.USD);
req.setTermDays(30); // fixed term — do NOT set renewalPeriod alongside this

Quote quote = tq.createQuote(req);

// Auto-renewal quote — termDays -1 REQUIRES renewalPeriod.
CreateQuoteRequest subReq = new CreateQuoteRequest();
subReq.setName("Annual Subscription");
subReq.setCompanyId(companyId);
subReq.setContactId(contactId);
subReq.setCurrency(Currency.USD);
subReq.setTermDays(-1); // -1 = auto-renewal
subReq.setRenewalPeriod(RenewalPeriod.ANNUALLY); // WEEKLY | MONTHLY | QUARTERLY | ANNUALLY

Quote subscription = tq.createQuote(subReq);
termDays and renewalPeriod are coupled

termDays defaults to 60 when left unset. Valid values are -1 (auto-renewal) or 03650 (0 = one-time).

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.

getQuote

Quote getQuote(String id)

Get a quote by ID. Returns the Quote with statusInfo (expiry dates, status transitions) and preparedBy (the resolved "Prepared by" identity shown on the quote PDF) merged in.

Quote quote = tq.getQuote(quoteId);
System.out.println("Status: " + quote.getStatus());
// quote.getStatusInfo() — expiry/transition metadata
if (quote.getPreparedBy() != null) {
System.out.println(quote.getPreparedBy().getName()); // e.g. "Acme Billing Integration"
System.out.println(quote.getPreparedBy().getEmail()); // may be null — render a placeholder
}

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.

updateQuote

Quote updateQuote(String id, UpdateQuoteRequest request)

Update an existing quote. Only fields explicitly set on UpdateQuoteRequest are patched; unset fields are omitted from the request body. Fields explicitly set to null are cleared on the server (e.g., setValidUntil(null) clears the expiry date).

UpdateQuoteRequest req = new UpdateQuoteRequest();
req.setName("Revised Proposal — Q2");
req.setTermDays(60);

Quote updated = tq.updateQuote(quoteId, req);

deleteQuote

SuccessResponse deleteQuote(String id)

Delete a quote.

SuccessResponse resp = tq.deleteQuote(quoteId);
System.out.println(resp.getMessage());

duplicateQuote

Quote duplicateQuote(String id)

Duplicate a quote (creates a draft copy).

Quote copy = tq.duplicateQuote(quoteId);
System.out.println("New quote: " + copy.getId());

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.

applyPriceBook

ApplyPriceBookResponse applyPriceBook(String quoteId, String priceBookId)

Apply a price book to a quote, recalculating line item prices. Returns {quote, message, updatedCount, skippedCount}.

ApplyPriceBookResponse resp = tq.applyPriceBook(quoteId, priceBookId);
System.out.println("Updated " + resp.getUpdatedCount() + " items.");

removePriceBook

Quote removePriceBook(String quoteId)

Remove the applied price book from a quote, restoring original line item pricing.

Quote quote = tq.removePriceBook(quoteId);

downloadQuotePdf

byte[] downloadQuotePdf(String id)

Download the quote as a PDF. Returns raw bytes.

byte[] pdf = tq.downloadQuotePdf(quoteId);
Files.write(Paths.get("quote.pdf"), pdf);

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.

getQuoteNumberConfig

QuoteNumberConfig getQuoteNumberConfig()

Fetch the org's current quote numbering format and the current per-period issued floor.

QuoteNumberConfig config = tq.getQuoteNumberConfig();
System.out.println(config.getFormat().getPrefix()); // e.g. "Q-"
System.out.println(config.getCurrentFloor()); // the current per-period issued floor

updateQuoteNumberConfig

QuoteNumberConfig updateQuoteNumberConfig(QuoteNumberFormat format)

Update the numbering format. All eight fields are sent.

QuoteNumberFormat format = new QuoteNumberFormat();
format.setPrefix("INV");
format.setYearToken(QuoteNumberYearToken.NONE); // NONE | TWO | FOUR
format.setMonthToken(QuoteNumberMonthToken.OFF); // OFF | TWO
format.setSeparator("-");
format.setPadWidth(4); // 0–12
format.setSuffix("");
format.setStartNumber(1000); // >= 0
format.setResetCadence(QuoteNumberResetCadence.NEVER); // NEVER | YEARLY | MONTHLY

QuoteNumberConfig config = tq.updateQuoteNumberConfig(format);
System.out.println(config.getFormat().getStartNumber()); // 1000

Field reference, defaults & validation

All eight QuoteNumberFormat fields 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"-"
padWidthint012 (0 = no padding)5
suffixString≤ 12 characters""
startNumberint010000000001
resetCadenceenumNEVER | YEARLY | MONTHLYYEARLY

Tokens are the QuoteNumberYearToken / QuoteNumberMonthToken / QuoteNumberResetCadence enums (their wire values are the lowercase strings none/two/four, off/two, never/yearly/monthly). 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.


Quotes — Status Transitions

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:

ConditionCode
Quote is not a draftQuoteNotSendable
No validUntil date setQuoteValidUntilRequired
validUntil is in the pastQuoteExpired
No line itemsQuoteHasNoLineItems
Contact missing a name or emailQuoteContactRequired
Company or contact deleted/deactivatedQuoteCustomerInactive
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.

sendQuote

SendQuoteResponse sendQuote(String id)
SendQuoteResponse sendQuote(String id, SendQuoteRequest request)

Send a quote to the contact. Returns {quote, message}. Pass SendQuoteRequest to configure send options, or omit for defaults.

SendQuoteResponse resp = tq.sendQuote(quoteId);
System.out.println("Status: " + resp.getQuote().getStatus());

sendQuoteWithDeliverable

SendQuoteWithDeliverableResponse sendQuoteWithDeliverable(String id, SendQuoteWithDeliverableRequest request)

Send a quote with a TurboDocx deliverable attached. Returns {quote, message, documentId}.

SendQuoteWithDeliverableRequest req = new SendQuoteWithDeliverableRequest();
req.setDeliverableId("your-deliverable-id");
req.setMergePosition("end"); // "start" | "end"

SendQuoteWithDeliverableResponse resp = tq.sendQuoteWithDeliverable(quoteId, req);
System.out.println("Document ID: " + resp.getDocumentId());

declineQuote

Quote declineQuote(String id, DeclineQuoteRequest request)

Mark a sent quote as declined.

DeclineQuoteRequest req = new DeclineQuoteRequest();
req.setReason("Budget constraints");

Quote declined = tq.declineQuote(quoteId, req);

voidQuote

Quote voidQuote(String id, VoidQuoteRequest request)

Void a quote (cannot be undone).

VoidQuoteRequest req = new VoidQuoteRequest();
req.setReason("Replaced by new proposal");

Quote voided = tq.voidQuote(quoteId, req);

handleExpiredQuote

Quote handleExpiredQuote(String id, HandleExpiredQuoteRequest request)

Handle a quote that has passed its validUntil date. The endpoint closes out the original quote — voiding or declining it depending on the action — and then creates a duplicate carrying newValidUntil as its new validity date. The returned Quote is the new duplicate; the original stays terminal.

All three fields are required: action ("void" or "decline"), reason (max 190 characters), and newValidUntil (ISO date).

HandleExpiredQuoteRequest req = new HandleExpiredQuoteRequest();
req.setAction("void"); // required — "void" or "decline" only
req.setReason("Expired — re-quoting"); // required — max 190 characters
req.setNewValidUntil("2026-12-31"); // required — ISO date, carried onto the duplicate

Quote quote = tq.handleExpiredQuote(quoteId, req);
There is no extend or resend action

action accepts only "void" and "decline". "extend" and "resend" do not exist in the API and return a 400. Extending is what the endpoint already does — set newValidUntil and it lands on the duplicate it creates.

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.getStatusInfo() before attempting a transition, and use duplicateQuote when you need to revive a closed-out quote.


Line Items

listLineItems

LineItemListResponse listLineItems(String quoteId)
LineItemListResponse listLineItems(String quoteId, ListLineItemsOptions options)

List line items for a quote.

LineItemListResponse items = tq.listLineItems(quoteId);
items.getResults().forEach(i ->
System.out.println(i.getProductName() + " x" + i.getQuantity()));

addLineItems

List<LineItem> addLineItems(String quoteId, AddLineItemRequest item)
List<LineItem> addLineItems(String quoteId, List<AddLineItemRequest> items)

Add one or more product line items to a quote. A single AddLineItemRequest is automatically wrapped; a list is capped at 50 items.

productId, productName, unitPrice, and billingFrequency are all required on every item. productId is special: the key must be present on the wire, but its value may be null — call setProductId(null) for a custom (freeform) line item. quantity is optional and defaults to 1.

AddLineItemRequest item = new AddLineItemRequest();
item.setProductId(productId); // required — null for a custom line item
item.setProductName("Enterprise License");
item.setUnitPrice(1200.00);
item.setQuantity(5.0);
item.setBillingFrequency("annual");

// Custom (freeform) line item — productId is still set, explicitly to null
AddLineItemRequest custom = new AddLineItemRequest();
custom.setProductId(null);
custom.setProductName("Implementation Credit");
custom.setUnitPrice(-250.00);
custom.setBillingFrequency("one-time");

List<LineItem> added = tq.addLineItems(quoteId, Arrays.asList(item, custom));
List caps

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.

addBundleLineItems

List<LineItem> addBundleLineItems(String quoteId, AddBundleLineItemRequest item)
List<LineItem> addBundleLineItems(String quoteId, List<AddBundleLineItemRequest> items)

Add one or more bundle line items to a quote. bundleId and bundleName are both required; the server expands the bundle's child products for you. A list is capped at 50 items.

AddBundleLineItemRequest bundleItem = new AddBundleLineItemRequest();
bundleItem.setBundleId(bundleId);
bundleItem.setBundleName("Starter Bundle"); // required
bundleItem.setQuantity(2.0);

List<LineItem> added = tq.addBundleLineItems(quoteId, bundleItem);

updateLineItem

LineItem updateLineItem(String quoteId, String itemId, UpdateLineItemRequest request)

Update a line item on a quote. Only explicitly set fields are patched.

UpdateLineItemRequest req = new UpdateLineItemRequest();
req.setQuantity(10.0);
req.setDiscountPercent(15.0);

LineItem updated = tq.updateLineItem(quoteId, itemId, req);

removeLineItem

SuccessResponse removeLineItem(String quoteId, String itemId)

Remove a line item from a quote.

tq.removeLineItem(quoteId, itemId);

Products

MethodSignatureReturns
listProductslistProducts() / listProducts(ListProductsOptions)ProductListResponse
createProductcreateProduct(CreateProductRequest)Product
getProductgetProduct(String id)Product
updateProductupdateProduct(String id, UpdateProductRequest)Product
deleteProductdeleteProduct(String id)SuccessResponse
duplicateProductduplicateProduct(String id)Product
getProductPrimaryImagesgetProductPrimaryImages(List<String> productIds)Map<String, ProductImage>
categoryId is required on create

createProduct requires name, categoryId, listPrice, and billingFrequency. categoryId must be the UUID of an existing type (CategoryType.PRODUCT_CATEGORY) — resolve or create it first with listTypes / createType. It is optional on updateProduct, so you only need to pass it when creating.

// Resolve the product category first — createProduct needs its UUID.
ListTypesOptions typeOpts = new ListTypesOptions();
typeOpts.setCategoryType(CategoryType.PRODUCT_CATEGORY);

QuoteType category = tq.listTypes(typeOpts).getResults().stream()
.filter(t -> "Software".equals(t.getName()))
.findFirst()
.orElse(null);

if (category == null) {
CreateQuoteTypeRequest typeReq = new CreateQuoteTypeRequest();
typeReq.setName("Software");
typeReq.setCategoryType(CategoryType.PRODUCT_CATEGORY);
category = tq.createType(typeReq);
}

// Create a product
CreateProductRequest req = new CreateProductRequest();
req.setName("Pro Platform");
req.setCategoryId(category.getId()); // required
req.setSku("PRO-001");
req.setListPrice(500.00);
req.setBillingFrequency("monthly");

Product product = tq.createProduct(req);

// Upload with images — pass byte[][] via setImages()
req.setImages(new byte[][] { imageBytes });
Product productWithImages = tq.createProduct(req);

// Get primary images for a set of products
Map<String, ProductImage> images = tq.getProductPrimaryImages(
Arrays.asList(product.getId(), anotherProductId));
// images.get(productId) — ProductImage or null
Multipart Upload

When CreateProductRequest.getImages() is non-empty, the SDK automatically switches to multipart form upload with magic-byte MIME type detection (PNG, JPEG, GIF, WebP supported).


Price Books

MethodSignatureReturns
listPriceBookslistPriceBooks() / listPriceBooks(ListPriceBooksOptions)PriceBookListResponse
createPriceBookcreatePriceBook(CreatePriceBookRequest)PriceBook
getPriceBookgetPriceBook(String id)PriceBook
updatePriceBookupdatePriceBook(String id, UpdatePriceBookRequest)PriceBook
deletePriceBookdeletePriceBook(String id)SuccessResponse
duplicatePriceBookduplicatePriceBook(String id)PriceBook
listPriceBookProductslistPriceBookProducts(String id) / listPriceBookProducts(String id, ListPriceBookProductsOptions)PriceBookProductListResponse
// Create a price book with per-product pricing overrides
PriceBookProductPricingInput pricing = new PriceBookProductPricingInput();
pricing.setProductId(productId);
pricing.setDiscountType(DiscountType.PERCENT);
pricing.setDiscountPercent(20.0);

CreatePriceBookRequest req = new CreatePriceBookRequest();
req.setName("Partner Discount"); // required
req.setPriceBookTypeId(typeId); // required — from a createType(categoryType=PRICEBOOK_TYPE)
req.setValidFrom("2025-01-01"); // required
req.setDiscountPercent(15.0); // required
req.setProductPricing(Arrays.asList(pricing));
req.setShowInQuoteBuilder(true);

PriceBook pb = tq.createPriceBook(req);

// List products in a price book
PriceBookProductListResponse pbProducts = tq.listPriceBookProducts(pb.getId());
System.out.println("Products: " + pbProducts.getTotalRecords());

Bundles

MethodSignatureReturns
listBundleslistBundles() / listBundles(ListBundlesOptions)BundleListResponse
createBundlecreateBundle(CreateBundleRequest)Bundle
getBundlegetBundle(String id)Bundle
updateBundleupdateBundle(String id, UpdateBundleRequest)Bundle
deleteBundledeleteBundle(String id)SuccessResponse
duplicateBundleduplicateBundle(String id)Bundle

name and categoryId are required on the bundle itself. Each BundleItemInput requires productId, unitPrice, and billingFrequency; quantity is optional and defaults to 1.

categoryId is required

createBundle needs the UUID of an existing type with CategoryType.BUNDLE_CATEGORY — resolve or create it first with listTypes / createType, exactly as you would for a product category.

// 1. Resolve the bundle category — createBundle needs its UUID.
ListTypesOptions typeOpts = new ListTypesOptions();
typeOpts.setCategoryType(CategoryType.BUNDLE_CATEGORY);

QuoteType category = tq.listTypes(typeOpts).getResults().stream()
.filter(t -> "Starter Kits".equals(t.getName()))
.findFirst()
.orElse(null);

if (category == null) {
CreateQuoteTypeRequest typeReq = new CreateQuoteTypeRequest();
typeReq.setName("Starter Kits");
typeReq.setCategoryType(CategoryType.BUNDLE_CATEGORY);
category = tq.createType(typeReq);
}

// 2. Build the bundle items — productId, unitPrice and billingFrequency are all required.
BundleItemInput platform = new BundleItemInput();
platform.setProductId("product-uuid-1");
platform.setUnitPrice(199.00);
platform.setBillingFrequency("monthly");
platform.setQuantity(1.0);

BundleItemInput support = new BundleItemInput();
support.setProductId("product-uuid-2");
support.setUnitPrice(49.00);
support.setBillingFrequency("monthly");
support.setQuantity(2.0);

// 3. Create the bundle.
CreateBundleRequest req = new CreateBundleRequest();
req.setName("Starter Bundle"); // required
req.setCategoryId(category.getId()); // required
req.setItems(Arrays.asList(platform, support));
req.setBundleDiscountType(DiscountType.PERCENT);
req.setBundleDiscountPercent(5.0);
req.setShowInCatalog(true);

Bundle bundle = tq.createBundle(req);

Companies

MethodSignatureReturns
listCompanieslistCompanies() / listCompanies(ListCompaniesOptions)CompanyListResponse
createCompanycreateCompany(CreateCompanyRequest)Company
getCompanygetCompany(String id)Company
updateCompanyupdateCompany(String id, UpdateCompanyRequest)Company
deleteCompanydeleteCompany(String id)SuccessResponse
listCompanyContactslistCompanyContacts(String companyId) / listCompanyContacts(String companyId, PaginationParams)ContactListResponse
// Create a company — contacts list is required (minimum one contact)
CreateCompanyContactInput contact = new CreateCompanyContactInput();
contact.setName("Alice Buyer");
contact.setEmail("alice@example.com");

CreateCompanyRequest req = new CreateCompanyRequest();
req.setName("Acme Corp");
req.setCity("New York");
req.setContacts(Arrays.asList(contact));

Company company = tq.createCompany(req);

// List contacts for that company
ContactListResponse contacts = tq.listCompanyContacts(company.getId());
String contactId = contacts.getResults().get(0).getId();

Contacts

MethodSignatureReturns
listContactslistContacts() / listContacts(ListContactsOptions)ContactListResponse
createContactcreateContact(CreateContactRequest)Contact
updateContactupdateContact(String id, UpdateContactRequest)Contact
deleteContactdeleteContact(String id)SuccessResponse
No getContact

There is no getContact(id) method — the backend has no GET /v1/contacts/:id endpoint. Use listContacts with a query filter, or listCompanyContacts to retrieve contacts for a known company.

CreateContactRequest req = new CreateContactRequest();
req.setName("Bob Partner");
req.setEmail("bob@partner.example.com");

Contact contact = tq.createContact(req);

Quote Templates

MethodSignatureReturns
listTemplateslistTemplates() / listTemplates(PaginationParams)QuoteTemplateListResponse
getTemplategetTemplate()QuoteTemplate — auto-created if none exists
getTemplateByIdgetTemplateById(String id)QuoteTemplate
createTemplatecreateTemplate(CreateQuoteTemplateRequest)QuoteTemplate400 if one already exists
updateTemplateupdateTemplate(String id, UpdateQuoteTemplateRequest)QuoteTemplate
deleteTemplatedeleteTemplate(String id)SuccessResponse — resets to org branding defaults
Templates are auto-provisioned — use getTemplate()updateTemplate()

getTemplate() 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:

  • createTemplate() returns 400 TEMPLATE_ALREADY_EXISTS and is effectively unreachable. Do not build a get-then-create flow.
  • deleteTemplate() is really "reset to org branding defaults" — it soft-deletes, and the next getTemplate() regenerates a fresh one.

The correct flow is getTemplate()updateTemplate().

// 1. Get the org's template (created from org branding on first read)
QuoteTemplate defaultTemplate = tq.getTemplate();

// 2. Brand it by updating the template you just fetched
UpdateQuoteTemplateRequest brandReq = new UpdateQuoteTemplateRequest();
brandReq.setLogoUrl("https://cdn.example.com/logo.png");
brandReq.setPrimaryColor("#0057b8");
brandReq.setSenderName("TurboDocx Sales");

QuoteTemplate branded = tq.updateTemplate(defaultTemplate.getId(), brandReq);

// Get a specific template by ID
QuoteTemplate template = tq.getTemplateById(templateId);

// List all templates
QuoteTemplateListResponse templates = tq.listTemplates();
getTemplate() vs getTemplateById(id)

getTemplate() hits GET /v1/quote-template (singular) and returns the organization's default template. getTemplateById(id) hits GET /v1/quote-templates/:id (plural) and returns a specific template by ID.


Types / Categories

MethodSignatureReturns
listTypeslistTypes() / listTypes(ListTypesOptions)QuoteTypeListResponse
createTypecreateType(CreateQuoteTypeRequest)QuoteType
updateTypeupdateType(String id, UpdateQuoteTypeRequest)QuoteType
deleteTypedeleteType(String id)SuccessResponse

Types are used for categorization. CategoryType has four values — PRODUCT_CATEGORY, PRICEBOOK_TYPE, COMPANY_INDUSTRY, and BUNDLE_CATEGORY — and a type's id is the UUID you pass as categoryId when creating a product (PRODUCT_CATEGORY) or a bundle (BUNDLE_CATEGORY), or as priceBookTypeId when creating a price book (PRICEBOOK_TYPE).

CreateQuoteTypeRequest req = new CreateQuoteTypeRequest();
req.setName("Partner Pricing");
req.setCategoryType(CategoryType.PRICEBOOK_TYPE);

QuoteType type = tq.createType(req);
No getType

There is no getType(id) method — the backend has no GET /v1/types/:id endpoint by design. Use listTypes to retrieve all types.


Bulk Imports

Every create-family entity has a matching bulkCreate* method for seeding a catalog or migrating CRM data in one call. Each method sends POST {resource}/bulk with a list of the same request objects as that entity's single-create method (e.g. bulkCreateProducts takes List<CreateProductRequest>). Company rows require a contacts list with at least one contact; contact rows require a companyId.

Rows process sequentially with partial success — a failed row does not throw and does not roll back earlier rows. Every bulk method returns a BulkImportResult:

  • getImported() — count of rows created
  • getFailed() — list of BulkImportRowIssue (getRow() + getReason()) for rows that did not import; the row number is the 1-indexed position in your request list
  • getAdjusted() — list of BulkImportRowIssue for rows that imported with a server-side adjustment (e.g. a bundle item whose product wasn't found was dropped)

Requests are capped at 500 rows — anything above the cap returns a 400. Available to admin and contributor API keys.

bulkCreateProducts

BulkImportResult bulkCreateProducts(List<CreateProductRequest> rows)
Product rows require a real categoryId

Every bulkCreateProducts row requires name, categoryId, listPrice, and billingFrequency. categoryId must be the UUID of an existing type (CategoryType.PRODUCT_CATEGORY) — there is no categoryName field on the bulk row schema, and the API rejects unknown keys, so sending one returns a 400. Resolve or create the category first with listTypes / createType, then pass its ID.

// 1. Resolve the product category first — bulk rows need its UUID, not its name.
ListTypesOptions typeOpts = new ListTypesOptions();
typeOpts.setCategoryType(CategoryType.PRODUCT_CATEGORY);

String categoryId = tq.listTypes(typeOpts).getResults().stream()
.filter(t -> "Software".equals(t.getName()))
.map(QuoteType::getId)
.findFirst()
.orElseGet(() -> {
CreateQuoteTypeRequest typeReq = new CreateQuoteTypeRequest();
typeReq.setName("Software");
typeReq.setCategoryType(CategoryType.PRODUCT_CATEGORY);
try {
return tq.createType(typeReq).getId();
} catch (Exception e) {
throw new RuntimeException(e);
}
});

// 2. Import, passing the resolved UUID on every row.
CreateProductRequest row1 = new CreateProductRequest();
row1.setName("Enterprise License");
row1.setCategoryId(categoryId);
row1.setListPrice(1200.00);
row1.setBillingFrequency("annual");

CreateProductRequest row2 = new CreateProductRequest();
row2.setName("Onboarding Package");
row2.setCategoryId(categoryId);
row2.setListPrice(499.00);
row2.setBillingFrequency("one-time");

BulkImportResult result = tq.bulkCreateProducts(Arrays.asList(row1, row2));

System.out.println("Imported " + result.getImported() + " of 2 rows");
for (BulkImportRowIssue failure : result.getFailed()) {
System.err.println("Row " + failure.getRow() + " failed: " + failure.getReason());
}
for (BulkImportRowIssue adjustment : result.getAdjusted()) {
System.out.println("Row " + adjustment.getRow()
+ " imported with adjustment: " + adjustment.getReason());
}

The other five bulk methods follow the exact same pattern:

MethodRowsReturns
bulkCreatePriceBooksList<CreatePriceBookRequest>BulkImportResult
bulkCreateBundlesList<CreateBundleRequest>BulkImportResult
bulkCreateCompaniesList<CreateCompanyRequest> — each row needs contacts (min. 1)BulkImportResult
bulkCreateContactsList<CreateContactRequest> — each row needs companyIdBulkImportResult
bulkCreateTypesList<CreateQuoteTypeRequest>BulkImportResult

Convenience

createAndSend

CreateAndSendResponse createAndSend(CreateAndSendRequest request)

Create a quote, add line items and bundle items, and send it — all in one method call. Useful for programmatic quote generation pipelines.

AddLineItemRequest item = new AddLineItemRequest();
item.setProductId("product-uuid"); // required — null for a custom line item
item.setProductName("Starter License");
item.setUnitPrice(999.00);
item.setQuantity(1.0);
item.setBillingFrequency("annual"); // required

CreateAndSendRequest req = new CreateAndSendRequest();
req.setName("Quick Proposal");
req.setCompanyId(companyId);
req.setContactId(contactId);
req.setItems(Arrays.asList(item));
// req.setSend(sendOptions) — optional

CreateAndSendResponse result = tq.createAndSend(req);
System.out.println("Quote: " + result.getQuote().getId());

Error Handling

import com.turbodocx.TurboDocxException;

try {
Quote quote = tq.createQuote(req);
} catch (TurboDocxException.ValidationException e) {
// 400 — invalid request body (missing required field, bad enum value, etc.)
System.err.println("Validation: " + e.getMessage());
} catch (TurboDocxException.AuthenticationException e) {
// 401 — bad or revoked API key
System.err.println("Auth: " + e.getMessage());
} catch (TurboDocxException.AuthorizationException e) {
// 403 — key lacks required role
System.err.println("Forbidden: " + e.getMessage());
} catch (TurboDocxException.NotFoundException e) {
// 404 — quote, product, company, etc. not found
System.err.println("Not found: " + e.getMessage());
} catch (TurboDocxException.RateLimitException e) {
// 429 — back off and retry
System.err.println("Rate limited: " + e.getMessage());
} catch (TurboDocxException.NetworkException e) {
// request never reached the server (DNS, refused, timeout)
System.err.println("Network error: " + e.getMessage());
} catch (TurboDocxException e) {
// catch-all for any other typed SDK error
System.err.println("Error " + e.getStatusCode() + ": " + e.getMessage());
}

Common Error Codes

StatusTypeWhen
400TurboDocxException.ValidationExceptionInvalid request body, missing required field
401TurboDocxException.AuthenticationExceptionMissing or invalid API key
403TurboDocxException.AuthorizationExceptionValid key without required role
404TurboDocxException.NotFoundExceptionQuote, product, company, contact, etc. not found
429TurboDocxException.RateLimitExceptionRate limit exceeded — back off and retry

Runnable End-to-End Examples

Three fully runnable examples live in the SDK repo:

Run any example after exporting TURBODOCX_API_KEY and TURBODOCX_ORG_ID.

See Also