PDF

How do I make a quote PDF from my CRM data?

Map an approved CRM quote to a saved PDF design, check quantities and totals, and create a file you can review before sending.

Theo Whitfield 7 min read

Export an approved deal from your CRM, map its customer and line items to a quote template, and create a PDF. Keep the quote number and version with the file, so a later edit becomes a clear new revision. This guide uses one local JSON record and a saved thirds.ai design. Your CRM keeps control of prices, approval, and delivery. Each successful quote PDF costs one credit.

Where can I find quote templates?

The Price quote template includes client details, line items, a validity date, terms, and totals. You can also try five quote designs on the quote generator before you save one. Copy a template into your account and change the sample values.

This gallery sample shows a design project. The runnable recipe below uses a smaller campaign quote with different data.

Sample price quote with a blue side rule, item ledger, terms, and totals
Price quote templatePDF

Before you connect a CRM, check the longest client name and all item descriptions in the PDF. This saved template accepts one to ten item rows. A longer quote needs a layout and schema that support the extra rows.

Where can I download a free quotation template?

You can read the gallery's HTML and sample data without a paid plan. A verified Free account gets 50 credits each month. Hand edits and the live preview cost no credits. AI messages have separate costs, and free credits don't pay for them. See pricing before you choose an AI edit. The certificate recipe shows the same one-template, many-records pattern for a different document.

Open the template in the editor, keep its source and schema, and publish your copy. You can then use that copy for each approved quote with new data. The template data guide explains fields and exact versions.

Prepare the CRM record

This example uses Python 3 and curl on macOS or Linux. The script uses only the standard library. Set THIRDS_API_KEY through your secret store and set THIRDS_TEMPLATE_ID and THIRDS_TEMPLATE_VERSION to your published Price quote copy.

Your CRM export or private adapter must supply the shape below. The example doesn't connect to a named CRM. Save it as quote-input.json in a private folder and set THIRDS_WORK to that folder.

JSON
{
  "approved": true,
  "currency": "usd",
  "quote_number": "Q-SAMPLE-1042",
  "issued_on": "2026-10-12",
  "valid_until": "2026-11-12",
  "seller": {
    "name": "Fieldnote Studio",
    "address": "Sample studio address",
    "email": "hello@fieldnote.example"
  },
  "customer": {
    "name": "Northline Team",
    "address": "Sample client address"
  },
  "intro": "A sample quote for a campaign image set.",
  "lines": [
    {
      "name": "Campaign image layout",
      "quantity": 1,
      "unit_price_cents": 25000
    },
    {
      "name": "Image variant",
      "quantity": 3,
      "unit_price_cents": 5000
    }
  ],
  "tax_cents": 0,
  "tax_label": "Sample tax",
  "total_cents": 40000,
  "terms": "Sample terms: half before work, half on delivery. Amounts are in USD.",
  "footer": "Reply to discuss this sample quote. It is not a payment receipt."
}

Only a trusted approval step should set approved. The code checks that field, but a JSON value can't prove who approved the quote. Your app owns that check before it starts this workflow.

Map the approved values

Save this script as prepare-quote.py. It uses whole USD cents for calculations and checks the final total against the approved CRM value. The tax amount comes from the CRM. The script doesn't work out tax rates or decide which tax applies.

Python
"""Map one approved USD CRM quote to a saved price-quote template."""
import json
import sys
from pathlib import Path

fixture_path, template_id, version, output_path = sys.argv[1:]
try:
    deal = json.loads(Path(fixture_path).read_text())
    if deal["approved"] is not True:
        raise SystemExit("quote_not_approved")
    if deal["currency"] != "usd":
        raise SystemExit("unsupported_currency")
    items = []
    subtotal = 0
    for line in deal["lines"]:
        quantity, unit = line["quantity"], line["unit_price_cents"]
        if type(quantity) is not int or quantity < 1 or type(unit) is not int or unit < 0:
            raise SystemExit("invalid_amount")
        amount = quantity * unit
        subtotal += amount
        items.append({"description": line["name"], "quantity": quantity,
                      "unit_price": unit / 100, "amount": amount / 100})
    tax, total = deal["tax_cents"], deal["total_cents"]
    if any(type(value) is not int or value < 0 for value in [tax, total]):
        raise SystemExit("invalid_amount")
    if subtotal + tax != total:
        raise SystemExit("quote_total_mismatch")
    data = {
        "business_name": deal["seller"]["name"],
        "business_address": deal["seller"]["address"],
        "business_email": deal["seller"]["email"],
        "client_name": deal["customer"]["name"],
        "client_address": deal["customer"]["address"],
        "quote_number": deal["quote_number"],
        "issued_on": deal["issued_on"],
        "valid_until": deal["valid_until"],
        "intro": deal["intro"], "items": items,
        "subtotal": subtotal / 100, "tax_label": deal["tax_label"],
        "tax": tax / 100, "total": total / 100,
        "terms": deal["terms"], "footer": deal["footer"],
    }
    request = {"template_id": template_id, "version": int(version), "data": data,
               "pdf": {"format": "A4"}, "wait": False}
    Path(output_path).write_text(json.dumps(request, indent=2, allow_nan=False) + "\n")
