Developer Docs

IronWeft is a REST API. One key, a handful of endpoints, and every agent action in your system is registered, credentialed, authorized, and audited.

Quickstart

Get from zero to your first authorized agent action in under 5 minutes.

Requirements

No dependencies for raw HTTP. To use an official SDK:

Python
Node.js
Go
pip install ironweft
npm install ironweft
go get github.com/ironweft/ironweft-go

1. Get your API key

Sign up to get an iw_live_xxx key. Store it as an environment variable — it's only shown once.

2. Register an agent

curl
Python
Node.js
Go
# Register an agent with a human sponsor
curl -X POST https://ironweft.io/agents \
  -H "Authorization: Bearer $IRONWEFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_name": "Grace",
    "sponsor_id": "user_margaret_chen",
    "description": "Daily check-in call agent",
    "initial_roles": ["call_agent"]
  }'

# Response
{
  "agent_id": "agt_4ae283ac96dd4b40",
  "public_key": "ed25519:272741c6...",
  "status": "active",
  "created_at": "2026-05-13T14:00:00Z"
}
import requests

resp = requests.post(
    "https://ironweft.io/agents",
    headers={"Authorization": f"Bearer {IRONWEFT_API_KEY}"},
    json={
        "agent_name": "Grace",
        "sponsor_id": "user_margaret_chen",
        "description": "Daily check-in call agent",
        "initial_roles": ["call_agent"],
    }
)
agent_id = resp.json()["agent_id"]  # "agt_4ae283ac96dd4b40"
import { IronWeftClient } from "ironweft";

const client = new IronWeftClient({ apiKey: process.env.IRONWEFT_API_KEY });

const reg = await client.registerAgent({
  name: "Grace",
  sponsorId: "user_margaret_chen",
  description: "Daily check-in call agent",
  roles: ["call_agent"],
});
const agentId = reg.agent_id; // "agt_4ae283ac96dd4b40" — store this
import "github.com/ironweft/ironweft-go"

client := ironweft.New(os.Getenv("IRONWEFT_API_KEY"))

reg, err := client.RegisterAgent(ctx, ironweft.RegisterAgentRequest{
    AgentName:    "Grace",
    SponsorID:    "user_margaret_chen",
    Description:  "Daily check-in call agent",
    InitialRoles: []string{"call_agent"},
})
agentID := reg.AgentID // "agt_4ae283ac96dd4b40" — store this

3. Issue a credential

Credentials are short-lived JWTs scoped to specific actions. Issue one before each task or session.

curl
Python
Node.js
Go
curl -X POST https://ironweft.io/agents/credentials \
  -H "Authorization: Bearer $IRONWEFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agt_4ae283ac96dd4b40",
    "scopes": ["call_initiate"],
    "ttl_minutes": 60
  }'

{
  "credential": "eyJhbGciOiJIUzI1NiJ9...",
  "expires_at": "2026-05-13T15:00:00Z",
  "scopes": ["call_initiate"]
}
resp = requests.post(
    "https://ironweft.io/agents/credentials",
    headers={"Authorization": f"Bearer {IRONWEFT_API_KEY}"},
    json={
        "agent_id": agent_id,
        "scopes": ["call_initiate"],
        "ttl_minutes": 60,
    }
)
credential = resp.json()["credential"]
const grace = client.agent("agt_4ae283ac96dd4b40");

const credential = await grace.credential({
  scopes: ["call_initiate"],
  ttlMinutes: 60,
});
grace := client.Agent("agt_4ae283ac96dd4b40")

credential, err := grace.Credential(ctx, []string{"call_initiate"}, 60)

4. Authorize before every action

curl
Python
Node.js
Go
curl -X POST https://ironweft.io/authorize \
  -H "Content-Type: application/json" \
  -d '{
    "credential": "eyJhbGciOiJIUzI1NiJ9...",
    "action": "call_initiate",
    "resource": "phone:+15550100"
  }'

