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.
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..."
}
| Field | Value |
|---|---|
action | "open", for an Open / close object also "close" |
object_id | the object id in your account |
timestamp | unix send time, in seconds |
nonce | 16 hex characters (8 random bytes), single use |
signature | HMAC-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)
- Build the same string
base = timestamp.nonce.action.object_idfrom the fields you received. - Compute
HMAC-SHA256(webhook_secret, base)and compare it withsignatureusing a constant-time comparison (hash_equals/hmac.compare_digest). - Anti-replay: reject the request if
|now − timestamp| > 300seconds. Optionally, keep recentnoncevalues and reject repeats. - The signature matches and the request is fresh — perform the opening.
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}
| Field | Value |
|---|---|
level | success, warning, danger or info: how the app marks the result |
message | the text the app shows next to the press |
position | for an Open / close object: open, closed or unknown |
close_in | seconds 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.
- From your phone: one redirect to the same host is followed, for example from http to https or to the address with a trailing slash.
- From Entrixy server: redirects are not followed, and addresses on private and internal networks are refused.
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.