> ## 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.

# Configure Webhooks

> Generates a temporary link to the Svix webhook portal where users can manage 
their webhook endpoints and view message logs.


## Overview

Configure webhook endpoints to receive real-time notifications about job status changes. This endpoint returns a portal link where you can manage your webhook configurations.

<Note>
  Webhook event delivery is currently under development. The configuration portal is fully functional, but events are not yet being sent.
</Note>

## Get Portal Link

<CodeGroup>
  ```python Python SDK theme={null}
  from pulse import Pulse

  client = Pulse(api_key="YOUR_API_KEY")

  # Get webhook portal link
  response = client.webhooks.create_webhook_link()
  print(f"Portal URL: {response.link}")

  # Open this URL in your browser to configure webhooks
  ```

  ```typescript TypeScript SDK theme={null}
  import { PulseClient } from 'pulse-ts-sdk';

  const client = new PulseClient({
      apiKey: 'YOUR_API_KEY'
  });

  // Get webhook portal link
  const response = await client.webhooks.createWebhookLink();
  console.log(`Portal URL: ${response.link}`);

  // Open this URL in your browser to configure webhooks
  ```

  ```bash curl theme={null}
  curl -X POST https://api.runpulse.com/webhook \
    -H "x-api-key: YOUR_API_KEY"
  ```
</CodeGroup>

## How It Works

<Steps>
  <Step title="Request Portal Link">
    Call this endpoint to get your unique portal URL
  </Step>

  <Step title="Visit Portal">
    Open the portal link in your browser
  </Step>

  <Step title="Add Endpoints">
    Configure one or more webhook URLs to receive events
  </Step>

  <Step title="Test & Save">
    Test your endpoints and save the configuration
  </Step>
</Steps>

## Portal Features

The webhook configuration portal allows you to:

* **Add Multiple Endpoints** - Configure different URLs for different event types
* **Set Authentication** - Add headers or basic auth to your webhooks
* **Filter Events** - Choose which events to receive at each endpoint
* **Test Endpoints** - Send test events to verify your setup
* **View Logs** - See delivery attempts and debug failed webhooks

## Webhook Security

Each webhook request includes security headers for verification:

```
webhook-id: msg_2Jv7pYGL7UwXqF3v6RjLVxQYPZG
webhook-timestamp: 1704067200
webhook-signature: v1,g0hM9SsE+OTPJTjfm/kBRBOlqPmYFYpwTEFfQK6UHdI=
```

### Verifying Webhook Signatures