{
  "decision": "allow",
  "reason": "scope match",
  "allowed_scopes": ["call_initiate"],
  "audit_event_id": "evt_a1b2c3d4e5f6"
}
resp = requests.post(
    "https://ironweft.io/authorize",
    json={
        "credential": credential,
        "action": "call_initiate",
        "resource": "phone:+15550100",
    }
)
decision = resp.json()["decision"]  # "allow" | "deny" | "challenge"

# Only proceed if allowed
if decision == "allow":
    make_phone_call("+15550100")
// grace.check() throws on deny — only continues on allow
await grace.check("call_initiate", {
  credential,
  resource: "phone:+15550100",
});
await makePhoneCall("+15550100");
// grace.Check() returns error on deny — only continues on allow
_, err = grace.Check(ctx, "call_initiate", credential, "phone:+15550100")
if err != nil {
    return err // *ironweft.AuthorizationDenied, *ironweft.AgentSuspended, etc.
}
makePhoneCall("+15550100")
The pattern: issue credential once per session → call /authorize before every action → only proceed on "allow". Every decision is automatically logged to the hash-chained audit trail.

Authentication

All API requests require your tenant API key in the Authorization header:

HTTP
Authorization: Bearer iw_live_your_key_here

Your key is shown once at signup. If lost, contact forged@ironweft.io for a reset.

The /authorize endpoint does not require your API key — it accepts the agent's short-lived credential JWT instead.

Python SDK

The IronWeft Python SDK wraps the REST API so you work with decorators and typed exceptions instead of raw HTTP calls. Raw HTTP still works — the SDK is a convenience layer, not a requirement.

Install

shell
pip install ironweft

Initialize the client

Python
# One client per tenant — instantiate once and share
from ironweft import Client

iw = Client(api_key="iw_live_your_key_here")

Resilience options

By default, the SDK raises on network errors — if IronWeft is unreachable, the action does not proceed (fail-closed). To implement fail-open for non-critical actions, catch network exceptions specifically. Never swallow AuthorizationDenied or AgentSuspended — those are authoritative decisions from the server, not connectivity issues.

Python
import httpx
from ironweft import AuthorizationDenied, AgentSuspended

try:
    grace.check(action="kb_read", credential=credential)
except (httpx.ConnectError, httpx.TimeoutException):
    # IronWeft unreachable — proceed anyway (fail-open)
    # Remove this block to stay fail-closed (default, recommended)
    pass
except (AuthorizationDenied, AgentSuspended):
    raise  # Always respect auth decisions

Register an agent (one time)

Python
agent = iw.register_agent(
    agent_name="Grace",
    sponsor_id="user_margaret_chen",
    description="Daily check-in call agent",
    initial_roles=["call_agent"],
)
# Store agent.agent_id — you'll need it each time you run
AGENT_ID = agent["agent_id"]  # "agt_4ae283ac96dd4b40"

The gate decorator — recommended pattern

The decorator handles credential issuance and authorization automatically. If the check fails, it raises before your function body runs — no boilerplate required.

Python
from ironweft import Client, AuthorizationDenied, AgentSuspended

iw = Client(api_key="iw_live_your_key_here")
grace = iw.agent("agt_4ae283ac96dd4b40")

# Issues a fresh credential + calls /authorize before every invocation
@grace.gate(action="call_initiate", scopes=["call_initiate"])
def initiate_call(phone_number: str):
    # Only reached if IronWeft returns "allow"
    make_phone_call(phone_number)

# Authorization, credential lifecycle, and audit logging handled automatically
initiate_call("+15550100")

Explicit check — when you need more control

Python
grace = iw.agent("agt_4ae283ac96dd4b40")

# Issue credential for this session
credential = grace.credential(scopes=["call_initiate"], ttl_minutes=60)

# Check before each action
grace.check(
    action="call_initiate",
    credential=credential,
    resource="phone:+15550100",
)

# Raises AuthorizationDenied or AgentSuspended on deny — only reach here on allow
make_phone_call("+15550100")

Exception handling

Python
from ironweft import AuthorizationDenied, AgentSuspended, AgentRetired

