Requests
A request is one ask: an action, who’s asking, and the params the operators should see. Your service creates it and reads it back.
The request object
Section titled “The request object”{
"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>,
"intent": null,
"execution": { "state": "none", ...the rest null... }
}
| FIELD | WHAT IT IS |
|---|---|
id | The request id. |
action | The action id the request was created for. |
requester | The operator the request was made in the name of. |
operation_hash | A fingerprint of what the operators authorized: the action and params, fixed when the request was created. Recompute it from the operation you're about to run and refuse on a mismatch. The operation hash concept page has the exact preimage. |
state | Where the request is. The values are below. |
live | Whether the request can still transition. An authorized request stays live while its integration makes its one call, then ends succeeded or failed. |
outcome | Null until there's a verdict; one of the outcomes once there is. |
approvers | The operators who committed. Empty unless authorized. |
params | The request's attributes as proposed. |
created_at | When the request was created. |
decided_at | When the verdict was reached. Null while live. |
intent | The sealed intent the request was created with, verbatim. |
execution | Where the intent stands. `state` is `none` on a request without one. |
States
Section titled “States”| 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. |
Outis keeps its own state names. If your system thinks in the usual words, here’s the mapping:
| Your word | Outis states |
|---|---|
| pending | proposed, notified, staging |
| approved | authorized, then succeeded |
| rejected | denied |
| cancelled | aborted |
| expired | expired |
executing, succeeded and failed describe an integration’s call to the system that asked, like GitHub merging a pull request. A request for an action with no such integration, including every action your organization defined itself, goes from authorized to succeeded with nothing run. failed only happens when that integration’s call was refused.
Outcomes
Section titled “Outcomes”| 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. |
Create a request
Section titled “Create a request”POST /v1/requests
Requires a key holding propose. Answers 202 with the request object, wrapped the same way GET /v1/requests/{id} wraps it, once the eligible operators have been asked.
| FIELD | WHAT IT IS | |
|---|---|---|
action | required | An action your organization holds. Its policy decides who's asked, how many must agree and in which mode. |
requester | required | The operator the request is made in the name of. Where the action denies self approval, this person is struck from their own quorum. |
params | optional | A flat string map, at most 24 entries, keys 1 to 64 characters and values at most 512. A key can't start with `outis.`. It's what the operators see, it's covered by `operation_hash`, and it comes back on the decision unchanged. |
summary | optional | One line a person should read: a commit subject, a pull request title. Whoever renders it escapes and caps it. |
callback_url | optional | Where to post the decision instead of polling for it. An absolute https URL, or http to a loopback address on a bench. Signed under the API key this request arrived with. Absent means you poll. |
quorum | optional | How many distinct operators must turn a key. Accepted only when the action marks its quorum selectable, and only within its bounds; anything else is a 422. Absent takes the action's default, or the quorum its param rules answer for these params. On an action with param rules that answer is a floor (the default when no rule matches), and a quorum below it is a 422 too. |
intent | optional | A sealed call for your worker to run once the request is authorized, at most 64 KiB. Needs `params.intent`, the plaintext's digest. Outis stores it verbatim and can't open it. |
execute_within | optional | Seconds after the verdict a worker may still claim the intent. Only with an intent. Absent is seven days; the most is thirty. |
intent and execute_within are for a request whose call your own worker runs once it’s authorized. Execution covers the envelope, the digest in params.intent, and how a worker claims and reports it.
Headers
Section titled “Headers”| Header | What it is | |
|---|---|---|
Idempotency-Key |
optional | 1 to 255 printable ASCII characters naming this operation from your side. The same key with the same action and params returns the original request with Idempotent-Replayed: true. The same key with anything else is a 409, kind idempotency_conflict. |
Example
Section titled “Example”curl -X POST "https://api.outis.tech/v1/requests" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OUTIS_KEY" \
-H "Idempotency-Key: deploy-acme-payments-api-8d93f71" \
-d '{
"action": "payments.deploy",
"requester": "keith",
"params": { "repo": "acme/payments-api", "env": "production", "sha": "8d93f71" },
"callback_url": "https://ci.example.com/outis/decision"
}'
const request = await outis.requests.create({
action: "payments.deploy",
requester: "keith",
params: { repo: "acme/payments-api", env: "production", sha: "8d93f71" },
callbackUrl: "https://ci.example.com/outis/decision",
});
request = outis.requests.create(
action="payments.deploy",
requester="keith",
params={"repo": "acme/payments-api", "env": "production", "sha": "8d93f71"},
callback_url="https://ci.example.com/outis/decision",
)
req, err := client.Requests.Create(ctx, outis.CreateParams{
Action: "payments.deploy",
Requester: "keith",
Params: map[string]string{"repo": "acme/payments-api", "env": "production", "sha": "8d93f71"},
CallbackURL: "https://ci.example.com/outis/decision",
})
Returns
Section titled “Returns”{
"server_now": <epoch ms>,
"request": { ...the request object, state notified, outcome null... }
}
On a replayed Idempotency-Key it’s the original request, in whatever state it’s reached, and the response carries Idempotent-Replayed: true.
Retrieve a request
Section titled “Retrieve a request”GET /v1/requests/{id}
Requires a key holding read. Answers the request object wrapped with the server’s clock. The same request object is posted to the request’s callback URL, if it named one, inside each event.
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")
Returns
Section titled “Returns”{
"server_now": <epoch ms>,
"request": { ...the request object... }
}
A request that belongs to another organization is a 404, the same as one that doesn’t exist.