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.

Note: at this time we only support fonts in the TTF file format. All other font types will be ignored.

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;
}

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 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. 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 the Node Client

For convenience, we provide language-specific API clients that help with authentication and PDF generation. Below is an example using the node API client. It handles authentication, PDF generation, and errors to help with debugging.

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.

Resources

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