try:
    initiate_call("+15550100")
except AgentSuspended as e:
    # Agent has been suspended — notify the sponsor
    alert_sponsor(e.agent_id, e.audit_event_id)
except AgentRetired:
    # Agent is permanently retired — re-register if needed
    pass
except AuthorizationDenied as e:
    # Scope or policy denied this specific action
    log_denial(e.action, e.reason)
No code changes to your agents. You add the decorator (or a check() call) at the invocation site — the agent itself doesn't know IronWeft exists. Swap from raw HTTP to the SDK at any time without touching the agents.

Node.js SDK

The IronWeft Node.js SDK is a zero-dependency TypeScript client for Node 18+ and edge runtimes. Raw HTTP still works — the SDK is a convenience layer.

Install

shell
npm install ironweft

Initialize the client

TypeScript
import { IronWeftClient } from "ironweft";

const client = new IronWeftClient({ apiKey: "iw_live_your_key_here" });

Resilience options

By default, the SDK throws on network errors — if IronWeft is unreachable, the action does not proceed (fail-closed). To implement fail-open for non-critical actions, re-throw only auth decisions and swallow connectivity errors. Never swallow AuthorizationDenied or AgentSuspended — those are authoritative server decisions.

TypeScript
import { AuthorizationDenied, AgentSuspended } from "ironweft";

try {
  await grace.check("kb_read", { credential });
} catch (e) {
  if (e instanceof AuthorizationDenied || e instanceof AgentSuspended) {
    throw e; // Always respect auth decisions
  }
  // IronWeft unreachable — proceed anyway (fail-open)
  // Remove this catch to stay fail-closed (default, recommended)
}

Register an agent (one time)

TypeScript
const reg = await client.registerAgent({
  name: "Grace",
  sponsorId: "user_margaret_chen",
  description: "Daily check-in call agent",
  roles: ["call_agent"],
});
// Store reg.agent_id — reuse it every time you run
const AGENT_ID = reg.agent_id; // "agt_4ae283ac96dd4b40"

The gate() function — recommended pattern

Wraps any async function with automatic credential issuance and authorization. The inner function only runs on an explicit allow.

TypeScript
import { IronWeftClient, AuthorizationDenied, AgentSuspended } from "ironweft";

const client = new IronWeftClient({ apiKey: "iw_live_your_key_here" });
const grace = client.agent("agt_4ae283ac96dd4b40");

// Issues a fresh credential + calls /authorize before every invocation
const initiateCall = grace.gate(
  "call_initiate",
  { scopes: ["call_initiate"] },
  async (phoneNumber: string) => {
    // Only reached if IronWeft returns "allow"
    await makePhoneCall(phoneNumber);
  }
);

await initiateCall("+15550100");

Explicit check — when you need more control

TypeScript
const grace = client.agent("agt_4ae283ac96dd4b40");

// Issue credential for this session
const credential = await grace.credential({
  scopes: ["call_initiate"],
  ttlMinutes: 60,
});

// Check before each action — throws on deny
await grace.check("call_initiate", { credential, resource: "phone:+15550100" });
await makePhoneCall("+15550100");

Exception handling

TypeScript
import { AuthorizationDenied, AgentSuspended, AgentRetired } from "ironweft";

try {
  await initiateCall("+15550100");
} catch (e) {
  if (e instanceof AgentSuspended) {
    alertSponsor(e.agentId, e.auditEventId);
  } else if (e instanceof AgentRetired) {
    // Permanently retired — re-register if needed
  } else if (e instanceof AuthorizationDenied) {
    logDenial(e.action, e.reason);
  }
}
Source: github.com/ironweft/ironweft-node — zero runtime dependencies, full TypeScript types.

Go SDK

The IronWeft Go SDK uses the standard library only — no external dependencies. Context-aware throughout.

Install

shell
go get github.com/ironweft/ironweft-go

Initialize the client

Go
import "github.com/ironweft/ironweft-go"

client := ironweft.New("iw_live_your_key_here")

