Core API

Quickstart: approvals and control for any agent, workflow or internal system

Put a person in front of a risky step in any agent, company workflow or internal system, record every step, and get signed evidence. One API call and a callback handler, no MCP server required.

Updated Sep 28, 2026

The API is for everything that runs its own steps: internal systems, company workflows and agents you build, including the ones with no MCP server or clean API of their own. Your code keeps doing the work; Contro1 decides who approves it, records it and proves it.

Key takeaways

  • Use the API for internal systems, company workflows and your own agents, especially those with no MCP server or clean API.
  • One POST to /v1/requests before the risky step, and a signed callback when a person decides.
  • Record the steps that need no approval as audit records, so the whole workflow is measured, not only the gated step.
  • Fail closed: anything other than an approval means the step does not run.

What the API is for

Most of what a company wants to govern is not an AI app with an MCP server. It is its own software: an internal refund tool, a back-office script, an n8n or Airflow workflow, a deploy pipeline, an agent a team built. Many of these have no MCP server and no clean API of their own, and they are exactly the places where one wrong step is expensive.

The Contro1 API is how those get governed without being rebuilt. Your code keeps doing the work. Before a risky step it asks Contro1, a person decides, and your code continues only on an approval.

Control

A person approves, rejects or answers before the step runs, routed to the right role, department or on-call person, with escalation when nobody answers in time.

Measurement

Every step is recorded, gated or not, per agent and per workflow, so you can see what each one actually does and how often it needs a person.

Evidence

Who asked, who decided, what they saw and what happened next, as a signed record you can hand to an auditor.

What teams use it for

Use caseExampleWhat you call
Internal systems and back-office toolsA refund tool, an admin panel, a script that edits the customer databasePOST /v1/requests before the step
Company workflows and automationsn8n, Zapier, Airflow, a cron job, an RPA botA request at the risky node, an audit record for every other node
Deploys and infrastructureA production deploy in CI, a database migration, rotating a keyA request as a pipeline gate that fails closed
Money movementRefunds, payouts, a change to a vendor's bank details over a thresholdA request with risk_level and an approval policy of two approvers
Data access and exportsExporting customer records, opening a restricted datasetA request routed to the data owner's department
Content before it goes outAn AI-written email, post, reply or contract clauserequest_type review, with the draft in the context
Asking a person, not approvingA missing detail, a choice between optionsrequest_type input or decision
Steps that need no approvalLow-risk autonomous actions you still want countedPOST /v1/audit-records, measured on Activity
Compliance evidenceHuman oversight records for the EU AI Act or an internal auditGET /v1/requests/:id/evidence

API, Action Gateway or MCP

Three ways in, by who performs the step.

IfUseBecause
Your own code performs the stepThe Requests API (this page)Contro1 decides and records; your system keeps its own credentials and does the work.
The application is in Contro1's catalogThe Action GatewayContro1 holds the account and makes the call, so there is nothing to integrate and the record is what Contro1 observed.
A person works through an AI appThe Contro1 MCPClaude, ChatGPT or Cursor act with that person's access.
You only want it recordedAudit recordsNo person in the loop, still measured and searchable.

Action Gateway · Connect an agent

What you need

  • A credential for the agent or workflow: an Agent Credential from Settings, API keys. Each workflow is its own agent, so its steps are attributed to it.
  • A callback URL your server controls. Contro1 POSTs the signed decision there.
  • A clear question, the context a reviewer needs, and the role that should answer.

How the approval loop works

Your code creates a request when it reaches a risky or policy-sensitive step. Contro1 routes it to the right person by required_role, department, shift coverage and priority, and escalates if nobody answers within the SLA.

If your code waits for the answer, set its own timeout longer than the Contro1 SLA. A 10 minute SLA needs a wait above 10 minutes, so your code does not give up before Contro1 can return timed_out or escalate.

The request moves through these states:

  • pending - created, not yet claimed by an operator
  • in_review - an operator opened and is reading the request
  • approved - approved; the signed callback is on its way to your server
  • rejected - rejected; your code should stop or take another route
  • timed_out - nobody answered within the SLA; treat it as rejected and fail closed
  • cancelled - you cancelled the request before it was answered

Create your first request

