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 ​

Enable Developer Mode in Settings → Account, then manage webhooks from the dashboard under Settings → Webhooks, or via the GraphQL API.

Webhook management

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 ​

Event triggered → pending
                    ├─ 2xx response → success ✓
                    └─ Non-2xx / timeout → retrying
                                            ├─ 2xx on retry → success ✓
                                            ├─ Retries remaining → retrying ↻
                                            └─ Max retries exhausted → dead_letter ✗

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.

active ──── Success ────→ active (reset counter)
  │
  └── 5 consecutive failures ──→ paused
                                    │
                        resumeWebhook mutation ──→ active

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
    }
  }
}