Generating PDFs

Anvil allows you to dynamically generate new PDFs using JSON data you provide via the /api/v1/generate-pdf REST endpoint. You can generate PDFs from your own HTML and CSS, or structured Markdown.

PDF generation is useful for agreements, invoices, disclosures, or any other text-heavy documents.

Generate Invoice PDF Example

Features

  • Number of pages are dynamically generated to fit your content.
  • Documents are generated with customizable page width, height, and margin, with 8.5" x 11" pages as the default.
  • Convert your HTML and CSS into PDFs.
  • Supports Markdown formatting, including tables, images, headings, etc.
  • Supports custom fonts in both HTML to PDF and Markdown to PDF.

Authentication

First you will need an API key. You can find your API key on the Organization Settings -> API Settings page. We provide language-specific API clients that wrap authentication and make using our API easier.

For more information on generating an API key and handling API authentication, check out the API getting started article.

Postman Collection

Quickly see the PDF Generation endpoint in action with our PDF generation Postman collection.

Generating a PDF

POST to /api/v1/generate-pdf with the data you want to embed in the PDF. Anvil will respond with the raw binary data for the generated PDF.

There are two types of data accepted by our PDF generation API: HTML / CSS and Markdown. Both approaches are covered in the following sections.

POST https://app.useanvil.com/api/v1/generate-pdf
{
// Optional - set the type of generation
// Accepts `html` or `markdown` (default)
"type": "markdown",
// Optional - set the title encoded into the PDF document
"title": "Example Invoice",
// Required - the data to actually generate the PDF
// `data` accepts either HTML/CSS or Markdown in a structured JSON format.
// See the sections below for details.
"data": ...
// Optional - page settings
// width - width of the page in pixels
// height - height of the page in pixels
// margin - margin between the page border and content in pixels
// margin* - margin on a specific side
// pageCount - display a page counter at location
// 'topLeft', 'topCenter', 'topRight'
// 'bottomLeft', 'bottomCenter', 'bottomRight'
"page": {
"width": "612px",
"height": "792px",
"margin": "50px",
"marginTop": "30px",
"marginBottom": "30px",
"marginLeft": "60px",
"marginRight": "60px",
"pageCount": "bottomCenter",
},
// The following options are only used by `type: "markdown"` generation
// Optional - set the font size, family, and text color
// of the PDF (Markdown to PDF only)
"fontSize": 16,
"textColor": "#171717",
"fontFamily": "Roboto",
// Optional - show or hide the timestamp shown at the bottom of the PDF
// defaults to true
"includeTimestamp": true,
// Optional - display a logo on the right corner of the first page
"logo": {
"src": "https://example.com/mtnlogo.png",
"maxWidth": 200,
"maxHeight": 200,
},
}
// => binary PDF data

Saving the response as a PDF

The response from the PDF generation endpoint will be binary PDF bytes.

Make sure you save the response to PDF as binary data.

By default, many HTTP clients give you the HTTP response body as a text string. Additionally, many file handling libraries, even those built into your language of choice will default to encoding a file as UTF-8 on save. This can cause a corrupt file on disk even though the HTTP response was a valid PDF.

If you are seeing corrupt files:

  • Make sure you are receiving the HTTP response body from your HTTP client as binary (no encoding)
  • When you save the file, ensure you save the the file as binary (also no encoding)

HTML & CSS to PDF

The PDF generation API will create the PDF with data in HTML & CSS format. All that's necessary is html and css string fields in the data object:

{
"data": {
"html": `
<h1 class='header-one'>What is Lorem Ipsum?</h1>
<p>
Lorem Ipsum is simply dummy text of the printing and typesetting
industry. Lorem Ipsum has been the industry's standard dummy text
ever since the <strong>1500s</strong>, when an unknown printer took
a galley of type and scrambled it to make a type specimen book.
</p>
<h3 class='header-two'>Where does it come from?</h3>
<p>
Contrary to popular belief, Lorem Ipsum is not simply random text.
It has roots in a piece of classical Latin literature from
<i>45 BC</i>, making it over <strong>2000</strong> years old.
</p>
`,
"css": `
body { font-size: 14px; color: #171717; }
.header-one { text-decoration: underline; }
.header-two { font-style: underline; }
`,
},
// Page formatting options, etc.
...,
}