except (KeyError, TypeError, ValueError, OSError):
    raise SystemExit("invalid_input") from None

The code maps seller and customer names, quote dates, item quantities, and prices to the saved design. It converts cents to the numeric dollar values that the template's currency filter prints. This adapter accepts whole item quantities and USD. A new currency or fractional quantity needs a reviewed adapter and matching template.

Make the file

The mapping command writes a local request. The first curl command creates one PDF and costs one credit on success. Use the quote number and revision as the Idempotency-Key:

Shell
umask 077
python3 "$THIRDS_WORK/prepare-quote.py" \
  "$THIRDS_WORK/quote-input.json" \
  "$THIRDS_TEMPLATE_ID" "$THIRDS_TEMPLATE_VERSION" \
  "$THIRDS_WORK/request.json"
curl --fail-with-body --silent --show-error https://thirds.ai/v1/pdf \
  -H "Authorization: Bearer $THIRDS_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: quote-Q-SAMPLE-1042-rev-1' \
  --data @"$THIRDS_WORK/request.json"

Save the id from the response with your quote record. Read the job until status is succeeded. Replace the sample ID with yours.

Shell
curl --fail-with-body --silent --show-error \
  https://thirds.ai/v1/pdf/pdf_00000000000000000000000000000000 \
  -H "Authorization: Bearer $THIRDS_API_KEY"

Copy download.url from that response and add https://thirds.ai before it. The download needs no API key.

Shell
curl --fail --silent --show-error \
  'https://thirds.ai/v1/downloads/YOUR_SIGNED_DOWNLOAD_TOKEN' \
  --output "$THIRDS_WORK/quote.pdf"
shasum -a 256 "$THIRDS_WORK/quote.pdf"

The hash must match artifact.sha256 in the job, and the file size must match artifact.byte_size.

Open quote.pdf. The sample has one layout at USD 250.00 and three variants at USD 50.00 each, for USD 400.00 total. Check the validity date and terms before delivery.

Keep one folder and one key per quote number and revision. If a create request times out, send it again with the same request and key. For a price change, approve a new quote revision and give it its own folder and key. Don't overwrite the original record or send a file before review. The API workflows guide covers this repeat-use pattern.

Resolve errors

Code or stateNext step
quote_not_approvedReturn the quote to your approval step.
quote_total_mismatchReconcile line totals and tax with the approved total.
unsupported_currencyUse the correct currency adapter and template.
invalid_amount or invalid_inputFix missing fields, negative values, or invalid types.
template_data_invalidCheck required fields, string lengths, and the ten-row limit.
idempotency_conflictUse the original request with its key. Give a new approved revision its own key.

Is there a quote template in Word?

You can make a quote layout in Word and save it as a reusable template. Microsoft's template guide explains that path.

Word fits a quote you edit by hand. A data-driven PDF workflow fits a quote your app fills from approved records. This recipe doesn't import a Word template. It fills the saved HTML design shown above.

Does Google Docs have a quote template?

Google Docs has a template gallery and a Make a copy command. Google's template help explains both. Not every account shows a quotation template, so check your gallery or copy a layout you own.

Use the Price quote template when you want the repeat PDF workflow in this guide. Make one sample and review the file before you connect approved CRM records. For the later billing step, the invoice workflow keeps invoices separate from quotes and receipts.

Make the file this guide describes

Start from a real template, put in your own words and colours, and click one button to get the finished file.