Resilience options

By default, the SDK returns an error on network failures — if IronWeft is unreachable, the action does not proceed (fail-closed). To implement fail-open, check whether the error is an auth decision before deciding whether to proceed. Never ignore *ironweft.AuthorizationDenied or *ironweft.AgentSuspended — those are authoritative server decisions.

Go
_, err = grace.Check(ctx, "kb_read", cred, "")
if err != nil {
    var denied *ironweft.AuthorizationDenied
    var suspended *ironweft.AgentSuspended
    if errors.As(err, &denied) || errors.As(err, &suspended) {
        return err // Always respect auth decisions
    }
    // IronWeft unreachable — proceed anyway (fail-open)
    // Return err here to stay fail-closed (default, recommended)
}

Register an agent (one time)

Go
reg, err := client.RegisterAgent(ctx, ironweft.RegisterAgentRequest{
    AgentName:    "Grace",
    SponsorID:    "user_margaret_chen",
    Description:  "Daily check-in call agent",
    InitialRoles: []string{"call_agent"},
})
// Store reg.AgentID — reuse it every time you run
agentID := reg.AgentID // "agt_4ae283ac96dd4b40"

The Gate() function — recommended pattern

Returns a closure that issues a fresh credential, calls /authorize, and only runs your function on allow.

Go
grace := client.Agent("agt_4ae283ac96dd4b40")

// Gate returns a reusable gated closure
call := grace.Gate("call_initiate", []string{"call_initiate"}, "phone:+15550100", 15)

err := call(ctx, func() error {
    return makePhoneCall("+15550100")
})
if err != nil {
    // handle *ironweft.AuthorizationDenied, *ironweft.AgentSuspended, etc.
}

Explicit check — when you need more control

Go
grace := client.Agent("agt_4ae283ac96dd4b40")

// Issue credential for this session
cred, err := grace.Credential(ctx, []string{"call_initiate"}, 60)
if err != nil { return err }

// Check before each action — returns error on deny
_, err = grace.Check(ctx, "call_initiate", cred, "phone:+15550100")
if err != nil { return err }

return makePhoneCall("+15550100")

Error handling

Go
var denied *ironweft.AuthorizationDenied
var suspended *ironweft.AgentSuspended
var retired *ironweft.AgentRetired

switch {
case errors.As(err, &suspended):
    alertSponsor(suspended.AgentID, suspended.AuditEventID)
case errors.As(err, &retired):
    // Permanently retired — re-register if needed
case errors.As(err, &denied):
    log.Printf("denied: %s — %s", denied.Action, denied.Reason)
}
Source: github.com/ironweft/ironweft-go — standard library only, context-aware, zero external dependencies.

A2A Integration

When agents call other agents, every hop in the chain needs authorization. IronWeft is designed for this — the right enforcement strategy depends on the risk tier of the agents involved.

Tiered enforcement

Risk tierPatternWhen to use
minimal Sidecar / async Internal read-only agents, data pipelines. Authorize at the orchestrator level; sub-agents run without individual gates. Log everything for post-hoc review.
limited Cached inline Agents with write access or external calls. Use the SDK cache — allow decisions are served locally after the first check. One network round-trip per action type per credential TTL.
high_risk Full gate + challenge Agents that touch money, PII, or external systems. Enforce a live /authorize call before every action. Use challenge policies to require human approval at key steps.

Batch authorize

When a sub-agent plans a sequence of actions upfront, use /authorize/batch instead of N individual calls. One credential verification, one policy load, one DB write — dramatically lower latency with the same audit guarantee. See Authorize for the full batch API.

Delegation depth enforcement

Set chain_depth policy rules to automatically tighten enforcement the deeper the delegation chain goes. A third-level sub-agent can face a challenge for actions that an orchestrator agent gets an allow for. See Delegation depth for examples.

Mesh visibility

The IronWeft dashboard includes a live Mesh tab — a force-directed graph of all agent-to-agent delegation events in your tenant over a rolling time window. Nodes are sized by call volume; edges are colored by outcome ratio. Use it to spot unexpected delegation patterns or identify agents generating excessive denies.