An example with curl:

curl \
-X POST \
-u YOUR_API_KEY: \
-H 'Content-Type: application/json' \
-d '{ "type": "html", "title": "World", "data": { "html": "<h1>HTML to PDF</h1>", "css": "h1 { color: purple; }" } }' \
https://app.useanvil.com/api/v1/generate-pdf > test.pdf

Note: The logo, includeTimestamp, fontSize, fontFamily, and textColor payload properties are ignored when generating a PDF using HTML & CSS. Instead you can use HTML and CSS to style the document however you like :).

Supported format of data

data is an object consisting of the html and css properties.

{
title: '...',
data: {
html: '<p>some html</p>',
css: 'p { color: blue; }',
},
}

html

Accepts HTML in string format.

{
data: {
html: "<h1 id='hello' class='title' style='color:grey;'>Hello World!</h1><p>Lots of paperwork</p>",
},
}

css

Accepts CSS in string format.

{
data: {
css: "body { font-size: 14px; } #hello { color: #00ff77; } .title { font-weight: bold; } p { margin-bottom: 0px; }",
},
}

Using custom fonts

By default, Anvil's HTML to PDF generator uses the Noto Sans font family. You can override the default font and load custom fonts by way of either the @import or @font-face CSS directives. These methods allow you to use webfonts from providers like Google Fonts, or self-hosted fonts.

Default font stack

It's important to first note the default font stack. You should include these fonts in your custom font stack. They will give you a fallback if your custom font doesn't support a glyph used in your HTML. For example, if you specify a font with only latin characters, then render Japanese hiragana in your HTML, the hiragana characters would not display unless the "Noto CJK" font was specified.

The defaults:

body {
font-family: "Noto Sans", "Noto CJK", sans-serif;
}

Using @import

The @import directive is generally used with webfonts like Google Fonts. Here is an example using the Barlow font family:

@import url('https://fonts.googleapis.com/css?family=Barlow:ital,wght@0,400;0,700;1,400;1,700');
html body {
/* We include the default font stack as a fallback */
font-family: "Barlow", "Noto Sans", "Noto CJK", sans-serif;
}

Most webfont providers offer a way to include the font via @import. For example, with Google Fonts, after you "select" a style, you can click on the @import option to get the import code. Note that with Google, you can omit the &display=swap parameter.

Webfont html to pdf

Using @font-face

Using @font-face gives you the flexibility to self-host fonts that are not covered by a webfont provider.

An example using @font-face:

@font-face {
font-family: 'Pacifico';
src: url('https://example.com/fonts/PacificoNormal.ttf');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Pacifico';
src: url('https://example.com/fonts/PacificoBold.ttf');
font-weight: bold;
font-style: normal;
}
html body {
/* We include the default font stack as a fallback */
font-family: "Pacifico", "Noto Sans", "Noto CJK", sans-serif;
}

Note: at this time we only support fonts in the TTF file format. Font file HTTP responses must set the Content-Type response header to either font/ttf or application/x-font-ttf. All other font formats and content types will be ignored.

Advanced HTML & CSS features

There are a few additions to HTML & CSS to help rendering in the PDF context. Generally, the features covered here will help you control page-level rendering on a PDF. HTML and CSS don't have the concept of a page in the same way a PDF does, so there are no mechanisms built into vanilla HTML / CSS to help with page-level rendering. Our HTML renderer has a few extensions that make working with pages a lot easier.

A couple of HTML to PDF resources:

This guide will cover

  • Repeating table headers on each page
  • Forcing page breaks
  • Rendering page numbers
  • Rendering HTML elements in the page margins
  • Rendering a PDF with React, Vue, or the renderer of your choice

Repeating table headers

When a table overflows onto a new page, you can have the headers and footers repeat on the new page. For example:

HTML table in PDF with paginated headers

Table header repeating is supported and turned on by default. All you have to do is make sure you put your header th in a thead element. If you'd like to repeat the footer, make sure your footer content is in a tfoot element:

<table>
<thead>
<tr>
<th>Qty</th>
<!-- ... -->
</tr>
</thead>
<tbody>
<tr>
<td>2</td>
<!-- ... -->
</tr>
<!-- ...Many more rows... -->
</tbody>
<tfoot>
<tr>
<!-- Footer Content here -->
</tr>
</tfoot>
</table>

