Core API

Quickstart: add approvals to an AI agent in minutes

Create your first Contro1 request, route it to the right operator, and return a signed callback to your agent.

Use this quickstart when you want to add a human approval step to an existing agent without redesigning the whole workflow.

Key takeaways

  • A Contro1 request is a single POST to https://api.contro1.com/api/centcom/v1/requests with a question, context, and callback URL.
  • The human answer arrives as a signed webhook you verify before resuming the agent.
  • Idempotency keys keep retries safe; required_role keeps the decision with the right human.
  • Optional policy_context and approval_comment_required fields let any custom policy layer preserve why review was required.
  • Add correlation_id when this request belongs to a larger agent run or case timeline.

What you need

  • A Contro1 API key (get one in Settings -> APIs & Webhooks)
  • A callback URL your server controls - Contro1 will POST the signed decision there
  • A clear question, supporting context, and the role that should answer

How the approval loop works

Your agent creates a request when it reaches a risky or policy-sensitive action. CENTCOM routes it to the right human based on required_role, shift coverage, and priority.

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 - operator approved; the signed callback is on its way to your server
  • rejected - operator rejected; your agent should halt or route differently
  • timed_out - no one answered within the SLA window; treat as rejected and fail closed
  • cancelled - you called DELETE on 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 cc_live_your_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
from centcom import CentcomClient

client = CentcomClient(api_key="cc_live_your_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: 'cc_live_your_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

What the response looks like

The API returns immediately with a pending request. Store the request id and wait for the signed callback - do not poll.

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": "2025-01-15T10:30:00Z",
  "expires_at": "2025-01-15T11:30:00Z",
  "metadata": { "case_id": "refund-8842" }
}

Handle the callback

When an operator responds, Contro1 sends a signed POST to your callback_url. Always verify the signature and timestamp before applying the decision.

New handlers can usually read top-level status plus response. protocol_response and structured_response are included 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 resumeAgent(request_id, response);
      break;
    case 'rejected':
    case 'denied':
      await haltAgent(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_agent(request_id, event['response'])
    elif status in ('rejected', 'denied'):
        halt_agent(request_id, event['response'].get('comment'))
    else:
        fail_closed(request_id)

    return '', 200

Map all outcomes

Your agent must handle every terminal state. The default should always be fail closed.

  • approved - operator approved the action; resume with the decision payload attached
  • rejected - operator rejected; halt and log the comment if provided
  • timed_out - no one answered within the SLA; treat as rejected and optionally route to a fallback approver
  • cancelled - you called DELETE before anyone answered; treat as rejected

What to do next

  • Read the Webhooks doc to understand signature verification in detail
  • Add an idempotency key (external_request_id) so retries never create duplicate requests
  • Add policy_context when your own rules, risk engine, or policy service caused the approval
  • Set approval_comment_required when a low or medium risk workflow still needs reviewer justification
  • Use correlation_id and audit records when the same agent run has both human decisions and autonomous follow-up actions
  • Use the API endpoint index when you want the short list of every available call
  • Read the Requests API reference to see all fields, priorities, and cancellation

API endpoint index · Webhooks: signature verification · Requests API reference · Audit records and cases · Authentication and API keys

Frequently asked questions

When should an agent create a Contro1 request?

Create a request when the action is high-impact, irreversible, financially sensitive, or policy-sensitive.

Can I use Contro1 without changing my whole stack?

Yes. The normal starting point is a single API call plus a callback handler around the risky action.

What happens if the operator does not answer in time?

The request expires with a timed_out status. Your workflow should fail closed by default and optionally route to a fallback approver.

Do I need a dedicated SDK?

No. Any HTTP client works. We publish framework-specific helpers (LangGraph, OpenAI Agents, CrewAI, n8n, Claude managed agents) so you do not have to glue the HTTP calls yourself.