[{"data":1,"prerenderedAt":1239},["ShallowReactive",2],{"guide-\u002Fdocs\u002Fguides\u002Foperations\u002Fstoring-pdfs":3,"guides-all":1171},{"id":4,"title":5,"body":6,"category":1162,"description":1163,"extension":1164,"icon":1165,"meta":1166,"navigation":285,"order":144,"path":1167,"seo":1168,"stem":1169,"__hash__":1170},"guides\u002Fdocs\u002Fguides\u002Foperations\u002Fstoring-pdfs.md","Storing & serving the generated PDF",{"type":7,"value":8,"toc":1148},"minimark",[9,13,26,31,34,50,59,63,66,71,85,243,249,255,259,262,537,540,665,670,674,677,847,850,855,859,862,870,877,880,896,900,903,1053,1059,1066,1069,1096,1100,1103,1117,1120,1124,1144],[10,11,5],"h1",{"id":12},"storing-serving-the-generated-pdf",[14,15,16,20,21,25],"p",{},[17,18,19],"code",{},"POST \u002Fv1\u002Fgenerate"," returns a signed URL pointing at our CloudFront\u002FS3 distribution. The URL is ",[22,23,24],"strong",{},"short-lived"," by design — it's not a permanent storage solution. This page is the playbook for keeping PDFs around past the URL's expiry.",[27,28,30],"h2",{"id":29},"why-the-url-is-temporary","Why the URL is temporary",[14,32,33],{},"The signed URL has two properties:",[35,36,37,44],"ul",{},[38,39,40,43],"li",{},[22,41,42],{},"Time-limited"," — expires within the day, sometimes within the hour",[38,45,46,49],{},[22,47,48],{},"Account-scoped"," — we serve only the buckets we control",[14,51,52,53],{},"That's deliberate. It limits exposure if a URL leaks, and it lets us evict old PDFs without breaking your integration. The contract is: ",[22,54,55,58],{},[17,56,57],{},"\u002Fv1\u002Fgenerate"," is the source of truth; the URL is a one-shot delivery vehicle.",[27,60,62],{"id":61},"the-three-strategies","The three strategies",[14,64,65],{},"Pick the one that matches your retention needs.",[67,68,70],"h3",{"id":69},"strategy-1-re-generate-on-demand","Strategy 1 — Re-generate on demand",[14,72,73,74,76,77,80,81,84],{},"Don't store anything. Every time a user asks for the PDF, your backend calls ",[17,75,57],{}," again with the same ",[17,78,79],{},"documentId"," and ",[17,82,83],{},"variables",".",[86,87,92],"pre",{"className":88,"code":89,"language":90,"meta":91,"style":91},"language-ts shiki shiki-themes github-light github-dark","app.get('\u002Finvoices\u002F:id\u002Fpdf', async (req, res) => {\n  const invoice = await Invoice.findById(req.params.id)\n  const {url} = await generatePdf({\n    documentId: process.env.INVOICE_TEMPLATE_UUID!,\n    variables: invoice.toTemplateVariables(),\n  })\n  res.redirect(url)\n})\n","ts","",[17,93,94,142,167,192,207,219,225,237],{"__ignoreMap":91},[95,96,99,103,107,110,114,117,121,124,128,130,133,136,139],"span",{"class":97,"line":98},"line",1,[95,100,102],{"class":101},"sVt8B","app.",[95,104,106],{"class":105},"sScJk","get",[95,108,109],{"class":101},"(",[95,111,113],{"class":112},"sZZnC","'\u002Finvoices\u002F:id\u002Fpdf'",[95,115,116],{"class":101},", ",[95,118,120],{"class":119},"szBVR","async",[95,122,123],{"class":101}," (",[95,125,127],{"class":126},"s4XuR","req",[95,129,116],{"class":101},[95,131,132],{"class":126},"res",[95,134,135],{"class":101},") ",[95,137,138],{"class":119},"=>",[95,140,141],{"class":101}," {\n",[95,143,145,148,152,155,158,161,164],{"class":97,"line":144},2,[95,146,147],{"class":119},"  const",[95,149,151],{"class":150},"sj4cs"," invoice",[95,153,154],{"class":119}," =",[95,156,157],{"class":119}," await",[95,159,160],{"class":101}," Invoice.",[95,162,163],{"class":105},"findById",[95,165,166],{"class":101},"(req.params.id)\n",[95,168,170,172,175,178,181,184,186,189],{"class":97,"line":169},3,[95,171,147],{"class":119},[95,173,174],{"class":101}," {",[95,176,177],{"class":150},"url",[95,179,180],{"class":101},"} ",[95,182,183],{"class":119},"=",[95,185,157],{"class":119},[95,187,188],{"class":105}," generatePdf",[95,190,191],{"class":101},"({\n",[95,193,195,198,201,204],{"class":97,"line":194},4,[95,196,197],{"class":101},"    documentId: process.env.",[95,199,200],{"class":150},"INVOICE_TEMPLATE_UUID",[95,202,203],{"class":119},"!",[95,205,206],{"class":101},",\n",[95,208,210,213,216],{"class":97,"line":209},5,[95,211,212],{"class":101},"    variables: invoice.",[95,214,215],{"class":105},"toTemplateVariables",[95,217,218],{"class":101},"(),\n",[95,220,222],{"class":97,"line":221},6,[95,223,224],{"class":101},"  })\n",[95,226,228,231,234],{"class":97,"line":227},7,[95,229,230],{"class":101},"  res.",[95,232,233],{"class":105},"redirect",[95,235,236],{"class":101},"(url)\n",[95,238,240],{"class":97,"line":239},8,[95,241,242],{"class":101},"})\n",[14,244,245,248],{},[22,246,247],{},"When this works:"," PDFs are deterministic (same input = same output), and you have credit headroom. Low-traffic SaaS — pricing this against \"5k generations \u002F month\" plan is fine if each customer looks at their invoice 1–2 times a month.",[14,250,251,254],{},[22,252,253],{},"Tradeoff:"," every view costs a credit. Don't do this on a viral document.",[67,256,258],{"id":257},"strategy-2-generate-once-cache-to-your-bucket","Strategy 2 — Generate once, cache to your bucket",[14,260,261],{},"The standard pattern. Generate the PDF, immediately download and save it to your own S3 \u002F Cloud Storage \u002F Backblaze.",[86,263,265],{"className":88,"code":264,"language":90,"meta":91,"style":91},"import {S3Client, PutObjectCommand} from '@aws-sdk\u002Fclient-s3'\n\nconst {url} = await generatePdf({documentId: TEMPLATE_UUID, variables})\n\nconst pdfRes = await fetch(url)\nif (!pdfRes.ok) throw new Error(`download failed: ${pdfRes.status}`)\nconst buf = Buffer.from(await pdfRes.arrayBuffer())\n\nawait s3.send(new PutObjectCommand({\n  Bucket: 'invoices.acme.example',\n  Key: `2026\u002F05\u002F${invoice.number}.pdf`,\n  Body: buf,\n  ContentType: 'application\u002Fpdf',\n}))\n\nawait db.update(Invoice, invoice.id, {\n  pdfKey: `2026\u002F05\u002F${invoice.number}.pdf`,\n  generatedAt: new Date(),\n})\n",[17,266,267,281,287,313,317,333,373,401,405,426,437,459,465,476,482,487,501,519,532],{"__ignoreMap":91},[95,268,269,272,275,278],{"class":97,"line":98},[95,270,271],{"class":119},"import",[95,273,274],{"class":101}," {S3Client, PutObjectCommand} ",[95,276,277],{"class":119},"from",[95,279,280],{"class":112}," '@aws-sdk\u002Fclient-s3'\n",[95,282,283],{"class":97,"line":144},[95,284,286],{"emptyLinePlaceholder":285},true,"\n",[95,288,289,292,294,296,298,300,302,304,307,310],{"class":97,"line":169},[95,290,291],{"class":119},"const",[95,293,174],{"class":101},[95,295,177],{"class":150},[95,297,180],{"class":101},[95,299,183],{"class":119},[95,301,157],{"class":119},[95,303,188],{"class":105},[95,305,306],{"class":101},"({documentId: ",[95,308,309],{"class":150},"TEMPLATE_UUID",[95,311,312],{"class":101},", variables})\n",[95,314,315],{"class":97,"line":194},[95,316,286],{"emptyLinePlaceholder":285},[95,318,319,321,324,326,328,331],{"class":97,"line":209},[95,320,291],{"class":119},[95,322,323],{"class":150}," pdfRes",[95,325,154],{"class":119},[95,327,157],{"class":119},[95,329,330],{"class":105}," fetch",[95,332,236],{"class":101},[95,334,335,338,340,342,345,348,351,354,356,359,362,364,367,370],{"class":97,"line":221},[95,336,337],{"class":119},"if",[95,339,123],{"class":101},[95,341,203],{"class":119},[95,343,344],{"class":101},"pdfRes.ok) ",[95,346,347],{"class":119},"throw",[95,349,350],{"class":119}," new",[95,352,353],{"class":105}," Error",[95,355,109],{"class":101},[95,357,358],{"class":112},"`download failed: ${",[95,360,361],{"class":101},"pdfRes",[95,363,84],{"class":112},[95,365,366],{"class":101},"status",[95,368,369],{"class":112},"}`",[95,371,372],{"class":101},")\n",[95,374,375,377,380,382,385,387,389,392,395,398],{"class":97,"line":227},[95,376,291],{"class":119},[95,378,379],{"class":150}," buf",[95,381,154],{"class":119},[95,383,384],{"class":101}," Buffer.",[95,386,277],{"class":105},[95,388,109],{"class":101},[95,390,391],{"class":119},"await",[95,393,394],{"class":101}," pdfRes.",[95,396,397],{"class":105},"arrayBuffer",[95,399,400],{"class":101},"())\n",[95,402,403],{"class":97,"line":239},[95,404,286],{"emptyLinePlaceholder":285},[95,406,408,410,413,416,418,421,424],{"class":97,"line":407},9,[95,409,391],{"class":119},[95,411,412],{"class":101}," s3.",[95,414,415],{"class":105},"send",[95,417,109],{"class":101},[95,419,420],{"class":119},"new",[95,422,423],{"class":105}," PutObjectCommand",[95,425,191],{"class":101},[95,427,429,432,435],{"class":97,"line":428},10,[95,430,431],{"class":101},"  Bucket: ",[95,433,434],{"class":112},"'invoices.acme.example'",[95,436,206],{"class":101},[95,438,440,443,446,449,451,454,457],{"class":97,"line":439},11,[95,441,442],{"class":101},"  Key: ",[95,444,445],{"class":112},"`2026\u002F05\u002F${",[95,447,448],{"class":101},"invoice",[95,450,84],{"class":112},[95,452,453],{"class":101},"number",[95,455,456],{"class":112},"}.pdf`",[95,458,206],{"class":101},[95,460,462],{"class":97,"line":461},12,[95,463,464],{"class":101},"  Body: buf,\n",[95,466,468,471,474],{"class":97,"line":467},13,[95,469,470],{"class":101},"  ContentType: ",[95,472,473],{"class":112},"'application\u002Fpdf'",[95,475,206],{"class":101},[95,477,479],{"class":97,"line":478},14,[95,480,481],{"class":101},"}))\n",[95,483,485],{"class":97,"line":484},15,[95,486,286],{"emptyLinePlaceholder":285},[95,488,490,492,495,498],{"class":97,"line":489},16,[95,491,391],{"class":119},[95,493,494],{"class":101}," db.",[95,496,497],{"class":105},"update",[95,499,500],{"class":101},"(Invoice, invoice.id, {\n",[95,502,504,507,509,511,513,515,517],{"class":97,"line":503},17,[95,505,506],{"class":101},"  pdfKey: ",[95,508,445],{"class":112},[95,510,448],{"class":101},[95,512,84],{"class":112},[95,514,453],{"class":101},[95,516,456],{"class":112},[95,518,206],{"class":101},[95,520,522,525,527,530],{"class":97,"line":521},18,[95,523,524],{"class":101},"  generatedAt: ",[95,526,420],{"class":119},[95,528,529],{"class":105}," Date",[95,531,218],{"class":101},[95,533,535],{"class":97,"line":534},19,[95,536,242],{"class":101},[14,538,539],{},"Serving is then your responsibility — either a signed URL from your bucket, or a streaming endpoint:",[86,541,543],{"className":88,"code":542,"language":90,"meta":91,"style":91},"app.get('\u002Finvoices\u002F:id\u002Fpdf', async (req, res) => {\n  const invoice = await Invoice.findById(req.params.id)\n  const obj = await s3.send(new GetObjectCommand({\n    Bucket: 'invoices.acme.example',\n    Key: invoice.pdfKey,\n  }))\n  res.setHeader('Content-Type', 'application\u002Fpdf')\n  obj.Body.pipe(res)\n})\n",[17,544,545,573,589,613,622,627,632,650,661],{"__ignoreMap":91},[95,546,547,549,551,553,555,557,559,561,563,565,567,569,571],{"class":97,"line":98},[95,548,102],{"class":101},[95,550,106],{"class":105},[95,552,109],{"class":101},[95,554,113],{"class":112},[95,556,116],{"class":101},[95,558,120],{"class":119},[95,560,123],{"class":101},[95,562,127],{"class":126},[95,564,116],{"class":101},[95,566,132],{"class":126},[95,568,135],{"class":101},[95,570,138],{"class":119},[95,572,141],{"class":101},[95,574,575,577,579,581,583,585,587],{"class":97,"line":144},[95,576,147],{"class":119},[95,578,151],{"class":150},[95,580,154],{"class":119},[95,582,157],{"class":119},[95,584,160],{"class":101},[95,586,163],{"class":105},[95,588,166],{"class":101},[95,590,591,593,596,598,600,602,604,606,608,611],{"class":97,"line":169},[95,592,147],{"class":119},[95,594,595],{"class":150}," obj",[95,597,154],{"class":119},[95,599,157],{"class":119},[95,601,412],{"class":101},[95,603,415],{"class":105},[95,605,109],{"class":101},[95,607,420],{"class":119},[95,609,610],{"class":105}," GetObjectCommand",[95,612,191],{"class":101},[95,614,615,618,620],{"class":97,"line":194},[95,616,617],{"class":101},"    Bucket: ",[95,619,434],{"class":112},[95,621,206],{"class":101},[95,623,624],{"class":97,"line":209},[95,625,626],{"class":101},"    Key: invoice.pdfKey,\n",[95,628,629],{"class":97,"line":221},[95,630,631],{"class":101},"  }))\n",[95,633,634,636,639,641,644,646,648],{"class":97,"line":227},[95,635,230],{"class":101},[95,637,638],{"class":105},"setHeader",[95,640,109],{"class":101},[95,642,643],{"class":112},"'Content-Type'",[95,645,116],{"class":101},[95,647,473],{"class":112},[95,649,372],{"class":101},[95,651,652,655,658],{"class":97,"line":239},[95,653,654],{"class":101},"  obj.Body.",[95,656,657],{"class":105},"pipe",[95,659,660],{"class":101},"(res)\n",[95,662,663],{"class":97,"line":407},[95,664,242],{"class":101},[14,666,667,669],{},[22,668,247],{}," legal\u002Fcompliance requires a stable archive (you need to retain invoices for 7 years), or your read traffic dwarfs your write traffic.",[67,671,673],{"id":672},"strategy-3-stream-through-proxy","Strategy 3 — Stream-through proxy",[14,675,676],{},"Don't store, don't re-generate. Stream the PDF through your backend the first time, set a cache header.",[86,678,680],{"className":88,"code":679,"language":90,"meta":91,"style":91},"app.get('\u002Finvoices\u002F:id\u002Fpdf', async (req, res) => {\n  const invoice = await Invoice.findById(req.params.id)\n  const {url} = await generatePdf({documentId: TEMPLATE_UUID, variables: invoice.toVars()})\n\n  const pdfRes = await fetch(url)\n  res.setHeader('Content-Type', 'application\u002Fpdf')\n  res.setHeader('Content-Disposition', `attachment; filename=\"invoice-${invoice.number}.pdf\"`)\n  res.setHeader('Cache-Control', 'private, max-age=3600')\n  pdfRes.body.pipe(res)\n})\n",[17,681,682,710,726,755,759,773,789,816,834,843],{"__ignoreMap":91},[95,683,684,686,688,690,692,694,696,698,700,702,704,706,708],{"class":97,"line":98},[95,685,102],{"class":101},[95,687,106],{"class":105},[95,689,109],{"class":101},[95,691,113],{"class":112},[95,693,116],{"class":101},[95,695,120],{"class":119},[95,697,123],{"class":101},[95,699,127],{"class":126},[95,701,116],{"class":101},[95,703,132],{"class":126},[95,705,135],{"class":101},[95,707,138],{"class":119},[95,709,141],{"class":101},[95,711,712,714,716,718,720,722,724],{"class":97,"line":144},[95,713,147],{"class":119},[95,715,151],{"class":150},[95,717,154],{"class":119},[95,719,157],{"class":119},[95,721,160],{"class":101},[95,723,163],{"class":105},[95,725,166],{"class":101},[95,727,728,730,732,734,736,738,740,742,744,746,749,752],{"class":97,"line":169},[95,729,147],{"class":119},[95,731,174],{"class":101},[95,733,177],{"class":150},[95,735,180],{"class":101},[95,737,183],{"class":119},[95,739,157],{"class":119},[95,741,188],{"class":105},[95,743,306],{"class":101},[95,745,309],{"class":150},[95,747,748],{"class":101},", variables: invoice.",[95,750,751],{"class":105},"toVars",[95,753,754],{"class":101},"()})\n",[95,756,757],{"class":97,"line":194},[95,758,286],{"emptyLinePlaceholder":285},[95,760,761,763,765,767,769,771],{"class":97,"line":209},[95,762,147],{"class":119},[95,764,323],{"class":150},[95,766,154],{"class":119},[95,768,157],{"class":119},[95,770,330],{"class":105},[95,772,236],{"class":101},[95,774,775,777,779,781,783,785,787],{"class":97,"line":221},[95,776,230],{"class":101},[95,778,638],{"class":105},[95,780,109],{"class":101},[95,782,643],{"class":112},[95,784,116],{"class":101},[95,786,473],{"class":112},[95,788,372],{"class":101},[95,790,791,793,795,797,800,802,805,807,809,811,814],{"class":97,"line":227},[95,792,230],{"class":101},[95,794,638],{"class":105},[95,796,109],{"class":101},[95,798,799],{"class":112},"'Content-Disposition'",[95,801,116],{"class":101},[95,803,804],{"class":112},"`attachment; filename=\"invoice-${",[95,806,448],{"class":101},[95,808,84],{"class":112},[95,810,453],{"class":101},[95,812,813],{"class":112},"}.pdf\"`",[95,815,372],{"class":101},[95,817,818,820,822,824,827,829,832],{"class":97,"line":239},[95,819,230],{"class":101},[95,821,638],{"class":105},[95,823,109],{"class":101},[95,825,826],{"class":112},"'Cache-Control'",[95,828,116],{"class":101},[95,830,831],{"class":112},"'private, max-age=3600'",[95,833,372],{"class":101},[95,835,836,839,841],{"class":97,"line":407},[95,837,838],{"class":101},"  pdfRes.body.",[95,840,657],{"class":105},[95,842,660],{"class":101},[95,844,845],{"class":97,"line":428},[95,846,242],{"class":101},[14,848,849],{},"A CDN in front (Cloudflare, Fastly) caches by URL. Hit rate solves the \"every view costs a credit\" problem.",[14,851,852,854],{},[22,853,247],{}," simple stack, no S3 dependency, the PDF is only meaningful to one user (no shared link).",[27,856,858],{"id":857},"dont-store-the-signed-url-itself","Don't store the signed URL itself",[14,860,861],{},"This is the most common mistake. Storing the URL in your database means:",[35,863,864,867],{},[38,865,866],{},"Tomorrow it 403s (signature expired)",[38,868,869],{},"Customer-facing emails point to broken URLs",[14,871,872,873,876],{},"If your code looks like ",[17,874,875],{},"invoice.pdfUrl = response.url",", replace that pattern.",[14,878,879],{},"Store either:",[35,881,882,889],{},[38,883,884,885,888],{},"The ",[22,886,887],{},"PDF bytes"," in your bucket (Strategy 2)",[38,890,891,892,895],{},"Or just the ",[22,893,894],{},"invoice id + template UUID + variables"," so you can regenerate on demand (Strategy 1)",[27,897,899],{"id":898},"mailable-attachments","Mailable attachments",[14,901,902],{},"PDFs in emails are downloaded once and live in the recipient's mailbox. Generate inline:",[86,904,906],{"className":88,"code":905,"language":90,"meta":91,"style":91},"\u002F\u002F Pseudo-mailer\nconst {url} = await generatePdf({documentId, variables})\nconst pdf = await fetch(url).then(r => r.arrayBuffer())\n\nawait mailer.send({\n  to: customer.email,\n  subject: `Invoice ${invoice.number}`,\n  body: `Your invoice is attached.`,\n  attachments: [\n    {filename: `invoice-${invoice.number}.pdf`, content: Buffer.from(pdf)},\n  ],\n})\n",[17,907,908,914,933,967,971,982,987,1005,1015,1020,1044,1049],{"__ignoreMap":91},[95,909,910],{"class":97,"line":98},[95,911,913],{"class":912},"sJ8bj","\u002F\u002F Pseudo-mailer\n",[95,915,916,918,920,922,924,926,928,930],{"class":97,"line":144},[95,917,291],{"class":119},[95,919,174],{"class":101},[95,921,177],{"class":150},[95,923,180],{"class":101},[95,925,183],{"class":119},[95,927,157],{"class":119},[95,929,188],{"class":105},[95,931,932],{"class":101},"({documentId, variables})\n",[95,934,935,937,940,942,944,946,949,952,954,957,960,963,965],{"class":97,"line":169},[95,936,291],{"class":119},[95,938,939],{"class":150}," pdf",[95,941,154],{"class":119},[95,943,157],{"class":119},[95,945,330],{"class":105},[95,947,948],{"class":101},"(url).",[95,950,951],{"class":105},"then",[95,953,109],{"class":101},[95,955,956],{"class":126},"r",[95,958,959],{"class":119}," =>",[95,961,962],{"class":101}," r.",[95,964,397],{"class":105},[95,966,400],{"class":101},[95,968,969],{"class":97,"line":194},[95,970,286],{"emptyLinePlaceholder":285},[95,972,973,975,978,980],{"class":97,"line":209},[95,974,391],{"class":119},[95,976,977],{"class":101}," mailer.",[95,979,415],{"class":105},[95,981,191],{"class":101},[95,983,984],{"class":97,"line":221},[95,985,986],{"class":101},"  to: customer.email,\n",[95,988,989,992,995,997,999,1001,1003],{"class":97,"line":227},[95,990,991],{"class":101},"  subject: ",[95,993,994],{"class":112},"`Invoice ${",[95,996,448],{"class":101},[95,998,84],{"class":112},[95,1000,453],{"class":101},[95,1002,369],{"class":112},[95,1004,206],{"class":101},[95,1006,1007,1010,1013],{"class":97,"line":239},[95,1008,1009],{"class":101},"  body: ",[95,1011,1012],{"class":112},"`Your invoice is attached.`",[95,1014,206],{"class":101},[95,1016,1017],{"class":97,"line":407},[95,1018,1019],{"class":101},"  attachments: [\n",[95,1021,1022,1025,1028,1030,1032,1034,1036,1039,1041],{"class":97,"line":428},[95,1023,1024],{"class":101},"    {filename: ",[95,1026,1027],{"class":112},"`invoice-${",[95,1029,448],{"class":101},[95,1031,84],{"class":112},[95,1033,453],{"class":101},[95,1035,456],{"class":112},[95,1037,1038],{"class":101},", content: Buffer.",[95,1040,277],{"class":105},[95,1042,1043],{"class":101},"(pdf)},\n",[95,1045,1046],{"class":97,"line":439},[95,1047,1048],{"class":101},"  ],\n",[95,1050,1051],{"class":97,"line":461},[95,1052,242],{"class":101},[14,1054,1055,1056,1058],{},"Don't link to ",[17,1057,57],{}," URLs in emails — the URL expires before the user opens the inbox.",[27,1060,1062,1063,1065],{"id":1061},"what-to-do-when-v1generate-is-slow","What to do when ",[17,1064,57],{}," is slow",[14,1067,1068],{},"Render time is usually 200–600 ms depending on template complexity (number of fonts, chart count, page count). If it's a hot path:",[35,1070,1071,1080,1086],{},[38,1072,1073,1076,1077,1079],{},[22,1074,1075],{},"Pre-generate on creation, not on view."," When the invoice is finalized, queue a job that calls ",[17,1078,57],{}," and stashes the bytes (Strategy 2). Reads then never wait.",[38,1081,1082,1085],{},[22,1083,1084],{},"Queue the call"," with BullMQ \u002F Celery \u002F Sidekiq. Don't block the HTTP request.",[38,1087,1088,1091,1092,1095],{},[22,1089,1090],{},"Cache aggressively"," if the same ",[17,1093,1094],{},"documentId + variables"," is hit by multiple users (rare for transactional, common for marketing one-pagers).",[27,1097,1099],{"id":1098},"long-term-storage-compliance","Long-term storage compliance",[14,1101,1102],{},"For regulated industries (finance, healthcare):",[35,1104,1105,1108,1111,1114],{},[38,1106,1107],{},"Strategy 2 is the only one that makes the auditor happy.",[38,1109,1110],{},"Encrypt at rest (S3 SSE-KMS or your bucket's equivalent).",[38,1112,1113],{},"Set lifecycle policies (e.g. Glacier after 90 days, delete after 7 years).",[38,1115,1116],{},"Log every read to your own access log.",[14,1118,1119],{},"We don't currently offer a \"long-term archive\" tier — that's by design. Your bucket, your compliance boundary.",[27,1121,1123],{"id":1122},"next-steps","Next steps",[35,1125,1126,1135],{},[38,1127,1128],{},[22,1129,1130],{},[1131,1132,1134],"a",{"href":1133},"\u002Fdocs\u002Fguides\u002Foperations\u002Fmonitoring-usage","Monitoring usage & credits →",[38,1136,1137,1143],{},[22,1138,1139],{},[1131,1140,1142],{"href":1141},"\u002Fdocs\u002Fguides\u002Fintegrations\u002Fnode-bun","Calling \u002Fv1\u002Fgenerate from Node →"," — the full retry\u002Ferror wrapper used in these examples.",[1145,1146,1147],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":91,"searchDepth":144,"depth":144,"links":1149},[1150,1151,1156,1157,1158,1160,1161],{"id":29,"depth":144,"text":30},{"id":61,"depth":144,"text":62,"children":1152},[1153,1154,1155],{"id":69,"depth":169,"text":70},{"id":257,"depth":169,"text":258},{"id":672,"depth":169,"text":673},{"id":857,"depth":144,"text":858},{"id":898,"depth":144,"text":899},{"id":1061,"depth":144,"text":1159},"What to do when \u002Fv1\u002Fgenerate is slow",{"id":1098,"depth":144,"text":1099},{"id":1122,"depth":144,"text":1123},"operations","The \u002Fv1\u002Fgenerate URL is signed and short-lived. Patterns for keeping the PDF around — your bucket, your CDN, your DB.","md","hard-drive",{},"\u002Fdocs\u002Fguides\u002Foperations\u002Fstoring-pdfs",{"title":5,"description":1163},"docs\u002Fguides\u002Foperations\u002Fstoring-pdfs","OtGwVHKu_XNITTTKijpVWDZmLxvMRP7pzj-uYeRCKHw",[1172,1177,1181,1185,1189,1193,1197,1201,1206,1210,1214,1218,1223,1227,1231,1235,1238],{"path":1173,"title":1174,"description":1175,"category":1176,"order":209},"\u002Fdocs\u002Fguides\u002Fai-mcp\u002Fchatgpt","ChatGPT (via Custom GPT Actions)","ChatGPT doesn't speak MCP natively yet — but you can give it the same powers through a Custom GPT pointed at the Transactional REST API.","ai-mcp",{"path":1178,"title":1179,"description":1180,"category":1176,"order":169},"\u002Fdocs\u002Fguides\u002Fai-mcp\u002Fclaude-code","Claude Code (CLI)","Connect Transactional to Claude Code so it can read your templates and generate PDFs from your terminal.",{"path":1182,"title":1183,"description":1184,"category":1176,"order":144},"\u002Fdocs\u002Fguides\u002Fai-mcp\u002Fclaude-desktop","Claude Desktop","Connect Transactional to Claude Desktop on macOS or Windows so Claude can read your templates and generate PDFs.",{"path":1186,"title":1187,"description":1188,"category":1176,"order":194},"\u002Fdocs\u002Fguides\u002Fai-mcp\u002Fcursor","Cursor","Wire Transactional into Cursor's MCP support so you can generate PDFs from inside your editor.",{"path":1190,"title":1191,"description":1192,"category":1176,"order":221},"\u002Fdocs\u002Fguides\u002Fai-mcp\u002Fgemini","Gemini Code Assist \u002F Gemini CLI","Connect Transactional to Google's Gemini agents through their MCP support.",{"path":1194,"title":1195,"description":1196,"category":1176,"order":227},"\u002Fdocs\u002Fguides\u002Fai-mcp\u002Ftools-reference","MCP tools reference","Every tool the Transactional MCP server exposes, with arguments, return shapes, and a prompt that typically triggers each one.",{"path":1198,"title":1199,"description":1200,"category":1176,"order":98},"\u002Fdocs\u002Fguides\u002Fai-mcp\u002Fuse-from-ai","Use Transactional from your AI assistant","Connect Transactional to Claude, Cursor, ChatGPT, or Gemini via MCP so your assistant can read your templates and generate PDFs directly.",{"path":1202,"title":1203,"description":1204,"category":1205,"order":144},"\u002Fdocs\u002Fguides\u002Fauthoring\u002Fdesign-for-pdf","Designing templates that survive PDF rendering","PDFs are static — drop the animations, oversample your canvas charts, lean on vectors. The rules that make a template look sharp at print resolution.","authoring",{"path":1207,"title":1208,"description":1209,"category":1205,"order":98},"\u002Fdocs\u002Fguides\u002Fauthoring\u002Fhandlebars","Handlebars cheat sheet","The exact subset of Handlebars supported in Transactional templates — variables, conditionals, loops, and what NOT to reach for.",{"path":1211,"title":1212,"description":1213,"category":1205,"order":169},"\u002Fdocs\u002Fguides\u002Fauthoring\u002Fmodeling-variables","Modeling your variables","When to make something a variable vs. inline. Keep the API contract small, your templates portable, and your integration code boring.",{"path":1215,"title":1216,"description":1217,"category":1205,"order":194},"\u002Fdocs\u002Fguides\u002Fauthoring\u002Fworking-with-ai","Working with the AI assistant","Prompts and patterns to get good templates fast — what to ask, when to iterate, when to start over.",{"path":1219,"title":1220,"description":1221,"category":1222,"order":98},"\u002Fdocs\u002Fguides\u002Fgetting-started\u002Fquickstart","Quickstart — your first PDF in 5 minutes","Sign up, design a template, render your first PDF through the API. End-to-end in five minutes.","getting-started",{"path":1141,"title":1224,"description":1225,"category":1226,"order":98},"Calling \u002Fv1\u002Fgenerate from Node.js & Bun","Production-grade integration using native fetch — retries, error handling, streaming the PDF to your storage.","integrations",{"path":1228,"title":1229,"description":1230,"category":1226,"order":144},"\u002Fdocs\u002Fguides\u002Fintegrations\u002Fphp-laravel","Calling \u002Fv1\u002Fgenerate from PHP & Laravel","cURL extension, Guzzle, or Laravel's HTTP client — render PDFs with retries and proper error handling.",{"path":1232,"title":1233,"description":1234,"category":1226,"order":169},"\u002Fdocs\u002Fguides\u002Fintegrations\u002Fpython","Calling \u002Fv1\u002Fgenerate from Python","urllib (stdlib), requests, or httpx with retry — render PDFs from Django, FastAPI, or any Python service.",{"path":1133,"title":1236,"description":1237,"category":1162,"order":98},"Monitoring usage & credits","Read the dashboard gauges, set sane alerts, and decide when to top up vs. upgrade.",{"path":1167,"title":5,"description":1163,"category":1162,"order":144},1780347734449]