Skip to main content

Pipedrive

Prerequisites

  • A PolyDoc account and API key. Sign up for free, no credit card required; the free plan includes 150 PDF conversions per month.
  • A PolyDoc invoice template. Note its short ID for the source field (e.g. [template:jlE-whg]).
  • A Pipedrive account with an API token (Settings, Personal preferences, API) and webhook access. Start a free trial while you build this flow.

Use Case 1: Compliant E-Invoice on a Won Deal

Turn a won Pipedrive deal into a compliant e-invoice: a single PDF/A-3 that carries the human-readable invoice and the embedded Factur-X / ZUGFeRD XML, then deliver it to your customer.
Pipedrive (deal Won) → your handler → fetch products + buyer → PolyDoc API → email / upload

Set up the webhook trigger and an API token

Create the trigger. In Pipedrive, go to Settings → Tools and apps → Webhooks → Create new webhook, set Event action to Change and Event object to Deal (the v2 equivalent of the old updated.deal; act when the deal's status becomes won), and point it at your handler URL. Set an HTTP Basic Auth username and password on the webhook. Pipedrive will send those on every delivery so your handler can authenticate the request (see Step 2).

app.pipedrive.com

Verify the webhook

Your handler receives the POST. Unlike most webhook providers, Pipedrive does not HMAC-sign the body. Instead it sends the HTTP Basic Auth credentials you set in Step 1 in the Authorization header. Compare them in constant time and serve the endpoint only over HTTPS:

import crypto from "node:crypto";

function verifyPipedriveWebhook(request) {
const got = request.headers["authorization"] ?? "";
const expected =
"Basic " +
Buffer.from(
`${process.env.PIPEDRIVE_WEBHOOK_USER}:${process.env.PIPEDRIVE_WEBHOOK_PASSWORD}`,
).toString("base64");
return (
got.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))
);
}

if (!verifyPipedriveWebhook(request)) {
return new Response("unauthorized", { status: 401 });
}

const event = JSON.parse(rawBody);
if (event.data?.status !== "won") return; // only invoice won deals

Fetch the deal's products and buyer

The webhook gives you the deal, but the line items and the buyer come from separate calls. Read the deal's products, plus the linked person and organization, with the API token from Step 1:

# Line items attached to the deal
curl "https://your-company.pipedrive.com/api/v2/deals/$DEAL_ID/products" \
-H "x-api-token: $PIPEDRIVE_API_TOKEN"

# Buyer: the linked person (B2C) and organization (B2B)
curl "https://your-company.pipedrive.com/api/v2/persons/$PERSON_ID" \
-H "x-api-token: $PIPEDRIVE_API_TOKEN"
curl "https://your-company.pipedrive.com/api/v2/organizations/$ORG_ID" \
-H "x-api-token: $PIPEDRIVE_API_TOKEN"

Map the deal and call PolyDoc

Map the deal to PolyDoc's eInvoice block. The seller is your own business (set it once). Prefer the linked organization as the buyer for B2B, falling back to the person:

PolyDoc eInvoice.invoicePipedrive sourceTransform
numberdeal custom field, else deal iddeals have no native invoice number
issueDatedeal won_time.slice(0,10) (datetime string)
dueDatewon_time + payment term, or a custom fieldsee the BR-CO-25 note below
currencyCodedeal currency.toUpperCase()
buyer.nameorganization name, else person nameprefer organization
buyer.addressorganization address_* partsaddress_postal_code→postalCode; map address_country (a name) to ISO-2
lines[]/deals/{id}/products.map() (below)
lines[].quantityproduct quantityNumber(...) (numeric, no /100)
lines[].unitPriceproduct item_price + tax_methodinclusive divides out the tax; else direct
lines[].vatRateproduct taxdirect (already a percent)
totalNet / Tax / Grosscomputed from the linesdo not assume deal value is net or gross

Then POST to PolyDoc. source renders the visual invoice from your template; eInvoice generates and embeds the CII XML; verify: true runs VeraPDF and fails the request if the output isn't PDF/A-3b compliant:

const deal = event.data; // the deal from the webhook payload
const buyer = organization ?? person; // prefer the organization
const issueDate = (deal.won_time ?? deal.add_time).slice(0, 10);
const contactEmail = person?.emails?.find((e) => e.primary)?.value;

