Skip to main content

TurboQuote PHP 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 PHP 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 PHP 8.1+. Available on Packagist 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

composer require turbodocx/sdk

Requirements

  • PHP 8.1 or higher
  • Composer 2.x
  • ext-json
  • A TurboDocx API key (TDX- prefix) — generate one in Settings → API Keys

Configuration

<?php
use TurboDocx\TurboQuote;
use TurboDocx\Config\QuoteClientConfig;

TurboQuote::configure(new QuoteClientConfig(
apiKey: $_ENV['TURBODOCX_API_KEY'],
orgId: $_ENV['TURBODOCX_ORG_ID'], // optional, but required for most endpoints
));
No senderEmail required

Unlike TurboSign, TurboQuote does not require senderEmail or senderName. Quotes are not signature emails — only apiKey and, optionally, orgId are needed.

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
API Credentials Required

Both apiKey and orgId are needed for most API requests. To get your credentials, follow the Get Your Credentials steps from the SDKs main page.


Quick Start

Full quote lifecycle: create → add items → send → download PDF

<?php
require __DIR__ . '/vendor/autoload.php';

use TurboDocx\TurboQuote;
use TurboDocx\Config\QuoteClientConfig;
use TurboDocx\Types\Requests\Quote\CreateQuoteRequest;
use TurboDocx\Types\Requests\Quote\AddLineItemRequest;
use TurboDocx\Types\Requests\Quote\SendQuoteRequest;

TurboQuote::configure(QuoteClientConfig::fromEnvironment());

// 1. Create the quote
$quote = TurboQuote::createQuote(new CreateQuoteRequest(
name: 'Enterprise License Q3',
companyId: 'company-uuid',
contactId: 'contact-uuid',
validUntil: '2026-09-30',
));

echo "Quote created: {$quote->id}\n";

// 2. Add a product line item
$items = TurboQuote::addLineItems($quote->id, new AddLineItemRequest(
productId: 'product-uuid',
productName: 'Enterprise Seat',
unitPrice: 199.00,
billingFrequency: 'monthly',
quantity: 5,
));

echo "Added {count($items)} item(s)\n";

// 3. Send the quote
$sent = TurboQuote::sendQuote($quote->id);
echo "Status: {$sent->quote->status}\n"; // 'sent'

// 4. Download the PDF
$pdfBytes = TurboQuote::downloadQuotePdf($quote->id);
file_put_contents('proposal.pdf', $pdfBytes);
echo "PDF saved.\n";
Always Handle Errors

The above example omits error handling for brevity. In production, wrap all TurboQuote calls in try-catch blocks. See Error Handling for complete patterns.

Convenience: createAndSend

<?php
use TurboDocx\Types\Requests\Quote\CreateAndSendRequest;
use TurboDocx\Types\Requests\Quote\AddLineItemRequest;
use TurboDocx\Types\Requests\Quote\SendQuoteRequest;

$result = TurboQuote::createAndSend(new CreateAndSendRequest(
name: 'Starter Plan',
companyId: 'company-uuid',
contactId: 'contact-uuid',
items: [
new AddLineItemRequest(
productId: 'product-uuid',
productName: 'Starter Plan',
unitPrice: 49.00,
billingFrequency: 'monthly',
quantity: 1,
),
],
send: new SendQuoteRequest(),
));

echo "Quote sent: {$result->quote->id}\n";

Method Reference

All methods are static. Configure once with TurboQuote::configure(...), then call on the class directly.

Quotes

listQuotes

use TurboDocx\Types\Requests\Quote\ListQuotesRequest;

$page = TurboQuote::listQuotes(new ListQuotesRequest(
limit: 10,
offset: 0,
query: 'Enterprise',
));

echo "Total: {$page->totalRecords}\n";
foreach ($page->results as $q) {
echo " [{$q->status}] {$q->name}\n";
}

createQuote

use TurboDocx\Types\Requests\Quote\CreateQuoteRequest;

