Webhooks

Webhooks allow your application to receive real-time notifications when events occur in your AS2aaS account. Instead of polling the API, webhooks push data to your application as events happen.

Overview

AS2aaS sends webhook events as HTTP POST requests to endpoints you configure. Each webhook delivery includes:

  • Event type - What happened (e.g., message.sent)
  • Event data - Relevant object data
  • Timestamp - When the event occurred
  • Signature - HMAC signature for verification

Webhook Flow

The following diagram shows how webhook events are triggered and delivered:

Loading diagram...

Webhook Endpoints

Create Webhook Endpoint

POST /v1/webhook-endpoints

Request:

{
  "url": "https://your-app.com/webhooks/as2",
  "events": [
    "message.sent",
    "message.delivered", 
    "message.failed",
    "partner.created"
  ],
  "secret": "your-webhook-secret"
}

Response (201):

{
  "message": "Webhook endpoint created successfully",
  "data": {
    "id": "wh_000001",
    "url": "https://your-app.com/webhooks/as2",
    "events": ["message.sent", "message.delivered", "message.failed", "partner.created"],
    "active": true,
    "secret": "whsec_abc123def456...",
    "created_at": "2024-01-15T10:30:00Z"
  }
}

List Webhook Endpoints

GET /v1/webhook-endpoints

Update Webhook Endpoint

PATCH /v1/webhook-endpoints/{webhook_id}

Delete Webhook Endpoint

DELETE /v1/webhook-endpoints/{webhook_id}

Test Webhook Endpoint

POST /v1/webhook-endpoints/{webhook_id}/test

Sends a test event to verify your endpoint is working.

Event Types

Message Events

message.queued

Fired when a message is queued for delivery.

{
  "event": "message.queued",
  "data": {
    "id": "msg_000001",
    "message_id": "MSG_20240115_103000_ABC123",
    "partner": {
      "id": "prt_000001",
      "name": "Acme Corp",
      "as2_id": "ACME-CORP-AS2"
    },
    "status": "queued",
    "direction": "outbound",
    "subject": "Invoice #12345",
    "content_type": "application/edi-x12",
    "bytes": 1024,
    "created_at": "2024-01-15T10:30:00Z"
  },
  "timestamp": "2024-01-15T10:30:00Z"
}

message.sent

Fired when a message has been successfully transmitted to the partner.

{
  "event": "message.sent",
  "data": {
    "id": "msg_000001",
    "message_id": "MSG_20240115_103000_ABC123",
    "partner": {
      "id": "prt_000001",
      "name": "Acme Corp", 
      "as2_id": "ACME-CORP-AS2"
    },
    "status": "sent",
    "direction": "outbound",
    "sent_at": "2024-01-15T10:30:15Z",
    "transmission_time_ms": 2340
  },
  "timestamp": "2024-01-15T10:30:15Z"
}

message.delivered

Fired when an MDN is received confirming successful delivery.

{
  "event": "message.delivered",
  "data": {
    "id": "msg_000001",
    "message_id": "MSG_20240115_103000_ABC123",
    "partner": {
      "id": "prt_000001",
      "name": "Acme Corp",
      "as2_id": "ACME-CORP-AS2"
    },
    "status": "delivered",
    "direction": "outbound",
    "mdn_data": {
      "disposition": "automatic-action/MDN-sent-automatically; processed",
      "received_content_mic": "abc123def456...",
      "reporting_ua": "Partner AS2 Server/1.0"
    },
    "delivered_at": "2024-01-15T10:30:45Z"
  },
  "timestamp": "2024-01-15T10:30:45Z"
}

message.failed

Fired when message delivery fails.

{
  "event": "message.failed",
  "data": {
    "id": "msg_000001",
    "message_id": "MSG_20240115_103000_ABC123",
    "partner": {
      "id": "prt_000001",
      "name": "Acme Corp",
      "as2_id": "ACME-CORP-AS2"
    },
    "status": "failed",
    "direction": "outbound",
    "error_code": "PARTNER_UNREACHABLE",
    "error_message": "Connection timeout after 60 seconds",
    "failed_at": "2024-01-15T10:31:00Z"
  },
  "timestamp": "2024-01-15T10:31:00Z"
}

message.received

Fired when an AS2 message is received from a trading partner.

{
  "event": "message.received",
  "data": {
    "id": "msg_000002",
    "message_id": "PARTNER_MSG_20240115_103100_XYZ789",
    "partner": {
      "id": "prt_000001",
      "name": "Acme Corp",
      "as2_id": "ACME-CORP-AS2"
    },
    "status": "received",
    "direction": "inbound",
    "from_as2_id": "ACME-CORP-AS2",
    "to_as2_id": "YOUR-AS2-ID",
    "subject": "Purchase Order #67890",
    "content_type": "application/edi-x12",
    "bytes": 2048,
    "encrypted": true,
    "signed": true,
    "mdn_requested": true,
    "received_at": "2024-01-15T10:31:00Z"
  },
  "timestamp": "2024-01-15T10:31:00Z"
}

Partner Events

partner.created

Fired when a new trading partner is added.

{
  "event": "partner.created",
  "data": {
    "id": "prt_000002",
    "name": "New Partner Corp",
    "as2_id": "NEW-PARTNER-AS2",
    "url": "https://partner.example.com/as2",
    "active": true,
    "created_at": "2024-01-15T10:30:00Z"
  },
  "timestamp": "2024-01-15T10:30:00Z"
}

partner.updated

partner.deleted

Certificate Events

certificate.uploaded

Fired when a new certificate is uploaded.

certificate.expiring

Fired 30 days before certificate expiry.

{
  "event": "certificate.expiring",
  "data": {
    "id": "cert_000001",
    "name": "Partner Certificate",
    "type": "partner",
    "subject": "CN=Partner AS2",
    "expires_at": "2024-02-15T00:00:00Z",
    "days_until_expiry": 30
  },
  "timestamp": "2024-01-15T10:30:00Z"
}

certificate.expired

certificate.activated

Billing Events

billing.usage_threshold

Fired when usage reaches threshold (75%, 90%, 100% of plan limit).

{
  "event": "billing.usage_threshold",
  "data": {
    "account_id": "acc_000001",
    "threshold_percentage": 90,
    "current_usage": 90000,
    "plan_limit": 100000,
    "usage_type": "messages",
    "billing_period": {
      "start": "2024-01-01T00:00:00Z",
      "end": "2024-02-01T00:00:00Z"
    }
  },
  "timestamp": "2024-01-15T10:30:00Z"
}

billing.limit_exceeded

Fired when account exceeds plan limits.

{
  "event": "billing.limit_exceeded",
  "data": {
    "account_id": "acc_000001",
    "resource_type": "messages",
    "current_usage": 105000,
    "plan_limit": 100000,
    "overage_amount": 5000,
    "billing_period": "2024-01"
  },
  "timestamp": "2024-01-15T10:30:00Z"
}

billing.payment_failed

billing.subscription_updated

Webhook Security

Signature Verification

AS2aaS signs all webhook payloads with HMAC-SHA256. Verify signatures to ensure requests are from AS2aaS:

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSignature, 'hex')
  );
}

// Express.js example
app.post('/webhooks/as2', express.raw({type: 'application/json'}), (req, res) => {
  const signature = req.headers['x-signature'];
  const payload = req.body;
  
  if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  
  // Process webhook
  const event = JSON.parse(payload);
  console.log('Received event:', event.event);
  
  res.status(200).send('OK');
});

Best Practices

  1. Verify Signatures: Always verify webhook signatures
  2. Idempotency: Handle duplicate deliveries gracefully
  3. Quick Response: Respond with 200 status quickly (< 10 seconds)
  4. Retry Logic: Handle webhook delivery retries
  5. Error Handling: Log and monitor webhook processing errors

Webhook Delivery

Delivery Guarantees

  • At-least-once delivery: Events may be delivered multiple times
  • Retry Logic: Failed deliveries are retried with exponential backoff
  • Timeout: 30 second timeout for webhook responses
  • Max Retries: 5 retry attempts over 24 hours

Retry Schedule

AttemptDelay
1Immediate
215 seconds
315 minutes
41 hour
56 hours
624 hours

Response Requirements

Your webhook endpoint must:

  • Respond quickly (< 10 seconds)
  • Return 2xx status for successful processing
  • Handle duplicates idempotently
  • Validate signatures for security

Testing Webhooks

Webhook Testing Tool

Use the dashboard testing tools:

  1. Go to Webhooks in your dashboard
  2. Click Test next to your webhook endpoint
  3. Select event type to test
  4. Review delivery results

Manual Testing

POST /v1/webhook-endpoints/{webhook_id}/test

Sends a test webhook.test event to verify connectivity.

Monitoring Webhooks

Webhook Statistics

GET /v1/webhook-endpoints-stats

Response:

{
  "data": [
    {
      "endpoint_id": "wh_000001",
      "url": "https://your-app.com/webhooks/as2",
      "total_deliveries": 1250,
      "successful_deliveries": 1248,
      "failed_deliveries": 2,
      "success_rate": 99.84,
      "avg_response_time_ms": 145,
      "last_delivery_at": "2024-01-15T10:30:00Z"
    }
  ]
}

Failed Deliveries

Monitor failed webhook deliveries in your dashboard under WebhooksDelivery Logs.

Common failure reasons:

  • Endpoint timeout (> 30 seconds)
  • Invalid SSL certificate
  • Non-2xx response status
  • Network connectivity issues

Event Reference

Complete Event List

EventTriggerPayload
message.queuedMessage queued for sendingMessage object
message.sentMessage transmitted to partnerMessage object
message.deliveredMDN received confirming deliveryMessage object with MDN
message.failedMessage delivery failedMessage object with error
message.receivedMessage received from partnerInbound message object
partner.createdNew partner addedPartner object
partner.updatedPartner configuration changedPartner object
partner.deletedPartner removedPartner object
certificate.uploadedNew certificate uploadedCertificate object
certificate.activatedCertificate activatedCertificate object
certificate.expiringCertificate expires in 30 daysCertificate object
certificate.expiredCertificate has expiredCertificate object
billing.usage_thresholdUsage threshold reachedAccount usage data
billing.limit_exceededPlan limit exceededAccount usage data
billing.payment_failedPayment processing failedAccount billing data
billing.subscription_updatedPlan changedAccount subscription data
account.createdNew account createdAccount object
account.updatedAccount configuration changedAccount object
tenant.createdNew tenant addedTenant object
tenant.updatedTenant configuration changedTenant object
webhook.testManual webhook testTest data

Account and Tenant Events

account.created

Fired when a new account is created.

{
  "event": "account.created",
  "data": {
    "id": "acc_000001",
    "name": "Acme Corporation",
    "slug": "acme-corp",
    "plan": "enterprise",
    "status": "active",
    "created_at": "2024-01-15T10:30:00Z"
  },
  "timestamp": "2024-01-15T10:30:00Z"
}

tenant.created

Fired when a new tenant is added to an account.

{
  "event": "tenant.created",
  "data": {
    "id": "tnt_000001",
    "account_id": "acc_000001",
    "name": "East Coast Division",
    "slug": "east-coast",
    "as2_id": "ACME-EAST-AS2",
    "status": "active",
    "created_at": "2024-01-15T10:30:00Z"
  },
  "timestamp": "2024-01-15T10:30:00Z"
}

Integration Patterns

Event-Driven Processing

// Complete webhook handler
app.post('/webhooks/as2', async (req, res) => {
  try {
    // Verify signature
    if (!verifySignature(req.body, req.headers['x-signature'])) {
      return res.status(401).send('Invalid signature');
    }
    
    const { event, data, timestamp } = req.body;
    
    switch (event) {
      case 'message.received':
        await handleIncomingMessage(data);
        break;
        
      case 'message.delivered':
        await updateOrderStatus(data.message_id, 'delivered');
        break;
        
      case 'message.failed':
        await handleDeliveryFailure(data);
        break;
        
      case 'certificate.expiring':
        await notifyAdminCertificateExpiry(data);
        break;
        
      case 'billing.usage_threshold':
        await handleUsageAlert(data);
        break;
        
      default:
        console.log('Unhandled event:', event);
    }
    
    res.status(200).send('OK');
  } catch (error) {
    console.error('Webhook processing error:', error);
    res.status(500).send('Internal error');
  }
});

Automatic Document Processing

async function handleIncomingMessage(messageData) {
  // Download the payload
  const response = await fetch(
    `https://api.as2aas.com/v1/messages/${messageData.id}/payload`,
    {
      headers: {
        'Authorization': 'Bearer ' + process.env.AS2AAS_API_KEY
      }
    }
  );
  
  const payload = await response.text();
  
  // Process based on content type
  switch (messageData.content_type) {
    case 'application/edi-x12':
      await processEDIDocument(payload, messageData);
      break;
      
    case 'application/xml':
      await processXMLDocument(payload, messageData);
      break;
      
    default:
      console.log('Unknown content type:', messageData.content_type);
  }
}

Error Recovery

