Webhooks & Async Processing
cloudlayer.io supports both synchronous and asynchronous processing modes. Understanding when to use each — and how to configure webhooks for async results — is key to building reliable document generation workflows.
Sync vs Async Processing
Synchronous Mode (async: false)
In synchronous mode, the API waits for the document to be generated and returns the result directly in the HTTP response.
{
"url": "https://example.com",
"async": false
}
Response:
{
"assetUrl": "https://storage.cloudlayer.io/assets/abc123/document.pdf",
"status": "success",
"jobId": "job_abc123def456"
}
Characteristics:
- The HTTP connection stays open until the document is ready
- The
assetUrlis populated in the response - Simpler to implement — no webhook setup needed
- Subject to connection timeouts for long-running jobs
Asynchronous Mode (async: true, default)
In asynchronous mode, the API immediately returns a job ID and processes the document in the background. When complete, the result is delivered to your webhook URL.
{
"url": "https://example.com",
"async": true
}
Immediate response:
{
"status": "queued",
"jobId": "job_abc123def456"
}
Webhook delivery (when complete):
{
"jobId": "job_abc123def456",
"status": "success",
"assetUrl": "https://storage.cloudlayer.io/assets/abc123/document.pdf",
"timestamp": "2024-01-15T10:30:00.000Z"
}
Characteristics:
- The HTTP connection closes immediately after queuing
- No risk of connection timeouts
- Results delivered via webhook callback
- Better for long-running and batch operations
When to Use Each Mode
| Scenario | Recommended Mode | Reason |
|---|---|---|
| Simple, fast conversions (single page) | Sync | Simpler implementation, immediate result |
| User-facing “download PDF” button | Sync | User expects immediate response |
| Batch processing (multiple documents) | Async | Avoids timeouts, handles volume |
| Large or complex documents | Async | May exceed sync timeout limits |
| Background/scheduled generation | Async | No user waiting for the result |
| High-volume automated pipelines | Async | Better throughput, reliable delivery |
| Pages with heavy JavaScript rendering | Async | Render time may be unpredictable |
Webhook Configuration
Setting Up Your Webhook Endpoint
Your webhook endpoint is a URL on your server that cloudlayer.io calls when an async job completes. Configure it in your cloudlayer.io dashboard under Settings.
Your endpoint must:
- Accept HTTP POST requests
- Return a
200status code to acknowledge receipt - Process the webhook payload
Webhook Payload
When a job completes, cloudlayer.io sends a POST request to your webhook URL with a JSON body:
Success payload:
{
"jobId": "job_abc123def456",
"status": "success",
"assetUrl": "https://storage.cloudlayer.io/assets/abc123/document.pdf",
"previewUrl": "https://storage.cloudlayer.io/assets/abc123/document-preview.webp",
"timestamp": "2024-01-15T10:30:00.000Z"
}
Error payload:
{
"jobId": "job_abc123def456",
"status": "error",
"error": "Timeout: page took longer than 30000ms to load",
"timestamp": "2024-01-15T10:30:05.000Z"
}
Payload Fields
| Field | Type | Description |
|---|---|---|
jobId | string | Unique identifier for the generation job |
status | string | Job outcome: success or error |
assetUrl | string | URL to the generated document (on success) |
previewUrl | string | URL to the preview thumbnail, if generatePreview was used |
error | string | Error description (on failure) |
timestamp | string | ISO 8601 timestamp of when the job completed |
Example Webhook Handler
Node.js (Express)
app.post("/webhooks/cloudlayer", (req, res) => {
const { jobId, status, assetUrl, error } = req.body;
if (status === "success") {
console.log(`Job ${jobId} completed: ${assetUrl}`);
// Process the generated document -- download it, email it, etc.
} else {
console.error(`Job ${jobId} failed: ${error}`);
// Handle the error -- retry, notify, log, etc.
}
// Always return 200 to acknowledge receipt
res.status(200).send("OK");
});
Python (Flask)
from flask import Flask, request
app = Flask(__name__)
@app.route("/webhooks/cloudlayer", methods=["POST"])
def handle_webhook():
payload = request.get_json()
job_id = payload["jobId"]
status = payload["status"]
if status == "success":
asset_url = payload["assetUrl"]
print(f"Job {job_id} completed: {asset_url}")
# Process the generated document
else:
error = payload.get("error", "Unknown error")
print(f"Job {job_id} failed: {error}")
# Handle the error
return "OK", 200
Retry Behavior
If your webhook endpoint does not return a 200 status code, cloudlayer.io will retry delivery:
- Retry attempts: Up to 3 retries
- Retry interval: Exponential backoff (increasing delay between attempts)
- Final failure: If all retries fail, the webhook delivery is marked as failed. You can still retrieve the job result via the jobs API.
Ensuring Reliable Delivery
- Return
200quickly. Process the webhook payload asynchronously (e.g., push to a queue) rather than doing heavy work inside the handler. - Make your handler idempotent. Webhooks may be delivered more than once in edge cases. Use the
jobIdto deduplicate. - Log all webhook deliveries for debugging and auditing.
Best Practices
Use Async for Production Pipelines
Synchronous mode is convenient for development and simple use cases, but async with webhooks is more robust for production:
- No risk of HTTP timeouts cutting off long-running jobs
- Your application does not need to hold open connections
- Better scalability for high-volume generation
Correlate Jobs with Your Data
Pass a projectId or use a naming convention in your filename to correlate generated documents with your internal records:
{
"url": "https://example.com/invoice/INV-001",
"filename": "invoice-INV-001.pdf",
"projectId": "N77VCoAVTHHwgmUfCYlD",
"async": true
}
When the webhook fires, use the jobId to look up which internal record triggered the generation.
Handle Errors Gracefully
Your webhook handler should account for:
- Timeout errors — the page took too long to render. Consider increasing the
timeoutparameter or simplifying the page. - Navigation errors — the URL was unreachable. Verify the URL is accessible and correctly spelled.
- Rendering errors — the page had JavaScript errors or missing resources. Test the page in a browser first.