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 app.airpdf.io/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 quotaairpdf_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://api.airpdf.io/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 withdetails - 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.io — we reply within 24h on paid plans