Turning header repeating off

While table header repeating is on by default, you can turn it off with a custom CSS rule -fs-table-paginate:

table.no-header-repeat {
-fs-table-paginate: none;
}

Or you can disable it by simply not using thead, tbody, and tfoot elements.

Forcing page breaks

You can force page breaks with the page-break-before: always rule on any element. It will create a new page, then set the element with the page-break-before CSS rule as the first element of that page.

Here's a simple example with three pages:

<!-- Page 1-->
<div>Lonely Page 1 content</div>
<!-- .new-page causes a page break -->
<div class="new-page">Page 2 content</div>
<!-- .new-page causes another page break -->
<div class="new-page">Page 3 content</div>

CSS:

.new-page {
page-break-before: always;
}

Output:

HTML to PDF page break

Rendering page numbers

You can inject page numbers and total pages into any element by using a bit of special CSS. You target the content of your chosen element's ::after pseudo-element with a special directive:

<div class="page-container">
Page
<span class="page"></span>
of
<span class="pages"></span>
</div>

And the CSS

.page-container .page::after {
content: counter(page);
}
.page-container .pages::after {
content: counter(pages);
}

The targeted elements can be on any page you'd like. But it's likely most important to render the page number information on each page, in one of the margins.

Rendering in the page margins

The HTML renderer allows rendering anything you'd like in the margins by way of a "running" element.

You render an element in the body like any other element, define the element as a "running" element, then tell each page to use that element in one of the defined margin areas.

Here's an example that uses the page number example above and renders it in the bottom right margin on every page.

<body>
<!--
We'll make this a "running" element in the css.
This element should be the first element after the body
to show on all pages.
-->
<div class="page-container">Page <span class="page"></span></div>
<!-- other elements -->
<div>...content...</div>
</body>

CSS:

.page-container {
/* Define this element as a running element called "pageContainer" */
position: running(pageContainer);
}
@page {
/*
Use any of these locations to place your margin elements:
@top-left, @top-left-corner
@top-center
@top-right, @top-right-corner
@bottom-left, @bottom-left-corner
@bottom-center
@bottom-right, @bottom-right-corner
*/
@bottom-right {
/*
Reference "pageContainer" to be the content for the
bottom right page margin area
*/
content: element(pageContainer);
}
}

The output using the page breaks and applying a little styling to the page numbers:

HTML to PDF page break with page number

Element location

A "running" element's location in the HTML determines which pages it shows on. If you want it in a margin element on all pages, just make sure to place the "running" element before all non-running HTML elements in the HTML code. For example, place all your margin elements that show on all pages right after the <body> element.

To see this behavior in action, using the page number and page break examples above, we can start the page numbering on the 2nd page by placing the running pageContainer element on the second page.

<div>Lonely Page 1 content</div>
<div class="new-page">Page 2 content</div>
<!--
The page numbering will start on the 2nd page because
we're rendering it after page 2's pagebreak
-->
<div class="page-container">Page <span class="page"></span></div>
<!-- Page 3 will also have the page number in the margin -->
<div class="new-page">Page 3 content</div>

CSS:

.page-container {
/* Define the this element as a running element called "pageContainer" */
position: running(pageContainer);
}
.new-page {
page-break-before: always;
}
@page {
@bottom-right {
/* Reference "pageContainer" to be the content for the bottom right page margin area */
content: element(pageContainer);
}
}

Here's the output. Notice there is no Page 1 on the first page:

HTML to PDF page break with page number

Positioning margin elements

Margin elements may not render exactly where you want them to. It's possible to place them exactly where you want with margin-top.

<div class="margin-content">Margin Content</div>

CSS:

.margin-content {
position: running(marginContent);
/* Position the element with margins */
margin-top: 10px;
/* Style margin element like any other */
font-size: 12px;
color: #c00;
}
@page {
@top-left {
content: element(marginContent);
}
}

margin-top: 0:

Top Margin 0

margin-top: -25px:

Top Margin -25px

margin-top: 25px:

Top Margin 25px

Rendering fillable forms

You can render HTML forms to create fillable PDFs. All inputs must be within a <form/> element, must have a valid type attribute, and must have a valid name attribute.