Agents

POST/agents

Register a new agent under your tenant.

FieldTypeRequiredDescription
agent_namestringyesHuman-readable name
sponsor_idstringyesID of the human accountable for this agent
descriptionstringnoWhat this agent does
initial_rolesstring[]noRoles that define allowed actions (see Scopes & Roles)
metadataobjectnoArbitrary key/value pairs

GET/agents/{agent_id}/permissions

Returns the agent's current roles, status, and metadata.

PATCH/agents/{agent_id}

Update an agent's lifecycle status. Accepted values: active, suspended, retired. Retired agents cannot be reactivated.

FieldTypeDescription
statusstringNew lifecycle status

POST/agents/{agent_id}/delegate

Create a sub-agent under a parent. The sub-agent inherits the parent's sponsor and cannot exceed the parent's scopes.

FieldTypeDescription
agent_namestringName of the sub-agent
scopesstring[]Must be a subset of parent's scopes
descriptionstringOptional
initial_rolesstring[]Optional

Credentials

POST/agents/credentials

Issue a short-lived, scoped JWT for an agent. Pass this credential to /authorize — do not pass your API key.

FieldTypeRequiredDescription
agent_idstringyesThe agent receiving the credential
scopesstring[]yesActions this credential permits
ttl_minutesintyesExpiry in minutes. Recommend 15–60 for live agents.
contextobjectnoArbitrary context embedded in the JWT claims
Tip: Issue credentials with the narrowest scopes needed for the task. A credential scoped to call_initiate cannot be used to authorize payment_initiate.

Authorize

POST/authorize

The core endpoint. Call this before every agent action. Does not require your API key — the agent's credential is the auth token.

FieldTypeRequiredDescription
credentialstringyesJWT from /agents/credentials
actionstringyesAction being requested (e.g. call_initiate)
resourcestringnoTarget resource (e.g. phone:+15550100)
parametersobjectnoAction parameters for policy evaluation
contextobjectnoAdditional context logged to the audit trail. Pass human_facing: true + disclosure_logged: true for limited/high-risk agents.
initiatorstringnoWho triggered this call — e.g. user:bob, script:deploy, agent:agt_xxx. Required if tenant has require_initiator: true in policy config.

Decisions

DecisionMeaning
allowAction is permitted. Proceed.
denyAction is not permitted. Do not proceed. 3 consecutive denies → auto-suspend.
challengeAction requires human approval before proceeding.

Auto-suspend

IronWeft tracks consecutive deny and challenge decisions within a rolling time window. At 3 consecutive → agent is suspended. At 5 → agent is hard locked (retired, not reactivatable). Both thresholds are configurable per tenant.

POST/authorize/batch

Evaluate up to 50 actions in a single request. One credential verification, one tenant policy load, one DB write — dramatically lower latency for agent mesh workflows where an agent plans a sequence of actions upfront.

The hash chain is computed in-memory across the batch and written in a single bulk insert. Deny streak tracking applies across the batch — if auto-suspend is triggered mid-batch, remaining items are denied immediately.

FieldTypeRequiredDescription
credentialstringyesJWT from /agents/credentials — verified once for the whole batch
actionsarrayyesUp to 50 action objects. Max 50 — returns 400 above that.
actions[].actionstringyesAction being requested
actions[].resourcestringnoTarget resource
actions[].parametersobjectnoAction parameters for policy evaluation
actions[].contextobjectnoContext logged to audit trail
actions[].initiatorstringnoWho triggered this action
actions[].refstringnoCaller-supplied ID echoed back in results — use to correlate response items to your planned actions
RequestResponse
{
  "credential": "eyJ...",
  "actions": [
    { "ref": "step-1", "action": "kb_read",     "resource": "kb:articles" },
    { "ref": "step-2", "action": "email_send",  "resource": "user@example.com" },
    { "ref": "step-3", "action": "data_write",  "resource": "db:logs", "context": { "data_sensitivity": "low" } }
  ]
}
{
  "results": [
    { "ref": "step-1", "action": "kb_read",    "decision": "allow",     "reason": "Default: allowed", "audit_event_id": "evt_abc" },
    { "ref": "step-2", "action": "email_send", "decision": "allow",     "reason": "Default: allowed", "audit_event_id": "evt_def" },
    { "ref": "step-3", "action": "data_write", "decision": "challenge", "reason": "Custom policy: data_write requires review", "audit_event_id": "evt_ghi" }
  ],
  "summary": { "total": 3, "allow": 2, "deny": 0, "challenge": 1 }
}

