Integration guide
How to integrate Airpdf into your application — from signup to your first production PDF in under 10 minutes.
This guide is for customers of the Airpdf API. If you're developing
on Airpdf itself, see development.md.
1 — Create your workspace
- Go to airpdf.app/auth/register-tenant
-
Fill in: workspace code (lowercase, e.g.
acme), company name, admin email, password - Confirm via the email link
-
Land on
/<your-code>/app— the backoffice dashboard
Your workspace gets:
- A 14-day trial on the Free plan (50 PDFs/month, 5 templates, 1 API key)
-
Two API key environments:
liveandtest - A managed Wasabi bucket for generated PDFs (Bring Your Own S3 optional)
2 — Generate your first API key
- Backoffice → API Keys → + New key
-
Pick environment:
-
airpdf_test_*— sandbox, doesn't count against monthly quota -
airpdf_live_*— production, counts toward your plan
-
- Copy the token immediately — it's shown once, then only the SHA-256 hash is stored
airpdf_test_AbCdEf1234567890aBcDeF1234567890
Store it in your secrets manager (.env, AWS Secrets Manager, 1Password,
etc.). Never commit to git.
3 — Build your first template
Visual editor (recommended)
- Backoffice → Templates → + New → pick a starter (Invoice / Report / Letter / Badge / etc.)
- Edit the JSX in the REPL editor — left pane is code, right pane is live PDF
- Define your variables in the Variables drawer at the bottom (JSON Schema)
- Publish — the template becomes addressable by slug via the API
Variables and interpolation
In template content, use {{customer.name}} syntax:
<Document>
<Page size="A4" style={{ padding: 32 }}>
<Text style={{ fontSize: 24, fontWeight: 700 }}>
Invoice for {{customer.name}}
</Text>
<Text>Total: €{{total}}</Text>
</Page>
</Document>
Define matching variables schema (JSON Schema):
{
"type": "object",
"properties": {
"customer": {
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"]
},
"total": { "type": "number" }
},
"required": ["customer", "total"]
}
When a render request fails schema validation, the API returns 422 with the
violating field path in details.
See template-schema.md for the full primitives
reference.
4 — Render a PDF
Option A — REST (any language)
curl -X POST https://airpdf.app/api/v1/render \
-H "Authorization: Bearer airpdf_test_..." \
-H "Content-Type: application/json" \
-d '{
"template": "invoice",
"data": {
"customer": { "name": "Acme Inc." },
"total": 99
}
}' \
-o invoice.pdf
Option B — Official SDK (Node, Python, Go, Ruby — Tier 1)
npm install @airpdf/sdk
import Airpdf from "@airpdf/sdk";
import { writeFile } from "node:fs/promises";
const client = new Airpdf({ apiKey: process.env.AIRPDF_API_KEY! });
const result = await client.render({
template: "invoice",
data: { customer: { name: "Acme Inc." }, total: 99 },
});
if (result.kind === "binary") {
await writeFile("invoice.pdf", result.pdf);
}
See ../sdks/README.md for all 12 supported languages.
5 — Choose sync vs async
| Need | Use | How |
|---|---|---|
| Small PDFs (< 5 pages, no images) returned in HTTP response | sync |
omit async (default) |
| Large PDFs / batch jobs / latency-tolerant | async |
"async": true |
| Render to URL instead of bytes | sync + url |
"response": "url" → 303 redirect |
Sync (default)
Returns 200 application/pdf with the bytes inline. Hard timeout 25s.
Sync + URL
curl -X POST .../render -d '{..., "response": "url"}' -L -o invoice.pdf
Server returns 303 + Location: <presigned-url>. The presigned URL is valid
for 1 hour by default.
Async
curl -X POST .../render -d '{..., "async": true, "webhook_url": "https://your-app.com/airpdf"}'
# → {"id":"01939e...","status":"pending","poll_url":"/api/v1/render/01939e..."}
Then either poll GET /api/v1/render/:id until status is terminal, or wait
for the webhook delivery (see next section).
6 — Webhooks (signed)
Airpdf delivers a signed POST to your webhook_url when an async render
finishes. Use the helpers in any official SDK to verify the signature in
constant time.
Headers
X-Airpdf-Event: render.succeeded
X-Airpdf-Delivery: 019398...
X-Airpdf-Timestamp: 1714780800
X-Airpdf-Signature: sha256=8b7d3...
Content-Type: application/json
Payload
{
"id": "01939e6b-d6f4-7c4e-9c8f-2b1a4f5e6d7c",
"status": "succeeded",
"duration_ms": 412,
"bytes": 18324,
"artifact_id": "01939e6b-..."
}
Verification (Node SDK)
import express from "express";
import { verifyWebhook, AirpdfWebhookVerificationError } from "@airpdf/sdk";
const app = express();
app.post(
"/airpdf",
express.raw({ type: "application/json" }), // RAW body — do NOT json-parse first
(req, res) => {
try {
verifyWebhook({
body: req.body,
signature: req.header("x-airpdf-signature")!,
timestamp: req.header("x-airpdf-timestamp")!,
secret: process.env.AIRPDF_WEBHOOK_SECRET!,
});
} catch (err) {
if (err instanceof AirpdfWebhookVerificationError) {
return res.status(401).end();
}
throw err;
}
const event = JSON.parse(req.body.toString("utf8"));
// → process event
res.status(204).end();
},
);
Webhook secret
Each tenant has a single webhook secret derived from the workspace.
v0.x: secret is shared with us. v1.x: we'll publish it in backoffice → API Keys → Webhook secret with rotation support.
7 — Bring your own storage (optional)
By default Airpdf stores generated PDFs on our managed Wasabi bucket. To route PDFs to your S3-compatible bucket:
- Backoffice → Storage → choose provider (Amazon S3 / MinIO / Wasabi / Cloudflare R2)
- Enter endpoint, region, bucket name, access key, secret key
-
Save → Airpdf runs a
put+deletetest object to verify - From the next render onward, PDFs upload to your bucket; presigned URLs point to it
Credentials are encrypted at rest (AES-GCM via Cloak). They're decrypted
in-memory only at upload time.
8 — Quotas + plans
| Plan | $/month | Included | Overage | Rate limit |
|---|---|---|---|---|
| Free | $0 | 50 PDFs | — (returns 402) | 10/min |
| Starter | $9 | 250 PDFs | $0.015/extra | 30/min |
| Growth | $24 | 1,500 PDFs | $0.010/extra | 60/min |
| Business | $49 | 5,000 PDFs | $0.006/extra | 120/min |
| Scale | $99 | 15,000 PDFs | $0.004/extra | 300/min |
| Enterprise | Custom | 50k+ | — | Custom |
429 (rate limit) returns Retry-After seconds. 402 (quota exhausted on
Free) cannot be retried until the next billing cycle.
9 — Test mode vs live mode
airpdf_test_* |
airpdf_live_* |
|
|---|---|---|
| Counts toward quota? | No | Yes |
| Watermarks PDF? | No | No |
| Fires webhooks? | Yes | Yes |
| Stripe billing? | No | Yes |
| Use for? | CI, dev, smoke tests | Production traffic |
Both keys hit the same endpoint. Discriminate in your client code via env var.
10 — Production checklist
Before flipping to airpdf_live_*:
- [ ] Verified webhook signature with the Node SDK (or equivalent)
- [ ] Webhook endpoint is HTTPS + reachable
-
[ ] Retried at least one
quota_exceeded(429) and onerender_failed(500) -
[ ] Tested template variables schema with deliberately invalid
data→expect 422 with `details` -
[ ] Confirmed PDFs render correctly across all expected variants (locale,
currency, item counts, line wrap) - [ ] Set up monitoring on render duration p95 (alert on > 2 s)
-
[ ] Have a fallback plan if Airpdf is unreachable (queue + retry, or
degrade UX gracefully)
Need help?
- 📖 API reference and OpenAPI spec
- 🧪 Postman collection — import to test interactively
- 📚 Template schema reference
- 🛠 SDKs — 12 official languages
- 📧 hello@airpdf.app — we reply within 24h on paid plans