<form>
<input type="text" name="myInput" value="default" />
<input type="text" name="myOtherInput" />
<textarea type="text" name="myTextArea">Goodbye Cruel World!</textarea>
<input type="radio" name="myRadioGroup" value="One"/>
<input type="radio" name="myRadioGroup" value="Two"/>
</form>

Rendering with React, Vue, etc.

The generate-pdf endpoint only ingests vanilla HTML & CSS. But it is possible to use any renderer or preprocessor that outputs a string HTML or CSS. So, you can use React, Vue, Ember, Handlebars, styled-components, LESS, SASS, SCSS, or your own custom renderer.

See the Using React and styled-components to generate PDFs blog post for a tutorial using React and styled-components. We also have a Vue-specific blog post on generating PDFs. Note that this approach can be extended to any technologies you'd like to use.

Here's a quick Node example using React and styled-components to generate the HTML & CSS payload.

import React from 'react'
import ReactDOMServer from 'react-dom/server'
import styled, { ServerStyleSheet } from 'styled-components'
const Package = styled.span`
color: magenta;
`
const Hello = () => (
<div>
Hello from <Package>React</Package> & <Package>styled-components</Package>!
</div>
)
function buildHTMLToPDFPayload() {
const sheet = new ServerStyleSheet()
const html = ReactDOMServer.renderToStaticMarkup(
sheet.collectStyles(<Hello />)
)
const css = sheet.instance.toString()
return {
data: {
html,
css,
},
}
}

HTML to PDF Limitations

  • CSS Flexbox and Grid are currently not supported.
  • Custom fonts must be in the TTF file format.

Markdown to PDF

The PDF generation API will create the PDF with an array of objects, each supporting Markdown.

{
...,
"fontFamily": "Barlow",
"data": [
{
"label": "Name",
"content": "Sally Smith",
},
{
"content": "Lorem **ipsum** dolor sit _amet_...",
"fontSize": 12,
"textColor": "#616161",
},
{
"table": {
"rows": [
["Description", "Quantity", "Price"],
["4x Large Wigets", "4", "$40.00"],
["10x Medium Sized Widgets in dark blue", "10", "$100.00"],
["6x Small Widgets in white", "6", "$60.00"],
],
"columnOptions": [
{ "align": "left", "width": "60%" },
{ "align": "center", "width": "100px" },
{ "align": "right" },
],
"firstRowHeaders": true,
"rowGridlines": true,
"columnGridlines": true,
"verticalAlign": "center"
}
}
]
}

An example with curl:

curl \
-X POST \
-u YOUR_API_KEY: \
-H 'Content-Type: application/json' \
-d '{ "title": "Hello", "data": [ { "label": "Hello World", "content": "I like turtles" } ] }' \
https://app.useanvil.com/api/v1/generate-pdf > test.pdf

Supported format of data

data is specified as an array of objects. The objects have a handful of supported keys. Objects can contain multiple keys.

{
title: '...',
data: [
{
label: 'A label',
content: 'Some content',
fontSize: 12,
textColor: '#171717',
},
// ... more data to output
]
}

label

Generates a bolded label

{
data: [
{
label: 'A label',
},
]
}

heading

Generates a larger bolded heading intended to break up sections of a document.

{
data: [
{
heading: 'Some Heading',
},
]
}

content

Generates a block of text. Supports multiline text and markdown formatting, including headings, bold, italic, bullets, tables, blockquotes, links, images, etc.

{
data: [
{
content:
'Some content\n\nAnother line of content with [a link](https://google.com)',
},
]
}

table

While the content key supports markdown tables, they can be difficult to generate, so we provide a specific table key.

