Getting started and reference

Sheetfed API

Sheetfed turns HTML into a PDF. Send it a page and get back a file that prints the same every time: invoices, statements, tickets, certificates.

Base URL
https://sheetfed.example.com/v1
Authentication
An API key in the Authorization header
Format
JSON in, JSON out. The PDF itself comes back as a file.
Typical render
Under three seconds for a one-page invoice

Quickstart

Three requests take you from a string of HTML to a PDF on disk. You need curl and an API key.

1Set your key

Create a key under Settings, then API keys. Test keys start with sfd_test_ and cost nothing to use.

Shell
export SHEETFED_KEY="sfd_test_9c41e7d2a8"

2Start a render

Post your HTML to /renders. Everything except html is optional.

Shell
curl https://sheetfed.example.com/v1/renders \
  -H "Authorization: Bearer $SHEETFED_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<h1>Invoice 1042</h1><p>Total due: $380.00</p>",
    "page_size": "letter",
    "margin_mm": 18
  }'

Sheetfed answers 201 Created with the new render. It starts out queued.

Response · 201
{
  "id": "rnd_7Hq2Lx",
  "status": "queued",
  "page_size": "letter",
  "margin_mm": 18,
  "landscape": false,
  "pages": null,
  "created_at": "2027-01-12T15:04:09Z",
  "finished_at": null,
  "pdf_url": null
}

3Check on it

Ask for the render by its id. When status is done, pdf_url is filled in.

Shell
curl https://sheetfed.example.com/v1/renders/rnd_7Hq2Lx \
  -H "Authorization: Bearer $SHEETFED_KEY"
Response · 200
{
  "id": "rnd_7Hq2Lx",
  "status": "done",
  "page_size": "letter",
  "margin_mm": 18,
  "landscape": false,
  "pages": 1,
  "created_at": "2027-01-12T15:04:09Z",
  "finished_at": "2027-01-12T15:04:11Z",
  "pdf_url": "https://sheetfed.example.com/v1/renders/rnd_7Hq2Lx/pdf"
}

4Download the PDF

The file needs the same key as everything else. It is kept for 24 hours.

Shell
curl https://sheetfed.example.com/v1/renders/rnd_7Hq2Lx/pdf \
  -H "Authorization: Bearer $SHEETFED_KEY" \
  -o invoice-1042.pdf

The same thing in Python

One function that starts a render, waits for it and saves the file. It uses the requests library.

Python
import os
import time

import requests

API = "https://sheetfed.example.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SHEETFED_KEY']}"}


def render_pdf(html, path):
    r = requests.post(f"{API}/renders", headers=HEADERS,
                      json={"html": html, "page_size": "letter", "margin_mm": 18})
    r.raise_for_status()
    render = r.json()

    while render["status"] in ("queued", "rendering"):
        time.sleep(1)
        r = requests.get(f"{API}/renders/{render['id']}", headers=HEADERS)
        r.raise_for_status()
        render = r.json()

    if render["status"] == "failed":
        raise RuntimeError(render["error"]["message"])

    pdf = requests.get(render["pdf_url"], headers=HEADERS)
    pdf.raise_for_status()
    with open(path, "wb") as f:
        f.write(pdf.content)


render_pdf("<h1>Invoice 1042</h1><p>Total due: $380.00</p>", "invoice-1042.pdf")

Authentication

Every request carries Authorization: Bearer followed by your key. There are two kinds of key.

Key starts withOutputCost
sfd_test_A “TEST” watermark across every pageFree
sfd_live_Clean pagesCounts toward your plan

Keep keys on your server. A key in browser code is a key anyone can read. If one leaks, delete it under Settings and make another; the old one stops working at once.

Endpoints

Five endpoints, all under the base URL. The PDF download returns a file and the delete returns nothing. The other three return render objects like the ones in the quickstart; the list returns an array of them under data.

MethodPathWhat it does
POST/rendersStart a render. Answers 201.
GET/renders/{id}Get one render and its status.
GET/renders/{id}/pdfDownload the PDF. Answers 409 until the status is done.
GET/rendersList your renders, newest first. Takes limit (default 20, at most 100) and after, a render id.
DELETE/renders/{id}Delete a render and its PDF straight away. Answers 204 with no body.

Status values

A render moves left to right and stops at done or failed.

  1. queuedAccepted and waiting for a worker.
  2. renderingA worker is laying out the pages.
  3. donepages and pdf_url are set.
  4. failedAn error object says why.

Render options

The JSON body of POST /renders.

FieldTypeDefaultNotes
htmlstringrequiredThe whole page, up to 2 MB. Put CSS in a <style> tag. Images can be data URIs or https addresses.
page_sizestring"letter""letter", "a4" or "legal".
margin_mminteger120 to 50, the same on all four sides.
landscapebooleanfalseTurn the page on its side.
footer_htmlstringnoneRepeats at the foot of each page. {{page}} and {{pages}} are filled in for you.
webhook_urlstringnoneWhere to send the finished render, so you can skip step 3.

Webhooks

Set webhook_url and Sheetfed posts the render object to it when the status becomes done or failed. The body is the same JSON that GET /renders/{id} returns.

Answer with any 2xx status within 5 seconds. If you don’t, Sheetfed tries three more times, one minute apart, and then gives up. A failed render looks like this:

Webhook body
{
  "id": "rnd_Tm4c9P",
  "status": "failed",
  "page_size": "a4",
  "margin_mm": 12,
  "landscape": false,
  "pages": null,
  "created_at": "2027-01-12T15:20:41Z",
  "finished_at": "2027-01-12T15:20:52Z",
  "pdf_url": null,
  "error": {
    "code": "asset_unreachable",
    "message": "https://shop.example.com/logo.png did not load within 10 seconds."
  }
}

Errors

A request that can’t be carried out gets a 4xx or 5xx status and one error object. field appears only when a single field is to blame.

Response · 400
{
  "error": {
    "code": "invalid_request",
    "message": "margin_mm must be between 0 and 50.",
    "field": "margin_mm"
  }
}
StatusCodeWhenWhat to do
400invalid_requestA field is missing or out of range.Fix the body. Sending it again unchanged fails again.
401invalid_keyThe key is missing, mistyped or deleted.Check the header starts with Bearer and a space.
404render_not_foundNo render has that id, or it is more than 30 days old.Check the id.
409render_not_readyYou asked for the PDF before the status was done.Keep polling, or wait for the webhook.
410pdf_expiredThe PDF was deleted after 24 hours.Render it again.
413html_too_largeThe html field is over 2 MB.Shrink inlined images, or link to them.
429rate_limitedMore than 60 requests in a minute.Wait the number of seconds in the Retry-After header.
500internal_errorSomething broke on our side.Try again in a few seconds.

When a render fails

A render that was accepted can still fail. The request that started it got 201, so look for these in the render’s own error.code.

CodeWhat happened
render_timeoutLayout took longer than 30 seconds. Very long tables are the usual cause.
asset_unreachableAn image or font didn’t load within 10 seconds. The message names the address.
too_many_pagesThe document came to more than 200 pages.

Limits

LimitValuePast it
Requests per key60 a minute429 rate_limited
Size of html2 MB413 html_too_large
Render time30 secondsThe render fails with render_timeout
Pages in one PDF200The render fails with too_many_pages
How long a PDF is kept24 hours410 pdf_expired
How long a render record is kept30 days404 render_not_found

If a POST times out and you aren’t sure it arrived, send it again with an Idempotency-Key header set to any unique string, the same one both times. Sheetfed returns the first render instead of starting a second. Keys are remembered for 24 hours.