> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reducto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Direct Webhooks

> Simple HTTP POST webhooks for prototyping and basic integrations

<Warning>
  For production applications, use [Svix webhooks](/workflows/svix-webhooks) instead. Svix provides cryptographic signing, advanced retries, and a debugging dashboard. Direct webhooks are best for prototyping or simple internal integrations.
</Warning>

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](https://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:

<CodeGroup>
  ```python Python theme={null}
  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}")
  ```

  ```typescript TypeScript theme={null}
  import Reducto from "reductoai";

  const client = new Reducto();

  const job = await client.parse.runJob({
    input: "https://example.com/document.pdf",
    async: {
      webhook: {
        mode: "direct",
        url: "https://your-app.com/webhook"
      },
      metadata: { userId: "123", documentType: "invoice" }
    }
  });
  ```

  ```bash cURL theme={null}
  curl -X POST https://platform.reducto.ai/parse_async \
    -H "Authorization: Bearer $REDUCTO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "input": "https://example.com/document.pdf",
      "async": {
        "webhook": {"mode": "direct", "url": "https://your-app.com/webhook"},
        "metadata": {"user_id": "123"}
      }
    }'
  ```
</CodeGroup>

Works with all async endpoints: `/parse_async`, `/extract_async`, `/split_async`, `/pipeline_async`.

## Webhook payload

When the job completes, your endpoint receives:

```json theme={null}
{
  "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

<CodeGroup>
  ```python Python theme={null}
  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
  ```

  ```typescript TypeScript theme={null}
  import express from 'express';
  import Reducto from "reductoai";

  const app = express();
  const client = new Reducto();

  app.use(express.json());

  app.post('/webhook', async (req, res) => {
    const { job_id, status } = req.body;
    
    if (status === "Completed") {
      const job = await client.job.retrieve(job_id);
      // Process job.result
      console.log(`Processed job: ${job_id}`);
    }
    
    res.status(200).json({ received: true });
  });
  ```
</CodeGroup>

## Validating requests

Since direct webhooks lack cryptographic signing, validate requests using a secret token in metadata:

<CodeGroup>
  ```python Python theme={null}
  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
  ```

  ```typescript TypeScript theme={null}
  import express from 'express';
  import Reducto from "reductoai";

  const app = express();
  const client = new Reducto();
  const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;

  app.use(express.json());

  // When submitting jobs, include the secret
  async function submitJob() {
    return client.parse.runJob({
      input: "document.pdf",
      async: {
        webhook: { mode: "direct", url: "https://your-app.com/webhook" },
        metadata: { secret: WEBHOOK_SECRET, userId: "123" }
      }
    });
  }

  // When handling webhooks, verify the secret
  app.post('/webhook', async (req, res) => {
    // Validate secret
    if (req.body.metadata?.secret !== WEBHOOK_SECRET) {
      return res.status(401).json({ error: "Unauthorized" });
    }
    
    if (req.body.status === "Completed") {
      const job = await client.job.retrieve(req.body.job_id);
      // Process job.result
    }
    
    res.status(200).json({ received: true });
  });
  ```
</CodeGroup>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Webhook not received">
    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
  </Accordion>

  <Accordion title="Webhook received but job retrieval fails">
    Job IDs expire after 12 hours. Retrieve results promptly after receiving the webhook.
  </Accordion>

  <Accordion title="Duplicate webhooks received">
    Reducto retries failed deliveries. Make your handler idempotent by tracking processed job IDs.
  </Accordion>
</AccordionGroup>

## 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](/workflows/svix-webhooks).
