Skip to content
OUTIS DOCS

Read the decision

A service reads the decision at GET /v1/requests/{id} with a key holding read. A callback or webhook event carries the same request object under data.request, so this page applies to both.

curl "https://api.outis.tech/v1/requests/req-4f2a9c1b8d7e6f50" \
  -H "Authorization: Bearer $OUTIS_KEY"
{
  "server_now": <epoch ms>,
  "request": {
    "id":             "req-4f2a9c1b8d7e6f50",
    "action":         "payments.deploy",
    "requester":      "keith",
    "operation_hash": "sha256:e9d75458c3d5401ac8230f42b291ed231873183cb94ba125915eb6b034fd1dfc",
    "state":          "succeeded",
    "live":           false,
    "outcome":        "authorized",
    "approvers":      ["maya", "sam"],
    "params":         { "repo": "acme/payments-api", "env": "production", "sha": "8d93f71" },
    "created_at":     <epoch ms>,
    "decided_at":     <epoch ms>
  }
}

Successful calls aren’t rate limited, so polling every few seconds is fine. A ceremony takes as long as people take to reach their boxes. To skip polling, name a callback.

outcome is null while the request is live. Once set, act on authorized only. denied, expired and aborted are distinct so you can tell an operator’s no from a window nobody reached.

Use the params from the answer. They’re what the operators saw on the glass. approvers names who turned a key, and is empty unless the request was authorized.

Before your executor runs anything, it hashes the action and params it’s about to use and compares that with operation_hash. A match means it’s running what the operators approved. A mismatch means something changed between the approval and the run, and it stops. Operation hash has the exact preimage, a helper in each language and the test vectors.

if (request.outcome !== "authorized") return;
if (operationHash(request.action, request.params) !== request.operation_hash) {
  throw new Error(`refusing ${request.id}: the operation changed after it was approved`);
}
deploy(request.params);

No arming code, no per operator detail, no policy. The whole record, participants and timeline included, is on the dashboard.

state tracks the request through the ceremony and after it. authorized says the people agreed. succeeded and failed say whether an integration’s call to the system that asked went through. For an action with no such integration, your organization’s own included, authorized goes straight to succeeded and nothing ran: that’s your cue to run it.

In the usual words: pending is proposed, notified or staging; rejected is denied; cancelled is aborted; and expired and authorized mean what they say. Requests has the full mapping.

STATEMEANS
proposedCreated. Codes have not been minted and nobody has been told.
notifiedCodes are out. Nobody has staged, or a coincident round collapsed and every code was re-issued.
stagingAt least one operator has turned a key and typed their code.
authorizedEnough distinct operators committed. This is a fact about people, and it is not execution.
executingHanded to the integration's executor.
succeededTerminal. The side effect happened.
failedTerminal, and authorized. The ceremony worked; the thing it authorized did not.
expiredTerminal. The request TTL elapsed during staging.
abortedTerminal. Somebody declined with the ABORT key, or a coincident round was restarted.
deniedTerminal. Refused upstream.
OUTCOMEMEANS
authorizedEnough distinct operators turned their keys inside the window. Act on it.
deniedAn operator refused it.
expiredThe request TTL ran out with the quorum unmet.
abortedWithdrawn, by an operator's ABORT key or by the system.

Every time is UTC epoch milliseconds and nullable. A null time means not yet.

FIELDNULL MEANS
Execution.detailThe executor's own words. Nothing passes them through yet, so it is the empty string on every record, including failures.
RequestSummary.labelThe action's human name. Absent unless the deployment wires an action label lookup.
Participant.notified_viaNothing records which channel a code actually went out on. The field is here so it can be filled in without a version bump, and it is null in every response today.
Window.deadlineNull in counted mode, and null in coincident mode until the round opens. A null deadline means draw no countdown at all, and the device obeys that too.
Device.battery_pctVolunteered by the box at hello. Null is unknown, and unknown is a different answer from zero.
Device.rssiVolunteered by the box at hello. Null is unknown, and unknown is a different answer from zero.
Device.firmwareVolunteered by the box at hello. Empty is unknown, and unknown is a different answer from a version.
Device.last_seenVolunteered by the box at hello. Null is unknown, and unknown is a different answer from zero.
Decision.outcomeNothing has been decided yet. Poll again, or wait for the event.
Decision.decided_atNo verdict yet. It is set the instant the last key turns, the TTL runs out, or an operator aborts, and delivery afterwards does not move it.
Decision.intentThe request carries no intent, so there's nothing for a worker to run.
IntentExecution.claimed_atNobody has claimed it.
IntentExecution.lease_expires_atNobody has claimed it.
IntentExecution.reported_atNo report yet.
IntentExecution.referenceNo report, or the report named nothing.
IntentExecution.errorNo report, or the run didn't fail.
IntentExecution.execute_byNo intent, or no authorization yet.
Delivery.next_attempt_atIt isn't pending, so nothing is scheduled.