Create a request
A request names an action your organization holds, says who’s asking, and carries the params the operators will see. You get an answer right away. The decision comes later.
The call
Section titled “The call”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",
"summary": "Deploy acme/payments-api to production",
"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",
summary: "Deploy acme/payments-api to production",
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",
summary="Deploy acme/payments-api to production",
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",
Summary: "Deploy acme/payments-api to production",
Params: map[string]string{"repo": "acme/payments-api", "env": "production", "sha": "8d93f71"},
CallbackURL: "https://ci.example.com/outis/decision",
})
Only action and requester are required. The API reference lists every field.
What comes back
Section titled “What comes back”A 202 with the same body GET /v1/requests/{id} answers: the request object wrapped with the server’s clock.
{
"server_now": <epoch ms>,
"request": {
"id": "req-4f2a9c1b8d7e6f50",
"action": "payments.deploy",
"requester": "keith",
"operation_hash": "sha256:e9d75458c3d5401ac8230f42b291ed231873183cb94ba125915eb6b034fd1dfc",
"state": "notified",
"live": true,
"outcome": null,
"approvers": [],
"params": { "repo": "acme/payments-api", "env": "production", "sha": "8d93f71" },
"created_at": <epoch ms>,
"decided_at": null
}
}
Keep id to poll with. Keep operation_hash for your executor: it’s the fingerprint of exactly this action and these params, and the executor checks it before it runs anything. Operation hash has the details.
Which actions
Section titled “Which actions”Any action your organization holds. Some come from an integration, like GitHub’s deploy.production. An admin can also define your own on the dashboard’s policy screen: payments.deploy, treasury.transfer, database.restore, whatever your service does. Your own action can’t reuse an id an integration provides, since that integration would get the verdict and act on it. An action nobody provides has no integration to tell. Once enough keys turn it goes from authorized to succeeded with nothing run, and your service does the work.
For your own actions, the panel draws the action’s label as the title, the first param as the subject, then the first six characters of the operation hash, labeled SHA, and the rest. Declared params come first, in their declared order, and any others follow in key order. Policies has more.
An action your organization doesn’t hold is a 422.
Choosing within policy
Section titled “Choosing within policy”Every request runs under its action’s policy, frozen when the request is created. The only thing a request can pick is quorum, and only when the action marks its quorum selectable, within its bounds. Leave it out and you get the action’s default, or the quorum its param rules answer for your params. On an action with param rules, that answer is a floor (the default when no rule matches): you can ask for more, and a value below it is a 422, same as a value outside the bounds. Mode, windows, eligibility and self approval come from the policy alone.
requester is the operator the request is made in the name of. Where the action denies self approval, that person is struck from their own quorum, so name the human who asked rather than a service account.
Params
Section titled “Params”params is a flat map of strings: at most 24 entries, keys 1 to 64 characters, values up to 512. A number or a nested object is a 400, so send "2500.00", not 2500. Keys starting outis. are reserved.
The operators see the params, the operation hash covers them, and the decision hands them back unchanged. Act on the params from the decision, not on your own copy.
Retry safely
Section titled “Retry safely”Send an Idempotency-Key header, 1 to 255 printable ASCII characters, and a retry can’t make a second request. The same key with the same action and params returns the original request, whatever state it’s in now, with Idempotent-Replayed: true. The same key with a different action or params is a 409 with kind idempotency_conflict. A key is per organization and lasts as long as the request does.
Pick a key that names the operation from your side, like the deploy’s commit or the transfer’s id in your ledger. A random value per attempt defeats the point.
Poll or be called
Section titled “Poll or be called”Leave callback_url out and poll GET /v1/requests/{id} until outcome is set. Name one and each event the request fires is posted there, signed, with the same request object under data.request. See Receive callbacks.
Strictness
Section titled “Strictness”The body is application/json, capped at 128 KiB, which leaves room for an intent of up to 64 KiB. An unknown field is a 400.
Refusals
Section titled “Refusals”Errors from this route carry error, a message for your logs, and kind, for your code to branch on. A 422 means the server understood the request and refused it under policy:
{"error": "propose req-4f2a9c1b8d7e6f50: payments.deploy denies self-approval, leaving 1 of 2 eligible operators for a quorum of 2",
"kind": "refused"}
Here the requester can’t count toward their own quorum, so two keys need three eligible people. A 422 comes back the same however often you send it, so fix the roster on the dashboard. A param the action marks required that’s missing or blank is a 422 too, with kind invalid.
A 500 is different: something broke on our side, and the body says nothing more. Retry it after a pause with the same Idempotency-Key, and the retry can’t create a second request. Errors lists every status.