Webhooks Guide for eCourtDate
What is eCourtDate?
eCourtDate is a cloud-native communications platform used by justice systems such as courts and prosecutors. Agencies can automatically send and receive two-way messages via texts (SMS), email, and voice calls.
Webhooks allows system admins to integrate eCourtDate's scheduling and messaging capabilities into their own applications and workflows.
eCourtDate Agency Required
Getting Started
Webhooks are designed to provide real-time data about messages, events, and payments for machine-to-machine communication. You can use webhooks to notify an internal Teams channel about new payments or a kiosk system about new check-ins.
Step 1 - Create a Webhook
Login to the Console, then click Webhooks from the top navigation.
In the Add Webhook form, enter a name (you can change this later), choose a region, then click Add.
Step 2 - Customize Webhook
(optional) Choose the Agency to use for the webhook.
(required) Enter your webhook URL. This is the URL that we'll send webhook events to.
Step 3 - Use Webhooks
Webhooks can be used in connected systems to respond to real-time changes. Use our REST API to update data and trigger multilingual notifications.
Channels:
Sample Webhook
The following is a sample payload of an INBOUND_TEXTS webhook.
Securing Your Webhook
In production, we recommend using both IP Whitelisting and a Shared Secret to secure your webhook.
IP Whitelisting
To ensure that your webhook only receives requests from eCourtDate, whitelist the IP addresses that are shown in the Console when you edit your webhook.
There are two IP addresses to whitelist.
Shared Secret
To secure your webhook, we sign the payload using a shared secret. You provide us with this secret when setting up the webhook. We use it to generate a signature, allowing you to verify the request's authenticity.
You can set the shared secret when editing your webhook in the Console.
Note: the secret can be up to 24 characters.
Signature Header
The webhook request will include a header:
X-ECD-Signature: Contains the HMAC-SHA256 signature, generated using the shared secret.
Signature Verification
- Retrieve the Signature: Extract the value of the X-ECD-Signature header from the incoming request.
- Generate a Signature: Use the shared secret to compute the HMAC-SHA256 hash of the payload (raw request body). Ensure you use the exact payload content as received, then JSON encode the payload.
- Compare Signatures: Compare the generated signature with the received signature. If they match, the request is authentic. Use a constant-time comparison function to prevent timing attacks.
Sample node.js code to generate a signature:
const crypto = require('crypto');
const secret = 'your-shared-secret';
const payload = JSON.stringify(request.body);
const signature = crypto.createHmac('sha256', secret).update(payload).digest('hex');
console.log('Generated Signature:', signature);
Testing Webhooks
When editing your webhook, you can test the webhook by clicking the Test Request button.
You should see the console output indicating the received response message and status code.
You can also test the webhook by sending or receiving a message to your Agency Phone or Agency Email.
In staging, outbound channels are not available. You can only test inbound channels.
Note: test requests use the Console's IP address. You can include the IP address in your whitelist or remove it after testing.
Sample Implementation Code
Node.js Implementation
Below is a secure Node.js implementation that validates webhook signatures and handles incoming webhook events, using only built-in Node.js modules.
Prerequisites
- Node.js 18.0.0 or higher
- Your webhook's shared secret from the Console
Project Setup
1. Create project directory and initialize:
mkdir ecd-webhook-demo
cd ecd-webhook-demo
npm init -y
touch config.js app.js
2. Add the following code to config.js:
module.exports = {
port: process.env.PORT || 3000,
webhookSecret: process.env.WEBHOOK_SECRET || 'your-webhook-secret-here',
// Add your whitelisted IPs from the Console
allowedIPs: [
'123.45.67.89',
'98.76.54.32'
]
};
3. Add the following code to app.js:
const http = require('http');
const crypto = require('crypto');
const config = require('./config');
// Security headers middleware
const securityHeaders = {
'Content-Security-Policy': "default-src 'none'",
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'
};
// Verify webhook signature
function verifySignature(signature, payload) {
if (!signature) return false;
const hmac = crypto.createHmac('sha256', config.webhookSecret);
const calculatedSignature = hmac
.update(payload)
.digest('hex');
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(calculatedSignature)
);
} catch {
return false;
}
}
// IP whitelist check
function isIPAllowed(ip) {
return config.allowedIPs.includes(ip);
}
// Parse JSON safely
function parseJSON(str) {
try {
return JSON.parse(str);
} catch {
return null;
}
}
const server = http.createServer(async (req, res) => {
// Add security headers
Object.entries(securityHeaders).forEach(([key, value]) => {
res.setHeader(key, value);
});
// Only allow POST requests to /webhook
if (req.method !== 'POST' || req.url !== '/webhook') {
res.writeHead(404);
res.end();
return;
}
// IP whitelist check
const clientIP = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
if (!isIPAllowed(clientIP)) {
res.writeHead(403);
res.end();
return;
}
// Collect request body
let body = '';
req.on('data', chunk => {
body += chunk.toString();
// Basic protection against large payloads
if (body.length > 1e6) req.destroy();
});
req.on('end', () => {
// Parse JSON payload
const payload = parseJSON(body);
if (!payload) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'Invalid JSON' }));
return;
}
// Verify signature
const signature = req.headers['x-ecd-signature'];
if (!verifySignature(signature, body)) {
res.writeHead(401);
res.end(JSON.stringify({ error: 'Invalid signature' }));
return;
}
// Log the webhook event
console.log('Received webhook event:', {
uuid: payload.uuid,
channel: payload.channel,
direction: payload.direction,
created_at: payload.created_at
});
// Handle inbound text message
if (payload.channel === 'text' && payload.direction === 'inbound') {
console.log('Processing text message:', {
from: payload.from,
to: payload.to,
content: payload.content,
token: payload.token,
created_at: payload.created_at,
status: payload.last_status
});
// Add your text message handling logic here
}
// Send response
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
received: true,
timestamp: new Date().toISOString()
}));
});
// Handle request errors
req.on('error', (err) => {
console.error('Request error:', err);
res.writeHead(500);
res.end(JSON.stringify({ error: 'Internal server error' }));
});
});
// Start server
server.listen(config.port, () => {
console.log(`Webhook server running on port ${config.port}`);
});
Security Features
- No external dependencies
- Built-in security headers
- IP whitelisting
- Signature verification using HMAC-SHA256
- Timing-safe signature comparison
- JSON parsing protection
- Request size limiting
- Error handling
Running the Server
1. Start the server:
node app.js
2. The server will listen for webhook events on http://localhost:3000/webhook
- Use HTTPS (Node.js https module or reverse proxy)
- Set up proper logging
- Use process manager (PM2 or systemd)
- Update the allowed IPs list
- Set the webhook secret via environment variable
Testing
You can test your webhook implementation using the Console's "Test Request" feature or by sending a real message to your Agency Phone/Email.
Technical Support
If you have any questions or issues while using eCourtDate's Webhooks, please contact our support team at help@ecourtdate.com. We're here to help.
We appreciate any feedback or suggestions to improve our Webhooks and technical resources.