Server-side
// Scripted REST API Resource - POST method
(function process(request, response) {
var webhookProcessor = new WebhookProcessor();
var result = webhookProcessor.processInboundWebhook(request, response);
if (result.success) {
response.setStatus(200);
response.getStreamWriter().writeString(JSON.stringify({status: 'received', id: result.stagingId}));
} else {
response.setStatus(result.statusCode || 400);
response.getStreamWriter().writeString(JSON.stringify({error: result.error}));
}
})(request, response);
// Script Include: WebhookProcessor
var WebhookProcessor = Class.create();
WebhookProcessor.prototype = {
initialize: function() {
this.WEBHOOK_SECRET = gs.getProperty('webhook.secret.key', '');
this.STAGING_TABLE = 'u_webhook_staging';
},
processInboundWebhook: function(request, response) {
try {
// Extract webhook data
var headers = request.getHeaders();
var body = request.getRequestBody();
var signature = headers['x-hub-signature-256'] || headers['x-signature-sha256'];
var source = headers['user-agent'] || 'unknown';
// Validate HMAC signature
if (!this._validateSignature(body, signature)) {
gs.warn('Webhook signature validation failed from source: ' + source);
return {success: false, statusCode: 401, error: 'Invalid signature'};
}
// Parse JSON payload
var payload;
try {
payload = JSON.parse(body);
} catch (e) {
gs.error('Invalid JSON payload in webhook: ' + e.getMessage());
return {success: false, statusCode: 400, error: 'Invalid JSON'};
}
// Store in staging table
var stagingRecord = new GlideRecord(this.STAGING_TABLE);
stagingRecord.initialize();
stagingRecord.setValue('source', source);
stagingRecord.setValue('payload', body);
stagingRecord.setValue('headers', JSON.stringify(headers));
stagingRecord.setValue('status', 'pending');
stagingRecord.setValue('received_at', new GlideDateTime());
var stagingId = stagingRecord.insert();
if (!stagingId) {
gs.error('Failed to insert webhook staging record');
return {success: false, statusCode: 500, error: 'Storage failed'};
}
// Trigger async processing via business event
gs.eventQueue('webhook.received', stagingRecord, stagingId, source);
gs.info('Webhook received and staged successfully: ' + stagingId);
return {success: true, stagingId: stagingId};
} catch (error) {
gs.error('Webhook processing error: ' + error.getMessage());
return {success: false, statusCode: 500, error: 'Internal server error'};
}
},
_validateSignature: function(payload, signature) {
if (!signature || !this.WEBHOOK_SECRET) {
return false;
}
// Remove 'sha256=' prefix if present
var providedSignature = signature.replace('sha256=', '');
// Calculate expected HMAC
var mac = new GlideMacGenerator();
mac.setAlgorithm('HmacSHA256');
mac.setKey(this.WEBHOOK_SECRET);
var expectedSignature = mac.generateMac(payload);
// Constant-time comparison to prevent timing attacks
return this._constantTimeEquals(providedSignature, expectedSignature);
},
_constantTimeEquals: function(a, b) {
if (a.length !== b.length) {
return false;
}
var result = 0;
for (var i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
},
type: 'WebhookProcessor'
};
// Business Rule on webhook staging table - After Insert
(function executeRule(current, previous) {
var processor = new WebhookAsyncProcessor();
processor.processWebhook(current);
})(current, previous);
// Script Include: WebhookAsyncProcessor
var WebhookAsyncProcessor = Class.create();
WebhookAsyncProcessor.prototype = {
initialize: function() {},
processWebhook: function(stagingRecord) {
try {
var payload = JSON.parse(stagingRecord.getValue('payload'));
var source = stagingRecord.getValue('source');
stagingRecord.setValue('status', 'processing');
stagingRecord.update();
// Route to appropriate handler based on source or payload structure
var processed = this._routeWebhook(payload, source);
if (processed) {
stagingRecord.setValue('status', 'completed');
stagingRecord.setValue('processed_at', new GlideDateTime());
} else {
stagingRecord.setValue('status', 'failed');
stagingRecord.setValue('error_message', 'Processing failed');
}
stagingRecord.update();
} catch (error) {
gs.error('Webhook async processing error: ' + error.getMessage());
stagingRecord.setValue('status', 'failed');
stagingRecord.setValue('error_message', error.getMessage());
stagingRecord.update();
}
},
_routeWebhook: function(payload, source) {
// Example routing logic - customize based on your needs
if (source.indexOf('GitHub') > -1) {
return this._processGitHubWebhook(payload);
} else if (payload.event_type) {
return this._processGenericWebhook(payload);
}
return false;
},
_processGitHubWebhook: function(payload) {
// Example GitHub webhook processing
if (payload.action === 'opened' && payload.pull_request) {
var changeRequest = new GlideRecord('change_request');
changeRequest.initialize();
changeRequest.setValue('short_description', 'PR: ' + payload.pull_request.title);
changeRequest.setValue('description', payload.pull_request.body);
changeRequest.insert();
return true;
}
return false;
},
_processGenericWebhook: function(payload) {
gs.info('Processing generic webhook: ' + payload.event_type);
return true;
},
type: 'WebhookAsyncProcessor'
};
The code implements a complete webhook listener using a Scripted REST API that validates HMAC signatures for security, stores payloads in a staging table for reliability, and uses business events for asynchronous processing. The pattern separates concerns with dedicated Script Includes for webhook reception and async processing, ensuring that external systems receive quick responses while complex processing happens in the background.