Jinja2PDF
US-Rechnung mit Sales Tax
Papierformat Letter, Dollar und US-Datumsangaben aus der Locale en-US der Vorlage. Die Sales Tax gilt nur für steuerpflichtige Positionen, eine Anzahlung verringert den offenen Betrag, und Status und QR-Code zum Bezahlen folgen aus dem, was noch offen ist.
Kostenloser Plan, keine Kreditkarte. Sie erhalten eine eigene Kopie im Editor, veröffentlicht und bereit für die API.
Gerendertes Ergebnis
Was dieses Beispiel zeigt
paper.formatistLetterund die Localeen-US,moneygibt also$11,278.46aus unddate('MMMM d, yyyy')September 28, 2026.- Die Sales Tax gilt nur für Positionen mit
taxable: true:where('taxable', true) | sum('amount')sind die$1,565.00, auf die 7,25 % fallen, undround(2)legt die Steuer auf$113.46fest, bevor sie addiert wird. paymentsist eine Liste; eine Anzahlung und jede spätere Teilzahlung bekommen je eine Zeile unter der Summe, und offen ist, was übrig bleibt:$8,278.46.- Status und QR-Code folgen aus dem offenen Betrag: „Paid“ bei null, „Partially paid“ nach einer Anzahlung und kein Bezahl-Code mehr, wenn nichts offen ist.
dateAdd(invoice.terms_days, 'days')macht aus Net 30 das FälligkeitsdatumOctober 28, 2026, unddefault('—')ersetzt eine fehlende Bestellnummer.
Verwendete Helfer
Vorlagenordner
{% set subtotal = invoice.lines | sum('amount') %}
{% set taxable = invoice.lines | where('taxable', true) | sum('amount') %}
{% set tax = (taxable * invoice.tax_rate / 100) | round(2) %}
{% set total = subtotal + tax %}
{% set paid = invoice.payments | sum('amount') %}
{% set balance = total - paid %}
<header class="top">
<div>
<p class="brand">{{ company.name }}</p>
<p class="muted">{{ company.street }}<br>{{ company.city }}</p>
</div>
<div class="title">
<h1>Invoice</h1>
<p class="muted">{{ invoice.number }}</p>
{% if balance <= 0 %}<p class="status paid">Paid</p>{% elif paid > 0 %}<p class="status partial">Partially paid</p>{% endif %}
</div>
</header>
<section class="parties">
<div>
<h2>Bill to</h2>
<p><strong>{{ customer.name }}</strong><br>Attn: {{ customer.contact }}<br>{{ customer.street }}<br>{{ customer.city }}</p>
</div>
<dl>
<div><dt>Invoice date</dt><dd>{{ invoice.date | date('MMMM d, yyyy') }}</dd></div>
<div><dt>Terms</dt><dd>Net {{ invoice.terms_days }}</dd></div>
<div><dt>Due date</dt><dd>{{ invoice.date | dateAdd(invoice.terms_days, 'days') | date('MMMM d, yyyy') }}</dd></div>
<div><dt>PO number</dt><dd>{{ invoice.po | default('—') }}</dd></div>
</dl>
</section>
<table class="lines">
<thead>
<tr><th>Description</th><th class="num">Qty</th><th class="num">Rate</th><th class="num">Amount</th></tr>
</thead>
<tbody>
{% for line in invoice.lines %}
<tr>
<td>{{ line.description }}{% if line.taxable %} <span class="taxed">T</span>{% endif %}</td>
<td class="num">{{ line.qty | number }}</td>
<td class="num">{{ line.rate | money }}</td>
<td class="num">{{ line.amount | money }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<section class="summary">
<div class="pay">
{% if balance > 0 %}
<img src="{{ qrcode(invoice.pay_url, { size: 96 }) }}" alt="Pay online">
<p><strong>Pay online</strong><br>Scan the code or open<br>{{ invoice.pay_url | replace('https://', '') }}</p>
{% endif %}
</div>
<table class="totals">
<tr><td>Subtotal</td><td class="num">{{ subtotal | money }}</td></tr>
<tr><td>Sales tax {{ invoice.tax_rate }}% on {{ taxable | money }} <span class="taxed">T</span></td><td class="num">{{ tax | money }}</td></tr>
<tr class="total"><td>Total</td><td class="num">{{ total | money }}</td></tr>
{% for payment in invoice.payments %}
<tr><td>Paid {{ payment.date | date('MMM d') }}, {{ payment.method }}</td><td class="num">−{{ payment.amount | money }}</td></tr>
{% endfor %}
<tr class="due"><td>Balance due</td><td class="num">{{ balance | money }}</td></tr>
</table>
</section>
<p class="notes">{{ invoice.notes }}</p>body { font-family: 'Inter', sans-serif; font-size: 9.5pt; color: #1e293b; }
.top { display: flex; justify-content: space-between; align-items: flex-start; padding-bottom: 7mm; border-bottom: 2px solid #0f172a; }
.brand { margin: 0; font-size: 15pt; font-weight: 700; color: #0f172a; }
.muted { margin: 1mm 0 0; color: #64748b; line-height: 1.5; }
.title { text-align: right; }
h1 { margin: 0; font-size: 24pt; letter-spacing: -0.01em; }
.status { display: inline-block; margin: 2mm 0 0; padding: 1mm 2.5mm; border-radius: 1mm; font-size: 7.5pt; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
.status.paid { background: #dcfce7; color: #166534; }
.status.partial { background: #fef3c7; color: #92400e; }
.parties { display: flex; justify-content: space-between; margin: 8mm 0; }
h2 { margin: 0 0 1.5mm; font-size: 7.5pt; letter-spacing: 0.08em; text-transform: uppercase; color: #64748b; }
.parties p { margin: 0; line-height: 1.5; }
dl { margin: 0; }
dl div { display: flex; justify-content: space-between; gap: 10mm; padding: 0.6mm 0; }
dt { color: #64748b; }
dd { margin: 0; font-weight: 600; }
table { border-collapse: collapse; }
.lines { width: 100%; }
.lines th { padding: 2mm 1.5mm; border-bottom: 1.5px solid #0f172a; text-align: left; font-size: 7.5pt; letter-spacing: 0.05em; text-transform: uppercase; color: #64748b; }
.lines td { padding: 2.4mm 1.5mm; border-bottom: 1px solid #e2e8f0; }
.num { text-align: right !important; white-space: nowrap; }
.taxed { display: inline-block; padding: 0 1mm; border: 1px solid #94a3b8; border-radius: 0.8mm; font-size: 6.5pt; color: #475569; }
.summary { display: flex; justify-content: space-between; align-items: flex-start; gap: 10mm; margin-top: 6mm; break-inside: avoid; }
.pay { display: flex; align-items: center; gap: 4mm; }
.pay img { width: 25mm; height: 25mm; }
.pay p { margin: 0; font-size: 8pt; line-height: 1.5; color: #475569; }
.totals { width: 88mm; }
.totals td { padding: 1.3mm 1.5mm; }
.totals .total td { border-top: 1.5px solid #0f172a; font-weight: 700; }
.totals .due td { border-top: 1px solid #cbd5e1; font-size: 12pt; font-weight: 700; color: #0f172a; }
.notes { margin-top: 10mm; font-size: 8pt; color: #64748b; }<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"><div style="width:100%;display:flex;justify-content:space-between;font-size:7.5px;color:#64748b">
<span>{{ company.name }} · EIN {{ company.ein }} · {{ company.email }}</span>
<span>Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
</div>{
"pdf": {
"metadata": {
"title": "Invoice {{ invoice.number }}"
}
},
"paper": {
"format": "Letter"
},
"margin": {
"top": "0.6in",
"right": "0.7in",
"bottom": "0.8in",
"left": "0.7in"
},
"locale": "en-US",
"timezone": "America/New_York",
"currency": "USD",
"footer": {
"height": "0.5in"
}
}{
"company": {
"name": "Fennlor Studio LLC",
"street": "100 Example Street, Suite 200",
"city": "Exampletown, NY 00000",
"email": "billing@fennlor.example",
"ein": "00-0000000"
},
"customer": {
"name": "Olvarest Inc.",
"contact": "John Doe",
"street": "200 Example Avenue",
"city": "Exampleville, NY 00000"
},
"invoice": {
"number": "INV-2026-0107",
"date": "2026-09-28",
"terms_days": 30,
"po": "PO-55120",
"tax_rate": 7.25,
"pay_url": "https://pay.fennlor.example/i/INV-2026-0107",
"lines": [
{
"description": "Template design, fixed fee",
"qty": 1,
"rate": 4800,
"amount": 4800,
"taxable": false
},
{
"description": "Implementation, hours",
"qty": 32,
"rate": 150,
"amount": 4800,
"taxable": false
},
{
"description": "Thermal label printer",
"qty": 2,
"rate": 649,
"amount": 1298,
"taxable": true
},
{
"description": "Label rolls, 4 × 6 in, case of 12",
"qty": 3,
"rate": 89,
"amount": 267,
"taxable": true
}
],
"payments": [
{
"date": "2026-09-02",
"method": "ACH",
"amount": 3000
}
],
"notes": "Please pay by ACH or card through the link above and quote the invoice number. Thank you for your business."
}
}Über die API rendern
Speichern Sie die Beispieldaten als data.json und senden Sie sie mit dem Namen der Vorlage. TypeScript und Python nutzen die SDKs; jede andere Sprache schickt dasselbe JSON mit ihrer Standardbibliothek an die REST-API. Renderings mit einem Schlüssel, der mit ff_test_ beginnt, werden nicht gezählt; im Free-Tarif tragen sie ein kleines Wasserzeichen.
jq '{template: "invoice-us", data: ., filename: "invoice-INV-2026-0107.pdf"}' data.json \
| curl https://api-eu.formfeed.dev/v1/renders \
-H "Authorization: Bearer ff_test_..." \
-H "Content-Type: application/json" \
--data-binary @-import { Formfeed } from '@formfeed/sdk';
import data from './data.json' with { type: 'json' };
const formfeed = new Formfeed({ apiKey: process.env.FORMFEED_API_KEY! });
const render = await formfeed.renders.create({
template: 'invoice-us',
data,
filename: 'invoice-INV-2026-0107.pdf',
});
console.log(render.status, render.download_url);import json
import os
from formfeed import Formfeed
formfeed = Formfeed(api_key=os.environ["FORMFEED_API_KEY"])
with open("data.json", encoding="utf-8") as file:
data = json.load(file)
render = formfeed.renders.create(
template="invoice-us",
data=data,
filename="invoice-INV-2026-0107.pdf",
)
print(render.status, render.download_url)<?php
$body = [
'template' => 'invoice-us',
'data' => json_decode(file_get_contents('data.json'), true),
'filename' => 'invoice-INV-2026-0107.pdf',
];
$curl = curl_init('https://api-eu.formfeed.dev/v1/renders');
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('FORMFEED_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$render = json_decode(curl_exec($curl), true);
echo $render['status'], ' ', $render['download_url'], PHP_EOL;package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
data, err := os.ReadFile("data.json")
if err != nil {
panic(err)
}
body, _ := json.Marshal(map[string]any{
"template": "invoice-us",
"data": json.RawMessage(data),
"filename": "invoice-INV-2026-0107.pdf",
})
request, _ := http.NewRequest("POST", "https://api-eu.formfeed.dev/v1/renders", bytes.NewReader(body))
request.Header.Set("Authorization", "Bearer "+os.Getenv("FORMFEED_API_KEY"))
request.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil {
panic(err)
}
defer response.Body.Close()
var render struct {
Status string `json:"status"`
DownloadURL string `json:"download_url"`
}
json.NewDecoder(response.Body).Decode(&render)
fmt.Println(render.Status, render.DownloadURL)
}using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
var body = new
{
template = "invoice-us",
data = JsonDocument.Parse(File.ReadAllText("data.json")).RootElement,
filename = "invoice-INV-2026-0107.pdf",
};
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("FORMFEED_API_KEY"));
var response = await http.PostAsJsonAsync("https://api-eu.formfeed.dev/v1/renders", body);
var render = await response.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine($"{render.GetProperty("status")} {render.GetProperty("download_url")}");import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
public class Render {
public static void main(String[] args) throws Exception {
String data = Files.readString(Path.of("data.json"));
String body = """
{
"template": "invoice-us",
"filename": "invoice-INV-2026-0107.pdf",
"data": %s
}
""".formatted(data);
HttpRequest request = HttpRequest.newBuilder(URI.create("https://api-eu.formfeed.dev/v1/renders"))
.header("Authorization", "Bearer " + System.getenv("FORMFEED_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body()); // status, download_url, page_count, units …
}
}require "json"
require "net/http"
body = {
template: "invoice-us",
data: JSON.parse(File.read("data.json")),
filename: "invoice-INV-2026-0107.pdf",
}
response = Net::HTTP.post(
URI("https://api-eu.formfeed.dev/v1/renders"),
JSON.generate(body),
"Authorization" => "Bearer #{ENV.fetch("FORMFEED_API_KEY")}",
"Content-Type" => "application/json"
)
render = JSON.parse(response.body)
puts render["status"], render["download_url"]Heute mit dem Rendern beginnen
Der kostenlose Plan enthält 100 Einheiten im Monat und kostenlose Testrenderings. Wechseln Sie den Plan, wenn Sie mehr brauchen.
