Async Conversion
The /v1/async endpoint accepts a PDF conversion request from a URL, HTML, or a published template with JSON data and immediately returns a requestId. PDFBolt processes the request in the background and sends the final result to your HMAC-signed webhook when the conversion succeeds or all conversion attempts fail. The webhook parameter is required.
The /v1/async endpoint is available on paid plans. Free plan users can use /v1/direct and /v1/sync.
Endpoint Details
Method: POST
https://api.pdfbolt.com/v1/async
Success Example
- Request
- cURL
- Response
- Webhook Request
Example request body with the required webhook URL:
{
"url": "https://example.com",
"webhook": "https://your-app.com/webhook"
}
Complete request with authentication:
curl 'https://api.pdfbolt.com/v1/async' \
-H 'API-KEY: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' \
-H 'Content-Type: application/json' \
-d '{"url": "https://example.com", "webhook": "https://your-app.com/webhook"}'
{
"requestId": "4da0a428-16e0-4c95-b1d3-a8f475ed717e"
}
PDFBolt sends a POST request to your webhook URL.
Headers:
Content-Type: application/json
x-pdfbolt-signature: sha256=a1b2c3d4e5f6...
x-pdfbolt-conversion-cost: 1
Body:
{
"requestId": "4da0a428-16e0-4c95-b1d3-a8f475ed717e",
"status": "SUCCESS",
"errorCode": null,
"errorMessage": null,
"documentUrl": "https://s3.pdfbolt.com/pdfbolt_89878444-79a5-4115-beeb-f36745d61cf7_2026-04-30T10-47-03Z.pdf",
"expiresAt": "2026-05-01T10:47:03Z",
"isAsync": true,
"duration": 574,
"documentSizeMb": 0.02,
"isCustomS3Bucket": false
}
Failure Example
Requests rejected before acceptance return an HTTP error and do not trigger a webhook. After a request is accepted, a final conversion failure, such as a timeout or target page error, is delivered to the webhook with status: "FAILURE" and an errorCode. See Async Conversion Failures for recommended actions and the OpenAPI Reference for the exact schema.
- Request
- Response
- Webhook Request
{
"url": "https://example.com",
"webhook": "https://your-app.com/webhook",
"waitForFunction": "() => document.body.innerText.includes('Ready to Download')"
}
{
"requestId": "a3bf2ab7-5ef3-4d8b-a715-765697611dce"
}
PDFBolt sends a POST request to your webhook URL.
Headers:
Content-Type: application/json
x-pdfbolt-signature: sha256=f7e8d9c0b1a2...
x-pdfbolt-conversion-cost: 0
Body:
{
"requestId": "a3bf2ab7-5ef3-4d8b-a715-765697611dce",
"status": "FAILURE",
"errorCode": "CONVERSION_TIMEOUT",
"errorMessage": "Conversion process timed out. Please see https://pdfbolt.com/docs/parameters#timeout, https://pdfbolt.com/docs/parameters#waituntil and https://pdfbolt.com/docs/parameters#waitforfunction parameters.",
"documentUrl": null,
"expiresAt": null,
"isAsync": true,
"duration": 30541,
"documentSizeMb": null,
"isCustomS3Bucket": false
}
Body Parameters
The parameters below configure /v1/async processing and webhook delivery. customS3PresignedUrl is also available on the /v1/sync endpoint. For parameters shared by all Conversion API endpoints, see Conversion Parameters.
webhook
Type: string
Required: Yes
Details: The HTTPS URL to which PDFBolt sends the conversion result. It must accept POST requests and be publicly reachable.
Validation rules:
- HTTPS only (HTTP is rejected).
- Maximum length: 2048 characters.
- Test domains (e.g.,
.test) are not accepted.
Usage:
{
"url": "https://example.com",
"webhook": "https://your-app.com/endpoint"
}
customS3PresignedUrl
Type: string
Required: No
Details:
Specifies an HTTPS pre-signed PUT URL for direct upload to your S3-compatible bucket. The URL must be no longer than 2048 characters. When provided, the webhook reports documentUrl and expiresAt as null, and isCustomS3Bucket as true. If not provided, the document is stored in PDFBolt's temporary storage for 24 hours. See Uploading to Your S3 Bucket for setup details.
Usage:
{
"url": "https://example.com",
"webhook": "https://your-app.com/endpoint",
"customS3PresignedUrl": "https://your-bucket.s3.amazonaws.com/document.pdf?<presigned-query-params>"
}
additionalWebhookHeaders
Type: object
Required: No
Details: Adds custom headers to webhook callbacks. Use them to pass context to your webhook endpoint.
Validation rules:
- JSON object with string keys and string values.
- Header values cannot be
null. - Maximum 10 headers.
- The combined UTF-8 size of all header names and values must not exceed 4 KB.
Set automatically by PDFBolt – do not include: Content-Type, x-pdfbolt-signature, x-pdfbolt-conversion-cost.
Usage:
{
"url": "https://example.com",
"webhook": "https://your-app.com/endpoint",
"additionalWebhookHeaders": {
"X-Custom-Header-1": "Value1",
"X-Custom-Header-2": "Value2"
}
}
Use additionalWebhookHeaders to:
- Add tenant or environment identifiers (e.g.,
X-Tenant-Id,X-Environment). - Forward user or session context (e.g.,
X-User-Id). - Include correlation IDs for request tracing across services.
retryDelays
Type: Array<integer>
Required: No
Details:
The retryDelays parameter defines a custom retry schedule for failed conversions. Each element is the delay in minutes before the next retry attempt, measured from the last failed attempt. Total attempts = 1 (initial) + length of the retryDelays array.
Validation rules:
- Array of positive integers in minutes. Floating-point values are rejected.
- Minimum 1 element, maximum 5 elements (empty array is rejected).
- Values must be in strictly ascending order (e.g.,
[5, 5, 10]is rejected). - Maximum value per element: 1440 (24 hours).
- PDFBolt attempts the webhook only after a conversion succeeds or all retries fail. Intermediate retry failures do not trigger a callback.
- Only successful conversions consume credits, regardless of how many retries occur.
Usage:
{
"url": "https://example.com",
"webhook": "https://your-app.com/endpoint",
"retryDelays": [1, 5, 15]
}
In the example above, there are 4 total attempts (1 initial + 3 retries). If the first conversion attempt fails:
- Retry 1: 1 minute after the first failure.
- Retry 2: 5 minutes after retry 1 fails.
- Retry 3: 15 minutes after retry 2 fails.
- If any attempt succeeds, the webhook is called immediately with
SUCCESSstatus and remaining retries are skipped. - If all retries fail, the webhook is called with
FAILUREstatus.
Response Parameters
| Parameter | Type | Description | Possible Values | Example Value |
|---|---|---|---|---|
requestId | string (UUID) |
| Any valid UUID | 4da0a428-16e0-4c95-b1d3-a8f475ed717e |
Webhook Request Parameters
| Parameter | Type | Description | Possible Values | Example Value |
|---|---|---|---|---|
requestId | string (UUID) |
| Any valid UUID | 4da0a428-16e0-4c95-b1d3-a8f475ed717e |
status | string (Enum) |
| SUCCESSFAILURE | SUCCESS |
errorCode | string or null |
| Refer to errorCode values in the HTTP Status Codes table for all possible values | CONVERSION_TIMEOUT |
errorMessage | string or null |
| Any string or null | Conversion process timed out. Please see https://pdfbolt.com/docs/parameters#timeout, https://pdfbolt.com/docs/parameters#waituntil and https://pdfbolt.com/docs/parameters#waitforfunction parameters. |
documentUrl | string (URL) or null |
| Any valid URL or null | https://s3.pdfbolt.com/pdfbolt_89878444-79a5-4115-beeb-f36745d61cf7_2026-04-30T10-47-03Z.pdf |
expiresAt | string (ISO 8601) or null |
| ISO 8601 datetime string in UTC or null | 2026-05-01T10:47:03Z |
isAsync | boolean |
| true | true |
duration | integer |
| Any non-negative integer | 574 |
documentSizeMb | number or null |
| Any non-negative number or null | 0.02 |
isCustomS3Bucket | boolean |
| truefalse | false |
Webhook Delivery Behavior
PDFBolt attempts one webhook callback per accepted conversion – after the conversion succeeds or all retries fail. retryDelays retries the conversion attempt itself – it does not retry webhook delivery.
If webhook delivery fails because of a timeout, a 5xx response, or a network error, PDFBolt does not retry it automatically. To investigate:
- Check the conversion status in your Dashboard using the
requestId. - Review the Webhook Result column in the Dashboard – it shows the HTTP response code returned by your endpoint. Unreachable means the delivery failed (timeout or network error).
Recommended client-side practices:
- After verifying the signature, return a fast
2xxresponse and run any slow follow-up work in your own background job. - Make your handler idempotent – use
requestIdas a deduplication key. - Monitor your endpoint uptime; if it goes down, expect missed deliveries during the outage.
For response and webhook headers, see API Response Headers.
Webhook Signature Verification
Each webhook request includes an x-pdfbolt-signature header, so you can confirm it genuinely came from PDFBolt. The signature is an HMAC-SHA256 hash formatted as:
x-pdfbolt-signature: sha256=<hex-encoded-hmac>
How It Works
- PDFBolt computes
HMAC-SHA256(webhook_signature_key, raw_request_body)using your webhook signature key. - The result is hex-encoded and prefixed with
sha256=. - The signature is sent in the
x-pdfbolt-signatureheader of every webhook request (both success and failure).
Finding Your Webhook Signature Key
Find your webhook signature key in the Webhook Signature section of the API Credentials page in your Dashboard.
Verification Examples
- Always compute the HMAC from the raw request body – before any JSON parsing. If you parse and re-serialize the JSON, key ordering or whitespace may change, producing a different hash.
- Use a constant-time comparison function to prevent timing attacks.
- Node.js
- Python
- Java
- PHP
- C#
- Go
- Rust
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signatureHeader, webhookSignatureKey) {
const expected = 'sha256=' + crypto
.createHmac('sha256', webhookSignatureKey)
.update(rawBody, 'utf8')
.digest('hex');
const expectedBuf = Buffer.from(expected);
const receivedBuf = Buffer.from(signatureHeader);
if (expectedBuf.length !== receivedBuf.length) {
return false;
}
return crypto.timingSafeEqual(expectedBuf, receivedBuf);
}
import hmac
import hashlib
def verify_webhook_signature(raw_body: bytes, signature_header: str, webhook_signature_key: str) -> bool:
expected = 'sha256=' + hmac.new(
webhook_signature_key.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public static boolean verifyWebhookSignature(byte[] rawBody, String signatureHeader, String webhookSignatureKey) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(webhookSignatureKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal(rawBody);
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
hex.append(String.format("%02x", b));
}
String expected = "sha256=" + hex;
return MessageDigest.isEqual(expected.getBytes(StandardCharsets.UTF_8), signatureHeader.getBytes(StandardCharsets.UTF_8));
}
function verifyWebhookSignature(string $rawBody, string $signatureHeader, string $webhookSignatureKey): bool
{
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $webhookSignatureKey);
return hash_equals($expected, $signatureHeader);
}
using System.Security.Cryptography;
using System.Text;
static bool VerifyWebhookSignature(byte[] rawBody, string signatureHeader, string webhookSignatureKey)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(webhookSignatureKey));
byte[] hash = hmac.ComputeHash(rawBody);
string expected = "sha256=" + Convert.ToHexString(hash).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(signatureHeader)
);
}
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
)
func verifyWebhookSignature(rawBody []byte, signatureHeader, webhookSignatureKey string) bool {
mac := hmac.New(sha256.New, []byte(webhookSignatureKey))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return subtle.ConstantTimeCompare([]byte(expected), []byte(signatureHeader)) == 1
}
// Cargo.toml dependencies: hmac = "0.12", sha2 = "0.10", hex = "0.4"
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
fn verify_webhook_signature(raw_body: &[u8], signature_header: &str, webhook_signature_key: &str) -> bool {
let hex_part = match signature_header.strip_prefix("sha256=") {
Some(hex) => hex,
None => return false,
};
let received_bytes = match hex::decode(hex_part) {
Ok(bytes) => bytes,
Err(_) => return false,
};
let mut mac = HmacSha256::new_from_slice(webhook_signature_key.as_bytes())
.expect("HMAC can take key of any size");
mac.update(raw_body);
mac.verify_slice(&received_bytes).is_ok()
}
Each framework reads request bodies differently. Here's how to access raw bytes for HMAC verification:
- Express: register
express.raw({ type: 'application/json' })middleware on the webhook route specifically (route-specific middleware avoids conflicts with globalexpress.json()). - Flask: use
request.get_data()(returns raw bytes) instead ofrequest.json. - Spring: declare your handler parameter as
@RequestBody byte[] rawBody(resolved viaByteArrayHttpMessageConverter). - ASP.NET Core: read
Request.Bodystream intobyte[]– use PipeReader (recommended) orMemoryStream.CopyToAsyncfor the simpler approach. - PHP / Laravel: read raw input with
file_get_contents('php://input')(or$request->getContent()in Laravel, inherited from Symfony HttpFoundation).
Go (Gin, Echo) and Rust (Axum, Actix-web) read raw body by default – no setup needed.
Next Steps
📄️ Quick Start Guide
Node.js, Python, and PHP SDKs, cURL, and REST examples
📄️ Conversion Parameters
Full reference for all PDF generation parameters
📄️ Async Workflow Details
Step-by-step async flow with diagram
📄️ Uploading to Your S3 Bucket
Pre-signed URL setup with Node.js example