{
data: [
{
table: {
// (required) The data!
rows: [
['Description', 'Quantity', 'Price'][('A widget', '3', '$3')][
('Some other widget', '10', '$10')
],
],
// (optional) firstRowHeaders defaults to true
// set to false for no header row on the table
firstRowHeaders: true,
// (optional) rowGridlines / columnGridlines defaults to false
// set to true to display gridlines in-between rows or columns
rowGridlines: true,
columnGridlines: false,
// (optional) verticalAlign defaults to 'top'
// adjust vertical alignment of table text
// accepts 'top', 'center', or 'bottom'
verticalAlign: 'center',
// (optional) columnOptions - An array of columnOption objects.
// You do not need to specify all columns. Accepts an
// empty object indicating no overrides on the
// specified column.
//
// Supported keys for columnOption:
// align (optional) - adjust horizontal alginment of table text
// accepts 'left', 'center', or 'right'; defaults to 'left'
// width (optional) - adjust the width of the column
// accepts width in pixels or as percentage of the table width
columnOptions: [
{ align: 'left' }, // the default
{ align: 'center', width: '100px' },
{ align: 'right', width: '20%' },
],
},
},
]
}

fontFamily

Will apply a custom font to the specified section of the PDF:

{
data: [
{
content: 'Lorem ipsum',
fontFamily: 'Roboto Mono', // Use the Roboto Mono google font
},
]
}

See the Custom fonts with fontFamily section below for more info.

fontSize / textColor

Applies the specified font size or text color to that section of the PDF.

{
data: [
{
content: 'Lorem ipsum',
fontSize: 20, // must be an int within 5 to 30
textColor: '#006ec2', // must be a 6 digit hex code
},
]
}

Custom fonts with fontFamily

By default, the markdown to PDF generator uses the Noto Sans font family. This is configurable at the document level, and for each individual field. Here is an example using multiple fonts.

{
fontFamily: 'Lato',
data: [
{
// Uses the fontFamily specified at the root
content: 'Content in Lato',
},
{
// Use the Roboto Mono font for only this piece of content
fontFamily: 'Roboto Mono',
content: 'I am Mr. Roboto',
},
]
}

Supported fonts

The markdown to PDF generator currently supports the following fonts:

  • Default: Noto Sans and various Noto fonts to handle CJK.
  • Builtin fonts: Courier, Helvetica, Times New Roman.
  • All Google fonts are supported. Specify the name exactly as the Google fonts website indicates.

Note that font names are case-sensitive and must be exact, i.e. 'Barlow', not 'barlow'. You will receive an error if your specified font is not found, or if the font name is incorrect.

If the font you specify does not contain a glyph used in your content, the Noto Sans and Noto CJK fonts will be used as fallbacks.

By default, the Anvil logo will be displayed on the right corner of the first page.

To remove the logo, set the logo property to false.

You can also set your own logo:

pdf-logo-example
{
logo: {
// (required) Supports http(s) and data URIs
src: 'https://example.com/mtnlogo.png'
// (optional) The max size to display the logo
// * Default is 200px
// * Maximum is 500px
maxWidth: 200,
maxHeight: 200,
},
title: 'Logo Example',
data: [{
content: 'Hello _World_!'
}]
}

Using images

Most PDFs our customers generate contain one or more images. Images used in HTML or markdown generation must be publicly accessible. Our system cannot login to your system. This may sound scary if you are referencing images with sensitive data in them, but there is a secure solution: signed URLs.

All major cloud providers have support for signed URLs. With a signed URL, you can send Anvil a URL to an image in your storage bucket with an expiring token. See the docs for your specific cloud provider:

See the performance section below for image performance tips. If you are having trouble with images, see the troubleshooting section.

Performance

PDF generation performance can vary with the HTML & CSS or Markdown input sent to the generation endpoint.

By far the greatest impact on performance is images in your HTML code or markdown payload. If you are experiencing slow HTML to PDF generation, it is extremely likely there are images referenced in your code that are slow to download. For example, if your HTML renders an image that takes one second to download, the generation cannot be any shorter than one second.

It is useful to test your images in a browser. All modern browsers have developer tools with a network panel. Open the network panel and paste your image's URL into the browser's address bar. The network panel will show you how long your image takes to load, its content-type, size, etc.

network panel with image request

It is fastest to load your images from a CDN or a cloud storage bucket with signed URLs. Beyond that, here are a few image performance guidelines:

  • Ideally your images will download in a browser environment in tens of milliseconds. If your images take longer, they likely can be optimized.
  • Make sure your image URLs do not make any redirects. Referencing images from services like Google Docs, Google Drive, Dropbox, etc. may make redirects which can take over a second. You can see the redirects in your network panel when you load these URLs in a browser.
  • Make sure your images are as small as possible. For png images, you can use pngcrush. Generally resizing or compressing images so they are max 2x the size they will be displayed can lead to significant reductions in size.
  • Make sure your images correctly set the Content-Type response header.
  • For very small images, you can use data URIs so there is nothing to download!
  • Make sure you don't have any broken images / URLs that do not load.