create_request.sh
curl -X POST https://api.contro1.com/api/centcom/v1/requests \
  -H "Authorization: Bearer $CONTRO1_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "approval",
    "question": "Approve refund over policy limit?",
    "context": "Customer requests a $2,400 refund after a shipping failure.",
    "callback_url": "https://your-app.com/webhooks/contro1",
    "required_role": "manager",
    "priority": "urgent",
    "external_request_id": "refund-8842:approval",
    "correlation_id": "case-refund-8842",
    "metadata": { "customer_id": "c-8821" }
  }'
create_request.py
import os
from centcom import CentcomClient

client = CentcomClient(api_key=os.environ["CONTRO1_API_KEY"])

request = client.create_request(
    type="approval",
    question="Approve refund over policy limit?",
    context="Customer requests a $2,400 refund after a shipping failure.",
    callback_url="https://your-app.com/webhooks/contro1",
    required_role="manager",
    priority="urgent",
    external_request_id="refund-8842:approval",
    correlation_id="case-refund-8842",
    metadata={"customer_id": "c-8821"},
)

print(request["id"], request["status"])  # req_abc123  pending
create_request.ts
import { CentcomClient } from '@contro1/sdk';

const client = new CentcomClient({ apiKey: process.env.CONTRO1_API_KEY! });

const request = await client.createRequest({
  type: 'approval',
  question: 'Approve refund over policy limit?',
  context: 'Customer requests a $2,400 refund after a shipping failure.',
  callback_url: 'https://your-app.com/webhooks/contro1',
  required_role: 'manager',
  priority: 'urgent',
  external_request_id: 'refund-8842:approval',
  correlation_id: 'case-refund-8842',
  metadata: { customer_id: 'c-8821' },
});

console.log(request.id, request.status); // req_abc123  pending

Where that context line came from

The context in the example ("Customer requests a $2,400 refund after a shipping failure") is not something the reviewer should hope your code volunteers. Build it from three sources: the exact input your code intercepted (machine-observed fact), the event that triggered the run, and, for an agent, its own justification, which you get reliably by making reason a required parameter of the risky tool.

Keep the two apart: facts your code observed, and text a model wrote. Model-written justification helps the reviewer decide, but it must never change routing or risk_level, because a prompt-injected agent writes very persuasive reasons.

Reviewer context in the Requests API

What the response looks like

The API returns at once with a pending request. Store the request id and wait for the signed callback.

response.json
{
  "id": "req_abc123",
  "status": "pending",
  "state": "queued",
  "type": "approval",
  "question": "Approve refund over policy limit?",
  "required_role": "manager",
  "priority": "urgent",
  "external_request_id": "refund-8842:approval",
  "correlation_id": "case-refund-8842",
  "routing_source": "api_key_default",
  "request_type": "review",
  "continuation_mode": "decision",
  "created_at": "2026-09-28T10:30:00Z",
  "expires_at": "2026-09-28T11:30:00Z",
  "metadata": { "case_id": "refund-8842" }
}

Handle the callback

When a person decides, Contro1 sends a signed POST to your callback_url. Always verify the signature and timestamp before acting on the decision.

New handlers can read the top-level status and response. protocol_response and structured_response are there for protocol adapters and compatibility.

webhook.ts
import express from 'express';
import { verifyWebhook } from '@contro1/sdk';

const app = express();
app.use(express.raw({ type: 'application/json' }));

app.post('/webhooks/contro1', async (req, res) => {
  const signature = req.headers['x-centcom-signature'] as string;
  const timestamp = req.headers['x-centcom-timestamp'] as string;
  const secret = process.env.CONTRO1_WEBHOOK_SECRET!;

  if (!verifyWebhook(req.body, signature, timestamp, secret)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body.toString());
  const { request_id, status, response } = event;

  switch (status) {
    case 'approved':
      await resumeWork(request_id, response);
      break;
    case 'rejected':
    case 'denied':
      await stopWork(request_id, response?.comment);
      break;
    default:
      await failClosed(request_id);
  }

  res.sendStatus(200);
});
webhook.py
import os
from flask import Flask, request, abort
from centcom import verify_webhook

app = Flask(__name__)

