Activity¶
Every action on your account is recorded in an Activity feed: who did it, what they did it to, and when. It is read-only, it is on every tier, and it is the one thing here that competing sandbox products either do not have or sell as an enterprise add-on.
Two endpoints read the same feed:
Scope: read or full. Both accept the same filters and return the same shape.
Metadata only — never your code and never its output
An entry records that you ran something, not what. The script text, stdout and stderr are never written to the feed, by construction: each action has a fixed builder that emits an exact allow-list of keys, and a unit test fails the build if any builder ever emits another one. exec stores the exit code and the timeout you asked for. That is the whole record.
The activity entry¶
{
"machine_id": "123e4567-e89b-12d3-a456-426614174000",
"action": "exec",
"actor": "user",
"metadata": { "exit_code": 0, "timeout_sec": 30 },
"created_at": "2026-09-22T10:14:03Z"
}
| Field | Type | Meaning |
|---|---|---|
machine_id | string | The machine acted on. Absent for actions with no machine (creating a standalone volume). |
action | string | What happened — see the table below. |
actor | string | Who did it: user, or a named system worker. |
metadata | object | Action-specific facts. The keys differ per action. |
created_at | string | RFC 3339 UTC. |
machine_id is a plain string with no foreign key, on purpose: the feed outlives the machine. Destroy a machine and you can still read everything it ever did, including the machine.destroyed entry itself.
Actions and their metadata¶
action | Category | metadata keys |
|---|---|---|
machine.created | machine | stack, vcpus, memory_mb, rootfs_gb |
machine.started | machine | — |
machine.stopped | machine | — |
machine.destroyed | machine | preserve_storage, snapshots_deleted |
exec | exec | exit_code, timeout_sec |
volume.created | volume | volume_id, size_gb |
volume.attached | volume | volume_id |
volume.detached | volume | volume_id |
volume.deleted | volume | volume_id |
snapshot.created | snapshot | snapshot_id |
snapshot.restored | snapshot | snapshot_id |
snapshot.deleted | snapshot | snapshot_id |
machine.destroyed carries snapshots_deleted because destroying a machine is the one action that quietly takes other resources with it. A count, not a list of ids — enough to see that backups went, without turning the feed into an inventory.
Volume and snapshot metadata carry ids only, never the name you chose. A name is free text you typed; the feed is not the place to store it.
Actors¶
actor | Means |
|---|---|
user | You, through the API or the dashboard. |
system:spend_guard | Your balance hit €0 and the spend guard stopped the machine. |
system:reclaimer | The abandoned-account reclaimer destroyed the resource after the grace window. |
This is why the feed is worth reading rather than reconstructing from your own logs: a machine you did not stop shows who stopped it, and your client never saw that call. See Usage & billing for what triggers each worker.
Filters¶
All parameters are optional and combine.
| Parameter | Values | Notes |
|---|---|---|
category | machine, volume, snapshot, exec, all | Anything else is a 400. all and omitting it are the same. |
q | free text | Case-insensitive match on action, actor and resource id. |
from | RFC 3339 | Inclusive lower bound on created_at. |
to | RFC 3339 | Must not be before from, or 400. |
page_size | 1–200 | Default 50; anything larger is clamped to 200. |
cursor | opaque | From a previous response's next_cursor. |
curl -sS -G https://api.sistemo.io/v1/activity \
-H "Authorization: Bearer $SISTEMO_API_KEY" \
--data-urlencode 'category=machine' \
--data-urlencode 'from=2026-09-01T00:00:00Z'
Pagination is keyset, not offset¶
Pass next_cursor back as cursor for the next page. When next_cursor is "" you have reached the end.
Do not build your own offset
The feed is append-only and newest-first, so anything written while you are paging shifts every offset down and you silently re-read rows you already had. The cursor is a position in the data (created_at + id), so new entries at the top cannot disturb it. A malformed cursor is a 400; it encodes only a position inside your own account, so tampering with it shifts your window and nothing else.
import os, urllib.parse, urllib.request, json
def activity(**filters):
"""Yield every activity entry, oldest page last."""
cursor = ""
while True:
params = {k: v for k, v in filters.items() if v}
if cursor:
params["cursor"] = cursor
url = "https://api.sistemo.io/v1/activity?" + urllib.parse.urlencode(params)
req = urllib.request.Request(url, headers={
"Authorization": f"Bearer {os.environ['SISTEMO_API_KEY']}",
"User-Agent": "my-app/1.0", # required — see Authentication
})
page = json.load(urllib.request.urlopen(req))
yield from page["activity"]
cursor = page.get("next_cursor") or ""
if not cursor:
return
for entry in activity(category="exec", page_size=200):
print(entry["created_at"], entry["action"], entry["metadata"])
Retention¶
Entries are kept for 365 days by default and then dropped a whole month at a time. If you need them for longer than that, page them out on a schedule and keep them yourself — there is no export endpoint.
What it is not¶
- Not a metrics feed. No CPU, memory or network numbers. Use
GET /v1/billing/usagefor consumption. - Not a log stream. It is written after an action succeeds, so a failed call produces no entry and the feed never claims something that did not happen.
- Not tamper-evident. It is an honest record, not a cryptographic one. There are no signatures and no hash chain, and we do not market it as compliance evidence.
Related¶
- Activity log guide — what to use it for, and the dashboard view
- Machines · Volumes · Snapshots