Here is an example of a well-behaved image: 50ms load time, less than 5kb in size, no redirects, and it has the Content-Type properly specified:

network panel with image request

Using the API clients

For convenience, we provide language-specific API clients that help with authentication and PDF generation. They handle authentication, PDF generation, and errors to help with debugging. See the API usage examples for an example in your favorite language. Below is an example using the node API client.

import fs from 'fs'
import Anvil from '@anvilco/anvil'
const apiKey = '7j2JuUWmN4fGjBxsCltWaybHOEy3UEtt'
const exampleData = {
title: 'My PDF Title',
data: [
{
label: 'Hello World!',
},
],
}
const anvilClient = new Anvil({ apiKey })
const { statusCode, data } = await anvilClient.generatePDF(exampleData)
console.log(statusCode) // => 200
// Data will be the generated binary PDF data
fs.writeFileSync('output.pdf', data, { encoding: null })

Encrypting Data Payloads

You can encrypt the data sent in to generate the PDF. Setup an RSA keypair, then encrypt the string value of the data property with your public key. You can use our node encryption library.

POST https://app.useanvil.com/api/v1/generate-pdf
{
// Encrypt with your RSA key
"data": 'an encrypted JSON string'
// Other metadata
"title": "Some Title",
}
// => binary PDF bytes

Handling Errors

Should you run into any errors, the response status will be >= 400, and the body will be a JSON payload.

POST https://app.useanvil.com/api/v1/generate-pdf
{
// empty request body!
}
// => 400
{
"name":"ValidationError",
"fields":[{
"message":"Required",
"property":"data"
}]
}

Rate Limits

This API enforces separate rate limits for Development and Production API keys.

  • Production: 40 requests over 1 second
  • Development: 2 requests over 1 second

Each response will contain a few headers that keep track of usage:

HTTP Status: 200
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 196
X-RateLimit-Reset: 148086765

When exceeded, the API will respond with a 429 status code plus a Retry-After header indicating how many seconds to wait:

HTTP Status: 429
Retry-After: 5

Troubleshooting

Sometimes things go wrong! That's ok, here are some common problems and steps to resolve them. If after reading this, you are still having issues, please contact support, and we can help sort it out.

The saved PDF file is corrupt / invalid

This is often an encoding issue. PDF is a binary format, and the PDF generation endpoint's response body will be binary PDF bytes.

  • Make sure you are receiving the HTTP response body as binary (no encoding) from your HTTP client
  • When you save the file, ensure the file is saved as binary (also no encoding)

See saving the response as a PDF for full details.

How do I remove the watermark from the generated PDF?

Please make sure you are using your production API key. All calls to the generation endpoint with a development API key will contain the Anvil watermark.

It is slow to generate the PDF

Slow PDF generations are usually due to image loading; often with payloads that have slow-to-download images, have broken images (404), or have images with redirects. See the performance section for more info on how you can improve your image URLs.

My images are not showing in the generated PDF

Images not rendering could be due to a couple things:

  • Images must be publicly accessible. Make sure your images can be loaded in an incognito browser window. See the using images section for more info.
  • Our image loader has a short timeout. Make sure your images load in less than two seconds, ideally in less than one second.
  • Make sure your image link is not broken! This seems obvious, but broken images are more common than you'd expect.
  • If your image is >= 10 megabytes in size, the system may not load your image.
  • Some obscure SVG features are not supported. If your SVG is not displaying and you are sure it loads quickly in a browser, convert it to a raster image format (png, jpg, etc.). You can email us SVGs that do not render at support@useanvil.com.
My @font-face fonts are not in the generated PDF
  • Make sure the font files are publicly accessible.
  • Make sure font files are in the TTF file format.
  • Check the Content-Type HTTP response header on your font files. Content-Type must be set to either font/ttf or application/x-font-ttf.
  • Make sure your font supports the glyphs you are trying to render.

Resources

Here are a few resources that may help getting going with PDF generation: