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:
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
- Verify Signatures: Always verify webhook signatures
- Idempotency: Handle duplicate deliveries gracefully
- Quick Response: Respond with 200 status quickly (< 10 seconds)
- Retry Logic: Handle webhook delivery retries
- 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
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 15 seconds |
| 3 | 15 minutes |
| 4 | 1 hour |
| 5 | 6 hours |
| 6 | 24 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:
- Go to Webhooks in your dashboard
- Click Test next to your webhook endpoint
- Select event type to test
- 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 Webhooks → Delivery Logs.
Common failure reasons:
- Endpoint timeout (> 30 seconds)
- Invalid SSL certificate
- Non-2xx response status
- Network connectivity issues
Event Reference
Complete Event List
| Event | Trigger | Payload |
|---|---|---|
message.queued | Message queued for sending | Message object |
message.sent | Message transmitted to partner | Message object |
message.delivered | MDN received confirming delivery | Message object with MDN |
message.failed | Message delivery failed | Message object with error |
message.received | Message received from partner | Inbound message object |
partner.created | New partner added | Partner object |
partner.updated | Partner configuration changed | Partner object |
partner.deleted | Partner removed | Partner object |
certificate.uploaded | New certificate uploaded | Certificate object |
certificate.activated | Certificate activated | Certificate object |
certificate.expiring | Certificate expires in 30 days | Certificate object |
certificate.expired | Certificate has expired | Certificate object |
billing.usage_threshold | Usage threshold reached | Account usage data |
billing.limit_exceeded | Plan limit exceeded | Account usage data |
billing.payment_failed | Payment processing failed | Account billing data |
billing.subscription_updated | Plan changed | Account subscription data |
account.created | New account created | Account object |
account.updated | Account configuration changed | Account object |
tenant.created | New tenant added | Tenant object |
tenant.updated | Tenant configuration changed | Tenant object |
webhook.test | Manual webhook test | Test 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
- Check endpoint URL is accessible from internet
- Verify webhook endpoint returns 2xx status codes
- Check firewall/security group settings
- Review webhook endpoint configuration
Signature Verification Failing
- Ensure you're using the correct webhook secret
- Verify signature calculation matches our implementation
- Check for encoding issues (UTF-8)
- Ensure raw request body is used for signature
High Failure Rate
- Check endpoint response times (< 10 seconds)
- Monitor for intermittent connectivity issues
- Implement proper error handling
- 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:
- Check delivery logs in your AS2aaS dashboard
- Test endpoint manually using the webhook test feature
- Review error logs for specific failure reasons
- 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);