<CodeGroup>
  ```python Python theme={null}
  import hmac
  import hashlib
  import time
  import base64

  def verify_webhook(payload: str, headers: dict, webhook_secret: str) -> bool:
      """
      Verify webhook authenticity using HMAC signature.
      
      Args:
          payload: Raw request body as string
          headers: Request headers dict
          webhook_secret: Your webhook signing secret from the portal
          
      Returns:
          True if signature is valid, False otherwise
      """
      webhook_id = headers.get('webhook-id')
      webhook_timestamp = headers.get('webhook-timestamp')
      webhook_signature = headers.get('webhook-signature')
      
      if not all([webhook_id, webhook_timestamp, webhook_signature]):
          return False
      
      # Check timestamp to prevent replay attacks (5 minute window)
      current_time = int(time.time())
      if abs(current_time - int(webhook_timestamp)) > 300:
          return False
      
      # Construct signed content
      signed_content = f"{webhook_id}.{webhook_timestamp}.{payload}"
      
      # Extract signature from header (format: v1,signature)
      signature = webhook_signature.split(',')[1] if ',' in webhook_signature else webhook_signature
      
      # Compute expected signature (base64-encoded HMAC-SHA256)
      expected = base64.b64encode(
          hmac.new(
              base64.b64decode(webhook_secret),
              signed_content.encode(),
              hashlib.sha256
          ).digest()
      ).decode()
      
      # Constant-time comparison
      return hmac.compare_digest(signature, expected)

  # Example usage with Flask
  from flask import Flask, request, abort

  app = Flask(__name__)
  WEBHOOK_SECRET = "whsec_your_secret_here"

  @app.route('/webhook', methods=['POST'])
  def handle_webhook():
      payload = request.get_data(as_text=True)
      
      if not verify_webhook(payload, request.headers, WEBHOOK_SECRET):
          abort(401)
      
      # Process the event
      event = request.json
      print(f"Received event: {event['type']}")
      
      return '', 200
  ```

  ```typescript TypeScript theme={null}
  import * as crypto from 'crypto';

  function verifyWebhook(
      payload: string,
      headers: Record<string, string>,
      webhookSecret: string
  ): boolean {
      /**
       * Verify webhook authenticity using HMAC signature.
       */
      const webhookId = headers['webhook-id'];
      const webhookTimestamp = headers['webhook-timestamp'];
      const webhookSignature = headers['webhook-signature'];
      
      if (!webhookId || !webhookTimestamp || !webhookSignature) {
          return false;
      }
      
      // Check timestamp to prevent replay attacks (5 minute window)
      const currentTime = Math.floor(Date.now() / 1000);
      if (Math.abs(currentTime - parseInt(webhookTimestamp)) > 300) {
          return false;
      }
      
      // Construct signed content
      const signedContent = `${webhookId}.${webhookTimestamp}.${payload}`;
      
      // Extract signature from header (format: v1,signature)
      const signature = webhookSignature.includes(',') 
          ? webhookSignature.split(',')[1] 
          : webhookSignature;
      
      // Compute expected signature (base64-encoded HMAC-SHA256)
      const secretBytes = Buffer.from(webhookSecret, 'base64');
      const expected = crypto
          .createHmac('sha256', secretBytes)
          .update(signedContent)
          .digest('base64');
      
      // Constant-time comparison
      return crypto.timingSafeEqual(
          Buffer.from(signature),
          Buffer.from(expected)
      );
  }

  // Example usage with Express.js
  import express from 'express';

  const app = express();
  const WEBHOOK_SECRET = "whsec_your_secret_here";

  app.use(express.raw({ type: 'application/json' }));

  app.post('/webhook', (req, res) => {
      const payload = req.body.toString();
      
      if (!verifyWebhook(payload, req.headers as Record<string, string>, WEBHOOK_SECRET)) {
          return res.status(401).send('Unauthorized');
      }
      
      // Process the event
      const event = JSON.parse(payload);
      console.log(`Received event: ${event.type}`);
      
      res.status(200).send('OK');
  });
  ```

  ```bash Bash theme={null}
  #!/bin/bash

  # Webhook verification in Bash
  # Note: This is for reference; typically you'd verify in your server

  WEBHOOK_SECRET="whsec_your_secret_here"
  PAYLOAD='{"type":"job.completed","data":{"job_id":"123"}}'
  WEBHOOK_ID="msg_abc123"
  WEBHOOK_TIMESTAMP=$(date +%s)

  # Construct signed content
  SIGNED_CONTENT="${WEBHOOK_ID}.${WEBHOOK_TIMESTAMP}.${PAYLOAD}"

  # Compute signature (base64-encoded HMAC-SHA256)
  SIGNATURE=$(echo -n "$SIGNED_CONTENT" | openssl dgst -sha256 -hmac "$(echo -n "$WEBHOOK_SECRET" | base64 -d)" -binary | base64)

  echo "Webhook ID: $WEBHOOK_ID"
  echo "Timestamp: $WEBHOOK_TIMESTAMP"
  echo "Signature: v1,$SIGNATURE"
  ```
</CodeGroup>

## Webhook Events (Coming Soon)

Once enabled, you'll receive events for:

### Job Status Events

```json theme={null}
{
  "type": "job.completed",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "job_id": "123e4567-e89b-12d3-a456-426614174000",
    "status": "completed",
    "pages_processed": 25,
    "processing_time": 12.5
  }
}
```

### Event Types

| Event            | Description                |
| ---------------- | -------------------------- |
| `job.created`    | New async job created      |
| `job.processing` | Job started processing     |
| `job.completed`  | Job completed successfully |
| `job.failed`     | Job failed with error      |
| `job.cancelled`  | Job was cancelled          |

## Example Implementation

### Webhook Handler

