Create a webhook portal link
curl --request POST \
--url https://api.runpulse.com/webhook \
--header 'x-api-key: <api-key>'import requests
url = "https://api.runpulse.com/webhook"
headers = {"x-api-key": "<api-key>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {'x-api-key': '<api-key>'}};
fetch('https://api.runpulse.com/webhook', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.runpulse.com/webhook",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.runpulse.com/webhook"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("x-api-key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.runpulse.com/webhook")
.header("x-api-key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.runpulse.com/webhook")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"link": "<string>"
}Jobs, Results & Webhooks
Configure Webhooks
Generates a temporary link to the Svix webhook portal where users can manage their webhook endpoints and view message logs.
POST
/
webhook
Create a webhook portal link
curl --request POST \
--url https://api.runpulse.com/webhook \
--header 'x-api-key: <api-key>'import requests
url = "https://api.runpulse.com/webhook"
headers = {"x-api-key": "<api-key>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {'x-api-key': '<api-key>'}};
fetch('https://api.runpulse.com/webhook', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.runpulse.com/webhook",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.runpulse.com/webhook"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("x-api-key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.runpulse.com/webhook")
.header("x-api-key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.runpulse.com/webhook")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"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.
Get Portal Link
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
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
curl -X POST https://api.runpulse.com/webhook \
-H "x-api-key: YOUR_API_KEY"
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
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');
});
#!/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"
Webhook Events
When webhooks are configured, 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
| 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
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)
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');
});
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"}
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"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://platform.runpulse.com/api/examples/637e5678-30b1-45fa-acc4-877f2d636419/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)
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://platform.runpulse.com/api/examples/637e5678-30b1-45fa-acc4-877f2d636419/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();
});
Best Practices
Endpoint Security
Endpoint Security
- Use HTTPS endpoints only
- Implement signature verification
- Add IP allowlisting if possible
- Use authentication headers
Error Handling
Error Handling
- Return 2xx status for successful receipt
- Implement idempotency to handle retries
- Log all received events
- Handle timeouts gracefully
Performance
Performance
- Process webhooks asynchronously
- Respond quickly (< 5 seconds)
- Queue events for processing
- Implement proper concurrency controls
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:- Use webhook testing tools like ngrok for local development
- Send test events from the portal
- Verify signature validation works
- Test error handling and retries
- Monitor initial production events closely
Next Steps
API Reference
Explore other endpoints
Async Processing
Learn about async jobs
⌘I