const lines = dealProducts.map((p) => {
const rate = Number(p.tax ?? 0); // percent, e.g. 19
const listed = Number(p.item_price);
const unitPrice =
p.tax_method === "inclusive" ? listed / (1 + rate / 100) : listed;
const quantity = Number(p.quantity);
return {
description: p.name,
quantity,
unitCode: "C62", // "HUR" for hours, "DAY" for days, etc.
unitPrice,
lineTotal: unitPrice * quantity,
vatRate: rate,
vatCategoryCode: rate > 0 ? "S" : "Z",
};
});

const totalNet = lines.reduce((s, l) => s + l.lineTotal, 0);
const totalTax = lines.reduce((s, l) => s + l.lineTotal * (l.vatRate / 100), 0);
const dueDate = new Date(new Date(issueDate).getTime() + 30 * 864e5)
.toISOString()
.slice(0, 10);

const body = {
source: "[template:YOUR_TEMPLATE_ID]",
templateData: {
invoice_number: deal.invoice_number ?? String(deal.id),
invoice_date: issueDate,
invoice_total: totalNet + totalTax,
customer_name: buyer.name,
customer_email: contactEmail,
items: lines.map((l) => ({
quantity: l.quantity,
name: l.description,
description: l.description,
price: l.unitPrice,
})),
},
eInvoice: {
standard: "facturx", // or "zugferd"
profile: "en16931",
verify: true,
invoice: {
number: String(deal.invoice_number ?? deal.id),
issueDate,
dueDate,
paymentTerms: "Net 30 days", // BT-9/BT-20: see note
currencyCode: (deal.currency ?? "EUR").toUpperCase(),
seller: YOUR_BUSINESS, // { name, address, taxId }: your own, set once
buyer: {
name: buyer.name,
email: contactEmail,
address: {
line1: organization?.address,
city: organization?.address_locality,
postalCode: organization?.address_postal_code,
countryCode: toIso2(organization?.address_country), // name -> ISO-2
},
},
lines,
totalNetAmount: totalNet,
totalTaxAmount: totalTax,
totalGrossAmount: totalNet + totalTax,
},
},
};

const res = await fetch("https://api.polydoc.tech/pdf/convert", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
"X-Sandbox": "true", // drop in production
},
body: JSON.stringify(body),
});
const pdf = Buffer.from(await res.arrayBuffer()); // the PDF/A-3 hybrid

Deliver the hybrid PDF

Deliver the hybrid PDF. Two common options:

a) Email it to the customer. Attach the PDF buffer from Step 4 to your transactional email (SES, Resend, Postmark, …). The single file is both the readable invoice and the compliant e-invoice, so one attachment covers both.

b) Attach it back to the deal. Upload the PDF to Pipedrive's Files API and link it to the deal, so the archive lives next to the record:

curl -X POST "https://your-company.pipedrive.com/api/v1/files" \
-H "x-api-token: $PIPEDRIVE_API_TOKEN" \
-F "file=@invoice.pdf;type=application/pdf" \
-F "deal_id=$DEAL_ID"

Use Case 2: Bulk Historical PDF/A-3 Archive

Back-fill a compliant PDF/A-3 archive for deals you've already won (useful for the EU's multi-year invoice-retention requirements), writing each archive straight to your own cloud bucket.
List won deals → PolyDoc API (+ cloudStorage) → PDF/A-3b in your bucket

List the deals to archive

Decide the window to archive (e.g. the last fiscal year) and list the won deals with the API token:

curl -G "https://your-company.pipedrive.com/api/v2/deals" \
-H "x-api-token: $PIPEDRIVE_API_TOKEN" \
-d "status=won" \
-d "limit=100"

Page through with the additional_data.next_cursor value from the response until you've collected every deal in range.

Generate and store each archive

For each deal, fetch its products and map it exactly as in Use Case 1, then POST to PolyDoc with a cloudStorage.presignedUrl: a pre-signed PUT URL you generate for your S3 / R2 / GCS / Azure bucket. PolyDoc renders, builds the hybrid, and PUTs it there; the response is just a status:

for (const deal of wonDeals) {
const body = await mapToPolydoc(deal); // Step 3 fetch + Step 4 mapping
body.cloudStorage = {
presignedUrl: await presignPut(`invoices/${body.eInvoice.invoice.number}.pdf`),
};

await fetch("https://api.polydoc.tech/pdf/convert", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
}

See the cloud-storage guides for generating presigned URLs per provider.

Index the archive

Keep a small index so you can find any archived invoice later. Map the invoice number to its archive location and retention deadline:

invoice_numberarchive_urlissuedretain_until
DEAL-2087s3://invoices/DEAL-2087.pdf2026-02-102036-02-10
DEAL-2091s3://invoices/DEAL-2091.pdf2026-02-142036-02-14