Airpdf docs

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

  1. Go to airpdf.app/auth/register-tenant
  2. Fill in: workspace code (lowercase, e.g. acme), company name, admin email, password
  3. Confirm via the email link
  4. 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: live and test
  • A managed Wasabi bucket for generated PDFs (Bring Your Own S3 optional)

2 — Generate your first API key

  1. Backoffice → API Keys+ New key
  2. Pick environment:
    • airpdf_test_* — sandbox, doesn't count against monthly quota
    • airpdf_live_* — production, counts toward your plan
  3. 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)

  1. Backoffice → Templates+ New → pick a starter (Invoice / Report / Letter / Badge / etc.)
  2. Edit the JSX in the REPL editor — left pane is code, right pane is live PDF
  3. Define your variables in the Variables drawer at the bottom (JSON Schema)
  4. 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:

  1. Backoffice → Storage → choose provider (Amazon S3 / MinIO / Wasabi / Cloudflare R2)
  2. Enter endpoint, region, bucket name, access key, secret key
  3. Save → Airpdf runs a put + delete test object to verify
  4. 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 one render_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?

Spotted a typo or stale claim? Open an issue or ping us in the workspace — docs are versioned with the product.