❮  Integrations

Webhook specification

Entrixy sends an open or close command to your address and signs it. Here is the request format and how to verify the signature, so you can accept the command in your own service, smart home or board.

A webhook is the mode where the opening is performed not by our controller but by your own system: Home Assistant, a relay on the local network, your own server. A Webhook object sends the same signed request in both of its modes. From your phone: the owner’s phone sends it, and the address and secret never leave the phone. From Entrixy server: our server sends it, so it keeps the address and secret. Ready-made receivers: curl, Flask, Home Assistant.

The request

On a press, Entrixy sends a POST to your webhook_url with a JSON body:

POST <your webhook_url>
Content-Type: application/json

{
  "action":    "open",
  "object_id": 42,
  "timestamp": 1750000000,
  "nonce":     "0011223344556677",
  "signature": "9d7b58066094fa88..."
}
FieldValue
action"open", for an Open / close object also "close"
object_idthe object id in your account
timestampunix send time, in seconds
nonce16 hex characters (8 random bytes), single use
signatureHMAC-SHA256, see below

An Open / close object alternates by its last known position: close when it is open, otherwise open. In the server mode the server decides, in the phone mode the owner’s phone.

The signature

What is signed is a canonical string of four dot-separated fields, not the JSON body itself, so you do not have to reproduce our serialisation byte for byte:

base = "<timestamp>.<nonce>.<action>.<object_id>"
signature = HMAC-SHA256(webhook_secret, base)   // hex

The app creates webhook_secret by itself when you add a Webhook object: 32 random hex characters. Copy it with the button next to the field into your receiver’s settings. You can replace it with your own. A webhook without a secret cannot be saved in either mode, so every request is signed.

Guests never receive the secret: their presses are signed by the owner’s phone or by the server, whichever sends the request.

How to verify it (on your side)

  1. Build the same string base = timestamp.nonce.action.object_id from the fields you received.
  2. Compute HMAC-SHA256(webhook_secret, base) and compare it with signature using a constant-time comparison (hash_equals / hmac.compare_digest).
  3. Anti-replay: reject the request if |now − timestamp| > 300 seconds. Optionally, keep recent nonce values and reject repeats.
  4. The signature matches and the request is fresh — perform the opening.
A webhook address is usually open without authentication, so verifying the signature is mandatory: without it, anyone who learns the address can open. If you replace the secret, use at least 16 characters, for example from openssl rand -hex 32. Never put the secret into the address itself.

Verification code

Each example checks the signature and the time. Keep the secret in your receiver’s settings or an environment variable.

PHP

$secret = getenv('ENTRIXY_WEBHOOK_SECRET');
$b = json_decode(file_get_contents('php://input'), true);
$base = $b['timestamp'] . '.' . $b['nonce'] . '.' . $b['action'] . '.' . $b['object_id'];
$ok = hash_equals(hash_hmac('sha256', $base, $secret), (string)($b['signature'] ?? ''))
   && abs(time() - (int)$b['timestamp']) <= 300;
if (!$ok) { http_response_code(401); exit; }
// $b['action'] is "open" or "close"

Python

import hashlib, hmac, os, time

def verify(b: dict) -> bool:
    secret = os.environ["ENTRIXY_WEBHOOK_SECRET"].encode()
    base = f'{b["timestamp"]}.{b["nonce"]}.{b["action"]}.{b["object_id"]}'
    mac = hmac.new(secret, base.encode(), hashlib.sha256).hexdigest()
    return (hmac.compare_digest(mac, str(b.get("signature", "")))
            and abs(time.time() - int(b["timestamp"])) <= 300)

Node.js

const crypto = require('crypto');

function verify(b, secret) {
  const base = `${b.timestamp}.${b.nonce}.${b.action}.${b.object_id}`;
  const mac = crypto.createHmac('sha256', secret).update(base).digest('hex');
  const sig = String(b.signature || '');
  return sig.length === mac.length
    && crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(sig))
    && Math.abs(Date.now() / 1000 - b.timestamp) <= 300;
}

To refuse a replay inside the five minutes as well, remember the nonce values until their timestamp is more than 300 seconds old, and reject a repeat.

Test vector

Run your verification against these values and check the signature:

webhook_secret = webhook_secret_example_key
timestamp      = 1750000000
nonce          = 0011223344556677
action         = open
object_id      = 42

base       = 1750000000.0011223344556677.open.42
signature  = 9d7b58066094fa88acc0afa7aa805fc5f0d008d33cb11b8aa2366102da8e20f1
  = HMAC-SHA256(webhook_secret, base)

The response

To have the app show a result, answer with JSON:

{"level":"success","message":"Opened","position":"open","close_in":30}
FieldValue
levelsuccess, warning, danger or info: how the app marks the result
messagethe text the app shows next to the press
positionfor an Open / close object: open, closed or unknown
close_inseconds until the receiver closes by itself, at most 3600: the app shows the object closed when they run out

The older answer with the field status (ok or error) is understood as well. Without either field the HTTP code decides: any 2xx answer is a success, anything else a failure. So an empty 200 OK is enough.

Without close_in the position is shown for two seconds after the answer and then becomes unknown: the next press opens.

Delivery

The request is always a POST with a JSON body, never a plain GET.

A receiver reachable only on your home network therefore works only when the owner’s phone sends the request, and only while the phone is on that network. Use https for a receiver reachable from the internet.

Try it without your own receiver

Open the demo barrier: it gives you a personal test address. Set it as the object’s address and paste the object’s secret into the field on that page — the signature of every press is checked right in your browser, and the secret is sent nowhere.

Ready-made receivers

curl sends a test request with a correct signature. Flask is a receiver that checks the signature and refuses repeats. Home Assistant takes the command through that receiver, or through its own webhook trigger without a signature check.

Shelly and Tasmota relays and openHAB do not check the signature. They connect through the Node.js client: it keeps its own connection to Entrixy and passes the command on inside your network.