Skip to content
sistemo.io beta

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:

GET /v1/activity                       # the whole account
GET /v1/machines/{id}/activity         # one machine

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

{
  "activity": [ … ],
  "total": 1284,
  "next_cursor": "MTc1ODUzMTI0MzAwMDAwMDAwMDo0NDE"
}

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/usage for 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.