Skip to main content
POST
/
webhook
Create Webhook Portal Link
curl --request POST \
  --url https://dev.api.runpulse.com/webhook \
  --header 'x-api-key: <api-key>'
{
  "link": "<string>"
}

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.
Webhook event delivery is currently under development. The configuration portal is fully functional, but events are not yet being sent.
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

How It Works

1

Request Portal Link

Call this endpoint to get your unique portal URL
2

Visit Portal

Open the portal link in your browser
3

Add Endpoints

Configure one or more webhook URLs to receive events
4

Test & Save

Test your endpoints and save the configuration

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

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

Webhook Events (Coming Soon)

Once enabled, you’ll receive events for:

Job Status Events

{
  "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

EventDescription
job.createdNew async job created
job.processingJob started processing
job.completedJob completed successfully
job.failedJob failed with error
job.cancelledJob was cancelled

Example Implementation

Webhook Handler

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)

Complete Integration Example

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"Content: {job_result.result.content[:100]}...")
            
            if job_result.result.structured_output:
                print(f"Structured data: {job_result.result.structured_output}")
    
    return '', 200

# Submit an async job
def submit_job():
    response = client.extract_async(
        file_url="https://www.impact-bank.com/user/file/dummy_statement.pdf",
        structured_output=json.dumps({
            "schema": {
                "type": "object",
                "properties": {
                    "account_holder": {"type": "string"},
                    "balance": {"type": "number"}
                }
            }
        })
    )
    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)

Best Practices

  • Use HTTPS endpoints only
  • Implement signature verification
  • Add IP allowlisting if possible
  • Use authentication headers
  • Return 2xx status for successful receipt
  • Implement idempotency to handle retries
  • Log all received events
  • Handle timeouts gracefully
  • Process webhooks asynchronously
  • Respond quickly (< 5 seconds)
  • Queue events for processing
  • Implement proper concurrency controls

Troubleshooting

Common Issues

IssueSolution
Not receiving webhooksCheck endpoint URL is publicly accessible
Signature verification failsEnsure you’re using the correct secret
TimeoutsProcess webhooks async and respond quickly
Duplicate eventsImplement 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

Authorizations

x-api-key
string
header
required

API key for authentication

Response

Webhook portal link created

URL to the Svix webhook portal