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:

For example:

The full event object is documented in the API reference.

Timestamps

Events have two timestamps:

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

The “list events” endpoint returns events in reverse chronological order, with cursor pagination. To poll for new events:

  1. Remember the ID of the newest event you’ve processed.
  2. Periodically fetch the newest events, following cursor_next until you reach the remembered event ID (or run out of events).
  3. 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:

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:

The endpoint is:

wss://api.akiles.app/v2/ws

Authentication

Authenticate the WebSocket handshake request using one of:

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

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:

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

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",...}}