Delegation depth

chain_depth tracks how many hops deep a delegation chain is — 0 for a direct call, 1 for a call from a first-level sub-agent, and so on. IronWeft passes this value automatically to policy evaluation on every /authorize request.

Use it to enforce tighter rules the deeper the chain grows. An action that returns allow from a user-facing agent can return challenge — or deny — from a third-level sub-agent making the same call.

Policy rules
# Challenge at depth 2+, hard deny at depth 4+
{ "name": "Tighten at depth 2", "action_pattern": "*",
  "conditions": [{ "field": "chain_depth", "op": "gte", "value": 2 }],
  "effect": "challenge", "priority": 50 }

{ "name": "Hard limit at depth 4", "action_pattern": "*",
  "conditions": [{ "field": "chain_depth", "op": "gte", "value": 4 }],
  "effect": "deny", "priority": 40 }

See Policies for the full condition field reference and operators.

Audit Trail

All /authorize calls are logged automatically to a hash-chained audit trail. The compliance endpoints below let you query, export, and summarize that trail — scoped to your tenant.

GET/audit/trail

Quick per-agent trail lookup. No auth required beyond being the owner — pass the agent ID as a query param.

ParamTypeDefaultDescription
agent_idstringrequiredAgent to query
limitint50Max 500
offsetint0Pagination offset

GET/audit/events

Compliance query across all agents in your tenant. Requires API key. Combine filters to narrow results.

ParamTypeDefaultDescription
agent_idstringFilter to a single agent
outcomestringallow | deny | challenge
actionstringFilter by action name, e.g. payment_initiate
event_typestringFilter by event type, e.g. policy_decision
from_datestringISO datetime, e.g. 2026-05-01T00:00:00Z
to_datestringISO datetime, e.g. 2026-05-31T23:59:59Z
limitint100Max 1000
offsetint0Pagination offset
curl
# All denied payment actions in May
curl "https://ironweft.io/audit/events?outcome=deny&action=payment_initiate&from_date=2026-05-01T00:00:00Z&to_date=2026-05-31T23:59:59Z" \
  -H "Authorization: Bearer $IRONWEFT_API_KEY"

GET/audit/export

Download up to 5,000 events as a file. Same filters as /audit/events. Use fmt=csv for Excel/compliance tools, fmt=json for programmatic processing.

ParamTypeDefaultDescription
fmtstringcsvcsv or json
All filters from /audit/events are supported.
curl
# Export last 30 days as CSV
curl "https://ironweft.io/audit/export?from_date=2026-04-21T00:00:00Z&fmt=csv" \
  -H "Authorization: Bearer $IRONWEFT_API_KEY" \
  -o ironweft_audit.csv

GET/audit/summary

Aggregate stats for your tenant over a date range: total events, breakdown by outcome, top actions, top denied actions, and unique agent count.

ParamTypeDescription
from_datestringISO datetime (optional)
to_datestringISO datetime (optional)
Response
{
  "total_events": 4821,
  "by_outcome": { "allow": 4710, "deny": 98, "challenge": 13 },
  "top_actions": [
    { "action": "call_initiate", "count": 3204 },
    { "action": "data_read", "count": 1617 }
  ],
  "top_denied_actions": [
    { "action": "payment_initiate", "count": 61 }
  ],
  "unique_agents": 7
}

POST/audit/log