$quote = TurboQuote::createQuote(new CreateQuoteRequest(
name: 'Q3 Proposal',
companyId: 'company-uuid',
contactId: 'contact-uuid',
validUntil: '2026-09-30',
currency: 'USD',
));

getQuote

$quote = TurboQuote::getQuote('quote-uuid');
// statusInfo is merged onto the returned Quote object when present
echo $quote->status;

updateQuote

use TurboDocx\Types\Requests\Quote\UpdateQuoteRequest;

$quote = TurboQuote::updateQuote('quote-uuid', new UpdateQuoteRequest(
name: 'Q3 Proposal — Revised',
validUntil: '2026-10-15',
));

deleteQuote

$result = TurboQuote::deleteQuote('quote-uuid');
echo $result->message;

duplicateQuote

$copy = TurboQuote::duplicateQuote('quote-uuid');
echo "Copy id: {$copy->id}";

downloadQuotePdf

Returns raw PDF bytes. Save to disk or stream to the browser.

$pdfBytes = TurboQuote::downloadQuotePdf('quote-uuid');
file_put_contents('quote.pdf', $pdfBytes);

// Or stream as HTTP response
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="quote.pdf"');
echo $pdfBytes;

Quote Status Transitions

sendQuote

use TurboDocx\Types\Requests\Quote\SendQuoteRequest;

$result = TurboQuote::sendQuote('quote-uuid', new SendQuoteRequest(
// optional overrides; pass null to use quote defaults
));
// $result->quote — updated Quote
// $result->message

sendQuoteWithDeliverable

Attach a Deliverable document (generated DOCX/PDF) alongside the quote.

use TurboDocx\Types\Requests\Quote\SendQuoteWithDeliverableRequest;

$result = TurboQuote::sendQuoteWithDeliverable('quote-uuid', new SendQuoteWithDeliverableRequest(
deliverableId: 'deliverable-uuid',
mergePosition: 'end',
));
// $result->quote
// $result->message
// $result->documentId — TurboSign document ID

declineQuote

use TurboDocx\Types\Requests\Quote\DeclineQuoteRequest;

$quote = TurboQuote::declineQuote('quote-uuid', new DeclineQuoteRequest(
reason: 'Price out of budget',
));

voidQuote

use TurboDocx\Types\Requests\Quote\VoidQuoteRequest;

$quote = TurboQuote::voidQuote('quote-uuid', new VoidQuoteRequest(
reason: 'Superseded by new proposal',
));

handleExpiredQuote

use TurboDocx\Types\Requests\Quote\HandleExpiredQuoteRequest;

$quote = TurboQuote::handleExpiredQuote('quote-uuid', new HandleExpiredQuoteRequest(
action: 'extend',
reason: 'Customer requested more time to review',
newValidUntil: '2026-12-31',
));

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.

$config = TurboQuote::getQuoteNumberConfig();
echo $config->format->prefix; // e.g. "Q-"
echo $config->currentFloor; // the current per-period issued floor

updateQuoteNumberConfig

Update the numbering format. All eight fields are sent.

use TurboDocx\Types\Quote\QuoteNumberFormat;

$config = TurboQuote::updateQuoteNumberConfig(new QuoteNumberFormat(
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'
));
echo $config->format->startNumber; // 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

The token sets also ship as native enums (TurboDocx\Types\Enums\QuoteNumberYearToken, QuoteNumberMonthToken, QuoteNumberResetCadence) — pass QuoteNumberYearToken::FOUR->value if you prefer named cases over raw strings. 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.

Price Books on Quotes

applyPriceBook

Apply a price book to a quote, updating matching line item prices.

$result = TurboQuote::applyPriceBook('quote-uuid', 'pricebook-uuid');
// $result->quote
// $result->message
// $result->updatedCount — number of items re-priced
// $result->skippedCount — items with no matching pricebook entry

removePriceBook

$quote = TurboQuote::removePriceBook('quote-uuid');

Line Items

listLineItems

use TurboDocx\Types\Requests\Quote\ListLineItemsRequest;

