Async Conversion
The /v1/async endpoint accepts a request, immediately returns a requestId, then delivers the result via HMAC-signed webhook callback once the PDF is ready. Use it when you want PDFBolt to process the conversion in the background and notify your application via webhook. The webhook parameter is mandatory.
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 mandatory 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
If the conversion fails (e.g., timeout, invalid URL, target server error), the webhook callback delivers a JSON object with status: "FAILURE" and an errorCode. See Error Handling for all error codes and recommended actions.
- 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
Below are only the parameters specific to the /async endpoint. For common parameters shared across all endpoints, see Conversion Parameters.
webhook
Type: string
Required: Yes
Details: Your webhook endpoint URL where PDFBolt delivers the conversion result. The endpoint must accept POST requests.
Validation rules:
- HTTPS only (HTTP is rejected).
- Maximum length: 2048 characters.
- Must be a publicly reachable domain. 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 URL for direct upload to your S3-compatible bucket. The URL must be no longer than 2048 characters. When provided, the webhook callback's documentUrl is null and isCustomS3Bucket: true (file goes directly to your bucket). If not provided, the document is stored in PDFBolt's S3 bucket 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: Includes custom headers in webhook callbacks. Use this parameter 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.
- Combined header key/value size must not exceed 4KB.
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<number>
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 (minutes), floats 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).
- Webhooks are sent only on final failure or success. Intermediate retry failures do not trigger a webhook 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 |
| Refer to errorCode values in the HTTP Status Codes table for all possible values | CONVERSION_TIMEOUT |
errorMessage | string |
| 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) |
| 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) |
| ISO 8601 datetime string in UTC or null | 2026-05-01T10:47:03Z |
isAsync | boolean |
| true | true |
duration | number |
| Any positive number | 574 |
documentSizeMb | number |
| Any positive number or null | 0.02 |
isCustomS3Bucket | boolean |
| truefalse | false |
Webhook Delivery Behavior
PDFBolt sends exactly one webhook per conversion — on success, or after all retries fail. retryDelays retries the conversion attempt itself – it does not retry webhook delivery.
If your webhook endpoint is unavailable when PDFBolt sends the callback (timeout, 5xx, network error), the delivery is not automatically retried. 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
200 OKresponse 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 your Dashboard, on the API Keys page, under Webhook Signature.

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
Code samples in Node.js, Python, Java, PHP, C#, Go, Rust
📄️ 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