Docs · The workflow

Every action your agents take, checked, approved where needed, and provable.

Put one action under control

An AI support agent recommends a $500 customer credit so someone stays. A credit over $100 needs a person. Agentics checks the policy, asks on the phone when it should, and leaves a receipt anyone can check. The numbers below are an example. They are not a measured result.

Allowed Example

A support agent recommended a $500 customer credit, and a person approved it.

Agent
support-agent · v1 · Example owner
Rule
A customer credit over $100 needs a person. Approved by the owner.
Amount
$500.00
Time
Outcome
Customer stayed. Retained value $2,400.00
Hash
7f3a…f3a9
Check
Verify

Five calls

The agent does not issue the credit first. It asks, waits when a person is required, issues the credit in your own billing system, then records what happened.

  1. 1

    Check the policy

    POST /api/policies/evaluate returns allow, warn, require_approval, or block. require_approval is Needs you. This response is the decision. It does not include a new receipt hash.

  2. 2

    Read the approval

    GET /api/approvals/{id} is the approval record: status, the action, and each step.

  3. 3

    A person decides

    POST /api/approvals/{id}/decide is the person's call, from a signed-in session. The agent key cannot make it. A terminal decision seals an outcome receipt when the approval carries a receipt hash. That hash is not in this response.

  4. 4

    Record the outcome

    POST /api/outcomes writes the outcome receipt, including business_impact_usd. The original receipt is not rewritten. A pointer on it names the new receipt.

  5. 5

    Check the receipt

    GET /api/proof/verify recomputes the leaf, the chain, the batch, and the anchor. verified is true only when all four match.

Authentication

Send Authorization: Bearer ak_…. The key is the tenant. These routes also accept a signed-in session cookie. Create the key in the console. It is shown once.

An agent key may check the policy, read an approval, record an outcome, and check a receipt. It may not approve. POST /api/approvals/{id}/decide returns 403 forbidden for an API key, because that role is not an owner, admin, or approver. The person decides at Needs you.

Turn three switches on for the company. Policy checks use policy_engine. Approvals use approvals_workflow. Receipt checks use proof_mesh. Outcomes use policy_engine as well. If a switch is off, the route returns 404 feature_not_enabled.

An agent key needs policies.read to check a policy or read an approval, policies.write to open an approval, receipts.write to record an outcome, and receipts.read to check a receipt. A key without that scope gets 403 missing_scope. A key for one company cannot read another company's records. Decide stays closed to API keys.

Pass receipt_hash in the policy check: the 64-character hash of the action receipt you already recorded. Re-checking the same hash reuses one open approval. The policy response does not mint that hash. If the hash is not a receipt in this company, the outcome call returns 404 not_found.

Check the policy

Send the action the agent wants to take. In this example a customer credit over $100 matches a rule with on_violation: require_approval, so the response includes approval_id.