<CodeGroup>
  ```python Python (Flask) theme={null}
  from flask import Flask, request, abort
  import json

  app = Flask(__name__)
  WEBHOOK_SECRET = "whsec_your_secret_here"

  @app.route('/webhook', methods=['POST'])
  def handle_webhook():
      payload = request.get_data(as_text=True)
      
      # Verify webhook signature
      if not verify_webhook(payload, request.headers, WEBHOOK_SECRET):
          abort(401)
      
      event = json.loads(payload)
      
      # Handle different event types
      if event['type'] == 'job.completed':
          job_id = event['data']['job_id']
          print(f"✓ Job {job_id} completed!")
          
          # Fetch the results
          # result = fetch_job_result(job_id)
          # process_extraction(result)
          
      elif event['type'] == 'job.failed':
          job_id = event['data']['job_id']
          error = event['data'].get('error', 'Unknown error')
          print(f"✗ Job {job_id} failed: {error}")
          
          # Handle failure (retry, notify, etc.)
          # handle_job_failure(job_id, error)
          
      elif event['type'] == 'job.cancelled':
          job_id = event['data']['job_id']
          print(f"⊘ Job {job_id} was cancelled")
      
      return '', 200

  if __name__ == '__main__':
      app.run(port=3000)
  ```

  ```typescript TypeScript (Express) theme={null}
  import express from 'express';

  const app = express();
  const WEBHOOK_SECRET = "whsec_your_secret_here";

  app.use(express.raw({ type: 'application/json' }));

  app.post('/webhook', (req, res) => {
      const payload = req.body.toString();
      
      // Verify webhook signature
      if (!verifyWebhook(payload, req.headers as Record<string, string>, WEBHOOK_SECRET)) {
          return res.status(401).send('Unauthorized');
      }
      
      const event = JSON.parse(payload);
      
      // Handle different event types
      switch (event.type) {
          case 'job.completed':
              console.log(`✓ Job ${event.data.job_id} completed!`);
              
              // Fetch the results
              // const result = await fetchJobResult(event.data.job_id);
              // await processExtraction(result);
              break;
              
          case 'job.failed':
              console.error(`✗ Job ${event.data.job_id} failed: ${event.data.error}`);
              
              // Handle failure
              // await handleJobFailure(event.data.job_id, event.data.error);
              break;
              
          case 'job.cancelled':
              console.log(`⊘ Job ${event.data.job_id} was cancelled`);
              break;
      }
      
      res.status(200).send('OK');
  });

  app.listen(3000, () => {
      console.log('Webhook handler listening on port 3000');
  });
  ```

  ```python Python (FastAPI) theme={null}
  from fastapi import FastAPI, Request, HTTPException
  import json

  app = FastAPI()
  WEBHOOK_SECRET = "whsec_your_secret_here"

  @app.post('/webhook')
  async def handle_webhook(request: Request):
      payload = await request.body()
      payload_str = payload.decode()
      
      # Verify webhook signature
      if not verify_webhook(payload_str, dict(request.headers), WEBHOOK_SECRET):
          raise HTTPException(status_code=401, detail="Unauthorized")
      
      event = json.loads(payload_str)
      
      # Handle different event types
      match event['type']:
          case 'job.completed':
              print(f"✓ Job {event['data']['job_id']} completed!")
          case 'job.failed':
              print(f"✗ Job {event['data']['job_id']} failed: {event['data'].get('error')}")
          case 'job.cancelled':
              print(f"⊘ Job {event['data']['job_id']} was cancelled")
      
      return {"status": "ok"}
  ```
</CodeGroup>

### Complete Integration Example

<CodeGroup>
  ```python Python theme={null}
  from pulse import Pulse
  from flask import Flask, request, abort
  import json

  # Initialize Pulse client
  client = Pulse(api_key="YOUR_API_KEY")

  # Flask app for webhook handler
  app = Flask(__name__)
  WEBHOOK_SECRET = "whsec_your_secret_here"

  @app.route('/webhook', methods=['POST'])
  def handle_webhook():
      payload = request.get_data(as_text=True)
      
      if not verify_webhook(payload, request.headers, WEBHOOK_SECRET):
          abort(401)
      
      event = json.loads(payload)
      
      if event['type'] == 'job.completed':
          job_id = event['data']['job_id']
          
          # Fetch full results using SDK
          job_result = client.jobs.get_job(job_id=job_id)
          
          if job_result.result:
              print(f"Markdown: {job_result.result.markdown[:100]}...")
              
              # Apply schema post-extraction via /schema endpoint
              schema_result = client.schema(
                  extraction_id=job_result.result.extraction_id,
                  schema_config={
                      "input_schema": {
                          "type": "object",
                          "properties": {
                              "account_holder": {"type": "string"},
                              "balance": {"type": "number"}
                          }
                      }
                  }
              )
              if schema_result.schema_output:
                  print(f"Structured data: {schema_result.schema_output}")
      
      return '', 200

  # Submit an async job
  def submit_job():
      response = client.extract(
          file_url="https://www.impact-bank.com/user/file/dummy_statement.pdf",
          async_=True
      )
      print(f"Job submitted: {response.job_id}")
      print("Waiting for webhook notification...")
      return response.job_id

  if __name__ == '__main__':
      # Submit a job, then start webhook handler
      submit_job()
      app.run(port=3000)
  ```

  ```typescript TypeScript theme={null}
  import { PulseClient } from 'pulse-ts-sdk';
  import express from 'express';

  // Initialize Pulse client
  const client = new PulseClient({
      apiKey: 'YOUR_API_KEY'
  });

  // Express app for webhook handler
  const app = express();
  const WEBHOOK_SECRET = "whsec_your_secret_here";

  app.use(express.raw({ type: 'application/json' }));

  app.post('/webhook', async (req, res) => {
      const payload = req.body.toString();
      
      if (!verifyWebhook(payload, req.headers as Record<string, string>, WEBHOOK_SECRET)) {
          return res.status(401).send('Unauthorized');
      }
      
      const event = JSON.parse(payload);
      
      if (event.type === 'job.completed') {
          const jobId = event.data.job_id;
          
          // Fetch full results using SDK
          const jobResult = await client.jobs.getJob({ jobId });
          
          if (jobResult.result) {
              console.log(`Markdown: ${jobResult.result.markdown?.slice(0, 100)}...`);
              
              // Apply schema post-extraction
              const schemaResult = await client.schema({
                  extraction_id: jobResult.result.extraction_id,
                  schema_config: {
                      input_schema: {
                          type: "object",
                          properties: {
                              account_holder: { type: "string" },
                              balance: { type: "number" }
                          }
                      }
                  }
              });
              if (schemaResult.schema_output) {
                  console.log(`Structured data:`, schemaResult.schema_output);
              }
          }
      }
      
      res.status(200).send('OK');
  });

  // Submit an async job
  async function submitJob(): Promise<string> {
      const response = await client.extract({
          fileUrl: "https://www.impact-bank.com/user/file/dummy_statement.pdf",
          async: true
      });
      
      console.log(`Job submitted: ${response.job_id}`);
      console.log("Waiting for webhook notification...");
      return response.job_id!;
  }

  // Start the server and submit a job
  app.listen(3000, async () => {
      console.log('Webhook handler listening on port 3000');
      await submitJob();
  });
  ```