@app.route('/webhooks/contro1', methods=['POST'])
def handle_callback():
    signature = request.headers.get('X-CentCom-Signature', '')
    timestamp = request.headers.get('X-CentCom-Timestamp', '')
    secret = os.environ['CONTRO1_WEBHOOK_SECRET']

    if not verify_webhook(request.data, signature, timestamp, secret):
        abort(401)

    event = request.get_json(force=True)
    status = event['status']
    request_id = event['request_id']

    if status == 'approved':
        resume_work(request_id, event['response'])
    elif status in ('rejected', 'denied'):
        stop_work(request_id, event['response'].get('comment'))
    else:
        fail_closed(request_id)

    return '', 200

Map all outcomes

Handle every final state. The default is always to fail closed: anything other than an approval means the step does not run.

  • approved - continue, with the decision attached
  • rejected - stop, and log the reviewer's comment if there is one
  • timed_out - nobody answered in time; treat it as rejected, and optionally route to a fallback approver
  • cancelled - you cancelled it; treat it as rejected

Measure the rest of the workflow

The gated step is one step. Record the others as audit records, with one trace_id for the run, so the whole workflow is measured: what it did, how often it needed a person, and where it failed. Steps from agents can be reported automatically by a hook in their runtime.

Record a step that needs no approval
curl -X POST https://api.contro1.com/api/centcom/v1/audit-records \
  -H "Authorization: Bearer $CONTRO1_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "refund.issued",
    "summary": "Refunded $48 to customer c-8821",
    "source": { "integration": "refund-service", "workflow_id": "refunds", "run_id": "run_5521" },
    "trace_id": "trc_9f2c41",
    "correlation_id": "case-refund-8842"
  }'

Traceability: reporting every step

What to do next

  • Add an idempotency key (external_request_id) so a retry never creates a second request.
  • Add policy_context when your own rules, risk engine or policy service decided a person must review.
  • Set approval_comment_required when even a low-risk decision needs a written reason.
  • Use correlation_id and one trace_id per run when a workflow has both human decisions and autonomous steps.
  • Read the Webhooks page for signature verification in detail.

Complete API guide · API endpoint index · Webhooks: signature verification · Requests API reference · Audit records and cases · Authentication and credentials

Make the approval the gate, not a suggestion

The signed webhook is cryptographic proof of a human decision. Verify it inside the system that executes the action - not inside the agent. Any tool that must never run without human sign-off (payments, deploys, data deletion) should refuse to act without a verified approval; that way no agent, including shadow agents nobody registered, can trigger it by skipping Contro1.

  • Verify the signature and reject timestamps older than 5 minutes (replay protection; the timestamp marks callback delivery, not request creation, so long SLAs are unaffected).
  • Bind the approval to the exact action parameters via metadata / correlation_id - never treat "an approval arrived" as permission for a different action.
  • Execute each request_id exactly once (idempotency on your side).
  • When in doubt, confirm state directly with GET /v1/requests/:id using a read-only API key.

Full guardrail pattern with code: Webhooks

Frequently asked questions

Does my system need an MCP server or an API of its own?

No. Your code calls Contro1 before the step and waits for the decision. The system being governed does not need to expose anything, which is why the API suits internal tools, legacy systems and workflows.

When should my code create a request?

Before a step that is high-impact, irreversible, financially sensitive or policy-sensitive. Record the other steps as audit records so they are still measured.

Can I use Contro1 without changing my whole stack?

Yes. The usual start is one API call and a callback handler around one risky step. Nothing else changes.

What happens if nobody answers in time?

The request ends as timed_out. Your code should fail closed by default, and can route to a fallback approver.

Do I need an SDK?

No. Any HTTP client works. The Python and TypeScript SDKs, and the guides for LangGraph, OpenAI Agents, CrewAI, n8n and others, save you writing the calls yourself.

Related resources

A Complete Guide to the API

A practical walkthrough of the Contro1 runtime API: which endpoint an agent calls, when to call it, what to send, and how approvals, audit records, traces, and evidence fit together.

Agent identity, traceability & signed evidence

Know which agent did what: identity from the credential, sub-agents that register themselves, one trace per run with every tool call, hooks and OpenTelemetry that report without relying on the model, and HMAC-signed evidence.