\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.

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
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:
{
"job_id": "job_abc123",
"status": "pending"
}The request returns immediately with a job ID. Compilation happens in the background.
Step 2: Poll for Status
curl https://api.formatex.io/api/v1/jobs/job_abc123 \
-H "X-API-Key: $FORMATEX_KEY"Response (while compiling):
{
"job_id": "job_abc123",
"status": "processing",
"created_at": "2026-03-11T10:00:00Z"
}Response (when complete):
{
"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
curl https://api.formatex.io/api/v1/jobs/job_abc123/pdf \
-H "X-API-Key: $FORMATEX_KEY" \
--output document.pdfYou can also retrieve the compilation log:
curl https://api.formatex.io/api/v1/jobs/job_abc123/log \
-H "X-API-Key: $FORMATEX_KEY"Full Async Flow in 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
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.contentWebhooks: No Polling Required
Instead of polling, you can register a webhook to receive a notification when a job completes.
Setting Up a Webhook
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:
{
"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
# 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:
- 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.pdfBatch Certificate Generation
Generate hundreds of certificates without overwhelming the API:
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:
- User clicks "Generate Report"
- Your backend submits an async job
- User sees "Generating..." with a progress indicator
- Webhook notifies your backend when ready
- 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
- Try the Playground — test compilation instantly
- Sign up for free — 15 compilations/month, no card required
- API documentation — complete endpoint reference
- Pricing — Pro, Max, and Enterprise plans
\end{article}
\related{posts}