Manually append an event to the chain. Use for application-level milestones you want in the audit record (e.g. call_completed, user_confirmed).

Chain integrity

Every event stores a chain_hash — SHA-256 of the previous event's hash plus the current event's content. Deleting or modifying any row breaks the chain, making tampering detectable.

Policies

Policy rules let you enforce custom governance logic at authorization time — beyond scope checks. Rules are evaluated before every /authorize call, in priority order. First match wins.

Rule schema

FieldTypeDescription
namestringHuman-readable label
descriptionstringShown as the deny/challenge reason when this rule matches
action_patternstringAction to match: * for all, payment_* for prefix, or exact action name
conditionsarrayList of condition objects — all must be true (AND logic)
effectstringallow | deny | challenge
priorityintLower number = higher priority. Default: 100.
enabledboolSet to false to disable without deleting. Default: true.

Condition fields & operators

FieldTypeDescription
chain_depthintNumber of hops in the delegation chain
risk_tierstringminimal | limited | high_risk
initiator_typestringuser | script | agent
actionstringThe action being requested
amountnumberFrom parameters.amount on the authorize request
data_sensitivitystringFrom context.data_sensitivity on the authorize request
human_facingboolFrom context.human_facing on the authorize request
hour_utcintUTC hour of the request (0–23)

Operators: eq, ne, gt, gte, lt, lte, in, not_in

POST/policies

Create a new policy rule. Requires API key.

curl
# Deny any agent with a delegation chain deeper than 3 hops
curl -X POST https://ironweft.io/policies \
  -H "Authorization: Bearer $IRONWEFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Block deep chains",
    "description": "Delegation chain too deep — max 3 hops allowed",
    "action_pattern": "*",
    "conditions": [
      { "field": "chain_depth", "op": "gt", "value": 3 }
    ],
    "effect": "deny",
    "priority": 10
  }'

{ "policy_id": "pol_a1b2c3d4e5f6", "ok": true }
curl
# Challenge high-risk agents on payments above $500
curl -X POST https://ironweft.io/policies \
  -H "Authorization: Bearer $IRONWEFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Challenge high-risk large payments",
    "description": "High-risk agent requesting payment over $500 — requires human review",
    "action_pattern": "payment_initiate",
    "conditions": [
      { "field": "risk_tier", "op": "in", "value": ["high_risk"] },
      { "field": "amount", "op": "gt", "value": 500 }
    ],
    "effect": "challenge",
    "priority": 20
  }'
curl
# Block all actions outside business hours (UTC)
curl -X POST https://ironweft.io/policies \
  -H "Authorization: Bearer $IRONWEFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Business hours only",
    "description": "Actions are only permitted between 06:00–22:00 UTC",
    "action_pattern": "*",
    "conditions": [
      { "field": "hour_utc", "op": "lt", "value": 6 }
    ],
    "effect": "deny",
    "priority": 5
  }'

GET/policies

List all rules for your tenant, ordered by priority.

GET/policies/{policy_id}

Get a single rule by ID.

PATCH/policies/{policy_id}

Update any field. Pass only the fields you want to change. To disable a rule without deleting it: {"enabled": false}.

DELETE/policies/{policy_id}

Permanently delete a rule. Cache is cleared immediately — the rule stops applying within milliseconds.

Evaluation order: scope check → tenant DB rules (priority ASC, first match wins) → built-in defaults. A rule with no conditions matches every request for that action pattern.

Scopes & Roles

Roles are assigned at agent registration. Scopes are requested at credential issuance. The policy engine checks that the requested action falls within both.

Scope / ActionDescription
call_initiateInitiate an outbound voice call
alert_sendSend an alert to a contact
data_readRead user or contact data
data_writeWrite or update user data
payment_initiateInitiate a payment transaction
email_readRead email messages
email_sendSend email messages
kb_writeWrite entries to a knowledge base

Custom scopes can be defined per tenant — contact forged@ironweft.io for enterprise policy configuration.

Tenant Management

Manage your tenant account. All endpoints require your API key.

