Appearance
Webhooks
Webhooks deliver HTTP POST notifications to your endpoint when device state changes.
For a getting-started guide, see React to Device Events.
Configuration
Create webhooks via the GraphQL API:
graphql
mutation {
createWebhook(
name: "My Automation"
url: "https://your-server.com/webhook"
eventTypes: ["state.changed"]
homeIds: ["home-uuid"]
) {
success
webhook {
id
name
rawSecret
}
}
}Configuration fields
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | Human-readable label |
url | string | required | HTTPS endpoint to receive deliveries |
eventTypes | string[] | ["*"] | Event types to deliver. ["*"] = all. |
homeIds | string[] | null | Filter to events from these homes |
roomIds | string[] | null | Filter to events from these rooms |
accessoryIds | string[] | null | Filter to events from these accessories |
collectionIds | string[] | null | Filter to events from accessories in these collections |
maxRetries | integer | 3 | Max retry attempts after failure |
rateLimitPerMinute | integer | 60 | Max deliveries per minute |
timeoutMs | integer | 30000 | HTTP request timeout (ms) |
Webhook status
| Status | Description |
|---|---|
active | Receiving deliveries normally |
paused | Deliveries suspended (manual or circuit breaker) |
disabled | Permanently disabled |
Event types
| Event | Description |
|---|---|
state.changed | A device characteristic changed value |
webhook.test | Test delivery (sent by testWebhook mutation) |
Use ["*"] in eventTypes to receive all event types.
Payload format
json
{
"id": "evt-uuid",
"type": "state.changed",
"timestamp": "2026-02-16T08:30:00Z",
"webhook_id": "webhook-uuid",
"data": {
"accessoryId": "acc-uuid",
"accessoryName": "Motion Sensor",
"characteristicType": "motion_detected",
"value": true,
"homeId": "home-uuid",
"roomId": "room-uuid"
},
"metadata": {
"webhook_version": "1.0",
"webhook_name": "My Automation"
}
}| Field | Type | Description |
|---|---|---|
id | string | Unique event ID (UUID) |
type | string | Event type |
timestamp | string | ISO 8601 timestamp |
webhook_id | string | Webhook that received this delivery |
data | object | Event-specific data |
metadata.webhook_version | string | Payload format version (1.0) |
metadata.webhook_name | string | Webhook name |
HTTP headers
Every delivery includes these headers:
| Header | Example | Description |
|---|---|---|
Content-Type | application/json | Always JSON |
X-Homecast-Signature | t=1708099200,v1=abc... | HMAC-SHA256 signature |
X-Homecast-Timestamp | 1708099200 | Unix timestamp |
X-Homecast-Event | state.changed | Event type |
X-Homecast-Delivery | delivery-uuid | Unique delivery ID |
User-Agent | Homecast-Webhook/1.0 | Sender identifier |
Signature verification
Webhook payloads are signed with HMAC-SHA256 using the webhook's secret.
Algorithm
- Parse the
X-Homecast-Signatureheader:t={timestamp},v1={signature} - Build the signed message:
{timestamp}.{raw_request_body} - Compute
HMAC-SHA256(secret, message)and hex-encode - Compare the computed signature with the
v1value - Optionally reject if the timestamp is more than 300 seconds (5 minutes) old
Verification code
python
import hmac, hashlib
def verify(payload: bytes, signature_header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
timestamp = parts["t"]
expected = parts["v1"]
message = f"{timestamp}.{payload.decode()}"
computed = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, expected)javascript
const crypto = require("crypto");
function verify(payload, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map(p => p.split("=", 2))
);
const message = `${parts.t}.${payload}`;
const computed = crypto.createHmac("sha256", secret).update(message).digest("hex");
return crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(parts.v1));
}go
func verify(payload []byte, sigHeader, secret string) bool {
parts := make(map[string]string)
for _, p := range strings.Split(sigHeader, ",") {
kv := strings.SplitN(p, "=", 2)
parts[kv[0]] = kv[1]
}
message := fmt.Sprintf("%s.%s", parts["t"], string(payload))
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(message))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(parts["v1"]))
}Delivery lifecycle
States
| Status | Description |
|---|---|
pending | Queued for delivery |
success | Delivered (2xx response) |
failed | Delivery failed (will not retry) |
retrying | Failed but scheduled for retry |
dead_letter | All retry attempts exhausted |
Retry backoff
Failed deliveries are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 second |
| 3 | 2 seconds |
| 4 | 4 seconds |
| 5 | 8 seconds |
| 6 | 16 seconds |
After maxRetries + 1 total attempts, the delivery is marked dead_letter.
Circuit breaker
If a webhook accumulates 5 consecutive failures, the circuit breaker opens and the webhook status is set to paused. The circuit breaker resets after 60 seconds.
To resume a paused webhook:
graphql
mutation { resumeWebhook(webhookId: "webhook-uuid") { success } }Management mutations
| Mutation | Parameters | Description |
|---|---|---|
createWebhook | name, url, eventTypes?, homeIds?, roomIds?, accessoryIds?, collectionIds?, maxRetries?, rateLimitPerMinute?, timeoutMs? | Create a webhook |
updateWebhook | webhookId, + any config field | Update configuration |
deleteWebhook | webhookId | Delete permanently |
pauseWebhook | webhookId | Pause deliveries |
resumeWebhook | webhookId | Resume deliveries |
rotateWebhookSecret | webhookId | Generate new signing secret |
testWebhook | webhookId | Send a test delivery |
Query delivery history
graphql
{
webhookDeliveryHistory(webhookId: "webhook-uuid", limit: 10) {
total
deliveries {
id
eventType
status
attemptNumber
responseStatusCode
latencyMs
errorMessage
createdAt
}
}
}