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

# Production Webhooks

> Use webhooks to receive async job notifications without tight polling loops.

## Goal

Run long document jobs asynchronously and notify your backend when each job finishes.

Use the [Legal Filing Platform example](https://platform.runpulse.com/dashboard/examples/d4dfc1e2-60ac-4776-a5a8-20b88e68bf9f) or [Attention Is All You Need](https://platform.runpulse.com/dashboard/examples/3be15d23-d622-4f27-9843-ec2929140eec) when you want a larger public file for async and webhook testing.

## Use This Workflow

```mermaid theme={null}
sequenceDiagram
    participant App
    participant Pulse
    participant Webhook
    App->>Pulse: POST /extract async=true
    Pulse-->>App: job_id
    Pulse->>Webhook: job completed event
    Webhook->>Pulse: GET /job/{jobId}
    Pulse-->>Webhook: result
```

Use webhooks when jobs may take long enough that polling every few seconds is wasteful or brittle.

## Setup

<Steps>
  <Step title="Create a portal link">
    Use the webhook portal endpoint to open the provider portal for your organization.
  </Step>

  <Step title="Add your endpoint">
    Configure your HTTPS endpoint in the portal.
  </Step>

  <Step title="Verify signatures">
    Validate webhook signatures before trusting event payloads.
  </Step>

  <Step title="Fetch final results">
    Treat the webhook as a notification. Fetch the canonical job result from `GET /job/{jobId}`.
  </Step>
</Steps>

## Minimal Handler Pattern

```python theme={null}
from flask import Flask, request, jsonify
from pulse import Pulse

app = Flask(__name__)
client = Pulse(api_key="YOUR_API_KEY")

@app.post("/webhooks/pulse")
def pulse_webhook():
    event = request.get_json()
    job_id = event.get("job_id") or event.get("data", {}).get("job_id")

    if not job_id:
        return jsonify({"ok": True})

    job = client.jobs.get_job(job_id=job_id)

    if job.status == "completed":
        # Store job.result in your database or enqueue downstream work.
        pass

    if job.status == "failed":
        # Record the failure and notify your operations path.
        pass

    return jsonify({"ok": True})
```

## Production Checklist

* Respond quickly with a 2xx status.
* Verify webhook signatures.
* Make the handler idempotent; the same event may be delivered more than once.
* Fetch the final job state from Pulse before mutating your database.
* Use a queue for slow downstream processing.
* Log `job_id`, status, and final output IDs.
* Keep polling fallback logic for recovery and local testing.
* Persist request metadata such as `file_url`, config IDs, user ID, and destination record ID so every completed job can be audited.
* Do not trust webhook payloads alone for regulated writes; verify signatures and fetch the final job result before updating source-of-truth systems.

## Related

<CardGroup cols={2}>
  <Card title="Webhooks Guide" icon="webhook" href="/svix-webhooks">
    Full setup and security details.
  </Card>

  <Card title="Poll Job" icon="clock" href="/api-reference/endpoint/poll">
    Polling strategy and result format.
  </Card>
</CardGroup>
