Events
Everything that happens in an Akiles organization is logged as an event: someone opening a door, a member being created or edited, a gadget changing state… Events are the audit trail shown in the admin panel’s log, and the way your application can react to what happens in the organization.
Events are structured in “subject - verb - object” form:
- Subject: who did the action. It can be a member, an administrator, an OAuth application…
- Verb: what action was done on the object. Create, edit, delete, use…
- Object: what object the action was done on. A member, a gadget…
For example:
- Gadget action events (i.e. someone opening a door) are reported with the member in the subject,
useas verb, and the gadget action as object. - Member editions are reported with the admin user who did the action in the subject,
editas the verb, and the member as the object.
The full event object is documented in the API reference.
Timestamps
Events have two timestamps:
created_at: the time the event was recorded by the server. Events are ordered by it.occurred_at: the time the event actually happened. For most events it equalscreated_at.
They differ for events that reach the server late. The typical case is offline devices: gadget action events originate from the Akiles device when the user uses PINs, NFC cards or phone to open. If the device is offline (i.e. has no connection to Akiles Cloud), it buffers events in local persistent memory and reports them later when it becomes online again. Such events get created_at set to the moment they were reported, and occurred_at set to the moment they originally happened.
Receiving events
There are three ways to receive events, all delivering the same event objects:
- Polling: periodically fetch new events from the list endpoint. Simplest, no extra infrastructure, but latency is your polling interval.
- Webhooks: Akiles POSTs each event to your HTTPS endpoint. Near real time, with automatic retries; requires a publicly reachable server.
- WebSockets: subscribe over an outbound WebSocket connection. Lowest latency, works behind NAT and firewalls, with lossless resume after reconnecting.
Polling
The “list events” endpoint returns events in reverse chronological order, with cursor pagination. To poll for new events:
- Remember the ID of the newest event you’ve processed.
- Periodically fetch the newest events, following
cursor_nextuntil you reach the remembered event ID (or run out of events). - Process the new events (oldest first, if order matters to you), and update the remembered ID.
The endpoint supports filters (object type, verb, member, gadget, time ranges…) to narrow what you fetch; they’re documented in the reference.
Polling is a good fit for batch-style consumers (e.g. importing events into your own database every night for a billing batch job). If you need lower latency, use webhooks or WebSocket streaming instead.
Webhooks
Webhooks allow your server to receive real-time notifications about events happening in the Akiles organization.
Webhooks send a POST request to the indicated URL when a matching event occurs. The request body is a JSON-encoded event, same as you would get in the response of the “get event” endpoint.
URL can be HTTP or HTTPS. We strongly recommend using HTTPS for production.
You manage webhooks with the webhook endpoints. For debugging, you can view the webhook delivery logs for your application in the Developer Center.
Webhook objects are scoped to your OAuth application. Webhooks you create won’t be visible to other OAuth applications installed in the same org and vice versa. All your application’s webhooks in an organization will be deleted when your app gets de-authorized.
Even if you have read-only access, you can still create/edit/delete webhooks. This allows you to receive real-time updates in an application that only needs to read data from the Akiles organization.
Filtering
Each webhook has a “filter” which specifies which events should be sent to it. A filter is a list of “filter rules”.
Each rule is an object mapping filter names to the value to match. The supported filters are exactly the ones of the “list events” endpoint, with the same names, value formats and operator suffixes (e.g. created_at:gt). All values are strings, formatted exactly as you’d pass them in the list query parameter.
A rule matches an event if all its filters match. object.type must always be specified, i.e. it’s NOT possible to subscribe to all object types. To match all verbs of an object type, simply don’t filter on verb.
The webhook filter matches if any of the rules in the array match.
Some examples:
"filter": []: Matches no events. Events match if at least one filter rule matches, which can’t happen if there’s no rules at all."filter": [{"verb": "use", "object.type": "gadget_action"}]: Matches gadget action events."filter": [{"object.type": "member"}]: Matches all events related to members (creation, edition, deletion…)"filter": [{"verb": "edit", "object.type": "member"}]: Matches member edition events."filter": [{"verb": "edit", "object.type": "member"}, {"verb": "use", "object.type": "gadget_action"}]: Matches member editions, and gadget actions."filter": [{"verb": "use", "object.type": "gadget_action", "object.gadget_id": "gad_3merw5pijgst6g8mrhgc"}]: Matches actions of one specific gadget."filter": [{"verb": "use", "object.type": "gadget_action", "subject.member_id": "mem_3merw5pijgst6g8mrhgc"}]: Matches actions performed by one specific member.
An event will be sent to this webhook if it matches at least one rule. If it matches multiple rules, it will only be sent once. If it matches multiple webhook objects, it will be sent once per webhook to its respective URL, even if the URL is the same.
Filters are validated when creating or editing the webhook: unknown filter names or invalid values fail with 400 Bad Request.
Expansion
Each webhook has an “expand” field, a list of expands applied to the event when delivering it. The delivered event embeds the expanded objects, the same way the response of the “get event” endpoint does when called with the expand query parameter. This saves you the extra API requests to fetch the objects the event references.
The supported expands are the ones of the event object, including nested expands with dots. For example, "expand": ["object_member", "object_member.group_associations"] delivers events with the full member object embedded under object_member, including its group associations.
Expands are validated when creating or editing the webhook: requesting an expand the event doesn’t support fails with 400 Bad Request.
Signature
If an attacker learns the URL of your webhook, they could send requests themselves to it, spoofing webhooks. To prevent this, Akiles signs the webhook requests so that your server can check they come from Akiles and not an attacker. It’s strongly recommended to verify the signature.
The signature is the result of applying HMAC-SHA256 to the raw request body bytes, using the webhook secret as a key. It’s sent in the X-Akiles-Sig-SHA256 HTTP header.
The secret is returned when you create the webhook. It’s not possible to view the secret of an existing webhook, you have to delete and recreate it instead.
The following is an example of how to receive webhooks and validate the signature using nodejs:
import url from 'url'
import http from 'http'
import crypto from 'crypto'
// replace with the secret you get when creating the webhook.
const webhookSecret = 'f305d484fd10de285b00bb203659e863';
const app = http.createServer((request, response) => {
let body = [];
request.on('data', (chunk) => {
body.push(chunk);
}).on('end', () => {
body = Buffer.concat(body).toString();
const gotSig = Buffer.from(request.headers['x-akiles-sig-sha256'], 'hex');
const expectedSig = crypto.createHmac("sha256", webhookSecret).update(body).digest();
const ok = crypto.timingSafeEqual(gotSig, expectedSig);
if (!ok) {
response.writeHead(400);
response.write('bad sig');
response.end();
return;
}
const event = JSON.parse(body.toString());
console.log('Got event!');
console.log(event);
response.writeHead(200);
response.write('cool');
response.end();
});
});
app.listen(8088);
Delivery
Webhook delivery is considered successful if your server returns a 2xx status code.
Unsuccessful deliveries are retried, with an exponential backoff starting at a few seconds and doubling every time, for up to 1 hour.
WebSockets
You can subscribe to events over a WebSocket connection and receive them in real time, as an alternative to webhooks:
- Lower latency: events are pushed over the open connection, typically within milliseconds.
- No public HTTPS endpoint needed: the connection is outbound from your side, so it works behind NAT and firewalls, on a laptop, or in environments where you can’t receive webhook deliveries.
- Lossless reconnection: when you reconnect, you can resume from the last event you processed and receive everything you missed, with no duplicates and no gaps.
The endpoint is:
wss://api.akiles.app/v2/ws
Authentication
Authenticate the WebSocket handshake request using one of:
- The
Authorization: Bearer <token>header, with the same OAuth access token you use for the REST API. - The WebSocket subprotocol, for clients that can’t set headers (e.g. browsers): pass the token as the requested subprotocol, and the server accepts it.
const ws = new WebSocket("wss://api.akiles.app/v2/ws", [token]);
If authentication fails, the handshake is rejected with the usual HTTP error response.
The connection deliberately outlives the access token’s expiry, so you don’t have to reconnect every time the token expires. Instead, the server periodically checks the authorization itself: if your application is deauthorized from the organization, the connection is closed within a minute.
Subscribing
After connecting, create one or more subscriptions. Each subscription is an independent stream with its own filter and position; this lets you multiplex several consumers over one connection.
{
"type": "subscribe",
"id": "sub1",
"filter": [
{"object.type": "gadget_action", "verb": "use"},
{"object.type": "member"}
],
"expand": ["object_member"],
"after": "evt_3merh17ttj5cd11a3xx1"
}
id: a string of your choice identifying this subscription within the connection. Required.filter: optional. Same format and semantics as webhook filters: an array of rules, where each rule maps event list filter names to the value to match. An event is delivered if it matches any rule. If omitted, you receive all events you have permission to see.expand: optional. Same format and semantics as the webhookexpandfield: a list of event expands embedded into every delivered event.after: optional. An event ID to resume from, see Resuming below. If omitted, you receive only events that happen after subscribing.
Filters and expands are validated when subscribing, exactly like creating a webhook: unknown filters or expands, invalid values, or rules missing object.type fail the subscribe with an error message.
The server confirms with:
{"type": "subscribed", "id": "sub1"}
and then delivers events:
{
"type": "event",
"id": "sub1",
"event": { "id": "evt_...", "verb": "use", "object": { ... }, ... }
}
The event payload is identical to what the events list endpoint returns, and to what webhooks deliver.
To stop a subscription:
{"type": "unsubscribe", "id": "sub1"}
The server confirms with {"type": "unsubscribed", "id": "sub1"}. A few in-flight events may still arrive immediately after.
Resuming
Pass the ID of the last event you processed as after when subscribing, and you’ll receive every event after it, in order, with no duplicates and no gaps — even events that were happening concurrently while you were disconnected. To use this, persist the ID of the last event you processed, and resubscribe with it after reconnecting.
Any event ID you’ve seen works as a cursor: from this stream, from the events list endpoint, or from a webhook delivery.
Two things to be aware of:
- Delivery order is commit order, not occurrence order.
created_atis the time the event was recorded by the server, and the stream delivers increated_atorder — but events can be recorded long after they happened: devices that were offline upload their events when they come back online. The original occurrence time is inoccurred_at. This is what you want for resumability: you never miss late-arriving events, no matter when they arrive. - Don’t use
occurred_atto deduplicate or order the stream; use the stream order and event IDs.
If the after event is unknown, the subscribe fails with an invalid_request error. In that case, do a full re-sync using the events list endpoint and subscribe without after.
Permissions
The same permission rules as the REST API apply: you only receive events you’d see in the events list endpoint, and subscribing with a filter requires get permission on each filtered object type, like creating a webhook does.
Errors
Errors are reported as:
{"type": "error", "id": "sub1", "error": {"type": "invalid_request", "message": "..."}}
id identifies the subscribe/unsubscribe message the error refers to, and is absent for connection-level errors. The error object is the same format as REST API error responses.
Keepalive and liveness
The server sends WebSocket protocol pings every 20 seconds; standard clients (including browsers) answer them automatically, no action needed. Connections that stop answering are closed.
You can also send {"type": "ping"} and the server will answer {"type": "pong"}, if you want application-level liveness checks.
If your client falls too far behind consuming events (more than 30 seconds of socket backpressure), the server closes the connection. Reconnect and resume with after — nothing is lost.
Limits
- At most 64 subscriptions per connection.
Example
Using websocat:
$ websocat -H 'Authorization: Bearer acctok_...' wss://api.akiles.app/v2/ws
{"type": "subscribe", "id": "s1"}
{"type": "subscribed", "id":"s1"}
{"type": "event", "id": "s1", "event": {"id":"evt_...","verb":"use",...}}