Abstract colourful light streaks and motion blur

Introducing Async Rendering: Background Document Generation with Webhooks

We’re excited to introduce async rendering — a new way to generate documents with TemplateTo that doesn’t require your application to wait around for the result. Start a background job, carry on with what you’re doing, and pick up the finished document when it’s ready.

Why Async?

Our synchronous rendering endpoints work brilliantly for quick, on-demand document generation. You send a request and get a PDF back in the response. Simple.

But there are situations where holding an HTTP connection open isn’t ideal:

  • Large or complex documents with many pages, embedded images, or heavy styling that take longer to render
  • Batch processing where you’re generating dozens or hundreds of documents in a loop
  • Serverless and event-driven architectures where you don’t want a function sitting idle waiting for a response
  • User-facing workflows where you’d rather show a “we’re generating your document” message than block the UI

Async rendering solves all of these. Fire off a request, get a job ID back immediately, and collect the result later.

How It Works

The async flow has three steps:

  1. Start a job — POST your template data and receive a job ID instantly
  2. Wait for completion — either poll the status endpoint or receive a webhook notification
  3. Get the result — download the finished document via a pre-signed URL

Here’s a quick look at starting a job:

const response = await fetch(
  'https://api.templateto.com/render/async/pdf/tpl_abc123',
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'your-api-key',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      customerName: 'Acme Corp',
      invoiceNumber: 'INV-2026-001',
      items: [
        { description: 'Widget', quantity: 5, price: 10.00 },
        { description: 'Gadget', quantity: 2, price: 25.00 }
      ]
    })
  }
);

const job = await response.json();
// { jobId: "01HQ...", status: "Pending", statusUrl: "...", resultUrl: "..." }

The response comes back immediately with a 202 Accepted status. Your document is now being generated in the background.

Checking Job Status

The simplest way to know when your document is ready is to poll the status endpoint. A job moves through these states: PendingProcessingCompleted (or Failed). Completed results are available for download for 24 hours.

async function waitForResult(jobId, apiKey) {
  const base = 'https://api.templateto.com';
  let status = 'Pending';

  while (status === 'Pending' || status === 'Processing') {
    await new Promise(resolve => setTimeout(resolve, 1000));

    const res = await fetch(
      `${base}/render/async/status/${jobId}`,
      { headers: { 'X-Api-Key': apiKey } }
    );
    const data = await res.json();
    status = data.status;
  }

  if (status !== 'Completed') {
    throw new Error(`Job failed with status: ${status}`);
  }

  // Download the result
  const result = await fetch(
    `${base}/render/async/result/${jobId}`,
    { headers: { 'X-Api-Key': apiKey }, redirect: 'follow' }
  );

  return await result.blob();
}

Polling works well for scripts, CLI tools, and simple integrations. But if you’d rather not poll at all, read on.

Webhooks: Don’t Call Us, We’ll Call You

Instead of repeatedly checking whether your document is ready, you can provide a webhookUrl when starting a job. TemplateTo will send a POST request to that URL the moment the job completes — whether it succeeded or failed.

// Start the job with a webhook URL
const response = await fetch(
  `https://api.templateto.com/render/async/pdf/tpl_abc123?webhookUrl=${
    encodeURIComponent('https://your-app.com/webhooks/templateto')
  }`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'your-api-key',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ customerName: 'Acme Corp' })
  }
);

// Your app continues immediately — no waiting

Your webhook handler receives the job result and can process it however you need:

// Express.js webhook handler
app.post('/webhooks/templateto', async (req, res) => {
  const { jobId, status, resultUrl, errorMessage } = req.body;

  if (status === 'Completed') {
    const pdf = await fetch(resultUrl, {
      headers: { 'X-Api-Key': process.env.TEMPLATETO_KEY },
      redirect: 'follow'
    });
    // Save the PDF, email it, or process it however you need
  } else {
    console.error(`Job ${jobId} failed: ${errorMessage}`);
  }

  res.status(200).send('OK');
});

Webhooks are retried automatically up to 3 times with exponential backoff if your endpoint doesn’t respond with a 2xx status. You can also check the webhookDelivered field on the status endpoint as a fallback.

Upload Directly to Your Own Storage

By default, completed documents are stored in our S3 storage and you download them via a pre-signed URL. But if you’d prefer the result to land directly in your own cloud storage, you can provide an outputPresignedUrl parameter.

This works with AWS S3, Azure Blob Storage, Google Cloud Storage, DigitalOcean Spaces, Cloudflare R2, Backblaze B2, and Wasabi. If you use a provider not on this list, get in touch and we can add support for it.

Combine this with webhooks for a fully push-based pipeline where you never need to download anything from TemplateTo:

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({ region: 'eu-west-1' });

// 1. Generate a presigned URL for your own bucket
const presignedUrl = await getSignedUrl(s3, new PutObjectCommand({
  Bucket: 'my-documents',
  Key: `invoices/${invoiceId}.pdf`,
  ContentType: 'application/pdf'
}), { expiresIn: 3600 });

// 2. Start the job — TemplateTo uploads directly to your bucket
const webhookUrl = 'https://your-app.com/webhooks/templateto';
const response = await fetch(
  `https://api.templateto.com/render/async/pdf/tpl_abc123?${new URLSearchParams({
    outputPresignedUrl: presignedUrl,
    webhookUrl: webhookUrl
  })}`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'your-api-key',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(invoiceData)
  }
);

// 3. When the webhook fires, the PDF is already in your S3 bucket

With this setup, TemplateTo generates the document, uploads it straight to your bucket, and then notifies your application via webhook. Zero polling, zero downloads.

All Output Formats Supported

Async rendering isn’t just for PDFs. You can use it with every output format TemplateTo supports:

  • PDFrender/async/pdf/{templateId}
  • TXTrender/async/txt/{templateId}
  • Image (PNG/JPEG)render/async/image/{templateId} — see our image generation announcement for the full details on PNG, JPEG, clips, and circular masks

You can also render from raw HTML using the /fromhtml variants, or use image clips to extract multiple image regions from a single render.

When to Use Async vs Sync

Synchronous rendering is still the best choice when you need a document immediately in response to a user action — for example, a “Download PDF” button that returns the file directly.

Async is the better fit when:

  • Your document takes more than a few seconds to render
  • You’re generating documents as part of a background workflow
  • You’re running in a serverless environment with execution time limits
  • You want to decouple document generation from the rest of your application
  • You need the result uploaded to your own storage

Both approaches use the same templates, the same data format, and the same rendering engine. The only difference is how you receive the output.

Getting Started

Async rendering is available now on all plans. To try it out, just change your render endpoint from /render/pdf/{templateId} to /render/async/pdf/{templateId}.

Here’s the simplest possible example with cURL:

curl -X POST \
  "https://api.templateto.com/render/async/pdf/tpl_abc123" \
  -H "X-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"customerName": "Acme Corp", "total": 150.00}'

For the full API reference — including all query parameters, response formats, and error codes — head over to our async rendering documentation.

Got questions or feedback? Get in touch via our contact page — we’d love to hear how you’re using async rendering in your workflows.