</CodeGroup>

## Best Practices

<AccordionGroup>
  <Accordion title="Endpoint Security">
    * Use HTTPS endpoints only
    * Implement signature verification
    * Add IP allowlisting if possible
    * Use authentication headers
  </Accordion>

  <Accordion title="Error Handling">
    * Return 2xx status for successful receipt
    * Implement idempotency to handle retries
    * Log all received events
    * Handle timeouts gracefully
  </Accordion>

  <Accordion title="Performance">
    * Process webhooks asynchronously
    * Respond quickly (\< 5 seconds)
    * Queue events for processing
    * Implement proper concurrency controls
  </Accordion>
</AccordionGroup>

## Troubleshooting

### Common Issues

| Issue                        | Solution                                   |
| ---------------------------- | ------------------------------------------ |
| Not receiving webhooks       | Check endpoint URL is publicly accessible  |
| Signature verification fails | Ensure you're using the correct secret     |
| Timeouts                     | Process webhooks async and respond quickly |
| Duplicate events             | Implement idempotency using webhook-id     |

### Testing Your Endpoint

Before going live:

1. Use webhook testing tools like ngrok for local development
2. Send test events from the portal
3. Verify signature validation works
4. Test error handling and retries
5. Monitor initial production events closely

## Next Steps

<CardGroup cols={2}>
  <Card title="API Reference" icon="book" href="/api-reference/introduction">
    Explore other endpoints
  </Card>

  <Card title="Async Processing" icon="clock" href="/api-reference/async-processing">
    Learn about async jobs
  </Card>
</CardGroup>


## OpenAPI

````yaml POST /webhook
openapi: 3.1.0
info:
  title: Pulse API
  description: >-
    Production-grade document extraction service that transforms complex
    documents  into structured, AI-ready data. This specification is the single
    source of truth  for the Pulse extraction APIs.
  version: 1.0.0
  contact:
    name: Pulse Support
    email: support@trypulse.ai
    url: https://docs.runpulse.com
servers:
  - url: https://api.runpulse.com
    description: Production server
security:
  - ApiKeyAuth: []
paths:
  /webhook:
    post:
      tags:
        - Webhooks
      summary: Create Webhook Portal Link
      description: >
        Generates a temporary link to the Svix webhook portal where users can
        manage 

        their webhook endpoints and view message logs.
      operationId: createWebhookLink
      responses:
        '200':
          description: Webhook portal link created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateWebhookLinkResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
      x-codeSamples:
        - lang: python
          label: Python SDK
          source: |
            from pulse import Pulse

            client = Pulse(api_key="YOUR_API_KEY")
            portal = client.webhooks.create_webhook_link()
            print(portal.link)  # Open this URL to manage webhooks
        - lang: typescript
          label: TypeScript SDK
          source: |
            import { PulseClient } from "pulse-ts-sdk";

            const client = new PulseClient({
                apiKey: "YOUR_API_KEY"
            });
            const portal = await client.webhooks.createWebhookLink();
            console.log(portal.link); // Open this URL to manage webhooks
        - lang: bash
          label: curl
          source: |
            curl -X POST https://api.runpulse.com/webhook \
              -H "x-api-key: YOUR_API_KEY"
components:
  schemas:
    CreateWebhookLinkResponse:
      type: object
      required:
        - link
      properties:
        link:
          type: string
          description: URL to the Svix webhook portal
    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: Error code (e.g., FILE_001, AUTH_002)
            message:
              type: string
              description: Human-readable error message
            details:
              type: object
              description: Additional error context
  responses:
    Unauthorized:
      description: Unauthorized - Invalid or missing API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    InternalServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: API key for authentication

````