$page = TurboQuote::listLineItems('quote-uuid', new ListLineItemsRequest(
limit: 50,
));
foreach ($page->results as $item) {
echo " {$item->productName}: {$item->unitPrice} x {$item->quantity}\n";
}

addLineItems

Pass a single AddLineItemRequest or an array of them.

use TurboDocx\Types\Requests\Quote\AddLineItemRequest;

$items = TurboQuote::addLineItems('quote-uuid', [
new AddLineItemRequest(
productId: 'product-uuid-1',
productName: 'Platform Subscription',
unitPrice: 199.00,
billingFrequency: 'monthly',
quantity: 2,
),
new AddLineItemRequest(
productId: 'product-uuid-2',
productName: 'Onboarding',
unitPrice: 499.00,
billingFrequency: 'one-time',
quantity: 1,
),
]);

addBundleLineItems

Pass a single AddBundleLineItemRequest or an array of them.

use TurboDocx\Types\Requests\Quote\AddBundleLineItemRequest;

$items = TurboQuote::addBundleLineItems('quote-uuid', [
new AddBundleLineItemRequest(
bundleId: 'bundle-uuid-1',
bundleName: 'Starter Bundle',
quantity: 1,
),
new AddBundleLineItemRequest(
bundleId: 'bundle-uuid-2',
bundleName: 'Premium Add-ons',
quantity: 2,
),
]);

updateLineItem

use TurboDocx\Types\Requests\Quote\UpdateLineItemRequest;

$item = TurboQuote::updateLineItem('quote-uuid', 'item-uuid', new UpdateLineItemRequest(
quantity: 3,
unitPrice: 189.00,
));

removeLineItem

$result = TurboQuote::removeLineItem('quote-uuid', 'item-uuid');
echo $result->message;

Products

MethodSignatureReturns
listProducts(?ListProductsRequest)ProductListResponse
createProduct(CreateProductRequest)Product
getProduct(string $id)Product
updateProduct(string $id, UpdateProductRequest)Product
deleteProduct(string $id)MessageResponse
duplicateProduct(string $id)Product
getProductPrimaryImages(string[] $productIds)array<string, mixed|null>
use TurboDocx\Types\Requests\Quote\CreateProductRequest;
use TurboDocx\Types\Requests\Quote\ListProductsRequest;

// List products
$page = TurboQuote::listProducts(new ListProductsRequest(limit: 20, query: 'license'));

// Create a product
$product = TurboQuote::createProduct(new CreateProductRequest(
name: 'Enterprise Seat',
listPrice: 299.00,
billingFrequency: 'monthly',
categoryId: 'category-uuid',
sku: 'ENT-001',
));

// Fetch primary images for a set of product IDs
$images = TurboQuote::getProductPrimaryImages(['product-uuid-1', 'product-uuid-2']);
// $images['product-uuid-1'] => image array or null
Product images

When CreateProductRequest or UpdateProductRequest includes an images key (array of file paths or raw bytes), the SDK automatically switches to multipart form upload. The MIME type is detected from magic bytes on the server side.

Bundles

MethodSignatureReturns
listBundles(?ListBundlesRequest)BundleListResponse
createBundle(CreateBundleRequest)Bundle
getBundle(string $id)Bundle
updateBundle(string $id, UpdateBundleRequest)Bundle
deleteBundle(string $id)MessageResponse
duplicateBundle(string $id)Bundle
use TurboDocx\Types\Requests\Quote\CreateBundleRequest;

$bundle = TurboQuote::createBundle(new CreateBundleRequest(
name: 'Starter Kit',
categoryId: 'category-uuid',
items: [
['productId' => 'product-uuid-1', 'quantity' => 1],
['productId' => 'product-uuid-2', 'quantity' => 2],
],
));

$copy = TurboQuote::duplicateBundle($bundle->id);

Price Books

