Working with Events
Configure event-driven automations — triggers, conditions, and actions like webhooks and email.
Working with events
Events are the automation engine of the Surfact platform. An event watches your variables for a condition (a trigger) and, when it's met, fires one or more actions — such as sending a webhook or an email. Events are managed through the v2.0 API (base URL https://api.surfact.com/api/v2.0/).
| Goal | Endpoint | Method |
|---|---|---|
| List all events | /events/ | GET |
| Create an event | /events/ | POST |
| Retrieve / update / delete an event | /events/{event_id} | GET / PATCH / DELETE |
| Read execution logs for an event | /events/{event_id}/logs | GET |
Full schemas are in the API Reference; this guide explains how the pieces fit together.
Anatomy of an event
An event is made of triggers and actions.
Triggers
Triggers are organized as an array of arrays. Each inner array is a trigger group, and the event fires when any group is satisfied — the outer array combines with OR.
So a two-group event like this fires on either condition:
[
[ { "type": "last_value", "condition": { "value": "-18", "operator": "<" }, "entity": { "type": "variable", "value": [{ "id": "<temp_id>" }], "operator": "or" } } ],
[ { "type": "last_value", "condition": { "value": "-15", "operator": ">" }, "entity": { "type": "variable", "value": [{ "id": "<temp_id>" }], "operator": "or" } } ]
]That is the shape to use for an out-of-band alert: one group for "too cold", one for "too warm", on the same variable.
Each trigger has a type, a condition and an entity (the variable or device group being watched):
{
"type": "last_value",
"condition": { "value": "-15", "operator": ">", "delay": 0 },
"entity": {
"type": "variable",
"value": [{ "id": "<temperature_variable_id>" }],
"operator": "or"
}
}Valid trigger type values are last_value, position, inactive, schedule and context.
The condition shape depends on the trigger type
There is no single condition schema — each trigger type stores a different set of keys:
Trigger type | Stored condition | Notes |
|---|---|---|
last_value | { "type": "value", "value": -15.0, "operator": "<", "delay": 0.0 } | value is a number, delay a float |
position | { "type": "value", "value": "GEOMETRYCOLLECTION(POLYGON((…)))", "operator": "out", "delay": 0.0 } | value is a WKT geometry string; the operator is in or out |
inactive | { "unit": "H", "value": 3.0 } | How long silence is tolerated. No operator, no delay, no type key at all |
Threshold condition values come back as numbersOn a
last_valuetrigger, send"value": "-15"as a string and the stored trigger reads"value": -15.0, with a"type": "value"key added by the platform and"delay"normalised to a float (0.0). That is normal — don't treat it as a failed write. Apositioncondition keeps its WKT string as-is, and aninactivecondition gets notypekey.
Watching a whole device group
entity.type is either variable or device_group. Set it to device_group to watch every device in a group rather than one variable. In the group form each value item names the group and the variable label to watch:
{
"type": "inactive",
"condition": { "unit": "H", "value": 3 },
"entity": {
"type": "device_group",
"value": [
{
"id": "<device_group_id>",
"variable_label": "temperature",
"deviceGroup_name": "<device_group_name>",
"isMultipleGlobalEvent": false
}
],
"operator": "or"
}
}variable_label is what makes a group trigger interpretable — without it there is nothing to say which of the group's variables is being watched. Events built this way come back with "isGlobalEvent": true. In the variable form, each value item carries only id.
Actions
Actions run when the triggers are satisfied. The type determines what happens:
{
"name": "Notify the cold-chain desk",
"type": "web-hook",
"data": {
"url": "https://ops.example.com/surfact-hook",
"method": "POST",
"headers": {},
"payload": "{}"
},
"back_to_normal": false
}Valid action type values are email, sms, telegram, whatsapp, voice, web-hook, slack-webhook, set-variable, user, contact, incident, ubifunction, particle-webhook and ttn-webhook.
It isweb-hook, notwebhookThe hyphen is required.
"type": "webhook"is rejected with400 Validation Error—"webhook" is not a valid choice.
datamay not be emptyEvery action needs a populated
dataobject;{}is rejected. For aweb-hookaction,data.payloadmust be a string, not an object — send"{}", not{}.
| Field | Description |
|---|---|
type | Required. One of the action types listed above |
data | Required. The action's configuration. Must not be empty |
name | Display name for the action |
back_to_normal | Whether to also fire when the variable returns to normal |
Repetition is controlled at the event level rather than per action, with cooldownPeriod and retriggerWhileActive.
repeatAction,maxRepetitionsandrepeatIntervalare not storedThese are accepted in a create or update request without complaint, but they are silently discarded — a created action returns only
type,name,data,back_to_normalandidGroupAction. Don't rely on them to cap how often an alert fires; use the event'scooldownPeriodinstead.
Creating an event
Alert the cold-chain desk when a frozen reefer climbs above −15 °C:
curl -X POST 'https://api.surfact.com/api/v2.0/events/' \
-H 'Content-Type: application/json' \
-H 'X-Auth-Token: your_token_here' \
-d '{
"label": "frozen-breach-nordic",
"name": "Frozen Breach — Nordic Fleet",
"description": "Fires when a frozen reefer rises above -15 C",
"isActive": true,
"triggers": [
[
{
"type": "last_value",
"condition": { "value": "-15", "operator": ">", "delay": 0 },
"entity": { "type": "variable", "value": [{ "id": "<temperature_variable_id>" }], "operator": "or" }
}
]
],
"actions": [
{
"name": "Notify the cold-chain desk",
"type": "web-hook",
"data": {
"url": "https://ops.example.com/surfact-hook",
"method": "POST",
"headers": {},
"payload": "{}"
},
"back_to_normal": false
}
],
"activeDates": {
"dates": [
[["00:00", "23:59"]], [["00:00", "23:59"]], [["00:00", "23:59"]],
[["00:00", "23:59"]], [["00:00", "23:59"]], [["00:00", "23:59"]],
[["00:00", "23:59"]]
],
"timezone": "Europe/Oslo"
},
"isGlobalEvent": false
}'A successful call returns 201 with the stored event, including its id.
name, triggers, actions and activeDates are all required — omitting activeDates returns 400 with {"activeDates": ["This field is required."]}. label, description, isActive, isGlobalEvent, cooldownPeriod, retriggerWhileActive, tags and organization are optional.
activeDates.datesis a seven-element weekday array
datesholds one entry per weekday, and each entry is a list of["HH:MM", "HH:MM"]windows — an array of arrays of two-element string arrays, not an array of strings. The value above is the always-on form (00:00–23:59, seven days) that every event on this account stores. To keep an alert quiet outside the shift, narrow the window on the days that matter, e.g.[["06:00", "18:00"]].timezoneis an IANA name such asEurope/Oslo.
Events never return anorganizationYou can send
organizationwhen creating an event — it is on the write serializer — but you can never read it back. It is absent from every event returned byGET /events/andGET /events/{event_id}, and requesting it with?fields=organizationreturns an empty object for every row. So don't rely on the API to tell you which organization an event belongs to.The
?organization=filter on/events/does work, but only with the internal numeric organization id (for example?organization=30) — an organization object id or a~labelis rejected with400 Validation Error,"Select a valid choice."
Inspecting event logs
Every time an event evaluates or fires, it records a log entry. Retrieve them with:
curl -X GET 'https://api.surfact.com/api/v2.0/events/<event_id>/logs' \
-H 'X-Auth-Token: your_token_here'Each entry carries a logType (event_create, event_update, and the firing types), a human-readable message, a context object holding a snapshot of the event, and a createdAt timestamp:
{
"next": null,
"previous": null,
"results": [
{
"id": 818171,
"event": "6a4f67917585e9f46d6abf26",
"logType": "event_create",
"message": "Event \"Frozen Breach — Nordic Fleet\" created",
"context": { "id": "6a4f67917585e9f46d6abf26", "name": "Frozen Breach — Nordic Fleet" },
"createdAt": "2026-09-03T12:28:56.393353Z"
}
]
}
createdAthere is an ISO 8601 stringEvent-log timestamps are not the POSIX milliseconds used for data points. Elsewhere in the API
createdAtis also ISO 8601, whilelastActivityand dottimestampvalues are POSIX milliseconds. Check the field before parsing.
Use the logs to confirm an automation is firing as expected and to debug conditions that aren't behaving.
Event logs are paginated without acountUnlike every other paginated endpoint, this envelope has only
next,previousandresults— there is nocount. Page through withnextrather than computing pages from a total.
Connecting to webhooks
A web-hook action is the bridge between events and your own systems. When the event fires, Surfact POSTs the data point to the url in the action's data. See Webhooks for the payload format and setup.
Updated 7 days ago