curl -s https://agentics.you/api/policies/evaluate \ -H "Authorization: Bearer $AGENTICS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": "issue_credit", "action_type": "issue_credit", "amount_usd": 500, "currency": "USD", "customer_id": "cus_4192", "reason": "Customer asked to cancel after a double renewal charge", "receipt_hash": "7f3a9c1e4b28d0aa91c04e77b6a1d903c4e8f012aa77b6c1d903e4b28d07f3a9", "agent": "support-agent", "agent_version_id": "support-agent.v1" }'
const res = await fetch("https://agentics.you/api/policies/evaluate", { method: "POST", headers: { Authorization: `Bearer ${process.env.AGENTICS_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ action: "issue_credit", action_type: "issue_credit", amount_usd: 500, currency: "USD", customer_id: "cus_4192", reason: "Customer asked to cancel after a double renewal charge", receipt_hash: "7f3a9c1e4b28d0aa91c04e77b6a1d903c4e8f012aa77b6c1d903e4b28d07f3a9", agent: "support-agent", agent_version_id: "support-agent.v1", }), }); const decision = await res.json();
import json, os, urllib.request req = urllib.request.Request( "https://agentics.you/api/policies/evaluate", data=json.dumps({ "action": "issue_credit", "action_type": "issue_credit", "amount_usd": 500, "currency": "USD", "customer_id": "cus_4192", "reason": "Customer asked to cancel after a double renewal charge", "receipt_hash": "7f3a9c1e4b28d0aa91c04e77b6a1d903c4e8f012aa77b6c1d903e4b28d07f3a9", "agent": "support-agent", "agent_version_id": "support-agent.v1", }).encode(), headers={ "Authorization": f"Bearer {os.environ['AGENTICS_API_KEY']}", "Content-Type": "application/json", }, ) decision = json.load(urllib.request.urlopen(req))
Needs you Example 200
{
  "decision": "require_approval",
  "violations": [
    {
      "policy_id": "pol_credit_example",
      "name": "Customer credits over $100 need a person",
      "on_violation": "require_approval",
      "severity": "high"
    }
  ],
  "approval_id": "6c1d8a20-4b7e-4e1a-9c33-1f0a2b7e4d10"
}

You should see decision and, when a person is required, approval_id. allow and warn mean the agent may continue. block means it must not. A credit of $80 in this example is allow. A credit over $5,000 in the sample stub is block. Your company's own rules decide the live response.

Wait for a person

Print the link https://agentics.you/console/needs and the approval id. Then poll until status is approved or rejected.

curl -s https://agentics.you/api/approvals/6c1d8a20-4b7e-4e1a-9c33-1f0a2b7e4d10 \ -H "Authorization: Bearer $AGENTICS_API_KEY"
const res = await fetch( "https://agentics.you/api/approvals/6c1d8a20-4b7e-4e1a-9c33-1f0a2b7e4d10", { headers: { Authorization: `Bearer ${process.env.AGENTICS_API_KEY}` } }, ); const approval = await res.json(); // Poll until approval.approval.status is "approved" or "rejected".
import json, os, urllib.request req = urllib.request.Request( "https://agentics.you/api/approvals/6c1d8a20-4b7e-4e1a-9c33-1f0a2b7e4d10", headers={"Authorization": f"Bearer {os.environ['AGENTICS_API_KEY']}"}, ) approval = json.load(urllib.request.urlopen(req))
Needs you Example 200
{
  "approval": {
    "id": "6c1d8a20-4b7e-4e1a-9c33-1f0a2b7e4d10",
    "status": "pending",
    "kind": "policy_gate",
    "title": "issue_credit $500 — approval required"
  },
  "steps": [
    { "step_index": 0, "required": true, "decision": null }
  ]
}

The person's decision

The person approves or denies from Needs you, on the phone or in the console. The HTTP call below is that session, not the agent key. decision is approve or reject. step_index starts at 0.

# Person's session. An agent key receives 403 forbidden. curl -s -X POST \ https://agentics.you/api/approvals/6c1d8a20-4b7e-4e1a-9c33-1f0a2b7e4d10/decide \ -H "Content-Type: application/json" \ -b "agentics_session=$SESSION" \ -d '{ "step_index": 0, "decision": "approve", "note": "Keep the customer" }'
// Person's session. An agent key receives 403 forbidden. await fetch( "https://agentics.you/api/approvals/6c1d8a20-4b7e-4e1a-9c33-1f0a2b7e4d10/decide", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ step_index: 0, decision: "approve", note: "Keep the customer" }), }, );
import json, os, urllib.request # Person's session. An agent key receives 403 forbidden. req = urllib.request.Request( "https://agentics.you/api/approvals/6c1d8a20-4b7e-4e1a-9c33-1f0a2b7e4d10/decide", data=json.dumps({"step_index": 0, "decision": "approve", "note": "Keep the customer"}).encode(), method="POST", headers={ "Content-Type": "application/json", "Cookie": f"agentics_session={os.environ['SESSION']}", }, ) print(json.load(urllib.request.urlopen(req)))
Allowed Example 200
{ "status": "approved" }

A rejection returns { "status": "rejected" }. While the company is paused, decide returns 423 tenant_paused and the credit stays held. After an approval, issue the credit in your billing system. Do not issue it before.

Record the outcome

Later, say what the credit did. settled with a positive business_impact_usd means the customer stayed. confirmed_bad with a negative amount means they left. In this example the retained value is $2,400. That figure is tracked on the receipt. It is not a saving caused by Agentics.

Outcome status business_impact_usd
Customer stayed Example settled $2,400.00
Customer left Example confirmed_bad −$2,400.00

Other accepted values are clean, reversed, and false_positive. pending is refused.

curl -s -X POST https://agentics.you/api/outcomes \ -H "Authorization: Bearer $AGENTICS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "receipt_hash": "7f3a9c1e4b28d0aa91c04e77b6a1d903c4e8f012aa77b6c1d903e4b28d07f3a9", "status": "settled", "ref": "billing:cr_0001", "note": "Customer stayed. Example estimate of retained value $2,400, credit $500.", "business_impact_usd": 2400 }'
const res = await fetch("https://agentics.you/api/outcomes", { method: "POST", headers: { Authorization: `Bearer ${process.env.AGENTICS_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ receipt_hash: "7f3a9c1e4b28d0aa91c04e77b6a1d903c4e8f012aa77b6c1d903e4b28d07f3a9", status: "settled", ref: "billing:cr_0001", note: "Customer stayed. Example estimate of retained value $2,400, credit $500.", business_impact_usd: 2400, }), }); const outcome = await res.json();
import json, os, urllib.request req = urllib.request.Request( "https://agentics.you/api/outcomes", data=json.dumps({ "receipt_hash": "7f3a9c1e4b28d0aa91c04e77b6a1d903c4e8f012aa77b6c1d903e4b28d07f3a9", "status": "settled", "ref": "billing:cr_0001", "note": "Customer stayed. Example estimate of retained value $2,400, credit $500.", "business_impact_usd": 2400, }).encode(), method="POST", headers={ "Authorization": f"Bearer {os.environ['AGENTICS_API_KEY']}", "Content-Type": "application/json", }, ) outcome = json.load(urllib.request.urlopen(req))
Allowed Example 201
{
  "ok": true,
  "receipt_hash": "7f3a9c1e4b28d0aa91c04e77b6a1d903c4e8f012aa77b6c1d903e4b28d07f3a9",
  "outcome_status": "settled",
  "outcome_receipt_hash": "a91c04e77b6a1d903c4e8f012aa77b6c1d903e4b28d07f3a97f3a9c1e4b28d0a"
}