MethodSignatureReturns
listPriceBooks(?ListPriceBooksRequest)PriceBookListResponse
createPriceBook(CreatePriceBookRequest)PriceBook
getPriceBook(string $id)PriceBook
updatePriceBook(string $id, UpdatePriceBookRequest)PriceBook
deletePriceBook(string $id)MessageResponse
duplicatePriceBook(string $id)PriceBook
listPriceBookProducts(string $id, ?ListPriceBookProductsRequest)PriceBookProductListResponse
use TurboDocx\Types\Requests\Quote\CreatePriceBookRequest;

$pb = TurboQuote::createPriceBook(new CreatePriceBookRequest(
name: 'Partner Discount',
priceBookTypeId: 'pricebook-type-uuid',
validFrom: '2026-01-01',
discountPercent: 15.0,
));

// List products enrolled in the price book
$products = TurboQuote::listPriceBookProducts($pb->id);

Companies

MethodSignatureReturns
listCompanies(?ListCompaniesRequest)CompanyListResponse
createCompany(CreateCompanyRequest)Company
getCompany(string $id)Company
updateCompany(string $id, UpdateCompanyRequest)Company
deleteCompany(string $id)MessageResponse
listCompanyContacts(string $companyId, ?ListContactsRequest)ContactListResponse
use TurboDocx\Types\Requests\Quote\CreateCompanyRequest;

// A company requires at least one contact on creation
$company = TurboQuote::createCompany(new CreateCompanyRequest(
name: 'Acme Corp',
contacts: [
['name' => 'Jane Doe', 'email' => 'jane@acme.com'],
],
));

$contacts = TurboQuote::listCompanyContacts($company->id);

Contacts

MethodSignatureReturns
listContacts(?ListContactsRequest)ContactListResponse
createContact(CreateContactRequest)Contact
updateContact(string $id, UpdateContactRequest)Contact
deleteContact(string $id)MessageResponse
No getContact

There is no getContact($id) — the backend does not expose a GET /v1/contacts/:id endpoint. Use listContacts with a search query or listCompanyContacts instead.

use TurboDocx\Types\Requests\Quote\CreateContactRequest;

$contact = TurboQuote::createContact(new CreateContactRequest(
name: 'John Smith',
companyId: 'company-uuid',
email: 'john@acme.com',
));

Quote Templates

MethodSignatureReturns
listTemplates(?ListTemplatesRequest)QuoteTemplateListResponse
getTemplate()QuoteTemplate
getTemplateById(string $id)QuoteTemplate
createTemplate(CreateQuoteTemplateRequest)QuoteTemplate
updateTemplate(string $id, UpdateQuoteTemplateRequest)QuoteTemplate
deleteTemplate(string $id)MessageResponse
getTemplate vs getTemplateById

getTemplate() (no argument) hits GET /v1/quote-template (singular) and returns the org's currently active template. getTemplateById($id) hits GET /v1/quote-templates/:id and returns any specific template by ID.

// Get the active org template
$active = TurboQuote::getTemplate();
echo $active->name;

// List all templates
$templates = TurboQuote::listTemplates();

Types / Categories

MethodSignatureReturns
listTypes(?ListTypesRequest)QuoteTypeListResponse
createType(CreateQuoteTypeRequest)QuoteType
updateType(string $id, UpdateQuoteTypeRequest)QuoteType
deleteType(string $id)MessageResponse
No getType

There is no getType($id) — the backend does not expose a GET /v1/types/:id endpoint. Use listTypes instead.

use TurboDocx\Types\Requests\Quote\CreateQuoteTypeRequest;

$type = TurboQuote::createType(new CreateQuoteTypeRequest(name: 'Renewal'));

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 the same typed request objects as that entity's single-create method (e.g. bulkCreateProducts takes an array of 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 returns a typed BulkImportResult:

  • $result->imported — count of rows created
  • $result->failed — array of BulkImportRowIssue (row + reason) for rows that did not import; row is the 1-indexed position in your request array
  • $result->adjusted — array 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.

use TurboDocx\Types\Requests\Quote\CreateProductRequest;

$result = TurboQuote::bulkCreateProducts([
new CreateProductRequest(
name: 'Enterprise Seat',
listPrice: 299.00,
billingFrequency: 'monthly',
),
new CreateProductRequest(
name: 'Onboarding Package',
listPrice: 499.00,
billingFrequency: 'one-time',
),
]);

