When a provider calls you back
When a provider pushes state into ALYT, it calls one generic callback route: POST /integrations/:integrationId/webhook. The integration id identifies which installed connector should interpret the delivery, while signature verification remains connector-specific. This keeps the callback surface fixed as new pushing connectors are added, and there is nothing to configure in NPM for this or for any connector that comes after it. The route accepts machine-to-machine deliveries without a session, verifies them in the connector, and stores accepted state through the same path polling uses.
The callback route
Section titled “The callback route”Use this URL shape for provider callbacks:
POST /integrations/:integrationId/webhookThe integration id in the path is enough for ALYT to route the delivery to the correct connector kind. There is no separate public callback endpoint per provider.
For developers installing a provider webhook, the useful rule is that the callback URL is this integration’s own ingress:
https://app.alyt.com/api/integrations/<id>/webhookThat URL is stable across connectors that use this pattern.
Authentication and error handling
Section titled “Authentication and error handling”This route does not use a user session. A provider callback is a machine delivery, so the route itself carries no token.
Instead, the provider’s signature is the credential. The connector verifies it, because only the connector knows how that provider signs deliveries. If verification fails, nothing is written.
The integration id is an address, not a secret. Knowing an id is not enough to forge a valid delivery.
These response rules matter:
| Status | When |
|---|---|
401 | The signature does not verify. Nothing is written |
404 | No such integration, or its kind takes no callbacks |
404 is intentionally shared by both cases, so callback probing does not reveal which integration ids are real.
Raw body requirements
Section titled “Raw body requirements”Signature verification is done over the exact bytes the provider sent. Any parse-and-re-encode step can break verification and turn a valid callback into what looks like an invalid signature.
For that reason, ALYT passes the body through unparsed and hands the connector the raw request body as a string. The connector verifies the signature first and parses the body only after verification succeeds.
An unverified body is treated as untrusted input and goes no further than the signature check.
What an accepted delivery does
Section titled “What an accepted delivery does”An accepted callback writes reported state through the same storage path a poll uses. That keeps pushed observations and polled observations aligned.
Two rules follow from that:
- The rest of the platform does not need to distinguish push from poll.
- A delivery must never be able to write something a read could not.
A callback is an observation, not a command result initiated by ALYT, so it does not write commanded state.
The callback outcomes are:
| Answer | When |
|---|---|
200 stored | Verified, and the state was written |
200 already seen | A retry that lost a race to a newer delivery |
200 unknown thing | About a thing nobody has discovered yet |
200 ignored | Verified, but nothing this connector acts on |
401 | The signature did not verify. Nothing is written |
404 | No such integration, or its kind takes no callbacks |
Duplicates return 200, not an error. That matters because providers retry on failure, and a duplicate is not one.
Ordering and de-duplication
Section titled “Ordering and de-duplication”When a provider includes a monotonic sequence, ALYT uses it to stop an older retry from overwriting newer state.
This de-duplication guard is temporary and expires after ten minutes. That limit is deliberate. Retries arrive within about 31 seconds, so after ten minutes the guard no longer protects anything useful, while keeping it longer risks blocking real future deliveries after a bad sequence value.
A restart clears the guard. The consequence is limited to an idempotent re-write.
Polling remains in place
Section titled “Polling remains in place”Push reduces latency, but it does not replace polling.
Local functions can keep working during a cloud outage; cloud-dependent connectors require the provider and network. A cloud webhook that stops arriving only adds delay. It must not be the only path that keeps the home correct.
For AROMI, the current behavior is:
- pushed READY is acted on immediately
- once a push has been seen in that warm-up episode, the warm-up read slows to a 60 s reconciliation read
- the fallback read is still never removed
- a lost push costs under a minute
The older poll behavior also remains relevant as the baseline fallback:
- 15 s warm-up read
- 5 min keep-honest read
Connector contract
Section titled “Connector contract”A connector that supports callbacks implements webhook().
That handler:
- receives the raw delivery body and headers
- verifies the provider signature
- parses the body only after verification succeeds
- returns
{ accepted, externalId, state, sequence? }in the same state shape the connector’s state route already returns
A connector without webhook() answers 404, and the platform answers 404 for that integration’s callback URL.
AROMI specifics
Section titled “AROMI specifics”AROMI registers its provider callback at the integration’s own webhook ingress and uses the provider event machine.status.
Verification rules for AROMI are:
- HMAC-SHA256 over
timestamp + "." + rawBody - replay protection with a five-minute timestamp skew window
- invalid signatures and stale replays answer
401 - nothing is written when verification fails
AROMI only pushes on state edges involving powerState, machineState, or isErogating. Its warm status tick, about 1.3 s, does not generate callbacks.
Observed end-to-end behavior shows a real power-on delivery arriving in about 200 ms, with the state stored and the routine engine then able to arm its READY watch from that push.
AROMI registration lifecycle
Section titled “AROMI registration lifecycle”For AROMI, webhook registration is managed for every integration, not by manual per-install setup.
ALYT ensures registration:
- at connect
- after new credentials are stored
- after a user-requested discovery
- on a periodic check
ALYT releases registration:
- at disconnect
- when the tenant is deleted
The connector talks to aromi.ai with the integration’s own API key, and the webhook secret is stored with that integration’s config beside the API key. It travels only to the connector.
The registration URL must exactly match this integration’s own ingress. Registrations at another host or for another integration are not touched.
For AROMI, provider limits and behaviors that matter are:
| Rule | Value |
|---|---|
| Active registrations per provider tenant | 10 |
| Provider disables a failing webhook after | 20 consecutive failed delivery attempts |
| Provider retries each delivery up to | 4 times |
| Periodic registration check when healthy | every 1 hour |
| Retry after failure or unstored result | 10 minutes |
When credentials change, ALYT first ensures the registration under the new key. Only after that registration is stored does it release registrations under the old key. This prevents an old registration from being deleted before a new one is ready.
If the provider disables a webhook, ALYT detects that on its periodic check and registers again.
Access boundaries
Section titled “Access boundaries”Callback delivery itself is machine-authenticated, not household-session-authenticated. Where access boundaries matter elsewhere on the platform, household permissions are narrower than staff, DEV credentials act in the developer sandbox, and LIVE credentials only for their own installed connector.