API reference
Version 1. Questions: hello@nexward.ai.
Overview
Your code asks Nexward before an AI agent uses a tool. Nexward answers ALLOW, BLOCK, or REQUIRE_APPROVAL, and your code runs the tool only on ALLOW. Nexward never connects to your systems or runs your tools.
- Agent key and tool name are names you choose and register in the dashboard (Agents page) for an application. The same names go in every request and in your policy.
- A tool must be granted to the agent. Unregistered, disabled, or ungranted agents and tools are blocked, as is any application without a published policy.
- Your application's policy decides the rest. Every matching rule counts; the most restrictive decision wins (BLOCK, then REQUIRE_APPROVAL, then ALLOW).
- Fail closed: on any error, timeout, or unexpected answer, do not run the tool.
Base URL and authentication
Base URL: https://api.nexward.ai. HTTPS only. JSON bodies (Content-Type: application/json, UTF-8, at most 64 KB). Every response has an x-request-id header; include it when contacting support.
Authenticate with an application API key, created in the dashboard under Settings, shown once: Authorization: Bearer nxw_live_.... Keys for production applications start with nxw_live_, others with nxw_test_. The key determines the application, so it determines which agents, tools, and policy apply. Use keys only on servers; never in browser or mobile code. Revoke a key in Settings if it may have leaked.
JavaScript/TypeScript SDK
@nexward/sdk (Node.js 20 or newer, server-side, ES module, no runtime dependencies) wraps the API with the fail-closed rules built in.
npm install @nexward/sdk
# environment
NEXWARD_API_KEY=nxw_test_...
NEXWARD_API_URL=https://api.nexward.aiimport { Nexward } from "@nexward/sdk";
const nexward = new Nexward(); // reads NEXWARD_API_KEY and NEXWARD_API_URL
const result = await nexward.protect(
{ agentId: "support-agent", tool: "crm.export_customers", arguments: { records: 1000 } },
(args) => exportCustomers(args.records), // runs only on ALLOW, at most once
);
// result.outcome: "executed" | "blocked" | "approval_required" | "not_executed"| Method | Does |
|---|---|
protect(action, fn) | Evaluates the action and runs fn with the evaluated arguments only on ALLOW |
evaluate(action) | Returns the decision without running anything |
getApproval(approvalId) | Status of an approval request |
resume(approvalId, action, fn) | Redeems an approved action (same action id, agent, tool, arguments) and runs fn once |
Options: apiKey, baseUrl, timeouts (default 1 s per attempt, 2.5 s total, 3 attempts), failOpenTools (tools allowed to run only when Nexward is unreachable; default none), onEvent (every outcome, for your logs). An action is { agentId, tool, arguments?, actionId?, sessionId?, context? }.
POST /v1/actions/evaluate
Ask for a decision before a tool runs. Other languages call this directly.
curl https://api.nexward.ai/v1/actions/evaluate \
-H "Authorization: Bearer $NEXWARD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "support-agent",
"action_id": "act_7f3c9a",
"invocation_id": "6f1c2b8e-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
"tool": "crm.export_customers",
"arguments": { "records": 1000 }
}'Request
| Field | Required | Description |
|---|---|---|
agent_id | Yes | Registered agent key |
tool | Yes | Registered tool name |
action_id | Yes | Your id for this attempt (1 to 128 of A-Z a-z 0-9 _ . : -). Re-sending the same action returns the original decision and is counted once; reusing it with different content is a conflict |
invocation_id | Yes | A UUID per call; reuse it when retrying the same call |
arguments | No | JSON object the tool will use; names your policy can refer to |
session_id | No | Groups actions, for example one conversation (same format as action_id) |
context | No | end_user_ref (opaque user id, never an email or name), channel (up to 64 characters), labels (up to 16 string pairs) |
Unknown fields are rejected. Do not send prompts, secrets, or whole documents.
Response 200
{
"decision": "BLOCK",
"decision_id": "0192f6a4-...",
"event_id": "0192f6a4-...",
"action_id": "act_7f3c9a",
"risk_score": 75,
"severity": "high",
"reason_codes": ["POLICY_BULK_EXPORT_LIMIT"],
"policy_version_id": "0192e1...",
"approval_id": null,
"approval_expires_at": null,
"replayed": false,
"duplicate_invocation": false,
"evaluated_at": "2026-10-27T15:04:05.123Z"
}Run the tool only if decision is exactly "ALLOW", action_id matches your request, and duplicate_invocation is false. On REQUIRE_APPROVAL, keep approval_id (valid until approval_expires_at). risk_score (0 to 100) and severity are informational. Unknown agents, tools, and a missing policy are not errors: they return 200 with BLOCK so they are recorded.
Approvals
Owners and admins approve or deny in the dashboard (Approvals). An approval covers that exact action, can be used once, and expires. Your code then:
GET /v1/approvals/{approval_id}
Returns approval_id, action_id, status (pending, approved, denied, expired, redeemed), expires_at, decided_at. Unknown or other applications' ids return 404.
POST /v1/approvals/{approval_id}/redeem
Body: the original request (same action_id, agent_id, tool, arguments) with a new invocation_id. Returns the evaluate response with ALLOW (run the tool once) or BLOCK if the current policy now blocks it (APPROVAL_SUPERSEDED_BY_BLOCK). Errors: 404 NOT_FOUND, 409 APPROVAL_NOT_APPROVED, 409 APPROVAL_ALREADY_REDEEMED, 409 APPROVAL_ACTION_MISMATCH, 410 APPROVAL_EXPIRED.
Errors and retries
Error body: { "error": { "code": "...", "message": "...", "request_id": "..." } }. In every error case, do not run the tool.
| Status | Code | Retry? |
|---|---|---|
| 400 | VALIDATION_ERROR | No: fix the request |
| 401 | UNAUTHENTICATED (missing, wrong, revoked, or expired key) | No |
| 409 | IDEMPOTENCY_CONFLICT (action_id reused with different content) | No |
| 413 | PAYLOAD_TOO_LARGE (over 64 KB) | No |
| 429 | RATE_LIMITED | Yes, after Retry-After seconds |
| 500 | INTERNAL_ERROR | No |
| 502, 503, 504 | EVALUATION_UNAVAILABLE | Yes, briefly, with the same invocation_id |
Retry only the request to Nexward, never the tool itself.
Reason codes
Your own rule codes (for example POLICY_BULK_EXPORT_LIMIT) mean that rule matched. Nexward's codes:
| Code | Meaning |
|---|---|
DEFAULT_DECISION | No rule matched; the policy default applied |
RULE_INDETERMINATE | A rule could not be checked (missing argument or wrong type); restrictive rules still applied |
UNKNOWN_AGENT | Agent key not registered in this application |
AGENT_DISABLED | Agent disabled in the dashboard |
UNKNOWN_TOOL | Tool name not registered in this application |
TOOL_DISABLED | Tool disabled in the dashboard |
TOOL_NOT_GRANTED | Tool not granted to this agent |
NO_ACTIVE_POLICY | No published policy for this application |
PLAN_LIMIT_REACHED | Free plan used more than twice its monthly allowance |
APPROVAL_GRANTED | An approved action was redeemed |
APPROVAL_SUPERSEDED_BY_BLOCK | Approved, but the current policy blocks it |
Policy reference
Each application has one policy, edited and published in the dashboard (Policies). Publishing validates it; every publish is a version you can roll back to.
{
"default_decision": "ALLOW",
"rules": [
{
"id": "bulk-export",
"decision": "BLOCK",
"reason_code": "POLICY_BULK_EXPORT_LIMIT",
"severity": "high",
"conditions": [
{ "field": "tool", "operator": "equals", "value": "crm.export_customers" },
{ "field": "arguments.records", "operator": "gt", "value": 500 }
]
},
{
"id": "email-approval",
"decision": "REQUIRE_APPROVAL",
"reason_code": "POLICY_EMAIL_REQUIRES_APPROVAL",
"severity": "medium",
"conditions": [{ "field": "tool", "operator": "equals", "value": "email.send" }],
"approval_display": ["arguments.to", "arguments.subject"]
}
]
}default_decision:ALLOW,BLOCK, orREQUIRE_APPROVAL, used when no rule matches.- Rule:
id,decision,reason_code,severity(low,medium,high,critical), 1 to 10conditionsthat must all hold, and on REQUIRE_APPROVAL rules optionallyapproval_display(up to 10arguments.*fields shown to the approver). field:tool,agent_id,arguments.name(dots for nested values, up to 8 levels), orcontext.channel,context.end_user_ref,context.labels.name. No wildcards.- Operators:
equals,not_equals(text, number, true/false, null);gt,gte,lt,lte(numbers);contains(text in text, or item in a list);in(one of up to 100 values);exists(no value). - A missing field or a different type makes a condition unknown: BLOCK and REQUIRE_APPROVAL rules still match, ALLOW rules do not. Send numbers as numbers.
- A matched BLOCK with severity high or critical opens an incident.
Names and limits
| Item | Format or limit |
|---|---|
| Agent key | Lowercase letters, digits, -, _; starts with a letter or digit; up to 64. Example support-agent |
| Tool name | Two or more dot-separated parts; each starts with a lowercase letter, then lowercase letters, digits, _; no hyphens; up to 128. Example crm.export_customers |
| Rule id | Lowercase letters, digits, -, _; up to 64; unique in the policy |
| Reason code | UPPER_SNAKE_CASE, 3 to 64 characters, not one of Nexward's codes |
| Arguments | JSON object; nesting up to 8 levels; 256 keys per object; 1,000 items per list; 8 KB per text value |
| Request body | 64 KB |
| Policy | 200 rules; 10 conditions per rule; 100 values per in list; 1,024 characters per text value |