EnvoyX Docs

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:

EventDescriptionTypical Time
invoice.uploadedInvoice file uploaded to cloud storageImmediate
invoice.processingAI extraction started+1-2 seconds
invoice.extractedRaw data extracted+5-10 seconds
invoice.validatedValidation checks completed+12-15 seconds
invoice.processedSuccessfully processed, ready for review+15-20 seconds
invoice.flaggedValidation failures detected+15-20 seconds
invoice.failedExtraction or processing errorVariable
invoice.approvedInvoice approved after reviewManual action
invoice.rejectedInvoice rejected after reviewManual action
invoice.paidInvoice marked as paidManual 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 OK quickly (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: 3f8c7b2a9d1e6f4c8b7a5d3e2f1c9b8a7d6e5f4c3b2a1d

Verification 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/envoyx

Using localhost.run

# Create tunnel (no installation required)
ssh -R 80:localhost:3000 nokey@localhost.run

# Use the provided URL for your webhook

Best 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

On this page