Skip to content

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

FieldTypeDefaultDescription
namestringrequiredHuman-readable label
urlstringrequiredHTTPS endpoint to receive deliveries
eventTypesstring[]["*"]Event types to deliver. ["*"] = all.
homeIdsstring[]nullFilter to events from these homes
roomIdsstring[]nullFilter to events from these rooms
accessoryIdsstring[]nullFilter to events from these accessories
collectionIdsstring[]nullFilter to events from accessories in these collections
maxRetriesinteger3Max retry attempts after failure
rateLimitPerMinuteinteger60Max deliveries per minute
timeoutMsinteger30000HTTP request timeout (ms)

Webhook status

StatusDescription
activeReceiving deliveries normally
pausedDeliveries suspended (manual or circuit breaker)
disabledPermanently disabled

Event types

EventDescription
state.changedA device characteristic changed value
webhook.testTest 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"
  }
}
FieldTypeDescription
idstringUnique event ID (UUID)
typestringEvent type
timestampstringISO 8601 timestamp
webhook_idstringWebhook that received this delivery
dataobjectEvent-specific data
metadata.webhook_versionstringPayload format version (1.0)
metadata.webhook_namestringWebhook name

HTTP headers

Every delivery includes these headers:

HeaderExampleDescription
Content-Typeapplication/jsonAlways JSON
X-Homecast-Signaturet=1708099200,v1=abc...HMAC-SHA256 signature
X-Homecast-Timestamp1708099200Unix timestamp
X-Homecast-Eventstate.changedEvent type
X-Homecast-Deliverydelivery-uuidUnique delivery ID
User-AgentHomecast-Webhook/1.0Sender identifier

Signature verification

Webhook payloads are signed with HMAC-SHA256 using the webhook's secret.

Algorithm

  1. Parse the X-Homecast-Signature header: t={timestamp},v1={signature}
  2. Build the signed message: {timestamp}.{raw_request_body}
  3. Compute HMAC-SHA256(secret, message) and hex-encode
  4. Compare the computed signature with the v1 value
  5. 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

StatusDescription
pendingQueued for delivery
successDelivered (2xx response)
failedDelivery failed (will not retry)
retryingFailed but scheduled for retry
dead_letterAll retry attempts exhausted

Retry backoff

Failed deliveries are retried with exponential backoff:

AttemptDelay
1Immediate
21 second
32 seconds
44 seconds
58 seconds
616 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

MutationParametersDescription
createWebhookname, url, eventTypes?, homeIds?, roomIds?, accessoryIds?, collectionIds?, maxRetries?, rateLimitPerMinute?, timeoutMs?Create a webhook
updateWebhookwebhookId, + any config fieldUpdate configuration
deleteWebhookwebhookIdDelete permanently
pauseWebhookwebhookIdPause deliveries
resumeWebhookwebhookIdResume deliveries
rotateWebhookSecretwebhookIdGenerate new signing secret
testWebhookwebhookIdSend a test delivery

Query delivery history

graphql
{
  webhookDeliveryHistory(webhookId: "webhook-uuid", limit: 10) {
    total
    deliveries {
      id
      eventType
      status
      attemptNumber
      responseStatusCode
      latencyMs
      errorMessage
      createdAt
    }
  }
}