// Handle failed message deliveries
async function handleDeliveryFailure(failureData) {
  const { error_code, error_message, partner } = failureData;
  
  switch (error_code) {
    case 'PARTNER_UNREACHABLE':
      // Notify admin, possibly disable partner temporarily
      await notifyAdmin(`Partner ${partner.name} is unreachable`);
      break;
      
    case 'CERTIFICATE_EXPIRED':
      // Request new certificate from partner
      await requestCertificateUpdate(partner.id);
      break;
      
    case 'MESSAGE_TOO_LARGE':
      // Split message or compress
      await handleLargeMessage(failureData.id);
      break;
      
    default:
      await logUnknownError(failureData);
  }
}

Webhook Development

Local Testing

For local development, use tools like ngrok to expose your local server:

# Install ngrok
npm install -g ngrok

# Expose local port
ngrok http 3000

# Use the https URL for your webhook endpoint
# Example: https://abc123.ngrok.io/webhooks/as2

Testing Framework

// Jest test example
describe('AS2aaS Webhooks', () => {
  test('handles message.received event', async () => {
    const webhookPayload = {
      event: 'message.received',
      data: {
        id: 'msg_test_001',
        partner: { name: 'Test Partner' },
        content_type: 'application/edi-x12'
      },
      timestamp: new Date().toISOString()
    };
    
    const response = await request(app)
      .post('/webhooks/as2')
      .send(webhookPayload)
      .expect(200);
  });
});

Production Monitoring

// Monitor webhook health
app.post('/webhooks/as2', async (req, res) => {
  const startTime = Date.now();
  
  try {
    await processWebhookEvent(req.body);
    
    // Log successful processing
    console.log('Webhook processed', {
      event: req.body.event,
      processing_time_ms: Date.now() - startTime,
      status: 'success'
    });
    
    res.status(200).send('OK');
  } catch (error) {
    // Log error for monitoring
    console.error('Webhook processing failed', {
      event: req.body.event,
      error: error.message,
      processing_time_ms: Date.now() - startTime,
      status: 'error'
    });
    
    res.status(500).send('Error');
  }
});

Troubleshooting

Common Issues

Webhook Not Receiving Events

  1. Check endpoint URL is accessible from internet
  2. Verify webhook endpoint returns 2xx status codes
  3. Check firewall/security group settings
  4. Review webhook endpoint configuration

Signature Verification Failing

  1. Ensure you're using the correct webhook secret
  2. Verify signature calculation matches our implementation
  3. Check for encoding issues (UTF-8)
  4. Ensure raw request body is used for signature

High Failure Rate

  1. Check endpoint response times (< 10 seconds)
  2. Monitor for intermittent connectivity issues
  3. Implement proper error handling
  4. Review server logs for application errors

Debug Mode

Enable debug logging to troubleshoot webhook issues:

app.post('/webhooks/as2', (req, res) => {
  // Log all webhook data for debugging
  console.log('Webhook received:', {
    headers: req.headers,
    body: req.body,
    timestamp: new Date().toISOString()
  });
  
  res.status(200).send('OK');
});

Getting Help

If you're experiencing webhook issues:

  1. Check delivery logs in your AS2aaS dashboard
  2. Test endpoint manually using the webhook test feature
  3. Review error logs for specific failure reasons
  4. Contact support with webhook endpoint ID for investigation

Advanced Features

Conditional Webhooks

Configure different webhook endpoints for different event types:

// Webhook for message events only
{
  "url": "https://your-app.com/webhooks/messages",
  "events": ["message.*"]
}

// Webhook for billing events only  
{
  "url": "https://your-app.com/webhooks/billing",
  "events": ["billing.*"]
}

Webhook Filtering

Filter events based on data attributes:

app.post('/webhooks/as2', (req, res) => {
  const { event, data } = req.body;
  
  // Only process high-priority partners
  if (event === 'message.received' && data.partner.priority === 'high') {
    await processUrgentMessage(data);
  }
  
  res.status(200).send('OK');
});

Batch Processing

Handle multiple events efficiently:

const eventQueue = [];

app.post('/webhooks/as2', (req, res) => {
  // Add to queue for batch processing
  eventQueue.push(req.body);
  
  // Quick response
  res.status(200).send('OK');
});

// Process events in batches
setInterval(async () => {
  if (eventQueue.length > 0) {
    const batch = eventQueue.splice(0, 10); // Process 10 at a time
    await processBatchEvents(batch);
  }
}, 5000);