outcome_receipt_hash is a new receipt. The hash you sent is unchanged.

Check the receipt

Anyone with a key for the company can recompute the receipt. The public page is /verify. Anchoring follows on a short delay, so anchor_ok can be false on a receipt that is otherwise intact. verified waits for the anchor.

curl -s "https://agentics.you/api/proof/verify?hash=7f3a9c1e4b28d0aa91c04e77b6a1d903c4e8f012aa77b6c1d903e4b28d07f3a9" \ -H "Authorization: Bearer $AGENTICS_API_KEY"
const hash = "7f3a9c1e4b28d0aa91c04e77b6a1d903c4e8f012aa77b6c1d903e4b28d07f3a9"; const res = await fetch(`https://agentics.you/api/proof/verify?hash=${hash}`, { headers: { Authorization: `Bearer ${process.env.AGENTICS_API_KEY}` }, }); const proof = await res.json(); console.log(proof.verified, proof.steps);
import json, os, urllib.request hash_ = "7f3a9c1e4b28d0aa91c04e77b6a1d903c4e8f012aa77b6c1d903e4b28d07f3a9" req = urllib.request.Request( f"https://agentics.you/api/proof/verify?hash={hash_}", headers={"Authorization": f"Bearer {os.environ['AGENTICS_API_KEY']}"}, ) proof = json.load(urllib.request.urlopen(req))
Example 200 · anchor not in yet, so verified is false
{
  "hash": "7f3a9c1e4b28d0aa91c04e77b6a1d903c4e8f012aa77b6c1d903e4b28d07f3a9",
  "receipt_recomputed": true,
  "chain_ok": true,
  "merkle_ok": true,
  "anchor_ok": false,
  "verified": false,
  "anchor_txid": null,
  "steps": [
    { "step": "fetch_receipt", "ok": true },
    { "step": "recompute_leaf", "ok": true },
    { "step": "chain", "ok": true },
    { "step": "merkle", "ok": true },
    { "step": "anchor", "ok": false, "detail": "not_anchored_yet" }
  ]
}

