Skip to main content
For production applications, use Svix webhooks instead. Svix provides cryptographic signing, advanced retries, and a debugging dashboard. Direct webhooks are best for prototyping or simple internal integrations.
Direct webhooks send HTTP POST requests directly to your endpoint when jobs complete. Reducto retries failed deliveries up to 3 times with exponential backoff.

Testing with webhook.site

For quick testing, use webhook.site to get a temporary endpoint URL. It shows you the exact payload Reducto sends.

Submitting jobs

Include your webhook URL in the async configuration:
from reducto import Reducto

client = Reducto()

job = client.parse.run_job(
    input="https://example.com/document.pdf",
    async_={
        "webhook": {
            "mode": "direct",
            "url": "https://your-app.com/webhook"
        },
        "metadata": {
            "user_id": "123",
            "document_type": "invoice"
        }
    }
)
print(f"Job ID: {job.job_id}")
Works with all async endpoints: /parse_async, /extract_async, /split_async, /pipeline_async.

Webhook payload

When the job completes, your endpoint receives:
{
  "status": "Completed",
  "job_id": "204a39e4-dd10-4c83-a978-0cee4af8cde2",
  "metadata": {
    "user_id": "123",
    "document_type": "invoice"
  }
}
The status is either Completed or Failed. Use job_id to retrieve results with client.job.get().

Handling webhooks

from flask import Flask, request, jsonify
from reducto import Reducto

app = Flask(__name__)
client = Reducto()

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    payload = request.json
    
    if payload['status'] == "Completed":
        job = client.job.get(payload['job_id'])
        # Process job.result
        print(f"Processed job: {payload['job_id']}")
    
    return jsonify({"received": True}), 200

Validating requests

Since direct webhooks lack cryptographic signing, validate requests using a secret token in metadata:
import os
from flask import Flask, request, jsonify, abort
from reducto import Reducto

app = Flask(__name__)
client = Reducto()
WEBHOOK_SECRET = os.environ["WEBHOOK_SECRET"]

# When submitting jobs, include the secret
def submit_job():
    return client.parse.run_job(
        input="https://example.com/document.pdf",
        async_={
            "webhook": {"mode": "direct", "url": "https://your-app.com/webhook"},
            "metadata": {"secret": WEBHOOK_SECRET, "user_id": "123"}
        }
    )

# When handling webhooks, verify the secret
@app.route('/webhook', methods=['POST'])
def handle_webhook():
    payload = request.json
    
    # Validate secret
    if payload.get('metadata', {}).get('secret') != WEBHOOK_SECRET:
        abort(401)
    
    if payload['status'] == "Completed":
        job = client.job.get(payload['job_id'])
        # Process job.result
    
    return jsonify({"received": True}), 200

Troubleshooting

  1. Verify your endpoint URL is publicly accessible (not localhost)
  2. Ensure your endpoint returns 2xx status codes
  3. Check your server logs for incoming requests
Job IDs expire after 12 hours. Retrieve results promptly after receiving the webhook.
Reducto retries failed deliveries. Make your handler idempotent by tracking processed job IDs.

Best practices

  1. Use HTTPS for your webhook endpoint
  2. Validate requests using the token-in-metadata pattern
  3. Return quickly: Return 2xx immediately, process results asynchronously
  4. Be idempotent: Handle duplicate deliveries gracefully
  5. Log everything: Direct webhooks have no dashboard, so log for debugging
For production applications with reliability requirements, use Svix webhooks.