POST
/
webhook
Configure Webhooks
curl --request POST \
  --url https://dev.api.runpulse.com/webhook \
  --header 'x-api-key: <api-key>'
{
  "link": "https://app.svix.com/app-portal/..."
}

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.

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

def verify_webhook(payload, headers, webhook_secret):
    """Verify webhook authenticity using HMAC signature."""
    
    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
    expected = hmac.new(
        webhook_secret.encode(),
        signed_content.encode(),
        hashlib.sha256
    ).hexdigest()
    
    # Constant-time comparison
    return hmac.compare_digest(signature, expected)

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

Express.js Webhook Handler

const express = require('express');
const crypto = require('crypto');

const app = express();
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, process.env.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`);
      // Process completed job
      break;
    case 'job.failed':
      console.error(`Job ${event.data.job_id} failed: ${event.data.error}`);
      // Handle failure
      break;
  }
  
  res.status(200).send('OK');
});

Python Flask Handler

from flask import Flask, request, abort
import json

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    payload = request.get_data(as_text=True)
    
    # Verify webhook
    if not verify_webhook(payload, request.headers, app.config['WEBHOOK_SECRET']):
        abort(401)
    
    event = json.loads(payload)
    
    # Process event
    if event['type'] == 'job.completed':
        process_completed_job(event['data'])
    elif event['type'] == 'job.failed':
        handle_failed_job(event['data'])
    
    return '', 200

Best Practices

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

200
application/json

Portal link generated

The response is of type object.