A self-reported span, such as a step your own SDK posts, is not this check. It stays marked self-reported until Agentics seals it.

Self-reported

Errors

StatuserrorWhen
401tenant_requiredMissing or unknown key, or a session with no company.
400bad_receipt_hashOutcome hash is not 64 hex characters.
400bad_hashVerify hash is not 64 hex characters.
400bad_statusOutcome status is not one of the accepted values.
400bad_business_impact_usdImpact is present and not a finite number.
400bad_decisionDecide was not approve or reject.
403forbiddenThe caller cannot decide. API keys always get this.
404feature_not_enabledThe company's switch for that route is off.
404not_foundNo approval, or no receipt for that hash in this company.
404step_not_foundThat step index is not on the approval.
405method_not_allowedOutcomes received a method other than GET or POST.
409reason stringThe outcome could not be sealed.
423tenant_pausedPause all is on. A decision cannot release the credit.

Errors are JSON: { "error": "forbidden" }. Some include detail.

Idempotency

Send the same receipt_hash on the policy check. While that approval is still open, the same approval_id comes back. A second check does not open a second queue item.

These routes do not read an Idempotency-Key header. Sending one does not change the response. Posting the outcome again appends another outcome receipt and moves the pointer to it. Send the outcome once.

Decide writes the step again if you call it twice. Treat the first terminal status as final.

Webhook

You can register a URL with POST /api/webhooks. The name approval.pending is on the list of event names. A decision does not send a webhook today, and creating an approval does not either. Poll GET /api/approvals/{id} until the status changes. That is the path the sample uses.

When a decision webhook exists, the body will look like this. It is not sent now.

Example Not sent
{
  "event": "approval.decided",
  "approval_id": "6c1d8a20-4b7e-4e1a-9c33-1f0a2b7e4d10",
  "status": "approved",
  "receipt_hash": "7f3a9c1e4b28d0aa91c04e77b6a1d903c4e8f012aa77b6c1d903e4b28d07f3a9",
  "decided_at": "2026-10-03T07:35:00Z"
}

Sample

The support agent is a tool, issue_credit(customer_id, amount_usd, reason), on the OpenAI Agents SDK. It calls the policy check first. On allow, it issues a credit in a local billing module. On Needs you, it prints the approval link and polls. Then it posts the outcome and prints the check URL.

Mock mode runs on this machine. It does not call OpenAI or Agentics.

cd examples/support-credit-openai-agents python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt python -m pytest python run.py
cd examples/support-credit-openai-agents/typescript npm install export OPENAI_API_KEY=... export AGENTICS_API_KEY=ak_... node --experimental-strip-types src/agent.ts \ "Customer cus_4192 asked to cancel. Recommend a $500 credit."
cd examples/support-credit-openai-agents python3 -m venv .venv source .venv/bin/activate pip install -r requirements-live.txt export OPENAI_API_KEY=... export AGENTICS_API_KEY=ak_... python run.py --live "Customer cus_4192 asked to cancel. Recommend a $500 credit."

The mock run prints Needs you, then the check URL. Live mode needs only OPENAI_API_KEY and AGENTICS_API_KEY. Do not commit either key.

Each step also reports itself to POST /v1/activity as self-reported, in one run. That route is not live yet. A miss does not stop the credit. Set AGENTICS_ACTIVITY=0 to skip the call. The same tool exists in TypeScript under typescript/src/agent.ts, and as a short graph in langgraph_credit.py.