TurboQuote JavaScript / TypeScript SDK
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.
$npx skills add TurboDocx/quickstart›/turbodocx-sdk turboquoteThe official TurboDocx TurboQuote SDK for Node.js and browser applications. Build quoting and CPQ (configure-price-quote) workflows: create and send quotes, manage line items, maintain a product and bundle catalog, apply price books, and handle the full quote lifecycle — all with zero runtime dependencies and complete TypeScript types. Available on npm as @turbodocx/sdk (same package as TurboSign and TurboWebhooks).
TurboQuote is TurboDocx's quoting and CPQ module. Quotes progress through a lifecycle: draft → pending_approval → sent → accepted / declined / voided. Each quote belongs to a company and contact, carries line items (individual products or bundles), and can optionally have a price book applied. Accepted quotes can be merged with a TurboDocx Deliverable (e.g. a contract generated from a template) and sent for e-signature through TurboSign via sendQuoteWithDeliverable.
Installation
- npm
- pnpm
- yarn
npm install @turbodocx/sdk
pnpm add @turbodocx/sdk
yarn add @turbodocx/sdk
Requirements
- Node.js 18 or higher (native
fetch) - TypeScript 4.7+ (optional, for type checking — declaration files are included)
- Zero runtime dependencies — the SDK uses only Node built-ins
Configuration
- TypeScript
- JavaScript
import { TurboQuote } from '@turbodocx/sdk';
TurboQuote.configure({
apiKey: process.env.TURBODOCX_API_KEY!,
orgId: process.env.TURBODOCX_ORG_ID, // optional — falls back to env var
// accessToken: process.env.TURBODOCX_ACCESS_TOKEN, // optional — OAuth token instead of apiKey
});
const { TurboQuote } = require('@turbodocx/sdk');
TurboQuote.configure({
apiKey: process.env.TURBODOCX_API_KEY,
orgId: process.env.TURBODOCX_ORG_ID,
// accessToken: process.env.TURBODOCX_ACCESS_TOKEN, // optional — OAuth token instead of apiKey
});
Unlike TurboSign, TurboQuote.configure() does not require senderEmail or senderName — quotes are not sent as signature emails. Only a credential is required — either apiKey or an OAuth accessToken (accessToken wins when both are set); orgId is recommended but falls back to TURBODOCX_ORG_ID. If you skip configure() entirely, the SDK auto-initialises from environment variables on the first method call.
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 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.updateTemplate({ senderEmail, senderName })) and all of them resolve cleanly.
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
The most common flow: create a quote for a company and contact, add a product line item, then send it.
- TypeScript
- JavaScript
import { TurboQuote } from '@turbodocx/sdk';
import { writeFileSync } from 'node:fs';
TurboQuote.configure({
apiKey: process.env.TURBODOCX_API_KEY!,
orgId: process.env.TURBODOCX_ORG_ID,
});
// 1. Create a draft quote
const quote = await TurboQuote.createQuote({
name: 'Acme Corp — Enterprise Plan',
companyId: 'company-uuid',
contactId: 'contact-uuid',
currency: 'USD',
termDays: 30,
});
// 2. Add a product line item
await TurboQuote.addLineItems(quote.id, {
productId: 'product-uuid',
productName: 'Enterprise Licence',
unitPrice: 1200,
billingFrequency: 'annual',
quantity: 5,
});
// 3. Send the quote
const { quote: sentQuote, message } = await TurboQuote.sendQuote(quote.id, {
validUntil: '2026-07-31',
});
console.log(message); // "Quote sent successfully"
console.log(sentQuote.status); // "sent"
// 4. Download the PDF
const pdf = await TurboQuote.downloadQuotePdf(sentQuote.id);
writeFileSync('quote.pdf', Buffer.from(pdf));
const { TurboQuote } = require('@turbodocx/sdk');
const { writeFileSync } = require('fs');
TurboQuote.configure({
apiKey: process.env.TURBODOCX_API_KEY,
orgId: process.env.TURBODOCX_ORG_ID,
});
// 1. Create a draft quote
const quote = await TurboQuote.createQuote({
name: 'Acme Corp — Enterprise Plan',
companyId: 'company-uuid',
contactId: 'contact-uuid',
currency: 'USD',
termDays: 30,
});
// 2. Add a product line item
await TurboQuote.addLineItems(quote.id, {
productId: 'product-uuid',
productName: 'Enterprise Licence',
unitPrice: 1200,
billingFrequency: 'annual',
quantity: 5,
});
// 3. Send the quote
const { quote: sentQuote, message } = await TurboQuote.sendQuote(quote.id, {
validUntil: '2026-07-31',
});
console.log(message); // "Quote sent successfully"
console.log(sentQuote.status); // "sent"
// 4. Download the PDF
const pdf = await TurboQuote.downloadQuotePdf(sentQuote.id);
writeFileSync('quote.pdf', Buffer.from(pdf));
Convenience: createAndSend
createAndSend combines quote creation, line item addition, and sending into a single call.
const { quote } = await TurboQuote.createAndSend({
name: 'Acme Corp — Starter',
companyId: 'company-uuid',
contactId: 'contact-uuid',
currency: 'USD',
termDays: 30,
items: [
{ productId: null, productName: 'Setup Fee', unitPrice: 500, billingFrequency: 'one-time' },
{ productId: 'product-uuid', productName: 'Monthly Subscription', unitPrice: 99, billingFrequency: 'monthly', quantity: 10 },
],
send: { validUntil: '2026-07-31' },
});
console.log(quote.status); // "sent"
Method Reference
All methods are static on the TurboQuote class. Configure once, then call on the class directly.
Quotes
listQuotes
List quotes with optional pagination and filters. Returns totals and pipeline stats alongside results.
const { results, totalRecords, stats } = await TurboQuote.listQuotes({
limit: 20,
offset: 0,
statuses: ['draft', 'sent'], // string or string[]
companyId: 'company-uuid',
currency: 'USD',
});
// stats.total, stats.winRate, stats.monthlyRecurringRevenue, ...
for (const q of results) {
console.log(q.name, q.status);
}
createQuote
Create a new quote in draft status.
// Fixed-term quote — termDays 0–3650, no renewalPeriod (0 = one-time, -1 = auto-renewal)
const quote = await TurboQuote.createQuote({
name: 'Q3 Renewal', // required
companyId: 'company-uuid', // required
contactId: 'contact-uuid', // required
currency: 'USD', // 'USD'|'EUR'|'GBP'|'CAD'|'AUD'|'INR'
termDays: 30, // fixed term in days — omit to get the default of 60
validUntil: '2026-09-30',
taxRate: 8.5,
priceBookId: 'pb-uuid',
});
// Auto-renewal quote — termDays: -1 REQUIRES renewalPeriod, and renewalPeriod is ONLY valid
// when termDays is -1. Pairing renewalPeriod with a fixed term (e.g. termDays: 30) returns a 400.
const subscription = await TurboQuote.createQuote({
name: 'Annual Subscription',
companyId: 'company-uuid',
contactId: 'contact-uuid',
currency: 'USD',
termDays: -1, // -1 = auto-renewal
renewalPeriod: 'annually', // 'weekly'|'monthly'|'quarterly'|'annually'
});
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 null or absent for every other termDays value — sending it alongside a fixed term returns a 400. The same rule applies on updateQuote.
getQuote
Fetch a single quote. The returned object includes a statusInfo field with transition flags (canSend, canAccept, canDecline, canVoid) and a preparedBy object — the resolved "Prepared by" identity shown on the quote PDF.
const quote = await TurboQuote.getQuote('quote-uuid');
console.log(quote.statusInfo?.canSend); // true when status is 'draft'
console.log(quote.preparedBy?.name); // e.g. "Acme Billing Integration" or the template sender
console.log(quote.preparedBy?.email); // may be undefined 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.
updateQuote
Patch any combination of quote fields. Pass null to clear nullable fields (renewalPeriod, validUntil, taxRate, priceBookId).
const updated = await TurboQuote.updateQuote('quote-uuid', {
name: 'Q3 Renewal — Revised',
taxRate: null, // clears the tax rate
});
deleteQuote
Soft-delete a quote.
const { message } = await TurboQuote.deleteQuote('quote-uuid');
duplicateQuote
Copy a quote (and its line items) into a new draft.
const copy = await TurboQuote.duplicateQuote('quote-uuid');
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
Apply a price book to all line items on a quote. Returns the updated quote plus counts of how many items were updated vs skipped.
const { quote, updatedCount, skippedCount, message } = await TurboQuote.applyPriceBook(
'quote-uuid',
'pricebook-uuid',
);
removePriceBook
Detach the price book from a quote (line item prices are not reverted).
const quote = await TurboQuote.removePriceBook('quote-uuid');
downloadQuotePdf
Download the quote as a PDF. Returns raw bytes as an ArrayBuffer.
const pdf = await TurboQuote.downloadQuotePdf('quote-uuid');
writeFileSync('quote.pdf', Buffer.from(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
Fetch the org's current quote numbering format and the current per-period issued floor.
const config = await TurboQuote.getQuoteNumberConfig();
console.log(config.format.prefix); // e.g. "Q-"
console.log(config.currentFloor); // the current per-period issued floor
updateQuoteNumberConfig
Update the numbering format. Pass the full format object; all eight fields are required.
const config = await TurboQuote.updateQuoteNumberConfig({
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'
});
console.log(config.format.startNumber); // 1000
Field reference, defaults & validation
All eight format fields 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 (yearTokenother thannone) — 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
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.
sendQuote
Send a draft quote to the contact. Optionally include CC recipients or set a validity deadline.
const { quote, message } = await TurboQuote.sendQuote('quote-uuid', {
validUntil: '2026-07-31',
ccEmails: ['manager@example.com'],
});
sendQuoteWithDeliverable
Send a quote paired with a TurboDocx deliverable (e.g., a contract generated from a template). The deliverable is merged before or after the quote PDF.
const { quote, message, documentId } = await TurboQuote.sendQuoteWithDeliverable(
'quote-uuid',
{
deliverableId: 'deliverable-uuid',
mergePosition: 'end', // 'beginning' | 'end'
ccEmails: ['legal@example.com'],
},
);
// documentId — TurboSign document created for the merged PDF
declineQuote
Mark a sent quote as declined (typically called on behalf of the recipient).
const quote = await TurboQuote.declineQuote('quote-uuid', {
reason: 'Budget constraints for this quarter',
});
voidQuote
Void a quote that should no longer be valid.
const quote = await TurboQuote.voidQuote('quote-uuid', {
reason: 'Superseded by revised quote #Q-102',
});
handleExpiredQuote
Handle 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 draft 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 (≤ 190 characters), and newValidUntil (ISO date).
const quote = await TurboQuote.handleExpiredQuote('quote-uuid', {
action: 'void', // 'void' | 'decline' — the only two valid actions
reason: 'Expired — re-quoting', // required, max 190 chars
newValidUntil: '2026-08-31', // required, ISO date carried onto the duplicate
});
extend or resend actionaction 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 for you — pass newValidUntil and it lands on the duplicate it creates.
Line Items
Line items attach products or bundles to a quote, each with a price, quantity, billing frequency, and optional discount.
listLineItems
const { results, totalRecords } = await TurboQuote.listLineItems('quote-uuid', {
limit: 50,
billingFrequency: 'monthly',
});
addLineItems
Add one or more product line items. Pass a single object or an array of up to 50 items.
productId, productName, unitPrice, and billingFrequency are all required on every item. productId is special: the key must be present, but its value may be null for a custom (freeform) line item. Omitting the key entirely returns a 400. quantity is optional and defaults to 1.
// Single item
await TurboQuote.addLineItems('quote-uuid', {
productId: 'product-uuid', // required key — pass null for a custom (freeform) line item
productName: 'Professional Services',
unitPrice: 150,
billingFrequency: 'one-time',
quantity: 8,
discountPercent: 10,
discountType: 'percent',
});
// Multiple items at once — array is capped at 50 items
await TurboQuote.addLineItems('quote-uuid', [
{ productId: 'p1', productName: 'Licence A', unitPrice: 500, billingFrequency: 'annual' },
{ productId: 'p2', productName: 'Licence B', unitPrice: 300, billingFrequency: 'annual' },
{ productId: null, productName: 'Custom Discount Credit', unitPrice: -100, billingFrequency: 'one-time' },
]);
addLineItems accepts a single object or an array of 1–50 items. A reorder request accepts up to 200 items. Exceeding either cap returns a 400.
addBundleLineItems
Add one or more bundle line items.
await TurboQuote.addBundleLineItems('quote-uuid', {
bundleId: 'bundle-uuid',
bundleName: 'Starter Bundle',
quantity: 2,
showItemsToEndUser: true,
});
updateLineItem
Update a single line item's price, quantity, discount, or billing frequency.
const updated = await TurboQuote.updateLineItem('quote-uuid', 'item-uuid', {
quantity: 12,
discountPercent: 15,
billingFrequency: 'monthly',
});
removeLineItem
Remove a line item from a quote.
const { message } = await TurboQuote.removeLineItem('quote-uuid', 'item-uuid');
Products
Manage your product catalog. Products can include images (uploaded as multipart form data — the SDK detects the MIME type from magic bytes automatically).
| Method | Signature | Returns |
|---|---|---|
listProducts | (opts?) → ProductListResponse | Paginated list + catalog stats |
createProduct | (req) → Product | Created product |
getProduct | (id) → Product | Single product |
updateProduct | (id, req) → Product | Updated product |
deleteProduct | (id) → SuccessResponse | Message |
duplicateProduct | (id) → Product | New duplicate |
getProductPrimaryImages | (productIds[]) → { [id]: ProductImage | null } | Primary image map |
// Create a product with an image
const product = await TurboQuote.createProduct({
name: 'Enterprise Licence',
listPrice: 1200,
billingFrequency: 'annual',
categoryId: 'category-uuid',
sku: 'ENT-001',
showInCatalog: true,
images: ['/path/to/product-image.png'], // file path, Buffer, or File object
});
// List with filters
const { results, totalProducts } = await TurboQuote.listProducts({
categoryIds: ['cat-1', 'cat-2'],
billingFrequency: 'monthly',
showInCatalog: true,
});
// Fetch primary images for multiple products at once
const images = await TurboQuote.getProductPrimaryImages(['p-uuid-1', 'p-uuid-2']);
// images['p-uuid-1'] → ProductImage | null
Bundles
Bundles group multiple products into a single purchasable unit with optional bundle-level discounts.
| Method | Signature | Returns |
|---|---|---|
listBundles | (opts?) → BundleListResponse | Paginated list + stats |
createBundle | (req) → Bundle | Created bundle |
getBundle | (id) → Bundle | Single bundle |
updateBundle | (id, req) → Bundle | Updated bundle |
deleteBundle | (id) → SuccessResponse | Message |
duplicateBundle | (id) → Bundle | New duplicate |
const bundle = await TurboQuote.createBundle({
name: 'Starter Pack',
categoryId: 'category-uuid',
currency: 'USD',
showInCatalog: true,
syncWithProducts: true,
items: [
{ productId: 'p1', unitPrice: 200, billingFrequency: 'monthly', quantity: 1 },
{ productId: 'p2', unitPrice: 50, billingFrequency: 'monthly', quantity: 3 },
],
});
Price Books
Price books let you define alternative pricing tiers. When a price book is applied to a quote, matching product line items are repriced automatically.
| Method | Signature | Returns |
|---|---|---|
listPriceBooks | (opts?) → PriceBookListResponse | Paginated list + stats |
createPriceBook | (req) → PriceBook | Created price book |
getPriceBook | (id) → PriceBook | Single price book |
updatePriceBook | (id, req) → PriceBook | Updated price book |
deletePriceBook | (id) → SuccessResponse | Message |
duplicatePriceBook | (id) → PriceBook | New duplicate |
listPriceBookProducts | (id, opts?) → PaginatedResponse<PriceBookProductPricing> | Per-product pricing |
const pb = await TurboQuote.createPriceBook({
name: 'Partner Tier',
priceBookTypeId: 'type-uuid',
validFrom: '2026-01-01',
validTo: '2026-12-31',
discountPercent: 20,
isDefault: false,
showInQuoteBuilder: true,
productPricing: [
{ productId: 'p1', discountPercent: 25, finalPrice: 900 },
],
});
// Apply to a quote
const { updatedCount, skippedCount } = await TurboQuote.applyPriceBook(
'quote-uuid',
pb.id,
);
Companies
Companies represent the buyer organisations your quotes are addressed to. Each company must have at least one contact.
| Method | Signature | Returns |
|---|---|---|
listCompanies | (opts?) → CompanyListResponse | Paginated list |
createCompany | (req) → Company | Created company |
getCompany | (id) → Company | Single company |
updateCompany | (id, req) → Company | Updated company |
deleteCompany | (id) → SuccessResponse | Message |
listCompanyContacts | (companyId, opts?) → ContactListResponse | Company's contacts |
const company = await TurboQuote.createCompany({
name: 'Acme Corporation',
phone: '+1-555-0100',
city: 'San Francisco',
state: 'CA',
country: 'US',
contacts: [
{ name: 'Alice Smith', email: 'alice@acme.example', title: 'VP of Engineering' },
],
});
const { results: contacts } = await TurboQuote.listCompanyContacts(company.id);
Contacts
Contacts belong to a company and are the individuals a quote is addressed to.
There is no getContact(id) method — the backend does not expose a GET /v1/contacts/:id route. Retrieve an individual contact via listContacts with a search query, or fetch all contacts for a company with listCompanyContacts.
| Method | Signature | Returns |
|---|---|---|
listContacts | (opts?) → ContactListResponse | Paginated list |
createContact | (req) → Contact | Created contact |
updateContact | (id, req) → Contact | Updated contact |
deleteContact | (id) → SuccessResponse | Message |
const contact = await TurboQuote.createContact({
name: 'Bob Jones',
companyId: 'company-uuid',
email: 'bob@acme.example',
title: 'Procurement Manager',
});
const { results } = await TurboQuote.listContacts({ companyId: 'company-uuid' });
Quote Templates
Quote templates control the visual presentation of the sent quote (logo, brand colours, disclaimer, terms, sender info). There is one active template per org, accessible via getTemplate(). You can also manage named templates.
getTemplate() returns the org's single active template via GET /v1/quote-template (singular path). listTemplates() returns all named templates via GET /v1/quote-templates (plural path).
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 400TEMPLATE_ALREADY_EXISTSand is effectively unreachable. Do not build a get-then-create flow.deleteTemplate()is really "reset to org branding defaults" — it soft-deletes, and the nextgetTemplate()regenerates a fresh one.
The correct flow is getTemplate() → updateTemplate(tmpl.id, …).
| Method | Signature | Returns |
|---|---|---|
getTemplate | () → QuoteTemplate | Active org template (auto-created if none exists) |
listTemplates | (opts?) → QuoteTemplateListResponse | All named templates |
getTemplateById | (id) → QuoteTemplate | Named template by ID |
createTemplate | (req) → QuoteTemplate | Created template — 400 if one already exists |
updateTemplate | (id, req) → QuoteTemplate | Updated template |
deleteTemplate | (id) → SuccessResponse | Resets to org branding defaults |
// 1. Fetch the active template — created from org branding on first read
const tmpl = await TurboQuote.getTemplate();
console.log(tmpl.primaryColor); // e.g. "#1a73e8"
// 2. Brand it by updating the template you just fetched
const branded = await TurboQuote.updateTemplate(tmpl.id, {
logoUrl: 'https://cdn.example.com/logo.png',
primaryColor: '#0057b8',
primaryTextColor: '#ffffff',
disclaimer: 'Prices valid for 30 days.',
termsAndConditions: 'See attached terms...',
senderName: 'TurboDocx Sales',
senderEmail: 'sales@example.com',
});
Types (Categories)
Types are reusable category/classification values used across products, bundles, price books, and companies (e.g., industry tags, price book types).
There is no getType(id) method — the backend does not expose GET /v1/types/:id. Use listTypes to retrieve individual types.
| Method | Signature | Returns |
|---|---|---|
listTypes | (opts?) → QuoteTypeListResponse | Paginated list |
createType | (req) → QuoteType | Created type |
updateType | (id, req) → QuoteType | Updated type |
deleteType | (id) → SuccessResponse | Message |
const type = await TurboQuote.createType({
name: 'Software',
categoryType: 'product_category', // 'product_category'|'pricebook_type'|'company_industry'|'bundle_category'
});
const { results } = await TurboQuote.listTypes({
categoryType: 'company_industry',
includeUsage: true,
});
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 an array of rows using the same shape as that entity's single-create request (e.g. bulkCreateProducts takes CreateProductRequest[]). Company rows require a contacts array with at least one contact; contact rows require a companyId.
Rows process sequentially with partial success — a failed row does not throw and does not roll back earlier rows. Every bulk method resolves to a BulkImportResult:
imported— count of rows createdfailed— array of{ row, reason }for rows that did not import;rowis the 1-indexed position in your request arrayadjusted— array of{ row, reason }for rows that imported with a server-side adjustment (e.g. a bundle item whose product wasn't found was dropped)
Requests are capped at 500 rows — anything above the cap returns a 400. Available to admin and contributor API keys.
categoryIdEvery 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, and the API rejects unknown keys, so passing 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.
const { results: categories } = await TurboQuote.listTypes({ categoryType: 'product_category' });
const category =
categories.find((t) => t.name === 'Software') ??
(await TurboQuote.createType({ name: 'Software', categoryType: 'product_category' }));
// 2. Import, passing the resolved UUID on every row.
const result = await TurboQuote.bulkCreateProducts([
{ name: 'Enterprise Licence', categoryId: category.id, listPrice: 1200, billingFrequency: 'annual' },
{ name: 'Onboarding Package', categoryId: category.id, listPrice: 499, billingFrequency: 'one-time' },
]);
console.log(`Imported ${result.imported} of 2 rows`);
for (const failure of result.failed) {
console.error(`Row ${failure.row} failed: ${failure.reason}`);
}
for (const adjustment of result.adjusted) {
console.warn(`Row ${adjustment.row} imported with adjustment: ${adjustment.reason}`);
}
The other five bulk methods follow the exact same pattern:
| Method | Argument | Returns |
|---|---|---|
bulkCreatePriceBooks | CreatePriceBookRequest[] | BulkImportResult |
bulkCreateBundles | CreateBundleRequest[] | BulkImportResult |
bulkCreateCompanies | CreateCompanyRequest[] — each row needs contacts (min. 1) | BulkImportResult |
bulkCreateContacts | CreateContactRequest[] — each row needs companyId | BulkImportResult |
bulkCreateTypes | CreateQuoteTypeRequest[] | BulkImportResult |
TypeScript Types
Key types exported from @turbodocx/sdk:
import type {
// Core quote
Quote,
QuoteStatusInfo,
CreateQuoteRequest,
UpdateQuoteRequest,
ListQuotesOptions,
QuoteListResponse,
SendQuoteRequest,
SendQuoteWithDeliverableRequest,
DeclineQuoteRequest,
VoidQuoteRequest,
HandleExpiredQuoteRequest,
ApplyPriceBookResponse,
CreateAndSendRequest,
// Line items
LineItem,
AddLineItemRequest,
AddBundleLineItemRequest,
UpdateLineItemRequest,
ListLineItemsOptions,
// Products
Product,
CreateProductRequest,
UpdateProductRequest,
ListProductsOptions,
// Bundles
Bundle,
CreateBundleRequest,
UpdateBundleRequest,
// Price books
PriceBook,
CreatePriceBookRequest,
// Companies & contacts
Company,
CreateCompanyRequest,
Contact,
CreateContactRequest,
// Templates & types
QuoteTemplate,
QuoteType,
// Enums
QuoteStatus,
BillingFrequency,
Currency,
RenewalPeriod,
LineItemType,
DiscountType,
CategoryType,
// Shared
SuccessResponse,
} from '@turbodocx/sdk';
Key Enum Values
| Type | Values |
|---|---|
QuoteStatus | 'draft' 'pending_approval' 'sent' 'accepted' 'declined' 'voided' |
BillingFrequency | 'monthly' 'quarterly' 'annual' 'one-time' |
Currency | 'USD' 'EUR' 'GBP' 'CAD' 'AUD' 'INR' |
RenewalPeriod | 'weekly' 'monthly' 'quarterly' 'annually' |
DiscountType | 'percent' 'amount' |
CategoryType | 'product_category' 'pricebook_type' 'company_industry' 'bundle_category' |
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 (canSend, canAccept, canDecline, canVoid) before attempting a transition, and duplicateQuote when you need to revive a closed-out quote.
Error Handling
import {
TurboQuote,
TurboDocxError,
AuthenticationError,
AuthorizationError,
ValidationError,
NotFoundError,
RateLimitError,
NetworkError,
} from '@turbodocx/sdk';
try {
await TurboQuote.sendQuote('quote-uuid');
} catch (e) {
if (e instanceof ValidationError) {
// 400 — e.g. quote is not in draft status, missing required fields
console.error('Validation failed:', e.message);
} else if (e instanceof NotFoundError) {
// 404 — quote, company, contact, or product does not exist
console.error('Not found:', e.message);
} else if (e instanceof AuthenticationError) {
// 401 — bad or revoked API key
console.error('Auth failed:', e.message);
} else if (e instanceof AuthorizationError) {
// 403 — API key does not have permission for this org
console.error('Forbidden:', e.message);
} else if (e instanceof RateLimitError) {
// 429 — back off and retry
console.error('Rate limited — retry after a moment');
} else if (e instanceof NetworkError) {
// request never reached the server
console.error('Network error:', e.message);
} else if (e instanceof TurboDocxError) {
// catch-all for any other typed SDK error
console.error(`Error ${e.statusCode}: ${e.message}`);
} else {
throw e;
}
}
Common Error Codes
| Status | Class | When |
|---|---|---|
| 400 | ValidationError | Invalid request body, wrong quote status for transition |
| 401 | AuthenticationError | Missing or invalid API key |
| 403 | AuthorizationError | Valid key without permission for this resource |
| 404 | NotFoundError | Quote, company, product, or contact not found |
| 429 | RateLimitError | Rate limit exceeded — back off and retry |
Runnable Examples
Validated end-to-end examples live in the SDK repo:
turboquote-basic.ts— full quote lifecycle (create → add line items → send → download PDF → delete)turboquote-products.ts— product and bundle catalog managementturboquote-pricebooks.ts— price book CRUD andapplyPriceBook
Run any example with:
export TURBODOCX_API_KEY=your_key
export TURBODOCX_ORG_ID=your_org_id
npx tsx examples/turboquote-basic.ts
See Also
- TurboSign JavaScript SDK — send documents for e-signature
- TurboWebhooks JavaScript SDK — receive real-time signature events
- Deliverable JavaScript SDK — generate documents from templates
- SDKs Overview — all SDKs across all six languages
- @turbodocx/sdk on npm
- TurboDocx SDK on GitHub