Basics
- Base URL:
https://api.neltava.com. JSON in, JSON out. - Authenticate with
x-api-key: sk_…. - Money is integer minor units —
amount_minor: 4900is 49.00 USD — in the currency’s own exponent (JPY 0, KWD 3). Currencies are ISO-4217, uppercase. POST /v1/decisionsrequires anidempotency-keyheader. Retrying with the same key and body returns the same decision (withidempotent-replay: true); the same key with a different body is a conflict.- Timestamps are ISO-8601 in UTC. The decision list paginates with
limitandcursor→next_cursor.
Keys and scopes
Create keys in the console (Developers → API keys). A key is shown once and stored only as a hash.
| Key | Scopes | For |
|---|---|---|
| Agent | authorize, bound to one agent | The agent itself: ask for decisions, report its outcomes, read its authority. |
| Admin | admin review read authorize | Your backend: agents, authority, keys, webhooks. |
| Reviewer | review read | A person or tool that allows, denies and labels. |
| Read-only | read | Dashboards, exports, the report. |
| Payment adapter | consume | Your payment service in Enforce: consume capabilities, report rail outcomes. More |
Errors
Errors have one shape. Branch on code; the message is for people.
{
"error": {
"code": "insufficient_scope",
"message": "this route requires the 'admin' scope",
"request_id": "7d1c…"
}
}| Status | code | Meaning |
|---|---|---|
| 400 | invalid_request | The body or query failed validation; the message names the field. |
| 401 | unauthenticated | Missing, unknown, revoked or expired key. |
| 403 | insufficient_scope | The key can't do this (e.g. an agent key calling an admin route). |
| 403 | agent_mismatch | An agent key named a different agent. |
| 403 | agent_not_active | The agent is paused or revoked. |
| 403 | self_review_forbidden | The requester can't review their own request. |
| 404 | not_found | No such resource in this workspace. |
| 409 | idempotency_conflict | The idempotency key was already used for a different request. |
| 409 | agent_exists | An agent with that slug already exists. |
| 409 | agent_limit | The plan's agent quota is reached. |
| 409 | plan_required | Enforce needs a paid plan. |
| 409 | authority_changed | The authority changed since the decision; ask again. |
| 409 | not_reviewable | Only a REVIEW decision can be reviewed. |
| 409 | review_final | An Enforce review is final. |
| 409 | outcome_conflict | The outcome contradicts what was already reported. |
| 422 | agent_required | A workspace key must name the agent. |
| 422 | currency_mismatch | The outcome's currency differs from the decision's. |
| 429 | rate_limited | Too many requests; retry after the Retry-After header. |
| 503 | auth_unavailable | Sign-in couldn't be verified right now; retry. |
Capability refusals (capability_used, payee_mismatch…) are listed in Enforce at the payment point.
Rate limits
600 requests a minute per key. Over it, the API answers 429 rate_limited with Retry-After: 60. Repeated requests with unknown keys are limited per client. The SDK retries 429s; it never treats one as permission to spend.
Decisions
POST/v1/decisionsscope: authorize
201 with the decision (200 on an idempotent replay).curl -X POST https://api.neltava.com/v1/decisions \
-H "x-api-key: $NELTAVA_AGENT_KEY" \
-H "idempotency-key: run-7-step-3" \
-H "content-type: application/json" \
-d '{
"task": "Q4 pipeline: enterprise developer leads",
"action": {
"type": "purchase",
"merchant": "Platform Weekly",
"amount_minor": 9000,
"currency": "USD",
"description": "Sponsored slot reaching platform engineers at enterprise companies"
}
}'| Body | |
|---|---|
action.type | purchase, subscription, top_up… Required. |
action.merchant | Who would be paid. Required. |
action.amount_minor, action.currency | Required. |
action.description | What, and why. |
action.payee_id | <rail>:<id>, e.g. domain:clearbit.com. A capability binds to it. |
action.category | A declared category — an untrusted claim. |
task | This run’s task, as the agent states it. |
trace | Up to 25 steps: { step, kind?, tool?, at? }. |
limitable, min_amount_minor | May the amount be capped, and to no less than what. |
agent | Only for workspace keys: the agent’s slug. |
context | Your own data, up to 8 KB, kept with the decision. |
{
"decision_id": "dec_7f3c…",
"decision": "ALLOW",
"effective_decision": "ALLOW",
"reason_code": "WITHIN_AUTHORITY",
"requested_amount_minor": 9000,
"authorized_amount_minor": 9000,
"currency": "USD",
"mode": "SHADOW",
"explanation": "Allowed: 90.00 USD at Platform Weekly is within growth-agent's authority."
}The full response also has reason_codes, policy_trace (every stage), purpose (the assessment), authority_version and authority_hash, reservation, and in Enforce a capability. What each field means.
GET/v1/decisionsscope: read (an agent key sees its own)
agent, decision, mode, reason, q (merchant), min_amount, max_amount, reviewed, labeled, from, to, order, limit (≤ 200), cursor.GET/v1/decisions/:idscope: read
Reviews and labels
POST/v1/decisions/:id/reviewsscope: review
{ action: "ALLOW" | "DENY", reason_code, comment? } on a REVIEW decision. Reasons: reviews. A review is attributed to the key’s principal_id (e.g. user:…); whoever asked for the spend can’t review it.POST/v1/decisions/:id/labelsscope: review
{ label: "PURPOSE_ALIGNED" | "PURPOSE_MISALIGNED" | "PURPOSE_UNCLEAR", comment? } on any decision.Both are append-only. An Enforce review is final; a Shadow review or any label is corrected by sending a new one with supersedes set to the id it replaces.
Outcomes
POST/v1/decisions/:id/outcomesscope: authorize · consume · admin
type: SPEND_EXECUTED, SPEND_FAILED, SPEND_CANCELLED, REFUNDED, PARTIALLY_REFUNDED. source: CLIENT_REPORTED (default; the only one an agent key may send) or PAYMENT_RAIL (a payment adapter key, for payments it made).curl -X POST https://api.neltava.com/v1/decisions/dec_7f3c…/outcomes \
-H "x-api-key: $NELTAVA_AGENT_KEY" \
-H "content-type: application/json" \
-d '{
"type": "SPEND_EXECUTED",
"amount_minor": 9000,
"currency": "USD",
"external_reference": "ch_3Pq…",
"occurred_at": "2026-09-27T14:03:00Z"
}'GET/v1/decisions/:id/outcomesscope: read
Agents and authority
POST/v1/agentsscope: admin
{ slug, name?, mode?, authority? }. New agents take the workspace mode (Shadow). Counts toward the plan’s agent quota.curl -X POST https://api.neltava.com/v1/agents \
-H "x-api-key: $NELTAVA_ADMIN_KEY" \
-H "content-type: application/json" \
-d '{
"slug": "growth-agent",
"name": "Growth agent",
"authority": {
"purpose": {
"objective": "Acquire qualified enterprise developer leads",
"success_criteria": [
"Leads are platform engineers at enterprise companies",
"Cost per qualified lead under $120"
],
"constraints": [
"No consumer social campaigns",
"Never buy contact lists"
],
"context": "B2B developer-tools company, Q4 pipeline push"
},
"currency": "USD",
"per_transaction_limit_minor": 50000,
"review_threshold_minor": 25000,
"monthly_budget_minor": 500000,
"blocked_merchants": [
"ListBroker"
]
}
}'GET/v1/agentsscope: read
GET/v1/agents/:idscope: read
PATCH/v1/agents/:idscope: admin
{ name?, status?: "active" | "paused" | "revoked", mode?: "SHADOW" | "ENFORCE" }. Revoking is permanent.POST/v1/agents/:id/authorityscope: admin
GET/v1/agents/:id/authorityscope: read
POST/v1/agents/:id/authority/simulatescope: admin
Keys, reports, ledger
Keys
POST/v1/api-keysscope: admin
{ name, agent_id } for an agent key, or { name, scopes, principal_id? }. Add expires_in_seconds (60 to 31,536,000) for a key that stops working after that long — then it answers 401 “API key expired”; without it, the key never expires. Returns the key once, with its expires_at.GET/v1/api-keysscope: admin
DELETE/v1/api-keys/:idscope: admin
Reports and workspace
GET/v1/reports/shadowscope: read
GET/v1/workspacescope: read
Ledger and audit
GET/v1/ledger/verifyscope: read
GET/v1/ledger/eventsscope: read
GET/v1/auditscope: read
Webhooks
Webhooks have their own page.