Guides

Async Polling

When a PDF is too large for synchronous processing or you explicitly request async mode, PDFPipe queues the job and returns a poll URL. This guide explains the flow and shows how to implement it.

When does async processing happen?

Async processing is used when either:

  • The PDF is an attachment (auto-detected or type: "attachment") - headless browser jobs always run in the queue
  • The PDF file is 10 MB or larger, or the host does not report a Content-Length (unless you set timeout)
  • You set "async": true in the request body
  • Synchronous processing exceeds your timeout (1-60 seconds) - the API queues the job and includes a message on the 202 response
  • You use the batch endpoint (always async)

The API returns 202 Accepted instead of 200, with a pollUrl you can use to check progress.

The async flow

1. Submit the request

curl - async request
curl -X POST https://api.pdfpipe.dev/v1/convert \
  -H "Authorization: Bearer pk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/large-report.pdf",
    "format": "json",
    "async": true
  }'

2. Receive the poll URL

202 - Accepted
{
  "requestId": "req_a1b2c3...",
  "status": "queued",
  "pollUrl": "/v1/status/req_a1b2c3..."
}

If you set webhook on the convert request, the body may also include webhook.url and webhook.secret. After a timeout-driven fallback to the queue, a message field may explain why the job is async.

202 - With webhook / message (optional)
{
  "requestId": "req_a1b2c3...",
  "status": "queued",
  "pollUrl": "/v1/status/req_a1b2c3...",
  "webhook": { "url": "https://example.com/hooks/pdfpipe", "secret": "whsec_..." },
  "message": "Processing did not complete within 20s timeout. Result will be delivered via polling or webhook."
}

3. Poll for status

Make GET requests to /v1/status/:requestId every 5 seconds (append ?returnMethod=inline if you want content in the JSON when status is complete). The status progresses through:

pendingprocessingcomplete
Still processing
{
  "requestId": "req_a1b2c3...",
  "status": "processing",
  "format": "json",
  "type": "inline",
  "createdAt": "2026-08-23T11:00:00.000Z",
  "updatedAt": "2026-08-23T11:00:02.500Z"
}
Complete - result ready
{
  "requestId": "req_a1b2c3...",
  "status": "complete",
  "format": "json",
  "type": "inline",
  "detectedType": "inline",
  "pagesProcessed": 48,
  "creditsUsed": 1,
  "resultUrl": "https://pdfpipe-results.s3...",
  "expiresAt": "2026-08-24T11:00:00.000Z",
  "processingDurationMs": 12450,
  "createdAt": "2026-08-23T11:00:00.000Z",
  "updatedAt": "2026-08-23T11:00:12.450Z"
}

4. Handle failures

If the status is failed, the response includes an error object with a code, message, and suggestion. The codes are listed in the API Reference (for example NOT_A_PDF, PDF_NO_TEXT_LAYER, ATTACHMENT_DOWNLOAD_FAILED). A failed request still counts against your quota.

Failed response
{
  "requestId": "req_a1b2c3...",
  "status": "failed",
  "format": "json",
  "type": "attachment",
  "detectedType": "attachment",
  "error": {
    "code": "REDIRECT_TO_LOGIN",
    "message": "The URL redirected to a login page instead of serving a PDF",
    "suggestion": "The PDF may require authentication. Try using type: 'attachment' or ensure the URL is publicly accessible"
  },
  "createdAt": "2026-08-23T11:00:00.000Z",
  "updatedAt": "2026-08-23T11:00:08.000Z"
}

Timeout instead of guessing sync vs async

Set timeout to the maximum seconds (1-60) you are willing to wait inline. If the converter cannot finish in that window, you receive 202 and poll (or use a webhook) as usual. This avoids choosing between sync and async up front for borderline files.

Webhooks (no polling)

Pass webhook: { url, secret? } on POST /v1/convert. When the job is asynchronous, the 202 body includes webhook.url and webhook.secret (generated if you omitted secret). PDFPipe POSTs to your HTTPS endpoint when processing completes; verify payloads with HMAC-SHA256 using the X-PDFPipe-Signature: sha256=... header (see the API Reference).

Polling best practices

  • Poll every 5 seconds. Most conversions complete within 10-30 seconds.
  • Set a timeout. Stop polling after 5 minutes (60 attempts) and treat it as a failure.
  • Check for terminal states. Stop polling when status is complete or failed.
  • Download the result promptly. The resultUrl is a presigned link; each signed URL expires after a tier-based TTL, and the API stops issuing new URLs after your plan's result retention window from request creation (1 hour to 30 days).

Full Node.js example

Node.js / TypeScript
async function convertPdf(apiKey: string, pdfUrl: string) {
  // 1. Submit the request
  const res = await fetch("https://api.pdfpipe.dev/v1/convert", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ url: pdfUrl, format: "json", async: true }),
  });

  const data = await res.json();

  // 2. If synchronous, return immediately
  if (res.status === 200 && data.status === "complete") {
    return data;
  }
  if (!res.ok) {
    // 4xx/5xx: { code, message, suggestion?, statusCode }
    throw new Error(`${data.code}: ${data.message}`);
  }

  // 3. Poll until complete or failed
  const pollUrl = `https://api.pdfpipe.dev/v1/status/${data.requestId}`;
  const maxAttempts = 60;

  for (let i = 0; i < maxAttempts; i++) {
    await new Promise(r => setTimeout(r, 5000));

    const poll = await fetch(pollUrl, {
      headers: { "Authorization": `Bearer ${apiKey}` },
    });
    const status = await poll.json();

    if (status.status === "complete") return status;
    if (status.status === "failed") throw new Error(status.error.message);
  }

  throw new Error("Polling timed out");
}