echo "Imported {$result->imported} of 2 rows\n";
foreach ($result->failed as $failure) {
echo "Row {$failure->row} failed: {$failure->reason}\n";
}
foreach ($result->adjusted as $adjustment) {
echo "Row {$adjustment->row} imported with adjustment: {$adjustment->reason}\n";
}

The other five bulk methods follow the exact same pattern:

MethodRowsReturns
bulkCreatePriceBooksCreatePriceBookRequest[]BulkImportResult
bulkCreateBundlesCreateBundleRequest[]BulkImportResult
bulkCreateCompaniesCreateCompanyRequest[] — each row needs contacts (min. 1)BulkImportResult
bulkCreateContactsCreateContactRequest[] — each row needs companyIdBulkImportResult
bulkCreateTypesCreateQuoteTypeRequest[]BulkImportResult

Convenience Method

createAndSend

Creates a quote, optionally adds line items and bundle items, then sends it — all in a single call.

use TurboDocx\Types\Requests\Quote\CreateAndSendRequest;
use TurboDocx\Types\Requests\Quote\AddLineItemRequest;
use TurboDocx\Types\Requests\Quote\AddBundleLineItemRequest;
use TurboDocx\Types\Requests\Quote\SendQuoteRequest;

$result = TurboQuote::createAndSend(new CreateAndSendRequest(
name: 'Annual Plan',
companyId: 'company-uuid',
contactId: 'contact-uuid',
validUntil: '2026-12-31',
items: [
new AddLineItemRequest(
productId: 'product-uuid',
productName: 'Annual License',
unitPrice: 999.00,
billingFrequency: 'annual',
quantity: 10,
),
],
bundleItems: [
new AddBundleLineItemRequest(
bundleId: 'bundle-uuid',
bundleName: 'Annual Bundle',
quantity: 1,
),
],
send: new SendQuoteRequest(),
));

echo "Sent quote: {$result->quote->id}\n";

Error Handling

use TurboDocx\Exceptions\TurboDocxException;
use TurboDocx\Exceptions\AuthenticationException;
use TurboDocx\Exceptions\AuthorizationException;
use TurboDocx\Exceptions\ValidationException;
use TurboDocx\Exceptions\NotFoundException;
use TurboDocx\Exceptions\RateLimitException;
use TurboDocx\Exceptions\NetworkException;

try {
$quote = TurboQuote::createQuote(new CreateQuoteRequest(
name: 'Q3 Proposal',
companyId: 'company-uuid',
contactId: 'contact-uuid',
));
} catch (ValidationException $e) {
// 400 — missing required fields, invalid values
echo "Validation error: {$e->getMessage()}\n";
} catch (AuthenticationException $e) {
// 401 — missing or revoked API key
echo "Auth failed: {$e->getMessage()}\n";
} catch (AuthorizationException $e) {
// 403 — key exists but lacks permission
echo "Forbidden: {$e->getMessage()}\n";
} catch (NotFoundException $e) {
// 404 — quote, company, product, etc. not found
echo "Not found: {$e->getMessage()}\n";
} catch (RateLimitException $e) {
// 429 — back off and retry
echo "Rate limited: {$e->getMessage()}\n";
} catch (NetworkException $e) {
// request never reached the server
echo "Network error: {$e->getMessage()}\n";
} catch (TurboDocxException $e) {
// catch-all for any other typed SDK error
echo "Error {$e->statusCode}: {$e->getMessage()}\n";
}

Common Error Codes

StatusExceptionWhen
400ValidationExceptionMissing required fields, invalid enum value, malformed body
401AuthenticationExceptionMissing or invalid API key
403AuthorizationExceptionValid key without sufficient permissions
404NotFoundExceptionQuote, product, company, etc. does not exist
409TurboDocxExceptionConflict (e.g., duplicate SKU)
429RateLimitExceptionRate limit exceeded — back off

See Also