FormaTeX

\begin{article}

Async LaTeX Compilation and Webhooks

Synchronous compilation is not always enough. Learn how to use FormaTeX async jobs, polling, PDF downloads, and webhooks for production-grade LaTeX compilation workflows.

·5 min read·
Async LaTeX Compilation and Webhooks

The synchronous FormaTeX endpoint (POST /compile) works perfectly for most documents — send LaTeX, receive PDF. But some workflows need more: long documents that take minutes to compile, batch jobs that should not block your API, or UI flows where you want to show progress instead of a loading spinner.

That is what async compilation and webhooks are for.

When Sync Is Not Enough

Synchronous compilation blocks the HTTP connection until the PDF is ready. This is fine for:

  • Documents that compile in under 10 seconds
  • Backend-to-backend API calls
  • Simple "compile and return" flows

It becomes a problem for:

  • Long documents (theses, books) that take 60–300 seconds with latexmk
  • Batch jobs (1,000 certificates) where you do not want 1,000 open HTTP connections
  • UI responsiveness — users should see "compiling..." status, not a frozen screen
  • CI/CD pipelines that should not block on a single step
  • SaaS products where you want to queue compilation and notify via webhook

Async Compilation Flow

The async flow has three steps: submit, poll (or webhook), download.

Step 1: Submit

bash
curl -X POST https://api.formatex.io/api/v1/compile/async \
  -H "X-API-Key: $FORMATEX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "\\documentclass{article}\\begin{document}Long document...\\end{document}",
    "engine": "latexmk"
  }'

Response:

json
{
  "job_id": "job_abc123",
  "status": "pending"
}

The request returns immediately with a job ID. Compilation happens in the background.

Step 2: Poll for Status

bash
curl https://api.formatex.io/api/v1/jobs/job_abc123 \
  -H "X-API-Key: $FORMATEX_KEY"

Response (while compiling):

json
{
  "job_id": "job_abc123",
  "status": "processing",
  "created_at": "2026-03-11T10:00:00Z"
}

Response (when complete):

json
{
  "job_id": "job_abc123",
  "status": "completed",
  "created_at": "2026-03-11T10:00:00Z",
  "completed_at": "2026-03-11T10:00:12Z",
  "duration_ms": 12340
}

Step 3: Download the PDF

bash
curl https://api.formatex.io/api/v1/jobs/job_abc123/pdf \
  -H "X-API-Key: $FORMATEX_KEY" \
  --output document.pdf

You can also retrieve the compilation log:

bash
curl https://api.formatex.io/api/v1/jobs/job_abc123/log \
  -H "X-API-Key: $FORMATEX_KEY"

Full Async Flow in TypeScript

typescript
async function compileAsync(latex: string, engine: string = "pdflatex"): Promise<Buffer> {
  const baseUrl = "https://api.formatex.io/api/v1";
  const headers = {
    "X-API-Key": process.env.FORMATEX_KEY!,
    "Content-Type": "application/json",
  };

  // Submit job
  const submitResponse = await fetch(`${baseUrl}/compile/async`, {
    method: "POST",
    headers,
    body: JSON.stringify({ content: latex, engine }),
  });

  if (!submitResponse.ok) {
    const error = await submitResponse.json();
    throw new Error(error.error);
  }

  const { job_id } = await submitResponse.json();

  // Poll for completion
  while (true) {
    const statusResponse = await fetch(`${baseUrl}/jobs/${job_id}`, { headers });
    const job = await statusResponse.json();

    if (job.status === "completed") break;
    if (job.status === "failed") {
      throw new Error(`Compilation failed: ${job.error}`);
    }

    // Wait 1 second between polls
    await new Promise((resolve) => setTimeout(resolve, 1000));
  }

  // Download PDF
  const pdfResponse = await fetch(`${baseUrl}/jobs/${job_id}/pdf`, { headers });
  return Buffer.from(await pdfResponse.arrayBuffer());
}

Full Async Flow in Python

python
import os
import time
import requests

BASE_URL = "https://api.formatex.io/api/v1"
HEADERS = {"X-API-Key": os.environ["FORMATEX_KEY"]}


