Webhooks
Webhooks
Webhooks allow you to receive real-time notifications when events occur in your EnvoyX account. Instead of polling for status updates, webhooks push event data to your server as soon as it happens.
Overview
When an invoice progresses through the processing pipeline, EnvoyX sends HTTP POST requests to your webhook endpoint with event details. This enables you to:
- Update your database in real-time
- Trigger downstream workflows
- Send notifications to users
- Log events for auditing
- Integrate with other systems
Event Types
EnvoyX emits the following webhook events during invoice processing:
| Event | Description | Typical Time |
|---|---|---|
invoice.uploaded | Invoice file uploaded to cloud storage | Immediate |
invoice.processing | AI extraction started | +1-2 seconds |
invoice.extracted | Raw data extracted | +5-10 seconds |
invoice.validated | Validation checks completed | +12-15 seconds |
invoice.processed | Successfully processed, ready for review | +15-20 seconds |
invoice.flagged | Validation failures detected | +15-20 seconds |
invoice.failed | Extraction or processing error | Variable |
invoice.approved | Invoice approved after review | Manual action |
invoice.rejected | Invoice rejected after review | Manual action |
invoice.paid | Invoice marked as paid | Manual action |
Processing Pipeline: All invoices go through: uploaded → processing → extracted → validated → (processed or flagged)
Setting Up Webhooks
1. Create Your Endpoint
Your webhook endpoint should:
- Accept POST requests
- Return
200 OKquickly (within 5 seconds) - Verify the webhook signature (recommended)
- Process payloads asynchronously
Example endpoint:
import express from 'express'
import crypto from 'crypto'
const app = express()
app.use(express.json())
app.post('/webhooks/envoyx', async (req, res) => {
const signature = req.headers['x-envoyx-signature']
const payload = JSON.stringify(req.body)
// Verify signature (recommended)
const expectedSignature = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(payload)
.digest('hex')
if (signature !== expectedSignature) {
return res.status(401).json({ error: 'Invalid signature' })
}
// Respond quickly
res.status(200).json({ received: true })
// Process asynchronously
const { event, data } = req.body
switch (event) {
case 'invoice.processed':
await handleInvoiceProcessed(data)
break
case 'invoice.flagged':
await handleInvoiceFlagged(data)
break
// ... handle other events
}
})
app.listen(3000)from flask import Flask, request, jsonify
import hmac
import hashlib
import os
app = Flask(__name__)
@app.route('/webhooks/envoyx', methods=['POST'])
def webhook():
signature = request.headers.get('X-EnvoyX-Signature')
payload = request.get_data()
# Verify signature (recommended)
expected_signature = hmac.new(
os.getenv('WEBHOOK_SECRET').encode(),
payload,
hashlib.sha256
).hexdigest()
if signature != expected_signature:
return jsonify({'error': 'Invalid signature'}), 401
# Respond quickly
response = jsonify({'received': True})
# Process asynchronously
event_data = request.get_json()
event = event_data['event']
data = event_data['data']
if event == 'invoice.processed':
handle_invoice_processed(data)
elif event == 'invoice.flagged':
handle_invoice_flagged(data)
# ... handle other events
return response, 200
if __name__ == '__main__':
app.run(port=3000)package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"os"
)
type WebhookPayload struct {
Event string `json:"event"`
Data map[string]interface{} `json:"data"`
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("X-EnvoyX-Signature")
body, _ := io.ReadAll(r.Body)
// Verify signature
secret := os.Getenv("WEBHOOK_SECRET")
h := hmac.New(sha256.New, []byte(secret))
h.Write(body)
expectedSignature := hex.EncodeToString(h.Sum(nil))
if signature != expectedSignature {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Respond quickly
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"received": true})
// Process asynchronously
var payload WebhookPayload
json.Unmarshal(body, &payload)
switch payload.Event {
case "invoice.processed":
go handleInvoiceProcessed(payload.Data)
case "invoice.flagged":
go handleInvoiceFlagged(payload.Data)
}
}
func main() {
http.HandleFunc("/webhooks/envoyx", webhookHandler)
http.ListenAndServe(":3000", nil)
}2. Register Your Webhook
Create a webhook subscription via the API:
curl -X POST https://staging-api.tryenvoyx.com/api/v1/webhooks \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-domain.com/webhooks/envoyx",
"events": ["invoice.processed", "invoice.flagged"],
"secret": "your_webhook_secret_key"
}'Response:
{
"success": true,
"status": 201,
"code": "WEBHOOK_CREATED",
"message": "Webhook created successfully",
"data": {
"id": "wh_clx1234567890",
"url": "https://your-domain.com/webhooks/envoyx",
"events": ["invoice.processed", "invoice.flagged"],
"is_active": true,
"created_at": "2024-02-10T12:00:00Z"
}
}Event Filtering: Subscribe to specific events or use ["*"] to receive all events.
Webhook Payload Structure
All webhook payloads follow this structure:
{
"event": "invoice.processed",
"timestamp": "2024-02-10T12:00:00Z",
"data": {
"id": "inv_clx1234567890",
"status": "PROCESSED",
"extracted_data": {
"claim_number": "CLM-2024-001",
"insured_id": "INS-123456",
"partner_name": "Blue Cross Blue Shield",
"service_category": "Medical Consultation",
"total_amount": 250.00,
"submission_date": "2024-02-08",
"provider_name": "Dr. Smith Medical Center"
},
"file_name": "invoice.pdf",
"uploaded_at": "2024-02-10T11:58:30Z",
"processed_at": "2024-02-10T12:00:00Z"
}
}Event Payloads
invoice.uploaded
{
"event": "invoice.uploaded",
"timestamp": "2024-02-10T11:58:30Z",
"data": {
"id": "inv_clx1234567890",
"file_name": "invoice.pdf",
"file_size": 245678,
"status": "PENDING"
}
}invoice.processing
{
"event": "invoice.processing",
"timestamp": "2024-02-10T11:58:32Z",
"data": {
"id": "inv_clx1234567890",
"status": "PROCESSING",
"started_at": "2024-02-10T11:58:32Z"
}
}invoice.processed
{
"event": "invoice.processed",
"timestamp": "2024-02-10T12:00:00Z",
"data": {
"id": "inv_clx1234567890",
"status": "PROCESSED",
"extracted_data": { /* ... */ },
"validation": {
"partner_valid": true,
"category_valid": true,
"amount_valid": true,
"date_valid": true,
"duplicate_check": true
}
}
}invoice.flagged
{
"event": "invoice.flagged",
"timestamp": "2024-02-10T12:00:00Z",
"data": {
"id": "inv_clx1234567890",
"status": "FLAGGED",
"extracted_data": { /* ... */ },
"flags": [
{
"type": "PARTNER_INVALID",
"message": "Insurance partner not in approved list",
"value": "Unknown Insurance Co"
},
{
"type": "AMOUNT_EXCEEDS_LIMIT",
"message": "Amount exceeds maximum limit of $1,000,000",
"value": 1500000
}
]
}
}invoice.failed
{
"event": "invoice.failed",
"timestamp": "2024-02-10T12:00:00Z",
"data": {
"id": "inv_clx1234567890",
"status": "FAILED",
"error": {
"code": "EXTRACTION_FAILED",
"message": "Could not extract text from PDF",
"details": "File may be corrupted or image-based"
}
}
}Security: Signature Verification
EnvoyX signs all webhook payloads using HMAC-SHA256. Verify signatures to ensure requests are authentic.
Signature Header
X-EnvoyX-Signature: 3f8c7b2a9d1e6f4c8b7a5d3e2f1c9b8a7d6e5f4c3b2a1dVerification Code
import crypto from 'crypto'
function verifySignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)
}import hmac
import hashlib
def verify_signature(payload, signature, secret):
expected_signature = hmac.new(
secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func verifySignature(payload []byte, signature string, secret string) bool {
h := hmac.New(sha256.New, []byte(secret))
h.Write(payload)
expectedSignature := hex.EncodeToString(h.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expectedSignature))
}Use Constant-Time Comparison: Always use timing-safe comparison functions (crypto.timingSafeEqual, hmac.compare_digest, etc.) to prevent timing attacks.
Retry Logic
If your endpoint doesn't respond with 200 OK, EnvoyX will retry delivery:
- Retry Schedule: 1 min, 5 min, 15 min, 1 hour, 6 hours
- Max Retries: 5 attempts
- Timeout: 5 seconds per attempt
- Backoff: Exponential with jitter
After 5 failed attempts, the webhook is marked as failed and you'll need to manually replay it.
Managing Webhooks
List All Webhooks
curl -X GET https://staging-api.tryenvoyx.com/api/v1/webhooks \
-H "X-API-Key: YOUR_API_KEY"Update Webhook
curl -X PUT https://staging-api.tryenvoyx.com/api/v1/webhooks/wh_clx1234567890 \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://new-domain.com/webhooks",
"events": ["*"],
"is_active": true
}'Delete Webhook
curl -X DELETE https://staging-api.tryenvoyx.com/api/v1/webhooks/wh_clx1234567890 \
-H "X-API-Key: YOUR_API_KEY"View Delivery History
curl -X GET https://staging-api.tryenvoyx.com/api/v1/webhooks/wh_clx1234567890/deliveries \
-H "X-API-Key: YOUR_API_KEY"Testing Webhooks Locally
Use a tunnel service to test webhooks on your local development machine:
Using ngrok
# Install ngrok
npm install -g ngrok
# Start your local server
node server.js # Listening on port 3000
# Create tunnel
ngrok http 3000
# Use the ngrok URL for your webhook
https://abc123.ngrok.io/webhooks/envoyxUsing localhost.run
# Create tunnel (no installation required)
ssh -R 80:localhost:3000 nokey@localhost.run
# Use the provided URL for your webhookBest Practices
1. Respond Quickly
// Good: Respond immediately, process async
app.post('/webhook', (req, res) => {
res.status(200).json({ received: true })
// Process in background
processWebhook(req.body).catch(console.error)
})
// Bad: Processing blocks response
app.post('/webhook', async (req, res) => {
await processWebhook(req.body) // Takes 5+ seconds
res.status(200).json({ received: true })
})2. Verify Signatures
Always verify webhook signatures to prevent spoofing attacks.
3. Handle Duplicates
Due to retries, you may receive the same event multiple times. Use idempotency keys:
const processedEvents = new Set()
app.post('/webhook', async (req, res) => {
const eventId = `${req.body.event}-${req.body.data.id}`
if (processedEvents.has(eventId)) {
return res.status(200).json({ received: true }) // Already processed
}
processedEvents.add(eventId)
// Process event...
})4. Monitor Failures
Track webhook failures and set up alerts:
app.post('/webhook', async (req, res) => {
try {
await processWebhook(req.body)
res.status(200).json({ received: true })
} catch (error) {
// Log error for monitoring
logger.error('Webhook processing failed', {
event: req.body.event,
error: error.message
})
// Still return 200 to prevent retries for application errors
res.status(200).json({ received: true, error: error.message })
}
})5. Use HTTPS
Always use HTTPS endpoints for webhooks to protect sensitive data in transit.
Common Issues
Timeouts
If your endpoint takes longer than 5 seconds, the request will timeout and retry. Process webhooks asynchronously.
Invalid Signature
Double-check:
- You're using the correct webhook secret
- You're signing the raw request body (not parsed JSON)
- You're using HMAC-SHA256
Missing Events
Events may be lost if:
- Your endpoint returns non-200 status codes
- Your endpoint is unreachable
- All retry attempts failed
Check the delivery history to see what happened.
Next Steps
- Set up WebSocket connection for real-time client updates
- View invoice endpoints for full invoice management