Skip to content

Event History

Every answer, reply, completion, and submission is an event on your account's stream. It is what powers sp events, sp collect, and the SDK streams, and it can be read raw when you want the events themselves rather than a demuxed handle.

Reading the stream

sp events prints the raw feed; in the SDKs it is client.events() on both Client and OrgClient. Either one streams live, or replays history first: pass --since on the CLI (24h, 7d, or ISO 8601) and add --follow to keep streaming once the replay catches up.

bash
sp events --since 24h --follow

Your personal stream carries the events for everything you sent. Answers, replies, and completions route to the sender, plus submissions created on your own account. An org API key reads the organization's stream instead.

The event envelope

json
{
  "streamId": "…",
  "version": 42,
  "eventType": "TaskInputCompleted",
  "createdAt": "2026-07-19T12:00:00Z",
  "actor": {
    "publicId": "usr_…",
    "name": "Alice",
    "devicePublicId": "dev_…",
    "deviceName": "Alice's iPhone"
  },
  "data": {
    "type": "taskInputCompleted",
    "taskId": "tsk_…",
    "inputUploaded": {
      "type": "choiceSelected",
      "id": "inp_…",
      "selectedIndex": 0,
      "selectedValue": "Approve"
    }
  }
}
FieldDescription
streamIdThe stream the event belongs to.
versionSequential number within the stream, used for ordering and exact resumption.
eventTypePascalCase type, see below.
actorWho acted, with display name and device. Present on receive-side events (someone answered, replied, submitted); absent on collective events.
dataType-specific payload, discriminated by a camelCase type.
encryption{ "type": "personal", "passwordFingerprint": … } or { "type": "org", "v": … } when the payload is encrypted; absent for plaintext.
createdAtISO 8601 timestamp.

Optional fields are omitted from the JSON when empty, never null. All entity ids are prefixed (tsk_, sub_, ntf_, sbm_, inp_, usr_, dev_, grptsk_, grpntf_); treat them as opaque strings.

Event types

eventTypeEmitted when
TaskInputUploadedAn input answer arrived (auto-commit off: not yet committed).
TaskInputCompletedAn input was answered and committed.
TaskCompletedAll required inputs are done (or the task was completed manually). data.inputsUploaded carries every answer.
TaskDeletedByRecipient / TaskDeletedThe task was removed before completion.
SubtaskInputUploaded / SubtaskInputCompleted / SubtaskCompletedThe same lifecycle for an appended subtask, with subtaskId and parentTaskId.
ReplyAppendedA reply-composer reply: text, photo, file, audio, or location.
NotificationCompletedA notification input was answered (data.reply is the text, choice, or action reply).
SubmissionCreatedSomeone sent a submission from the app: any of text, photo, file, audio, location in one event.

The inputUploaded payload varies by input kind:

typeFields
textUploadedid, value
choiceSelectedid, selectedIndex, selectedValue
multiChoiceSelectedid, selectedIndices, selectedValues (parallel arrays; may be empty for an optional input)
actionSelectedid, selectedKey
sliderUploadedid, value
photoUploaded / fileUploadedid, contentType, checksumSha256, size (+ filename for files)
voiceRecordedid, contentType, checksumSha256, size, durationSeconds
locationUploadedid, latitude, longitude, accuracy, altitude, heading, speed, timestamp — or a single encrypted blob when the task is end-to-end encrypted

Waiting on a single send

Besides the account-wide stream, each send can be observed in isolation with its wait token, returned by every send. The token replays that one send's events and streams new ones until it completes, and grants nothing else on the account.

That is what powers Wait: true on a curl send and sp task --wait, and it lets a process that holds only the wait token, not your API token, safely observe one task and nothing else.