def compile_async(latex_source: str, engine: str = "pdflatex") -> bytes:
    # Submit job
    response = requests.post(
        f"{BASE_URL}/compile/async",
        headers=HEADERS,
        json={"content": latex_source, "engine": engine},
    )
    response.raise_for_status()
    job_id = response.json()["job_id"]

    # Poll for completion
    while True:
        status = requests.get(f"{BASE_URL}/jobs/{job_id}", headers=HEADERS)
        job = status.json()

        if job["status"] == "completed":
            break
        if job["status"] == "failed":
            raise RuntimeError(f"Compilation failed: {job.get('error')}")

        time.sleep(1)

    # Download PDF
    pdf = requests.get(f"{BASE_URL}/jobs/{job_id}/pdf", headers=HEADERS)
    return pdf.content

Webhooks: No Polling Required

Instead of polling, you can register a webhook to receive a notification when a job completes.

Setting Up a Webhook

bash
curl -X POST https://api.formatex.io/api/v1/webhooks \
  -H "X-API-Key: $FORMATEX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/api/formatex-webhook",
    "events": ["job.completed", "job.failed"]
  }'

Webhook Payload

When a job completes, FormaTeX sends a POST request to your webhook URL:

json
{
  "event": "job.completed",
  "job_id": "job_abc123",
  "status": "completed",
  "duration_ms": 12340,
  "completed_at": "2026-03-11T10:00:12Z"
}

Your receiver then downloads the PDF using the job ID.

Managing Webhooks

bash
# List all webhooks
curl https://api.formatex.io/api/v1/webhooks \
  -H "X-API-Key: $FORMATEX_KEY"

# Delete a webhook
curl -X DELETE https://api.formatex.io/api/v1/webhooks/wh_123 \
  -H "X-API-Key: $FORMATEX_KEY"

Use Cases

CI/CD Pipelines

Submit compilation jobs at the start of your pipeline and collect results at the end, without blocking other steps:

yaml
- name: Submit LaTeX compilation
  run: |
    JOB_ID=$(curl -s -X POST https://api.formatex.io/api/v1/compile/async \
      -H "X-API-Key: $FORMATEX_KEY" \
      -H "Content-Type: application/json" \
      -d "{\"content\": $(cat report.tex | jq -Rs .), \"engine\": \"latexmk\"}" \
      | jq -r '.job_id')
    echo "JOB_ID=$JOB_ID" >> $GITHUB_ENV

# ... other pipeline steps run in parallel ...

- name: Download compiled PDF
  run: |
    # Poll until ready
    while true; do
      STATUS=$(curl -s https://api.formatex.io/api/v1/jobs/$JOB_ID \
        -H "X-API-Key: $FORMATEX_KEY" | jq -r '.status')
      [ "$STATUS" = "completed" ] && break
      sleep 2
    done
    curl -s https://api.formatex.io/api/v1/jobs/$JOB_ID/pdf \
      -H "X-API-Key: $FORMATEX_KEY" --output report.pdf

Batch Certificate Generation

Generate hundreds of certificates without overwhelming the API:

typescript
async function generateCertificatesBatch(recipients: CertificateData[]) {
  const jobIds: string[] = [];

  // Submit all jobs
  for (const recipient of recipients) {
    const latex = buildCertificateLatex(recipient);
    const response = await fetch("https://api.formatex.io/api/v1/compile/async", {
      method: "POST",
      headers: {
        "X-API-Key": process.env.FORMATEX_KEY!,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ content: latex, engine: "xelatex" }),
    });

    const { job_id } = await response.json();
    jobIds.push(job_id);
  }

  // Collect results (or let webhooks notify you)
  console.log(`Submitted ${jobIds.length} jobs`);
}

SaaS Products

Let users trigger document generation without waiting:

  1. User clicks "Generate Report"
  2. Your backend submits an async job
  3. User sees "Generating..." with a progress indicator
  4. Webhook notifies your backend when ready
  5. User receives a notification with a download link

For batch jobs, prefer async compilation with webhooks over synchronous compilation. This keeps your HTTP connections free, handles long documents gracefully, and gives you a natural retry mechanism for failed jobs.

Get Started

\end{article}

Back to blog

\related{posts}

One quick thing

We track anonymous usage — page views, feature usage, compilation events — to understand what works and what doesn't. No ads, no personal data, no third-party sharing.

Cookie policy