Event Subscription API
Overview
The Event Subscription API allows you to subscribe to real-time events from our platform via webhooks. When subscribed events occur, we'll send HTTP POST requests to your specified callback URL with the event data.
Getting Started
Prerequisites
- Account on app.heal.dev
- HTTPS endpoint capable of receiving webhooks (must use SSL on port 443)
- Ability to verify webhook signatures for security
Subscription Management
Creating a Subscription
Subscriptions are managed through the Heal web application at app.heal.dev.
- Navigate to Event Subscriptions
- Create New Subscription
Fill in the required information:
- Name: Friendly name for your subscription
- Event Types: Select
status
to receive status-related events - Callback URL: Your HTTPS webhook endpoint (must use port 443)
- Secret: Secret key for webhook verification (10-100 ASCII characters)
- View Your Subscriptions
Webhook Verification
After creating a subscription, you'll receive a verification request to confirm ownership of the webhook endpoint.
Verification Request:
- Header:
X-Heal-Notification-Type: callback_verification
- Body:
{
"code": "verification-code-12345",
"subscription": {
"id": "12345",
"status": "pending",
"type": "status",
"callback": "https://your-domain.com/webhooks/heal",
"createdAt": "2025-06-02T10:11:12.634234626Z"
}
}
Required Response:
- Status Code:
200
- Content-Type:
text/plain
- Body: The raw verification code (e.g.,
verification-code-12345
)
Once verified, your subscription status will change from pending
to enabled
.
Managing Existing Subscriptions
Edit Subscription
You can modify:
- Callback URL
- Secret key
Note: Changing the callback URL or secret will require re-verification before the subscription becomes active again.
Delete Subscription
Test Subscription
Use the test feature to verify your webhook endpoint is working correctly.
Event Types
Status Events
Status events are triggered during execution lifecycle changes:
status.execution.started
- When an execution beginsstatus.execution.ended
- When an execution completesstatus.run.ended
- When a story run finishesstatus.run.reviewed
- When a run receives a new review
Receiving Events
Event Notification Format
When subscribed events occur, you'll receive HTTP POST requests with the following structure:
Headers:
X-Heal-Notification-Type: notification
X-Heal-Notification-Signature: sha256=<hmac_signature>
Content-Type: application/json
Request Body:
{
"subscription": {
"id": "12345",
"types": ["status"],
"callback": "https://your-domain.com/webhooks/heal",
"status": "enabled"
},
"event": {
"type": "status.execution.started",
"executionId": "exec_123",
"triggerUserId": "user_456",
"runs": [
{
"runId": "run_789",
"status": "queued",
"result": null,
"storyId": "story_101",
"storyName": "User Login Flow"
}
]
}
}
Response Requirements
Your webhook endpoint must:
- Return a 2XX status code (200-299)
- Respond within 10 seconds
- Handle duplicate events idempotently
Security
Webhook Signature Verification
Critical: Always verify webhook signatures to ensure requests come from our service.
We sign each webhook using HMAC-SHA256 with your subscription's secret key. The signature is provided in the X-Heal-Notification-Signature
header as sha256=<signature>
.
Verification Steps:
- Extract the signature from the header
- Create an HMAC-SHA256 hash of the request body using your secret
- Compare the signatures using a constant-time comparison
Example Implementation
Here's a complete webhook handler example:
const crypto = require('node:crypto')
const express = require('express');
const app = express();
const port = 8090;
// Notification request headers
const MESSAGE_TYPE = 'X-Heal-Notification-Type'.toLowerCase();
const MESSAGE_SIGNATURE = 'X-Heal-Notification-Signature'.toLowerCase();
const MESSAGE_ID = 'X-Heal-Notification-Id'.toLowerCase();
const MESSAGE_TIMESTAMP = 'X-Heal-Notification-Timestamp'.toLowerCase();
// Notification message types
const MESSAGE_TYPE_VERIFICATION = 'callback_verification';
const MESSAGE_TYPE_NOTIFICATION = 'notification';
// Prepend this string to the HMAC that's created from the message
const HMAC_PREFIX = 'sha256=';
// Need raw message body for signature verification
app.use(express.raw({
type: 'application/json'
}))
app.post('/event', (req, res) => {
let secret = getSecret();
let message = getHmacMessage(req);
let hmac = HMAC_PREFIX + getHmac(secret, message); // Signature to compare
if (verifyMessage(hmac, req.headers[MESSAGE_SIGNATURE])) {
console.log("signatures match");
// Get JSON object from body, so you can process the message.
let notification = JSON.parse(req.body);
if (MESSAGE_TYPE_NOTIFICATION === req.headers[MESSAGE_TYPE]) {
// TODO: Do something with the event's data.
console.log(JSON.stringify(notification.event, null, 4));
res.sendStatus(204);
}
else if (MESSAGE_TYPE_VERIFICATION === req.headers[MESSAGE_TYPE]) {
res.set('Content-Type', 'text/plain').status(200).send(notification.code);
}
else {
res.sendStatus(204);
console.log(`Unknown message type: ${req.headers[MESSAGE_TYPE]}`);
}
}
else {
console.log('403'); // Signatures didn't match.
res.sendStatus(403);
}
})
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
})
function getSecret() {
return 's3cre77890ab';
}
// Build the message used to get the HMAC.
function getHmacMessage(request) {
return (request.headers[MESSAGE_ID] +
request.headers[MESSAGE_TIMESTAMP] +
request.body);
}
// Get the HMAC.
function getHmac(secret, message) {
return crypto.createHmac('sha256', secret)
.update(message)
.digest('hex');
}
// Verify whether our hash matches the hash that Twitch passed in the header.
function verifyMessage(hmac, verifySignature) {
return crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(verifySignature));
}
Support
For additional help or questions about the Event Subscription API, please contact our support team or refer to our troubleshooting guide.