GET/tenants/{tenant_id}/usage

Authorization volume for your tenant. Defaults to the current calendar month. Pass from_date and to_date (ISO 8601) to override the range.

Response
{
  "tenant_id": "ten_abc123",
  "period": { "from": "2026-05-01T00:00:00+00:00", "to": "2026-05-21T14:00:00+00:00" },
  "total_authorizations": 412,
  "by_outcome": { "allow": 398, "deny": 11, "challenge": 3 },
  "by_agent": { "agt_abc": 310, "agt_xyz": 102 },
  "unique_agents": 2
}

GET/agents/{agent_id}/health

Real-time health snapshot for an agent — status, consecutive deny streak, and last authorization outcome.

Response
{
  "agent_id": "agt_abc123",
  "status": "active",
  "risk_tier": "minimal",
  "consecutive_denies": 0,
  "deny_window_start": null,
  "last_authorize_at": "2026-05-21T13:45:00+00:00",
  "last_outcome": "allow",
  "registered_at": "2026-05-01T09:00:00+00:00"
}

POST/tenants/{tenant_id}/rotate-key

Generate a new API key for your tenant. The old key is immediately invalidated — all agents and integrations using it will stop working until updated.

One-time reveal: The new key is returned once and never stored. Copy it immediately.
Response
{
  "tenant_id": "ten_abc123",
  "api_key": "iw_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "warning": "Old key is immediately invalid. Store this key — it will never be shown again."
}

GET/tenants/{tenant_id}/keys

List all read-only API keys for your tenant.

Response
{
  "tenant_id": "ten_abc123",
  "keys": [
    {
      "id": "rok_abc123",
      "label": "grafana-reader",
      "created_at": "2026-05-01T09:00:00+00:00",
      "last_used_at": "2026-05-21T14:30:00+00:00"
    }
  ]
}

POST/tenants/{tenant_id}/keys

Create a read-only API key. Keys prefixed iw_ro_ are restricted to GET endpoints only — use them for dashboards, monitoring integrations, or any context where you don't want to expose your primary key.

One-time reveal: The key is returned once. Copy it immediately.
RequestResponse
{ "label": "grafana-reader" }
{
  "key_id": "rok_abc123",
  "label": "grafana-reader",
  "api_key": "iw_ro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "warning": "Store this key — it will never be shown again. Read-only: GET endpoints only."
}

DELETE/tenants/{tenant_id}/keys/{key_id}

Revoke a read-only key. Takes effect immediately.

Response
{ "deleted": true, "key_id": "rok_abc123" }

Rate Limits

Limits are per API key for authenticated endpoints and per IP for public endpoints. Exceeding a limit returns 429 Too Many Requests.

EndpointLimit
POST /authorize120 / minute
POST /authorize/batch120 / minute (shared with /authorize; each batch counts as one request)
POST /audit/log120 / minute
POST /agents/credentials60 / minute
GET /agents/{id}/permissions60 / minute
GET /audit/trail30 / minute
GET /audit/events, /audit/summary30 / minute
GET /audit/export10 / minute
POST /agents, PATCH, delegate20 / minute
POST /policies, PATCH /policies20 / minute
POST /signup5 / hour (per IP)
POST /tenants10 / hour (per IP)
POST /tenants/{id}/rotate-key20 / minute
GET /tenants/{id}/usage60 / minute
GET /agents/{id}/health60 / minute
GET /tenants/{id}/keys60 / minute
POST /tenants/{id}/keys, DELETE /tenants/{id}/keys/{key_id}20 / minute

Errors

All errors return JSON with a detail field.

StatusMeaning
400Bad request — invalid field value or missing required field
401Invalid or missing API key / expired credential
403Tenant suspended, agent suspended/retired, or scope exceeded
404Agent not found (or not owned by your tenant)
422Validation error — check field types and formats
429Rate limit exceeded — back off and retry
500Internal server error — contact forged@ironweft.io

Questions or issues? Email forged@ironweft.io  ·  Privacy  ·  Terms