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.
Polling
Section titled “Polling”curl "https://api.outis.tech/v1/requests/req-4f2a9c1b8d7e6f50" \
-H "Authorization: Bearer $OUTIS_KEY"
const request = await outis.requests.retrieve("req-4f2a9c1b8d7e6f50");
request = outis.requests.retrieve("req-4f2a9c1b8d7e6f50")
req, err := client.Requests.Retrieve(ctx, "req-4f2a9c1b8d7e6f50")
{
"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.
Acting on it
Section titled “Acting on it”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.
Check the operation hash
Section titled “Check the operation hash”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);
What isn’t here
Section titled “What isn’t here”No arming code, no per operator detail, no policy. The whole record, participants and timeline included, is on the dashboard.
States and outcomes
Section titled “States and outcomes”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.
| STATE | MEANS |
|---|---|
| proposed | Created. Codes have not been minted and nobody has been told. |
| notified | Codes are out. Nobody has staged, or a coincident round collapsed and every code was re-issued. |
| staging | At least one operator has turned a key and typed their code. |
| authorized | Enough distinct operators committed. This is a fact about people, and it is not execution. |
| executing | Handed to the integration's executor. |
| succeeded | Terminal. The side effect happened. |
| failed | Terminal, and authorized. The ceremony worked; the thing it authorized did not. |
| expired | Terminal. The request TTL elapsed during staging. |
| aborted | Terminal. Somebody declined with the ABORT key, or a coincident round was restarted. |
| denied | Terminal. Refused upstream. |
| OUTCOME | MEANS |
|---|---|
| authorized | Enough distinct operators turned their keys inside the window. Act on it. |
| denied | An operator refused it. |
| expired | The request TTL ran out with the quorum unmet. |
| aborted | Withdrawn, by an operator's ABORT key or by the system. |
Nulls and times
Section titled “Nulls and times”Every time is UTC epoch milliseconds and nullable. A null time means not yet.
| FIELD | NULL MEANS |
|---|---|
| Execution.detail | The executor's own words. Nothing passes them through yet, so it is the empty string on every record, including failures. |
| RequestSummary.label | The action's human name. Absent unless the deployment wires an action label lookup. |
| Participant.notified_via | Nothing 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.deadline | Null 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_pct | Volunteered by the box at hello. Null is unknown, and unknown is a different answer from zero. |
| Device.rssi | Volunteered by the box at hello. Null is unknown, and unknown is a different answer from zero. |
| Device.firmware | Volunteered by the box at hello. Empty is unknown, and unknown is a different answer from a version. |
| Device.last_seen | Volunteered by the box at hello. Null is unknown, and unknown is a different answer from zero. |
| Decision.outcome | Nothing has been decided yet. Poll again, or wait for the event. |
| Decision.decided_at | No 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.intent | The request carries no intent, so there's nothing for a worker to run. |
| IntentExecution.claimed_at | Nobody has claimed it. |
| IntentExecution.lease_expires_at | Nobody has claimed it. |
| IntentExecution.reported_at | No report yet. |
| IntentExecution.reference | No report, or the report named nothing. |
| IntentExecution.error | No report, or the run didn't fail. |
| IntentExecution.execute_by | No intent, or no authorization yet. |
| Delivery.next_attempt_at | It isn't pending, so nothing is scheduled. |