Skip to main content

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.

  1. Navigate to Event Subscriptions

Subscriptions table

  1. Create New Subscription

Create 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)
  1. View Your Subscriptions

Pending subscription

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.

Enabled subscription

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

Remove 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 begins
  • status.execution.ended - When an execution completes
  • status.run.ended - When a story run finishes
  • status.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:

  1. Extract the signature from the header
  2. Create an HMAC-SHA256 hash of the request body using your secret
  3. 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.