Webhooks
Configure LlamaCloud webhooks to receive POST notifications on parse, extract, and classify job events, covering event filtering, saved reusable configurations, custom headers, payload signing, payload format, and retry behavior.
Webhooks allow you to receive real-time notifications when events occur in your LlamaCloud jobs. Instead of continuously polling for status updates, you can configure webhook endpoints to be notified immediately when jobs complete, fail, or reach other states.
Overview
Section titled “Overview”LlamaCloud webhooks provide:
- Real-time notifications for jobs
- Configurable event filtering to receive only relevant events
- Retry logic with exponential backoff for reliability
- Custom headers support for authentication
- Payload signing so you can verify deliveries are authentic and untampered
Supported Events
Section titled “Supported Events”Currently, LlamaCloud supports the following webhook events:
Extract Events
Section titled “Extract Events”extract.pending- Extract job has been queued and is waiting to be processedextract.success- Extract job completed successfullyextract.error- Extract job failed with an errorextract.partial_success- Extract job completed with some warnings or partial failuresextract.cancelled- Extract job was cancelled before completion
Parse Events
Section titled “Parse Events”parse.pending- Parse job has been queued and is waiting to be processedparse.success- Parse job completed successfullyparse.error- Parse job failed with an errorparse.partial_success- Parse job completed with some warnings or partial failuresparse.cancelled- Parse job was cancelled before completion
Classify Events
Section titled “Classify Events”classify.pending- Classify job has been queued and is waiting to be processedclassify.running- Classify job has started processingclassify.success- Classify job completed successfullyclassify.partial_success- Classify job completed with some warnings or partial failuresclassify.error- Classify job failed with an errorclassify.cancelled- Classify job was cancelled before completion
Batch Events
Section titled “Batch Events”batch.pending- Batch has been queued and is waiting to be processedbatch.running- Batch has started processing its source directorybatch.success- Batch finished and its per-file results are availablebatch.error- Batch failed and cannot provide a reliable per-file result set
Batch events describe the batch as a whole. A batch.success event means the
batch finished mapping every source file to a job — individual files may still
have failed, so read results (with expand=results) to see per-file outcomes.
Configuration
Section titled “Configuration”Reference the full API schema here: https://api.cloud.llamaindex.ai/redoc#tag/LlamaExtract/operation/run_job_api_v1_extraction_jobs_post
Basic Configuration
Section titled “Basic Configuration”Configure webhooks by including webhook configurations in your API calls. You will want to include the webhook configurations as follows:
{ ... "webhook_configurations": [ { "webhook_url": "string", "webhook_signing_secret": "string", "webhook_headers": { "property1": "string", "property2": "string" }, "webhook_events": [ "extract.pending", "extract.success", "extract.error" ], "webhook_output_format": "json" } ] ...}Here’s how to build that array in code — it is passed as the webhook_configurations parameter in the request body:
webhook_configurations = [ { "webhook_url": "https://your-domain.com/webhook-endpoint", "webhook_events": ["extract.success", "extract.error", "parse.success", "parse.error"], "webhook_headers": { "Authorization": "Bearer your-token", "X-Custom-Header": "custom-value" }, "webhook_output_format": "json" }]const webhookConfigurations = [ { webhook_url: 'https://your-domain.com/webhook-endpoint', webhook_events: ['extract.success', 'extract.error', 'parse.success', 'parse.error'], webhook_headers: { Authorization: 'Bearer your-token', 'X-Custom-Header': 'custom-value', }, webhook_output_format: 'json', },];webhookConfigurations := []llamacloud.ParsingNewParamsWebhookConfiguration{ { WebhookURL: llamacloud.String("https://your-domain.com/webhook-endpoint"), WebhookEvents: []string{"extract.success", "extract.error", "parse.success", "parse.error"}, WebhookHeaders: map[string]any{ "Authorization": "Bearer your-token", "X-Custom-Header": "custom-value", }, WebhookOutputFormat: "json", },}ParsingCreateParams.WebhookConfiguration webhookConfiguration = ParsingCreateParams.WebhookConfiguration.builder() .webhookUrl("https://your-domain.com/webhook-endpoint") .addWebhookEvent("extract.success") .addWebhookEvent("extract.error") .addWebhookEvent("parse.success") .addWebhookEvent("parse.error") .webhookHeaders(ParsingCreateParams.WebhookConfiguration.WebhookHeaders.builder() .putAdditionalProperty("Authorization", JsonValue.from("Bearer your-token")) .putAdditionalProperty("X-Custom-Header", JsonValue.from("custom-value")) .build()) .webhookOutputFormat(ParsingCreateParams.WebhookConfiguration.WebhookOutputFormat.JSON) .build();llp parsing create \ --file-id "$FILE_ID" \ --tier agentic \ --version latest \ --webhook-configuration.webhook-url 'https://your-domain.com/webhook-endpoint' \ --webhook-configuration.webhook-events '[extract.success, extract.error, parse.success, parse.error]' \ --webhook-configuration.webhook-headers '{Authorization: "Bearer your-token", X-Custom-Header: custom-value}' \ --webhook-configuration.webhook-output-format jsonThe Go, Java, and CLI examples in this section target the Parse surface. Extract and Classify take the same webhook_configurations JSON field, and the CLI flags are identical, but Go and Java generate a separate type per product — ExtractV2JobCreateWebhookConfigurationParam / ExtractV2JobCreate.WebhookConfiguration for Extract, ClassifyCreateRequestWebhookConfigurationParam / ClassifyCreateRequest.WebhookConfiguration for Classify. Some member signatures differ along with the name (Go types webhook_headers as map[string]string and webhook_output_format as param.Opt[string] on those two, not map[string]any and string), so read the generated type instead of only swapping the name in.
Reusing a saved configuration
Section titled “Reusing a saved configuration”Passing webhook_configurations inline means resending the endpoint — and its signing secret — on every job. Instead, save an endpoint once per project and reference it by ID.
Save the endpoint with a POST to /api/v1/beta/webhook-configs. Only webhook_url is required; webhook_events, webhook_headers, webhook_output_format, and webhook_signing_secret are all optional and mean the same thing as their inline counterparts.
curl -X POST 'https://api.cloud.llamaindex.ai/api/v1/beta/webhook-configs?project_id=YOUR_PROJECT_ID' \ -H "Authorization: Bearer $LLAMA_CLOUD_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "webhook_url": "https://your-domain.com/webhook-endpoint", "webhook_events": ["parse.success", "parse.error"], "webhook_signing_secret": "your-signing-secret", "webhook_output_format": "json" }'The response carries the ID you will reference. The signing secret is write-only — it is never returned, so the response reports only whether one is set:
{ "id": "whc-...", "tenant_type": "project", "tenant_id": "YOUR_PROJECT_ID", "webhook_url": "https://your-domain.com/webhook-endpoint", "webhook_events": ["parse.success", "parse.error"], "webhook_output_format": "json", "has_secret": true}Then reference it when creating a job, via webhook_configuration_ids:
job = client.parsing.create( file_id=file_id, tier="agentic", version="latest", webhook_configuration_ids=["whc-..."],)const job = await client.parsing.create({ file_id: fileId, tier: 'agentic', version: 'latest', webhook_configuration_ids: ['whc-...'],});curl -X POST 'https://api.cloud.llamaindex.ai/api/v2/parse?project_id=YOUR_PROJECT_ID' \ -H "Authorization: Bearer $LLAMA_CLOUD_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "file_id": "'"$FILE_ID"'", "tier": "agentic", "version": "latest", "webhook_configuration_ids": ["whc-..."] }'The saved endpoints are resolved server-side and appended after any inline webhook_configurations on the same request, so you can mix the two — a saved endpoint for your standard alerting plus a one-off inline endpoint for a single job.
Because the secret stays on the server, the endpoint is signed exactly as if you had passed the secret inline. See Securing webhooks with signatures for how to verify a delivery.
Two limits apply:
- A single create request may reference at most 4 saved configuration IDs.
- The resolved endpoints plus any inline
webhook_configurationsmust total at most 10 per job.
A configuration ID that does not exist in the project the job is being created in is rejected with a 400. IDs are scoped to their project, so an ID from another project will not resolve.
Event Filtering
Section titled “Event Filtering”You can specify which events to receive by setting the webhook_events array. If not specified, all events will be sent.
# Receive only success and error eventswebhook_configurations = [ { "webhook_url": "https://your-domain.com/webhook", "webhook_events": ["extract.success", "extract.error", "parse.success", "parse.error"], "webhook_output_format": "json" }]
# Receive all events (default behavior)webhook_configurations = [ { "webhook_url": "https://your-domain.com/webhook", "webhook_output_format": "json" # webhook_events omitted = receive all events }]// Receive only success and error eventsconst webhookConfigurations = [ { webhook_url: 'https://your-domain.com/webhook', webhook_events: ['extract.success', 'extract.error', 'parse.success', 'parse.error'], webhook_output_format: 'json', },];
// Receive all events (default behavior)const allEventConfigurations = [ { webhook_url: 'https://your-domain.com/webhook', webhook_output_format: 'json', // webhook_events omitted = receive all events },];// Receive only success and error eventswebhookConfigurations := []llamacloud.ParsingNewParamsWebhookConfiguration{ { WebhookURL: llamacloud.String("https://your-domain.com/webhook"), WebhookEvents: []string{"extract.success", "extract.error", "parse.success", "parse.error"}, WebhookOutputFormat: "json", },}
// Receive all events (default behavior)allEventConfigurations := []llamacloud.ParsingNewParamsWebhookConfiguration{ { WebhookURL: llamacloud.String("https://your-domain.com/webhook"), WebhookOutputFormat: "json", // WebhookEvents omitted = receive all events },}// Receive only success and error eventsParsingCreateParams.WebhookConfiguration webhookConfiguration = ParsingCreateParams.WebhookConfiguration.builder() .webhookUrl("https://your-domain.com/webhook") .addWebhookEvent("extract.success") .addWebhookEvent("extract.error") .addWebhookEvent("parse.success") .addWebhookEvent("parse.error") .webhookOutputFormat(ParsingCreateParams.WebhookConfiguration.WebhookOutputFormat.JSON) .build();
// Receive all events (default behavior)ParsingCreateParams.WebhookConfiguration allEventConfiguration = ParsingCreateParams.WebhookConfiguration.builder() .webhookUrl("https://your-domain.com/webhook") .webhookOutputFormat(ParsingCreateParams.WebhookConfiguration.WebhookOutputFormat.JSON) // webhookEvents omitted = receive all events .build();# Receive only success and error eventsllp parsing create \ --file-id "$FILE_ID" \ --tier agentic \ --version latest \ --webhook-configuration.webhook-url 'https://your-domain.com/webhook' \ --webhook-configuration.webhook-events '[extract.success, extract.error, parse.success, parse.error]' \ --webhook-configuration.webhook-output-format json
# Receive all events (default behavior)llp parsing create \ --file-id "$FILE_ID" \ --tier agentic \ --version latest \ --webhook-configuration.webhook-url 'https://your-domain.com/webhook' \ --webhook-configuration.webhook-output-format jsonCustom Headers
Section titled “Custom Headers”Add custom headers for authentication or other purposes:
webhook_configurations = [ { "webhook_url": "https://your-domain.com/webhook", "webhook_headers": { "Authorization": "Bearer your-secret-token", "X-Source": "llamacloud", "Content-Type": "application/json" # This is set automatically }, "webhook_output_format": "json" }]const webhookConfigurations = [ { webhook_url: 'https://your-domain.com/webhook', webhook_headers: { Authorization: 'Bearer your-secret-token', 'X-Source': 'llamacloud', 'Content-Type': 'application/json', // This is set automatically }, webhook_output_format: 'json', },];webhookConfigurations := []llamacloud.ParsingNewParamsWebhookConfiguration{ { WebhookURL: llamacloud.String("https://your-domain.com/webhook"), WebhookHeaders: map[string]any{ "Authorization": "Bearer your-secret-token", "X-Source": "llamacloud", "Content-Type": "application/json", // This is set automatically }, WebhookOutputFormat: "json", },}ParsingCreateParams.WebhookConfiguration webhookConfiguration = ParsingCreateParams.WebhookConfiguration.builder() .webhookUrl("https://your-domain.com/webhook") .webhookHeaders(ParsingCreateParams.WebhookConfiguration.WebhookHeaders.builder() .putAdditionalProperty("Authorization", JsonValue.from("Bearer your-secret-token")) .putAdditionalProperty("X-Source", JsonValue.from("llamacloud")) // Content-Type is set automatically .putAdditionalProperty("Content-Type", JsonValue.from("application/json")) .build()) .webhookOutputFormat(ParsingCreateParams.WebhookConfiguration.WebhookOutputFormat.JSON) .build();# Content-Type is set automaticallyllp parsing create \ --file-id "$FILE_ID" \ --tier agentic \ --version latest \ --webhook-configuration.webhook-url 'https://your-domain.com/webhook' \ --webhook-configuration.webhook-headers '{Authorization: "Bearer your-secret-token", X-Source: llamacloud, Content-Type: application/json}' \ --webhook-configuration.webhook-output-format jsonWebhook Payload
Section titled “Webhook Payload”When an event occurs, LlamaCloud will send a POST request to your webhook URL with the following payload structure:
{ "event_id": "149744dd-9002-4411-a6c7-9635da372caa", "event_type": "parse.success", "timestamp": 1753985275.1154444, "data": { "id": "a9a57884-921e-4ec2-b555-f4e5a97ec02a", "job_id": "a9a57884-921e-4ec2-b555-f4e5a97ec02a" }}Payload Fields
Section titled “Payload Fields”event_id: Unique identifier for this webhook eventevent_type: The type of event that occurred (e.g., “extract.success”, “parse.success”)timestamp: Unix timestamp when the event occurreddata: Event-specific data containing job details and results
HTTP Headers
Section titled “HTTP Headers”LlamaCloud includes these headers with webhook requests:
Content-Type: application/jsonUser-Agent: llamaindex-webhook-service/1.0X-Webhook-Event-ID: {event_id}X-Webhook-Event-Type: {event_type}LC-Signature: sha256={signature}— present only when awebhook_signing_secretis configured (see Securing webhooks)- Any custom headers you configured
Securing webhooks with signatures
Section titled “Securing webhooks with signatures”To verify that a webhook delivery genuinely originated from LlamaCloud and was not tampered with in transit, configure a webhook_signing_secret for the endpoint. When a signing secret is set, every request is signed and the signature is sent in the LC-Signature header.
The signature is an HMAC-SHA256 hex digest of the raw request body, keyed by your signing secret, with a sha256= prefix. The sha256= prefix names the algorithm, so the scheme can evolve without renaming the header.
webhook_configurations = [ { "webhook_url": "https://your-domain.com/webhook", "webhook_signing_secret": "your-shared-secret", "webhook_events": ["parse.success", "parse.error"], "webhook_output_format": "json", }]const webhookConfigurations = [ { webhook_url: 'https://your-domain.com/webhook', webhook_signing_secret: 'your-shared-secret', webhook_events: ['parse.success', 'parse.error'], webhook_output_format: 'json', },];webhookConfigurations := []llamacloud.ParsingNewParamsWebhookConfiguration{ { WebhookURL: llamacloud.String("https://your-domain.com/webhook"), WebhookSigningSecret: llamacloud.String("your-shared-secret"), WebhookEvents: []string{"parse.success", "parse.error"}, WebhookOutputFormat: "json", },}ParsingCreateParams.WebhookConfiguration webhookConfiguration = ParsingCreateParams.WebhookConfiguration.builder() .webhookUrl("https://your-domain.com/webhook") // The Java SDK has no typed setter for the signing secret yet .putAdditionalProperty("webhook_signing_secret", JsonValue.from("your-shared-secret")) .addWebhookEvent("parse.success") .addWebhookEvent("parse.error") .webhookOutputFormat(ParsingCreateParams.WebhookConfiguration.WebhookOutputFormat.JSON) .build();llp parsing create \ --file-id "$FILE_ID" \ --tier agentic \ --version latest \ --webhook-configuration.webhook-url 'https://your-domain.com/webhook' \ --webhook-configuration.webhook-signing-secret 'your-shared-secret' \ --webhook-configuration.webhook-events '[parse.success, parse.error]' \ --webhook-configuration.webhook-output-format jsonVerifying a delivery
Section titled “Verifying a delivery”Recompute the HMAC over the raw request body with your secret and compare it to the header value using a constant-time comparison. Always compute the signature over the raw bytes of the request body — do not re-serialize the parsed JSON first, as that can change the bytes and break verification.
import hashlibimport hmac
def is_valid_signature(secret: str, body: bytes, signature_header: str) -> bool: """Validate the LC-Signature header against the raw request body.""" expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest() # Constant-time comparison avoids leaking the digest via timing. return hmac.compare_digest(expected, signature_header)A FastAPI receiver, for example:
from fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()WEBHOOK_SECRET = "your-shared-secret"
@app.post("/webhook")async def receive_webhook( request: Request, lc_signature: str = Header(default=""),) -> dict: body = await request.body() # raw bytes, before JSON parsing if not is_valid_signature(WEBHOOK_SECRET, body, lc_signature): raise HTTPException(status_code=401, detail="Invalid signature") # signature verified — safe to process return {"ok": True}import crypto from 'node:crypto';
/** Validate the LC-Signature header against the raw request body. */export function isValidSignature(secret: string, body: Buffer, signatureHeader: string): boolean { const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(body).digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(signatureHeader); // Constant-time comparison avoids leaking the digest via timing. return a.length === b.length && crypto.timingSafeEqual(a, b);}An Express receiver, for example:
import express from 'express';
const app = express();const WEBHOOK_SECRET = 'your-shared-secret';
// express.raw() keeps the raw bytes; express.json() would discard themapp.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.header('LC-Signature') ?? ''; if (!isValidSignature(WEBHOOK_SECRET, req.body, signature)) { return res.status(401).json({ detail: 'Invalid signature' }); } // signature verified — safe to process res.json({ ok: true });});
app.listen(8000);The helper and a net/http receiver:
package main
import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "log" "net/http")
const webhookSecret = "your-shared-secret"
// isValidSignature validates the LC-Signature header against the raw request body.func isValidSignature(secret string, body []byte, signatureHeader string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) expected := "sha256=" + hex.EncodeToString(mac.Sum(nil)) // Constant-time comparison avoids leaking the digest via timing. return hmac.Equal([]byte(expected), []byte(signatureHeader))}
func main() { http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) // raw bytes, before JSON parsing if err != nil { http.Error(w, "unreadable body", http.StatusBadRequest) return } if !isValidSignature(webhookSecret, body, r.Header.Get("LC-Signature")) { http.Error(w, "Invalid signature", http.StatusUnauthorized) return } // signature verified — safe to process w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"ok": true}`)) })
log.Fatal(http.ListenAndServe(":8000", nil))}The helper and a receiver built on the JDK’s HttpServer:
import com.sun.net.httpserver.HttpExchange;import com.sun.net.httpserver.HttpServer;import java.io.IOException;import java.io.OutputStream;import java.net.InetSocketAddress;import java.nio.charset.StandardCharsets;import java.security.MessageDigest;import javax.crypto.Mac;import javax.crypto.spec.SecretKeySpec;
public class WebhookReceiver {
private static final String WEBHOOK_SECRET = "your-shared-secret";
/** Validate the LC-Signature header against the raw request body. */ static boolean isValidSignature(String secret, byte[] body, String signatureHeader) throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); StringBuilder expected = new StringBuilder("sha256="); for (byte b : mac.doFinal(body)) { expected.append(String.format("%02x", b)); } // Constant-time comparison avoids leaking the digest via timing. return MessageDigest.isEqual( expected.toString().getBytes(StandardCharsets.UTF_8), signatureHeader.getBytes(StandardCharsets.UTF_8)); }
public static void main(String[] args) throws Exception { HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0); server.createContext("/webhook", exchange -> { byte[] body = exchange.getRequestBody().readAllBytes(); // raw bytes, before JSON parsing String signature = exchange.getRequestHeaders().getFirst("LC-Signature"); boolean valid; try { valid = signature != null && isValidSignature(WEBHOOK_SECRET, body, signature); } catch (Exception e) { valid = false; } if (!valid) { respond(exchange, 401, "{\"detail\": \"Invalid signature\"}"); return; } // signature verified — safe to process respond(exchange, 200, "{\"ok\": true}"); }); server.start(); }
static void respond(HttpExchange exchange, int status, String json) throws IOException { byte[] payload = json.getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set("Content-Type", "application/json"); exchange.sendResponseHeaders(status, payload.length); try (OutputStream out = exchange.getResponseBody()) { out.write(payload); } }}Treat the secret like any other credential: store it securely, never commit it to source control, and rotate it if it may have been exposed.
Source IP Addresses
Section titled “Source IP Addresses”LlamaCloud delivers webhooks from a fixed, region-specific egress IP address. If your webhook endpoint is behind a firewall or enforces network access rules, add the IP address for your account’s region to your allowlist so deliveries are accepted:
| Region | IP Address |
|---|---|
| US / North America | 52.5.178.213 |
| EU | 18.196.227.29 |
These addresses can change in rare cases, so do not rely on them as your only verification mechanism. To authenticate that a request genuinely came from LlamaCloud, configure a webhook_signing_secret and validate the signature on each incoming webhook (and/or validate custom headers such as an Authorization bearer token).
Retry Behavior
Section titled “Retry Behavior”LlamaCloud implements automatic retry logic for webhook deliveries:
- Maximum attempts: 3 attempts by default
- Exponential backoff: Wait time doubles between attempts (1s, 2s, 4s)
- Maximum wait time: 60 seconds maximum between retries
- Timeout: 30-second timeout per request
A webhook delivery is considered successful if your endpoint returns any HTTP status code in the 200-299 range.
Example: Receiving Webhooks with Inngest
Section titled “Example: Receiving Webhooks with Inngest”You can point webhook_url at an Inngest webhook to receive LlamaCloud events without running your own server.
In the Inngest dashboard, create a webhook and add this Transform Function so each event is named by its event_type:
function transform(evt) { return { name: `Llamacloud/${evt.event_type}`, data: evt };}Then use the Inngest URL as your webhook_url:
webhook_configurations = [ { "webhook_url": "https://inn.gs/e/<your-inngest-key>", "webhook_events": ["parse.success", "parse.error"], "webhook_output_format": "json" }]Subscribe an Inngest function to the transformed event and fetch the parsed result with the job_id from the payload:
inngest.createFunction( { id: "llamacloud-parse-success" }, { event: "Llamacloud/parse.success" }, async ({ event }) => { const jobId = event.data.data.job_id; const res = await fetch( `https://api.cloud.llamaindex.ai/api/v2/parse/${jobId}?expand=markdown`, { headers: { Authorization: `Bearer ${process.env.LLAMA_CLOUD_API_KEY}` } }, ); const { markdown } = await res.json(); // do something with the parsed markdown },);