Documentation
Everything you need to wire an agent through AgentRaaS: authentication, the two calling styles, MCP, SDKs, and self-hosting reference.
AgentRaaS is the reliability layer between your agents and the APIs they call (Stripe, Twilio, your own internal endpoint, any MCP server). Every call goes through four checks before anything real happens:
- Run once. The call claims an atomic dedup slot in Redis, so a retry from your agent, your framework or a flaky network gets the first result back instead of a second charge, message or side effect.
- Allowed? Action policies decide what each agent may do and where it may send data; spend caps limit how often it may do it.
- Needs a human? Risky calls wait for approval instead of running.
- Recorded. Every decision lands in one audit log with a per-agent identity, and goes out to your own tools through Prometheus and OpenTelemetry.
New to AgentRaaS? Start with Getting started for the click-by-click walkthrough of connecting your first agent. This page is the reference once you're wiring things up for real.
Quickstart
- Register and log in at the dashboard (Cloud), or self-host with
./install.sh. See Self-hosting below. - Click Connect a service, pick the service and paste its API key. You get your AgentRaaS URL, an AgentRaaS key (shown once) and an example call. AgentRaaS forwards with the service's key, so your agent code never touches it. (+ Connect agent gives a key and URL without a service, for example for MCP.)
- Call AgentRaaS instead of the real API, using either calling style below.
Tutorials
Every feature step by step, on its own page: Tutorials. Each one shows the clicks in the dashboard with a picture per step, then every case you can run into with the real request and response. Not a developer? Start with Start here (no code).
Library mode Free
Exactly-once execution with no AgentRaaS server at all. Decorate any function that has a side effect; retries and concurrent duplicates get the first result back instead of running it again. State lives in a local SQLite file by default, or in your own Redis.
pip install agentraas
from agentraas.local import exactly_once, RedisStore
@exactly_once() # SQLite file, one machine
def charge(customer, amount, idempotency_key=None):
return stripe.Charge.create(customer=customer, amount=amount,
idempotency_key=idempotency_key).id
charge("cus_1", 4200) # runs for real
charge("cus_1", 4200) # returns the cached id, no second charge
@exactly_once(store=RedisStore("redis://localhost:6379/0"), # across machines
key=lambda order: order["id"], ttl=3600)
async def ship(order):
...
| Option | Default | What it does |
|---|---|---|
store | SQLite file | SQLiteStore(path) for one machine, RedisStore(url) for several (needs pip install redis). |
key | all arguments | Function returning what counts as "the same call", e.g. an order ID. |
ttl | 86400 | Seconds a completed result is remembered. |
wait | 30 | Seconds a duplicate waits for an in-flight first call before raising InFlightError. |
If the function raises, nothing is cached and a retry runs it again. That includes the case where the provider did the work but the response was lost. Give the function an idempotency_key parameter and pass it to the provider: the decorator fills it with the same value on every retry, so the provider dedupes that case itself. Results must be JSON-serializable.
LangChain & LangGraph Free
Exactly-once tool calls for LangChain agents, create_react_agent and ToolNode. Wrap the tools that have side effects; when the model calls one again with the same arguments, or the graph re-runs the tool node after a crash or retry, the first result comes back instead of a second charge.
pip install "agentraas[langchain]"
from agentraas.langchain import protect_tool
tools = [protect_tool(charge_card), protect_tool(send_email), search]
agent = create_react_agent(model, tools)
Takes the same store, key and ttl options as library mode, e.g. protect_tool(refund, store=RedisStore(url), key=lambda order_id, **_: order_id). Tool results must be JSON-serializable.
CrewAI Free
Exactly-once tool calls for CrewAI agents. Wrap the tools that have side effects, either BaseTool subclasses or @tool functions; when the agent calls one again with the same arguments, or a crew is kicked off again after a crash, the first result comes back instead of a second charge.
pip install "agentraas[crewai]"
from agentraas.crewai import protect_tool
agent = Agent(role="Billing", goal="...", backstory="...",
tools=[protect_tool(ChargeCardTool()), protect_tool(send_email)])
Same store, key and ttl options as library mode. CrewAI needs Python 3.10 or newer. The original tool object is left unchanged.
Vercel AI SDK Free
Exactly-once tool calls for generateText and streamText. Wrap your tools object; when the model calls a tool again with the same input, or a request is retried, the first result comes back instead of a second charge.
npm install agentraas
import { protectTools, RedisStore } from "agentraas";
const tools = protectTools({ charge, sendEmail, search }, { store: new RedisStore(redis) });
const result = await generateText({ model, tools, prompt });
The key is the tool name plus its input; toolCallId and the other per-call options are passed through, never part of the key. Pass key: (toolName, input) => input.orderId to key on one field. Use MemoryStore for a single process, RedisStore when several serve requests. Results must be JSON-serializable.
MCP wrap Free
Exactly-once tool calls and an audit log for any stdio MCP server. Put agentraas wrap -- in front of the server's own command; everything passes through unchanged, except that an identical call to a write tool (same tool, same arguments) inside the dedup window gets the first call's result back instead of running again.
{
"mcpServers": {
"github": {
"command": "uvx",
"args": ["agentraas", "wrap", "--", "npx", "-y", "@modelcontextprotocol/server-github"]
}
}
}
| Option | Default | What it does |
|---|---|---|
--ttl | 600 | Seconds an identical write call is deduplicated. |
--dedupe TOOL | Always dedupe this tool, even if it looks read-only. Repeatable. | |
--no-dedupe TOOL | Never dedupe this tool. Repeatable. | |
--db | ~/.agentraas/mcp-dedup.db | SQLite file for dedup state. |
--log | ~/.agentraas/mcp-audit.jsonl | JSON-lines audit log of every tool call; '' disables it. |
Read-only tools always run, so lookups stay fresh: the server's readOnlyHint annotation decides, falling back to names like get_*, list_* and search_*. A failed call (JSON-RPC error or isError: true) is not cached, so a retry runs for real. A deduplicated result carries _meta: {"agentraas/deduplicated": true}.
MCP safety scan Free
agentraas scan starts each stdio MCP server in your Claude Desktop, Cursor or Claude Code config, asks it for its tools, and sorts them: moves money, deletes, runs code, sends (high risk), writes (medium), reads (low). It sends only initialize and tools/list; no tool is called. The sorting uses the tools' readOnlyHint and destructiveHint annotations and the words in their names, so check the list rather than trusting it blindly. More on the MCP safety scan page.
agentraas scan # every config found
agentraas scan --config .mcp.json --fail-on high # CI: exit 1 on a high-risk tool
agentraas scan -- npx -y @modelcontextprotocol/server-github
In GitHub Actions: uses: agentraas/agentraas/src/scan-action@main with config (default .mcp.json) or command, and fail-on (high, medium or none). The result goes to the job summary.
| Flag | What it does |
|---|---|
--config PATH | Scan the servers in this config file (mcpServers). Repeatable. Without it: Claude Desktop, ~/.cursor/mcp.json, ~/.claude.json, ./.mcp.json. |
--fail-on | high or medium: exit 1 when a tool at that level or above is found. |
--json | Print the result as JSON. |
--timeout | Seconds to wait for each server, default 60. |
Scanning runs the commands in the config, with their environment, as your MCP client would: only scan configs you trust. Remote (URL) servers are listed but not scanned.
Chaos tester Free
agentraas-chaos sits between your code and an API. The first time each distinct write request (POST, PUT, PATCH, DELETE) arrives, it lets the request execute and then drops the response: the network failure that turns a retry into a double charge. It then lists every action that executed more than once.
# safe: a built-in mock answers every call
agentraas-chaos --mock -- python my_agent.py
# a real API: use test-mode keys
agentraas-chaos --upstream https://api.stripe.com -- pytest
Your command gets AGENTRAAS_CHAOS_URL (for example http://127.0.0.1:8787) to use as its API base URL. Without a command it runs until Ctrl-C, so you can point an n8n workflow or any other tool at it by hand. It exits with code 1 when it finds a duplicate. A retry that repeats an Idempotency-Key it has already seen counts as protected.
| Flag | What it does |
|---|---|
--mock | Answer every call with a fake 200. Nothing real is called. |
--upstream URL | Forward to a real API base URL. |
--fault | lost-response (default) drops the connection; error-after-commit returns a 502 instead; none only records. |
--port | Local port, default 8787. |
GitHub Actions
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- uses: agentraas/agentraas/src/chaos-action@main
with:
run: python my_agent.py # optional: upstream, fault
Local tunnel
Test webhooks against code running on your laptop without deploying it or configuring a public URL. agentraas tunnel opens a connection to AgentRaaS, prints a public URL, and replays every request sent to that URL against a local port. Available on every plan, including Community.
pip install "agentraas[tunnel]"
agentraas tunnel --port 3000 --org org_acme --agent agent_1 --key ar_live_...
Tunnel is live: https://agentraas.io/t/tn_3f9c2a71d04b8e65
Forwarding to: http://localhost:3000
POST /webhooks/stripe -> 200 (4ms)
Paste the public URL (plus any path, for example /webhooks/stripe) into the provider's webhook settings. The key is the agent API key from the dashboard.
| Limit | Value |
|---|---|
| Tunnels per org | 1 (opening a second closes the first) |
| Session length | 2 hours, then re-run the command |
| Local response timeout | 30 seconds |
| Requests through the public URL | 120/min |
Authentication
Every route accepts your API key in a different place depending on the calling style you use; there's no single global header. Keys start with ar_live_ and are shown exactly once when you connect an agent; AgentRaaS only ever stores a hash of it.
| Calling style | Where the key goes |
|---|---|
| Webhook | Authorization: Bearer <key> |
| SDK-style REST | x-agentraas-key: <key> header |
| MCP | x-agentraas-key: <key> header |
One key, every service. Your services' own keys (Stripe, Slack, Alpaca and the rest) are stored once per org. An agent's AgentRaaS key works for every service the org has connected: only service in the call changes. To limit a key, open Connect agent, find the key and use Change next to "Works with" to switch services on or off for it, or PUT /api/v1/agents/keys/:id/services with {"services": ["stripe", "slack"]} (null for every service). A call to a service that is off gets 403. Regenerating a key keeps its switches. Free on every edition.
Lost a key? Connect a new agent and revoke the old one from the Credentials panel; there's no way to have a lost key re-displayed.
REST: webhook style
The simplest integration: point any no-code tool (n8n, Make, Zapier) or a plain curl/HTTP client at one URL per agent. org_id and agent_id live in the path, so nothing else needs configuring per request.
curl -X POST https://your-instance/v1/webhook/acme-corp/billing-bot \
-H "Authorization: Bearer ar_live_..." \
-H "Content-Type: application/json" \
-d '{
"service": "stripe",
"action": "charge.create",
"payload": { "amount": 5000, "currency": "usd", "customer": "cus_123" }
}'
| Field | Required | Description |
|---|---|---|
service | Yes | Which connector. See Supported services, or a Custom Action's name. |
action | Yes | Which operation on that service. |
payload | No | Fields the action needs, matching the real API's own shape. |
end_user_id | No | Your own identifier for who this action was on behalf of. Shows up in the audit trail. |
resource_id | No | Your own identifier for what was acted on (an order id, a ticket id, etc.). While one call naming it is in flight, a second call naming the same resource_id gets 409 instead of racing it; the lock is released when the first call finishes. |
A successful response, first call vs. a retry of the same request:
→ 200 OK // first call, forwarded, result cached
{"result":{"status":"success","charge_id":"ch_1N..."}}
→ 200 OK // identical retry, dedup slot already claimed
{"result":{"status":"success","charge_id":"ch_1N...","deduplicated":true}}
REST: SDK style
Used by the official SDKs (see SDKs): org_id/agent_id travel as headers instead of the URL, so one client instance can be reused across calls without rebuilding a path each time.
curl -X POST https://your-instance/v1/sdk/stripe/charge.create \
-H "x-agentraas-key: ar_live_..." \
-H "x-agentraas-org: acme-corp" \
-H "x-agentraas-agent: billing-bot" \
-H "Content-Type: application/json" \
-d '{ "amount": 5000, "currency": "usd", "customer": "cus_123" }'
The request body here is the payload directly (no wrapping service/action/payload envelope; those are already in the path and headers). Response shape is identical to the webhook style above.
Checks an agent key without doing anything: 200 {"ok": true, "org_id": "..."} for a valid key, 401 for a missing, unknown or revoked one. Useful as a credential test in integrations (the n8n node uses it).
MCP
For Claude Desktop, Cursor, or any MCP client: every tool call through this gateway gets the same dedup, validation, rate-limiting, and audit logging as the REST routes.
{
"mcpServers": {
"agentraas": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://your-instance/mcp",
"--header", "x-agentraas-key:ar_live_..."]
}
}
}
Standard JSON-RPC 2.0 over HTTP (tools/list, tools/call). Authenticate with the same x-agentraas-key header as SDK-style REST.
Idempotency & tracing
Optional headers accepted by both the webhook and SDK-style routes, useful once you're running agents in production and want to correlate calls with your own systems:
| Header | Purpose |
|---|---|
x-agentraas-idempotency-key | Your own dedup key, if you want to control the dedup window's scope yourself instead of relying on the request body being byte-identical. |
x-agentraas-run-id | Correlates every call from one agent "run" together in the audit trail. |
x-agentraas-step-id | Identifies which step within a run this call was, for multi-step agent workflows. |
x-agentraas-end-user | SDK-style equivalent of the webhook body's end_user_id. |
x-agentraas-resource-id | SDK-style equivalent of the webhook body's resource_id. |
What reaches the provider
Every forwarded call carries Idempotency-Key: agentraas-<hash>, one stable value per logical action. It stays the same when AgentRaaS retries, when your agent retries, and when you replay the call from the dead-letter queue, so a provider that honors the header (Stripe and others) runs the action once even if a copy reaches it twice. A Custom Action that sets its own Idempotency-Key header keeps it.
When the provider doesn't answer
If a call times out, or the connection drops after the request was sent, nobody can tell whether it ran. AgentRaaS does not retry it and does not free its dedup slot. You get a 504 with "outcome": "unknown", the call goes to the dead-letter queue, and identical calls get a 409 instead of running again. Replay it from the dead-letter queue to resolve it: the replay reuses the same Idempotency-Key, and once it succeeds, identical calls get its result. A refused connection or DNS failure means nothing was sent, so those are still retried as normal.
Errors
Every error response is a flat JSON object with the same shape, regardless of route:
{ "error": "A human-readable message." }
Some errors add a machine-readable code field alongside error for cases worth branching on in your own code (e.g. EMAIL_NOT_VERIFIED), and a few add extra context fields (e.g. replayed: true on a deduplicated dead-letter replay). Check the specific status code first, code isn't present on every error.
| Status | Meaning |
|---|---|
401 | Missing or invalid API key. |
402 | Monthly usage limit reached (Cloud only; self-hosted has no enforced limit on any tier). |
403 | Authenticated, but not permitted for this org/action, or blocked by an action policy (the message names it). |
422 | Request validation failed. Nothing was sent. validation_errors lists every failing field with a hint saying what to send instead (example below). |
424 | The service isn't set up for this org yet: no key saved for it (Setup, Connect a service, or Credentials). Nothing was sent. Not worth retrying until someone adds the key. |
429 | Rate limit exceeded. See Plans & limits. |
409 | An identical call is still in flight, or an earlier identical call has an unknown outcome (see Idempotency & tracing). |
502/503 | The upstream service (Stripe, Twilio, etc.) was unreachable or errored; AgentRaaS fails closed rather than silently succeeding un-deduplicated. |
504 | The upstream didn't respond, so the action may or may not have run. Not retried; "outcome": "unknown" in the body. |
A failed validation names every bad field at once, so an agent can fix them all in one retry. An MCP client gets the same body as an error tool result, which the model reads.
{
"error": "amount must be number, got string",
"validation_errors": [
{ "field": "amount", "rule": "type", "message": "amount must be number, got string",
"hint": "Send \"amount\" as a JSON number: 49.99, without quotes." },
{ "field": "currency", "rule": "enum", "message": "currency must be one of: usd, eur",
"hint": "Send \"usd\" (exact spelling) in \"currency\"." }
],
"next_step": "Nothing was sent. Fix every field listed in validation_errors, following each hint, and send the call again."
}
SDKs
Thin, dependency-light wrappers over the SDK-style REST routes above. Both work against Cloud or any self-hosted instance.
Python
pip install agentraas
import agentraas
client = agentraas.Client(
agentraas_key="ar_live_...",
org_id="acme-corp",
agent_id="billing-bot",
base_url="https://your-instance",
)
result = client.call("stripe", "charge.create", {"amount": 5000, "currency": "usd"})
TypeScript / JavaScript
Install from npm: npm install agentraas. Pass { idempotencyKey } as a fourth argument to call() to choose your own dedup key.
import { Client } from "agentraas";
const client = new Client({
agentraasKey: "ar_live_...",
orgId: "acme-corp",
agentId: "billing-bot",
baseUrl: "https://your-instance",
});
const result = await client.call("stripe", "charge.create", { amount: 5000, currency: "usd" });
Both SDKs are MIT-licensed. Neither retries automatically: since AgentRaaS claims the dedup slot server-side before forwarding, your own retry logic (as aggressive as you want) is always safe to call again with the same payload.
Supported services
Curated, pre-configured integrations, no code required:
Stripe, Twilio, HubSpot, Calendly, Shopify, Zoho, Razorpay, WhatsApp, Zapier, Make, Adyen, Mollie, Airwallex, Xendit, PayPal, Salesforce, Slack, Klarna, Paystack, GoCardless, Opn Payments, GitHub, Resend, SendGrid, Notion, Zendesk, Jira, Alpaca (trading, paper and live), Upstox and Dhan (India, NSE/BSE), Cashfree, RazorpayX, Gupshup, MSG91, Delhivery, Shiprocket and PhonePe (test mode) (online shopping and quick commerce), plus the built-in mockpay for safe testing.
Keys that expire: PayPal, PhonePe and Shiprocket give out a token that expires (PayPal after hours, Shiprocket after 10 days). Save the long-lived credentials once (PayPal's client ID and secret, PhonePe's client ID and secret, a Shiprocket API user's email and password); AgentRaaS logs in itself, keeps the token until shortly before it expires, and if the service answers 401 anyway (a token revoked early), logs in again and sends the call once more: a 401 means it never ran. Wrong credentials answer 424 with the reason. A PayPal access token saved earlier as a plain key still works until it expires.
Jira: connect it with your Atlassian account email and an API token (id.atlassian.com, Security, API tokens). Actions: issue.create, issue.comment, issue.transition, issue.delete. Every call names your site (the part before .atlassian.net); comment, transition and delete also name the issueKey. The body is Jira's own REST v2 shape, so description is plain text.
client.call("jira", "issue.create", {
"site": "yourcompany",
"fields": {"project": {"key": "OPS"}, "issuetype": {"name": "Task"},
"summary": "Refund failed for order 1042"}})
The dashboard's Outage alerts panel can also open Jira tickets for you: when a service stops answering, its response shape changes, or a call may have run without an answer back. At most one ticket per alert per day.
Need something not on this list? See Custom Actions below.
Custom Actions
Register any endpoint yourself from the Custom actions panel (any URL, any auth type) and it gets the same dedup, audit trail, and SSRF guarding (so it can't be pointed at your own internal infrastructure) as a curated service. Call it exactly like a built-in service, using its registered name as service.
Undo log Free
When an agent does something it shouldn't have, undo it from the dashboard (Monitor → Undo actions). Every action that ran and has a known reverse is recorded for 30 days with what reversing it takes: a Stripe charge is refunded, a Slack message is deleted. Each entry can be undone once: two clicks at the same moment still run the reverse once, and it is sent with the Idempotency-Key agentraas-undo-<id>. If the provider doesn't answer, the entry is marked outcome unknown and not retried. Actions without a reverse (an SMS, an email) don't appear. Duplicates AgentRaaS caught and failed calls never ran, so there is nothing to undo.
Built in: stripe charge.create → charge.refund, slack message.post → message.delete, jira issue.create → issue.delete, and mockpay payment.create → payment.refund for trying it. For your own endpoint, register its reverse as another custom action and name it when you register the action:
POST /api/v1/custom-actions
{ "org_id": "org_acme", "name": "create_ticket", "target_url": "https://api.example.com/tickets",
"undo": { "action": "close_ticket",
"with": { "ticket_id": "response.id", "reason": "payload.title" } } }
with fills each field of the reverse call from the original's response (response.<path>) or payload (payload.<path>); dotted paths, array items by index. If a value is missing the action is not recorded as undoable. API: GET /api/v1/undo-log?org_id= lists entries, POST /api/v1/undo-log/<id>/undo undoes one (any member who can write to the org). Undo calls appear in the audit log with status undo and are never billed.
Record and replay Free
Test an agent against what real providers answered, without calling them again. Run it once with X-AgentRaaS-Record: <name> on its calls (webhook or SDK endpoint): every call that succeeds is kept under that name for 30 days, payload and response encrypted. Then run it again, in a test or in CI or after changing a prompt, with X-AgentRaaS-Replay: <name>: each call is answered from the recording and nothing reaches the provider, nothing is deduplicated and nothing counts as usage.
curl -X POST https://agentraas.io/v1/webhook/org_acme/support-bot \
-H "Authorization: Bearer $AGENT_KEY" -H "X-AgentRaaS-Replay: refund-flow" \
-H "X-AgentRaaS-Run-Id: ci-1234" \
-d '{"service":"stripe","action":"charge.create","payload":{"amount":500}}'
{ "upstream_response": { ... as recorded ... }, "reqId": "...",
"replay": { "recording": "refund-flow", "position": 1, "payload_matches": true } }
Answers are played back in order: the second stripe charge.create of a replay gets the second one recorded. payload_matches is false when the agent now sends something different, and a call the recording doesn't have gets a 404, so a test can fail on either. The position is kept per agent and X-AgentRaaS-Run-Id for an hour: send a new run id to start from the top. From the SDKs: Client(..., record="refund-flow") or replay="refund-flow", run_id=... in Python (0.10.0), record / replay / runId in the JS Client options (0.4.0). Only successful, non-streaming calls are recorded. MCP calls record and replay the same way: set the headers on the MCP connection (X-AgentRaaS-Run-Id, or a run_id tool argument). In the dashboard: Monitor → Recordings lists them, shows each call and deletes one. API: GET /api/v1/recordings?org_id= lists recordings, GET /api/v1/recordings/<name>?org_id= shows each call with its payload and response, DELETE on the same URL removes it.
Shadow mode Free
Run a new or changed agent against real traffic before you let it act. Put an agent in shadow mode (dashboard: Rules → Shadow mode), for every service, one service or one action, and every write it makes is checked and recorded but never sent. Its stored provider keys are never read for those calls, and nothing is billed or counted as usage. Reads (GET actions, including a custom action registered as GET) are still sent so the agent sees real data, unless you tick "Don't send reads either". Tools on an MCP server you registered always count as writes. A held call approved after its agent was put in shadow mode, and a dead-letter replay for that agent, are not sent either; undoing an action that already ran still works. Shadow mode is per agent id, so give each agent its own key.
The agent gets a placeholder answer so it can carry on, with the body you set:
{ "shadow": true, "decision": "would_block:spend_cap_exceeded",
"detail": "more than 100 calls (spend cap #4)", "reqId": "req_...",
"response": { "id": "shadow_payment", "status": "succeeded" },
"message": "Shadow mode: nothing was sent to stripe. This is a placeholder answer, not the provider's." }
Each call is still run past your rules, without side effects: a live duplicate is found but no dedup slot is taken (so the same call made live later still runs), spend caps are checked but not counted, validation, action policies and approval rules are checked. decision says what would have happened: would_execute, would_deduplicate, would_wait_for_lock, would_block:<reason> (including an open circuit breaker or a used-up plan) or would_need_approval:<reason>. A policy limit that AgentRaaS reads from the provider first (max_from) is not checked in shadow mode, since that read would use the keys; detail says so. If AgentRaaS can't read whether an agent is in shadow mode, it refuses the call with 503 rather than send it.
The calls are listed under Monitor → Shadow calls, filterable by agent, service and decision, with a payload preview after PII/DLP redaction (no preview when redaction is off). Go live takes the agent out of shadow mode; the recorded calls are not replayed. API: GET/POST /api/v1/shadow-rules, DELETE /api/v1/shadow-rules/<id>?org_id=, POST /api/v1/shadow-rules/promote ({"org_id", "agent_id"}), GET /api/v1/shadow-calls?org_id=&agent_id=&service=&decision=. Shadow calls are in the audit log with status shadow. Any member who can write to the org can change shadow rules; with roles (Enterprise), only org admins can.
Savings reports Free
A report of what AgentRaaS caught for an org, for people who never open the dashboard: total actions, duplicates caught, calls blocked by a policy or a spend cap, calls held for approval (approved, denied, still pending), and per agent its duplicate, block and error rates. Per service: error rate, timeouts (calls whose outcome is unknown), circuit breaker trips (counted for the whole server, not one org) and schema drift alerts. Plus the five most duplicated actions and the number of shadow calls, which are never counted as actions.
Money protected is an estimate: the amounts of payments AgentRaaS stopped a duplicate of, per currency, never converted. Each payment counts once however often it was retried, so an agent stuck retrying one payment doesn't inflate it. It is read from the request body for payment services that send an amount and a currency (Stripe, Razorpay, RazorpayX, Adyen, Mollie, Airwallex, Xendit, PayPal, Klarna, Paystack, GoCardless, Opn, Cashfree) and recorded from the release that added this report, so duplicates caught before it have no amount.
In the dashboard: Monitor → Savings report, for the last 7 or 30 days, this month or last month, with downloads as a single HTML page (no scripts, opens anywhere) or CSV. Turn on the monthly email there to send last month's report to up to 10 addresses at the start of each month, through the server's SMTP settings. Nothing leaves the server otherwise. Reports hold only agent ids, service and action names, counts and amounts, never payloads. On Enterprise the HTML page and email use your white-label name, logo and colour (PUT /api/v1/org-branding/<org_id> with display_name, logo_url, accent_color).
GET /api/v1/reports?org_id=org_acme&period=month&month=2026-10&format=html
GET /api/v1/reports?org_id=org_acme&period=30d&format=csv
PUT /api/v1/reports/settings {"org_id": "org_acme", "enabled": true, "recipients": ["ops@acme.com"]}
period is 7d, 30d or month (with month=YYYY-MM, default this month); format is json (default), csv or html. Times are UTC.
Record locks Free
Duplicate protection catches the same call sent twice. Record locks catch two different calls on the same record at the same moment: a billing agent changing customer cus_123's subscription while a support agent refunds that customer, or two agents updating one HubSpot contact. They are on by default; turn them off per org in Rules → Record locks. While one call on a record runs, a second call on that record, from any agent, gets 409 instead of racing it:
{ "error": "Another call is changing this stripe customer right now. Retry after it finishes.",
"code": "resource_locked", "record": "stripe customer",
"held_by_agent": "billing-agent", "retry_after_ms": 1800, "reqId": "req_..." }
Which record an action changes is set in the service catalog: Stripe charges, subscriptions and invoices lock the customer, refunds lock the charge or payment intent, a subscription cancel locks the subscription, HubSpot contact.update the contact, Razorpay refunds the payment, Cashfree refunds the order, Jira transitions and deletes the issue, Shopify inventory updates the item, Zoho invoices the customer (the dashboard lists them). A call whose payload doesn't name the record runs without a lock, and calls on different records never wait for each other. The record id is hashed before it is stored, and the lock is released as soon as the call ends; it is kept only when the outcome is unknown (the call may still be running), at most for the longest a call can take.
To wait instead of getting the 409 at once, send lock_wait_ms (up to 5000): in the webhook body, as the X-AgentRaaS-Lock-Wait-Ms header on SDK calls (Python SDK 0.16.0+: client.call(..., lock_wait_ms=2000); JS SDK 0.6.0+: { lockWaitMs: 2000 }; your own lock id: resource_id / resourceId), or as an MCP tool argument. The call then runs as soon as the record is free, or gets the 409 when the time is up. Waiting callers are served in the order they arrived (0.30.3 and later), and while anyone waits, a new call can't jump ahead of them.
A custom action can name the record it changes when you register it: "lock": {"record": "order", "from": ["order_id", "order.id"]} (the first payload path that has a value wins).
A call held for approval gives up its lock while it waits and takes it again when it is approved, waiting up to 5 seconds for it; if another call still holds the record, the approved call goes to Failed requests to replay. If another call changed the record while it waited, the dashboard's Waiting for approval list and the approval link page say so, so the approver can check the call still makes sense. Shadow calls never take a lock; one that finds the record locked reports would_wait_for_lock. For your own endpoints, or to lock something else, send resource_id yourself; a call that also changes a catalog record takes both locks. API: GET /api/v1/record-locks?org_id=, PUT /api/v1/record-locks with {"org_id", "enabled"}.
Human approval Team
An approval rule (dashboard: Rules → Approval rules, or /api/v1/hitl-rules) holds a matching call instead of running it: always, or when a payload field passes a threshold (amount > 50000). The caller gets 202 with a status_url to poll; the call runs only after a person approves it, and a retry of the same call while it waits gets 409, never a second hold.
Where the request reaches people:
- Slack app: an Approve/Deny card in a channel (bot token and signing secret).
- Any alert channel: Slack or Discord webhooks, Telegram, Microsoft Teams (a Workflows webhook URL) and email get the request with a one-time approval link. Add them in the dashboard's Outage alerts panel. Jira is the exception: it gets alerts, not approval requests.
An approval link opens a page with the action and its payload. Opening it decides nothing, so link previews in chat apps and mail scanners can't approve a call; only the Approve or Deny button does. The first decision wins, from any channel, and the call runs once. A link expires after 7 days. Anyone holding the link can decide, so send approval requests only to channels the right people read.
Action policies Free
Decide what each agent is allowed to do: the support agent may refund up to 50, may never delete a customer, may only email your customers' domains. Set them in the dashboard (Rules, Action policies) or through the API. Every matching policy applies, and a refused call gets a 403 naming the policy and is recorded in the audit log.
POST /api/v1/action-policies
{ "org_id": "org_acme", "agent_id": "support-bot",
"service": "stripe", "action": "refunds.create", "effect": "require",
"fields": { "amount": { "max": 5000 } } }
{ "org_id": "org_acme", "agent_id": "support-bot",
"service": "hubspot", "action": "*", "effect": "deny" }
{ "org_id": "org_acme", "service": "resend", "action": "emails.send",
"effect": "require", "fields": { "to": { "domains": ["acme.com"] } } }
{ "org_id": "org_acme", "service": "resend", "action": "*", "effect": "require",
"fields": { "*": { "no_secrets": true }, "to": { "new_destination": true } },
"on_violation": "hitl" }
| Field | What it does |
|---|---|
agent_id | Optional. Leave it out to apply to every agent in the org. |
action | A service action, or * for every action of the service. |
effect | deny never allows the call; require allows it only when the payload passes fields. |
fields | Same syntax as validation rules (min, max, enum, format, required, ...), plus domains: an email or URL field (or a list of them) must be on one of these domains or a subdomain. Two rules guard against a prompt-injected agent: no_secrets: true refuses a call whose field contains an API key, token or private key (use the field name * to scan the whole payload), and new_destination: true sends a call to an email address, phone number or URL host this org has never approved for approval once; approving it remembers the destination (needs on_violation: hitl). |
on_violation | block (default), or hitl to send the call for approval instead. A block always wins over an approval. |
hours | Optional, on a require policy: the call is allowed only inside these hours, e.g. {"tz": "America/New_York", "days": ["mon", "tue", "wed", "thu", "fri"], "from": "09:30", "to": "16:00"}. tz is an IANA time zone (daylight saving is handled), days defaults to every day. A policy can have hours, fields or both. |
Limits the server looks up (max_from)
A limit like max or maxField is checked against the call itself, so an agent that is wrong or was talked into it can send any number it likes. max_from reads the number from the provider instead, with your saved key, right before the call runs. A support agent's refund can then be held to half of what the order really cost:
POST /api/v1/action-policies
{ "org_id": "org_acme", "agent_id": "support-bot",
"service": "cashfree", "action": "refund.create", "effect": "require",
"fields": { "refund_amount": { "max_from": {
"action": "order.get", "with": { "order_id": "order_id" },
"field": "order_amount", "factor": 0.5 } } },
"on_violation": "hitl" }
| Key | What it does |
|---|---|
action | The read call to make, such as order.get (Cashfree) or payment.get (Razorpay). |
with | Which fields of the call feed the read: {"order_id": "order_id"} sends the call's order_id as the read's order_id. |
field | Where the number is in the answer, a dotted path such as order_amount or data.total. |
factor | Optional. 0.5 allows half of it; the default allows all of it. |
minus | Optional. Subtracts a second number from the answer, such as Razorpay's amount_refunded, so earlier partial refunds count. |
service | Optional. Read from another service, such as a custom action on your own order API ("service": "custom"), when credits or wallet top-ups have no provider to ask. |
Anything that can't be checked is not let through: a missing amount (which for many providers means "refund everything"), a missing order id, a read that fails, or an answer with no number at field all count as a violation, and on_violation decides whether that blocks the call or sends it for approval. The agent's error gives the numbers: refund_amount 700 is more than the 600 allowed (50% of order_amount, read from cashfree.order.get). Each matching call makes one extra read call to the provider. To try it without a provider, the sandbox service mockpay has order.get: an order id ends in its amount, so order_1200 is an order of 1200.
Rules as code Free
Keep an org's dedup rules, validation rules, action policies and spend caps in a YAML file in your repo, review changes like code, and apply them from CI. agentraas apply makes the server match the file through the normal API: it creates what is missing and replaces what changed. Running it twice changes nothing.
pip install "agentraas[apply]"
# agentraas.yaml
apiVersion: agentraas.io/v1alpha1
kind: DedupRule
metadata:
name: charge-once # for people reading the file; not stored
org: org_acme
spec:
service: stripe
action: charges.create
fields: [customer, amount]
ttlSeconds: 300
---
apiVersion: agentraas.io/v1alpha1
kind: ActionPolicy
metadata: {org: org_acme}
spec: {agentId: support-bot, service: stripe, action: refunds.create, effect: require, fields: {amount: {max: 5000}}}
---
apiVersion: agentraas.io/v1alpha1
kind: SpendCap
metadata: {org: org_acme}
spec: {service: stripe, action: refunds.create, window: day, maxCalls: 100, onExceed: block}
export AGENTRAAS_URL=https://agentraas.example.com
export AGENTRAAS_EMAIL=ci@acme.com AGENTRAAS_PASSWORD=... # a user with write access to the org
agentraas apply -f agentraas.yaml --dry-run # print the plan, change nothing
agentraas apply -f agentraas.yaml # apply it
agentraas apply -f agentraas.yaml --prune # also delete rules the file doesn't list
| Kind | Same as | One rule per |
|---|---|---|
DedupRule | Dedup rules | service + action |
ValidationRule | Validation rules | service + action |
ActionPolicy | Action policies | agent + service + action + effect |
SpendCap | Spend caps | agent + service + action + window |
A SpendCap limits calls (maxCalls), a money total (amountField and maxAmount: the sum of that payload field per window, with a call that has no number in the field refused), or both; timezone (an IANA zone) sets where its day or hour starts, UTC by default. A call refused by the cap doesn't use up the window. Spec keys are the API's fields in camelCase (ttl_seconds becomes ttlSeconds, on_violation becomes onViolation). Without --prune, rules made in the dashboard are left alone; with it, every rule of these four kinds in the orgs the file names that the file doesn't list is deleted. The command exits non-zero if any change fails, so a CI job fails with it.
Inbound webhooks Enterprise
For the reverse direction: a third party (e.g. WhatsApp) calling into your system. AgentRaaS verifies the provider's real signature (HMAC or ECDSA, whichever that provider uses) before forwarding the payload on to your own destination URL, so you get signature verification without implementing it per provider.
Manage receivers from the dashboard's Outage alerts panel, or via /api/v1/inbound-webhooks.
Self-hosting
Self-hosted AgentRaaS has no enforced monthly usage limit on any tier; it's your infrastructure. One Docker Compose service, no data leaving your control.
./install.sh
Linux: Docker or Podman. macOS: Docker Desktop or Colima. Windows: Docker Desktop with WSL2, run from a WSL terminal with the repo cloned inside WSL. Details on Self-hosting.
On AWS with managed services (ECS Fargate, RDS, ElastiCache, KMS), use the Terraform in infra/aws; on Azure (Container Apps, Database for PostgreSQL, Cache for Redis, Key Vault) infra/azure; on Google Cloud (Cloud Run, Cloud SQL, Memorystore, Cloud KMS) infra/gcp. None of the three has been applied to a real account yet. On VMware, Nutanix, OpenShift or air-gapped networks, see Private platforms. On any other container platform, run agentraas-migrate from the same image before the API starts: the image carries the service catalog and the migrations.
./install.sh generates fresh secrets, runs migrations, and starts the stack. Key environment variables (full reference in .env.example):
| Variable | Purpose |
|---|---|
JWT_SECRET | Signs dashboard sessions. Required; the server won't start without it. |
CREDENTIALS_ENCRYPTION_KEY | Encrypts stored service credentials at rest. Required, 32 bytes. |
PUBLIC_URL | Used for email links and the Connect Agent webhook/MCP URLs. Set this to your real domain. |
AGENT_RATE_LIMIT_PER_MIN | Per-key rate limit across webhook/SDK/MCP routes. Default 300. |
CLIENT_IP_HEADER | Behind a reverse proxy, the header it sets with the real client IP (x-real-ip for nginx, cf-connecting-ip behind Cloudflare), so login and signup rate limits apply per visitor. Only name a header your proxy always sets; unset uses the connecting address. Never x-forwarded-for: clients can prepend to it. |
PROXY_TIMEOUT_SECONDS | How long one forwarded call may take (default 30). A call that hits it is held as outcome unknown, not retried. |
OTEL_EXPORTER_OTLP_ENDPOINT | Optional. Sends every call AgentRaaS handles (ran, deduplicated, blocked, held for approval) as an OpenTelemetry span over OTLP/HTTP to your collector or tracing tool (Langfuse, Grafana, Datadog, Honeycomb, Jaeger), named execute_tool service.action per the GenAI conventions. Calls sharing an X-AgentRaaS-Run-Id share a trace. Auth goes in OTEL_EXPORTER_OTLP_HEADERS (key=value,key2=value2); OTEL_EXPORTER_OTLP_TRACES_ENDPOINT takes a full URL instead. Payloads are never exported. |
SMTP_* | Optional. Without it, password-reset links are logged server-side instead of emailed. |
REDIS_URL | Set by ./install.sh. Use a rediss:// URL for Redis over TLS (0.30.2 and later; the certificate must come from a public CA). |
ALLOWED_PRIVATE_NETWORKS | Optional, self-hosted only. CIDR ranges agents may reach, for services that only have an internal address. See Private platforms. |
Without a license key the server runs free with Community and Team features (human approvals, semantic dedup, SSO), no action limit. Enterprise features (agent identity, inbound webhooks, white-label, secret references) need an Enterprise key in LICENSE_TOKEN: ask hello@agentraas.io. A missing or expired key falls back to the free tier; the server always starts.
Kubernetes (Helm)
The repo ships a Helm chart in infra/helm/agentraas. It runs the Community image (ghcr.io/agentraas/agentraas, tag = the chart's app version) with a bundled single-instance Postgres and Redis, or with your own.
git clone https://github.com/agentraas/agentraas && cd agentraas
helm install agentraas ./infra/helm/agentraas \
--set publicUrl=https://agentraas.example.com \
--set ingress.enabled=true --set ingress.host=agentraas.example.com \
--set clientIpHeader=x-real-ip
With a managed database and Redis:
helm install agentraas ./infra/helm/agentraas \
--set postgresql.enabled=false --set externalDatabase.url=postgres://user:pass@host:5432/agentraas \
--set redis.enabled=false --set externalRedis.url=redis://host:6379
| Value | What it does |
|---|---|
publicUrl | The URL people reach the dashboard at (email links, Connect Agent URLs). |
existingSecret | A Secret holding JWT_SECRET, CREDENTIALS_ENCRYPTION_KEY and, with an external database, DATABASE_URL. Leave empty and the chart generates them on first install and keeps them on upgrade. |
clientIpHeader | The header your ingress writes with the client IP (see CLIENT_IP_HEADER in Self-hosting). |
metricsToken | Turns on /metrics. |
extraEnv | Any other environment variable, e.g. SMTP_HOST, OTEL_EXPORTER_OTLP_ENDPOINT. |
replicaCount | Keep 1 with the bundled datastores. The API itself is stateless (dedup state lives in Redis). |
Every database migration runs before the API starts, in an init container that holds a Postgres advisory lock so replicas don't race; all migrations are safe to re-run. The API runs with a read-only root filesystem and is probed on /health.
Back up CREDENTIALS_ENCRYPTION_KEY (in the generated Secret). Without it, the third-party credentials AgentRaaS stores can't be decrypted. Redis holds the dedup slots: run it with persistence, or a restart reopens recent calls to duplicates.
Key management (KMS)
Service keys, SSO secrets and webhook targets are stored encrypted with one AES-256 key, CREDENTIALS_ENCRYPTION_KEY. By default that variable is the key itself. To keep it out of your .env, encrypt it with a key in your KMS and store the result instead: the server asks the KMS to decrypt it once at startup, and the master key never leaves the KMS. Stored data does not change, so you can switch an existing install over without re-encrypting anything.
Start from your current key (or a new one from openssl rand -base64 32 on a fresh install):
KEY='<current CREDENTIALS_ENCRYPTION_KEY>'
echo "$KEY" | base64 -d > key.bin # AWS and GCP read the raw bytes
HashiCorp Vault / OpenBao (transit engine; the token only needs update on transit/decrypt/agentraas):
vault secrets enable transit
vault write -f transit/keys/agentraas
vault write -field=ciphertext transit/encrypt/agentraas plaintext="$KEY"
CREDENTIALS_KEY_PROVIDER=vault
CREDENTIALS_KMS_KEY=agentraas
CREDENTIALS_ENCRYPTION_KEY=vault:v1:... # the output above
VAULT_ADDR=https://vault.example.com:8200
VAULT_TOKEN=... # optional: VAULT_NAMESPACE, VAULT_TRANSIT_MOUNT
AWS KMS (the server needs kms:Decrypt on the key; credentials come from AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, the ECS task role or the EC2 instance role):
aws kms encrypt --key-id arn:aws:kms:us-east-1:111122223333:key/... \
--plaintext fileb://key.bin --query CiphertextBlob --output text
CREDENTIALS_KEY_PROVIDER=aws
CREDENTIALS_KMS_KEY=arn:aws:kms:us-east-1:111122223333:key/...
CREDENTIALS_ENCRYPTION_KEY=AQICAH... # the output above
Google Cloud KMS (runs on Google Cloud; the service account needs roles/cloudkms.cryptoKeyDecrypter):
gcloud kms encrypt --location global --keyring agentraas --key credentials \
--plaintext-file key.bin --ciphertext-file - | base64 -w0
CREDENTIALS_KEY_PROVIDER=gcp
CREDENTIALS_KMS_KEY=projects/my-project/locations/global/keyRings/agentraas/cryptoKeys/credentials
CREDENTIALS_ENCRYPTION_KEY=CiQA... # the output above
Azure Key Vault (an RSA key; the managed identity needs the Key Vault Crypto User role, AZURE_CLIENT_ID picks a user-assigned one):
az keyvault key encrypt --vault-name my-vault --name agentraas --algorithm RSA-OAEP-256 \
--data-type base64 --value "$KEY" --query result -o tsv
CREDENTIALS_KEY_PROVIDER=azure
CREDENTIALS_KMS_KEY=https://my-vault.vault.azure.net/keys/agentraas/<version>
CREDENTIALS_ENCRYPTION_KEY=... # the output above
Then delete key.bin and the plain key from your shell history. If the server can't reach the KMS it refuses to start and says why. Keep deletion protection on the KMS key: if it is deleted, every stored secret is lost. To rotate, rotate the KMS key and re-encrypt the same 32 bytes (Vault: vault write transit/rewrap/agentraas ciphertext=...); stored data stays as it is.
Private platforms
AgentRaaS is one container image plus Postgres and Redis, so it runs on private infrastructure the same two ways it runs anywhere else: a Linux VM with ./install.sh, or Kubernetes with the Helm chart. Nothing in it calls home, and the license check works offline.
We have not tested AgentRaaS on any of the platforms on this page yet. ./install.sh is tested on fresh Linux and macOS hosts in CI, and the Helm chart passes helm lint and kubeconform and its migration step has been run against an empty Postgres, but neither has been run on these hypervisors or distributions. What follows is what should work given how the image and chart are built. If something doesn't, tell us in GitHub Discussions or at hello@agentraas.io.
| Platform | How | Tested by us |
|---|---|---|
| Nutanix AHV, VMware vSphere, Proxmox VE, Hyper-V, bare metal | A Linux VM (or host) with Docker or Podman, then ./install.sh | No |
| Nutanix Kubernetes Platform, VMware Tanzu, Rancher | Helm chart | No |
| Red Hat OpenShift | Helm chart, with the changes below | No |
| Air-gapped networks | The published image, mirrored to your registry | No |
Know this before you plan an internal deployment: AgentRaaS refuses to send agent calls to private or internal addresses (for example 10.x, 172.16-31.x, 192.168.x, 100.64-127.x, loopback, link-local, and .local names). This guard protects the server from being used to reach its own network, and it applies to built-in services, custom actions and registered MCP servers alike. To let agents reach services on your own network, list those ranges (from 0.30.1): ALLOWED_PRIVATE_NETWORKS=10.20.0.0/16,fd00:1::/64. List only the ranges your agents' services live in, not the one your database and Redis are on. With ranges listed, .local names (Kubernetes *.svc.cluster.local) are accepted too when they resolve inside them. Loopback and link-local addresses (including cloud metadata) stay blocked whatever you list, and the setting is ignored on AgentRaaS Cloud. It also applies to alert and approval webhooks you register. The KMS and secret-reference lookups below are not affected: they use their own connection, so a Vault on your internal network works.
Virtual machines and bare metal
On Nutanix AHV, vSphere, Proxmox, Hyper-V or a physical server, create a Linux VM and install as on any Linux host. Nothing in AgentRaaS depends on the hypervisor.
git clone https://github.com/agentraas/agentraas && cd agentraas
./install.sh
- The first run compiles the API from source, so the VM needs internet access to Docker Hub and crates.io for that step. Without it, see Air-gapped.
- On RHEL, Rocky or Alma,
install.shrelabels the project folder for SELinux and asks forsudoto do it. - The API listens on
127.0.0.1:13000only. Put your reverse proxy or load balancer in front of it with TLS, setPUBLIC_URLin.envto the address people use, and setCLIENT_IP_HEADERto the header that proxy writes (x-real-ipfor nginx). See Self-hosting. - A VM snapshot is not a database backup. Back up Postgres with
pg_dump, and keep a copy of.envsomewhere safe: it holdsCREDENTIALS_ENCRYPTION_KEY, without which stored service keys can't be decrypted.
Nutanix Kubernetes Platform, Tanzu, Rancher
These are standard Kubernetes, so the Helm chart in infra/helm/agentraas applies as it is. The values you will most likely set are the ingress class, the storage class for the bundled datastores, and the client IP header:
helm install agentraas ./infra/helm/agentraas \
--set publicUrl=https://agentraas.example.internal \
--set ingress.enabled=true --set ingress.className=nginx \
--set ingress.host=agentraas.example.internal \
--set postgresql.storageClass=<your storage class> \
--set redis.storageClass=<your storage class> \
--set clientIpHeader=x-real-ip
The bundled Postgres and Redis are single instances, meant to get started. For production, run Postgres the way your platform already does (a database service or an operator such as CloudNativePG) and pass it with postgresql.enabled=false and externalDatabase.url; the same for Redis with redis.enabled=false and externalRedis.url. Run Redis with persistence: it holds the dedup slots. For Redis over TLS, use a rediss:// URL (0.30.2 and later). The certificate must come from a public CA, as on AWS ElastiCache and Azure Cache for Redis, or set REDIS_CA_CERT to the PEM of the CA that signed it (0.30.3 and later), as on Google Memorystore.
OpenShift (not tested on a cluster)
OpenShift's default restricted-v2 security policy runs every container as a random user ID in group 0 and does not let images run as root. The image has been tested that way outside OpenShift: as an arbitrary user ID in group 0 with no account in the image, a read-only filesystem, every capability dropped and no privilege escalation, the migrations ran and the API served /health, the dashboard and the docs. How that affects each part of the chart:
| Part | Expected | What may need changing |
|---|---|---|
| API container | Works (tested as above) | The image has no USER line, so elsewhere it runs as root, but it doesn't need root: it only reads /usr/local/bin, /public, /config and /migrations (all readable by any user), listens on port 3000 and writes nothing to disk. The chart already runs it with a read-only root filesystem and no privilege escalation. |
| Pod security settings | Needs one value | Chart 0.7.2 and later run the API as non-root with a seccomp profile and every capability dropped (podSecurityContext), which meets the Kubernetes restricted level. Its default user ID 10001 is outside an OpenShift namespace's range, so set --set podSecurityContext.runAsUser=null and let OpenShift assign one. |
| Bundled Postgres and Redis | Most likely to fail | They use the Docker Hub images, which expect to start as root and switch to their own user. Turn both off and use a Postgres and Redis your cluster already runs: postgresql.enabled=false, redis.enabled=false. |
| Migration init container | Works | It runs agentraas-migrate from the AgentRaaS image (chart 0.7.1 and later), with a read-only root filesystem, so it needs nothing extra when the bundled database is off. |
| Ingress | Works | OpenShift turns the chart's Ingress into a Route. Its router adds the client address to the end of X-Forwarded-For, so set clientIpHeader=x-forwarded-for:last, but only if nothing else sits in front of the router. |
helm install agentraas ./infra/helm/agentraas \
--set publicUrl=https://agentraas.apps.example.internal \
--set ingress.enabled=true --set ingress.host=agentraas.apps.example.internal \
--set postgresql.enabled=false --set externalDatabase.url=postgres://user:pass@host:5432/agentraas \
--set redis.enabled=false --set externalRedis.url=redis://host:6379 \
--set clientIpHeader=x-forwarded-for:last \
--set podSecurityContext.runAsUser=null
Air-gapped
./install.sh compiles from source and needs the internet, so an air-gapped install uses the published image instead. Everything the server needs is inside it: the service catalog (/config), every database migration and agentraas-migrate to apply them (/migrations), and the dashboard (/public). Copy it, and the datastore images, to your own registry from a connected machine:
V=<version> # e.g. the chart's appVersion
skopeo copy docker://ghcr.io/agentraas/agentraas:$V docker://registry.example.internal/agentraas/agentraas:$V
skopeo copy docker://docker.io/postgres:15-alpine docker://registry.example.internal/library/postgres:15-alpine
skopeo copy docker://docker.io/redis:7-alpine docker://registry.example.internal/library/redis:7-alpine
With no route between the two networks, carry a file across instead:
podman pull ghcr.io/agentraas/agentraas:$V
podman save -o agentraas-$V.tar ghcr.io/agentraas/agentraas:$V
# on the inside
podman load -i agentraas-$V.tar
With Helm, point the chart at your registry: --set image.repository=registry.example.internal/agentraas/agentraas --set image.tag=$V, plus postgresql.image and redis.image if you use the bundled datastores (the migration step runs from the AgentRaaS image itself, chart 0.7.1 and later). With Docker Compose on a VM, bring a checkout of the same release (compose mounts its config and public folders), load the Postgres, Redis and MinIO images named in compose.yaml, replace the build: block of ar-api-rs in compose.yaml with image: ghcr.io/agentraas/agentraas:$V, then run ./install.sh. On any other container platform, run agentraas-migrate from the same image before the API starts.
- License key.
LICENSE_TOKENis checked offline: its signature against a public key built into the server, its expiry against the server's clock, re-read every 6 hours. No network call is made. Keep the clock right (an internal NTP source), since a key that looks expired falls back to the free tier. - What needs a way out. Calls to internet services (Stripe, Slack and the rest of the catalog) and approvals sent to Slack, Teams, Telegram or Discord need to reach those services. Approvals in the dashboard work offline, and email approvals work with an internal SMTP relay (
SMTP_HOST). In a fully isolated network, where every service has a private address, list those ranges inALLOWED_PRIVATE_NETWORKS(see the note above).
Keeping service keys on-premises
Use HashiCorp Vault or OpenBao for both layers AgentRaaS supports. Keep CREDENTIALS_ENCRYPTION_KEY encrypted by the Vault transit engine, so the master key never sits in .env (Key management, CREDENTIALS_KEY_PROVIDER=vault), and, on Enterprise, paste vault:// references instead of service keys so they never enter AgentRaaS at all (Secret references). Both work against a Vault on your internal network.
The key managers built into hypervisors and storage, such as Nutanix's native key manager or vSphere's Native Key Provider, encrypt disks and VMs. They don't give an application a way to decrypt its own key, so they can't stand in for Vault here. Disk encryption is still worth turning on: it protects the Postgres data at rest.
Secret references (self-hosted Enterprise)
Instead of pasting a service key into AgentRaaS, you can paste a reference to where it lives in your own secrets manager. AgentRaaS fetches the key when an agent call needs it, keeps it in memory for 5 minutes (SECRET_REF_CACHE_SECONDS), and never stores it. Rotate the key in your secrets manager and AgentRaaS picks up the new one on the next fetch; your secrets manager's audit log shows every read.
| Secrets manager | Reference to paste as the API key |
|---|---|
| Vault / OpenBao KV | vault://secret/data/stripe#api_key |
| AWS Secrets Manager | aws-sm://arn:aws:secretsmanager:us-east-1:111122223333:secret:stripe-AbCd |
| Google Secret Manager | gcp-sm://projects/my-project/secrets/stripe (latest version, or add /versions/3) |
| Azure Key Vault | azure-kv://my-vault.vault.azure.net/secrets/stripe |
Add #field to read one field of a JSON secret, for example aws-sm://...:secret:payments#stripe_key. Vault references always name a field. The server logs in to the secrets manager the same way as for the KMS above: VAULT_ADDR and VAULT_TOKEN, the AWS role or keys, the Google Cloud service account, or the Azure managed identity. It needs read access to those secrets only. Because the server reads them with its own access, secret references work on self-hosted servers only, not on AgentRaaS Cloud.
Paste the reference in the dashboard's Credentials panel (or in the api_key, username or password of POST /api/v1/credentials); the panel shows the reference itself, since it isn't secret. If a key can't be fetched, the call fails with the reason and nothing is sent to the service; if a refresh fails after an earlier fetch worked, the last value is used and the failure is logged.
Metrics (Prometheus)
GET /metrics serves Prometheus text format: how many calls AgentRaaS handled and how long it took to answer them, by service and outcome. It is off (404) until you set METRICS_TOKEN, and then needs that token as a bearer token. There is no org label, so one token never reveals another tenant's traffic.
# prometheus.yml
scrape_configs:
- job_name: agentraas
scheme: https
authorization:
credentials: your-metrics-token
static_configs:
- targets: [agentraas.example.com]
| Metric | Type | Labels |
|---|---|---|
agentraas_calls_total | counter | service, status (success, deduplicated, blocked, error, ...) |
agentraas_call_duration_ms | histogram (5 ms to 10 s) | service, status |
# duplicates caught per second, by service
sum by (service) (rate(agentraas_calls_total{status="deduplicated"}[5m]))
# p95 time to answer
histogram_quantile(0.95, sum by (le) (rate(agentraas_call_duration_ms_bucket[5m])))
Counters live in memory and reset when the server restarts, which Prometheus' rate() handles. For per-call detail (which agent, which run), use the OpenTelemetry export instead.
Latency
Every call passes through auth, the dedup claim, spend caps and the audit write before it is forwarded. Measured on one machine (Intel Core i3-12100, 8 GB RAM, local stack, 500 sequential calls per row, three runs):
| p50 | p95 | |
|---|---|---|
| Added by AgentRaaS to a forwarded call | 2.1 to 2.4 ms | 2.9 to 3.7 ms |
| Retried duplicate (answered from cache, upstream never called) | 2.1 to 2.3 ms total | 3.0 to 3.5 ms total |
Network distance between your agent and AgentRaaS comes on top of this. Self-hosting next to your agent keeps that near zero. Reproduce it with python3 infra/scripts/bench-overhead.py against your own instance.
Under load
Same machine, k6 at a fixed rate for 30 seconds, every call new so each one takes the full path (0.24.1, 2026-10-26; k6, the server, Postgres and Redis all on that one 4-core box, so this is a floor, not a ceiling):
| Calls per second | p50 | p99 | Errors |
|---|---|---|---|
| 500 | 2.5 ms | 24 ms | 0 |
| 800 | 2.7 ms | 101 ms | 0 |
| 1000 | 3.0 ms | 142 ms | 0 |
| 300 refunds with a lookup limit (two upstream calls each) | 2.3 ms | 3.4 ms | 0 |
Two server instances sharing one Redis and Postgres, with every call sent twice at the same moment, once to each instance: 8,784 different calls, 8,784 ran, none twice. The server keeps no state of its own, so more instances can share the load; the speedup on separate machines is not measured yet. Every number, and how to run it: infra/bench/README.md.
Plans & limits
| Community | Team | Enterprise | |
|---|---|---|---|
| Self-hosted | Free, unlimited actions, no license key | License key: talk to us | |
| Cloud | Free trial: 1,000 actions once, every feature | ||
| Features | Exactly-once execution, MCP gateway, dashboard, audit trail, DLQ, undo, record and replay, shadow mode, savings reports, record locks, blocking spend caps and action policies, DLP redaction | + human approvals (also on spend caps and policies), semantic dedup, SSO | + agent identity, inbound webhooks, white-label branding (also on savings reports), secret references |
| Seats | 3 | Unlimited | |
For production, self-host: your API keys and data stay on your servers. AgentRaaS Cloud is the quickest way to try it; use test or restricted keys there for now.
On Cloud, duplicates AgentRaaS caught, blocked calls, approval waits, undos and shadow calls are never counted. Need Enterprise, a contract, invoicing or SLA-backed support? Talk to us.
Troubleshooting
Ordered from quick checks to deeper digging: work down a list until the problem is gone. The free tools (library mode, MCP wrap, chaos tester) run entirely on your machine, so an AgentRaaS Cloud outage never affects them.
Start here: 60-second checks
- Is the server up? Cloud: /status, or
curl https://agentraas.io/healthshould return{"ok":true,"postgres":true,"redis":true}. Self-hosted:curl http://localhost:13000/health. - Read the response body. Every error has a message and a
reqId; the table below says what each status means. - Upgrade the package:
pip install -U agentraas. Check what you have withpip show agentraas.
API responses (webhook, SDK-style REST, MCP)
| Status | Meaning | What to do |
|---|---|---|
400 | Unknown service.action, missing fields, or no Custom Action with that name for this org | Check spelling against Supported services, or register it as a Custom Action. |
401 | API key missing, wrong, or revoked for this org and agent | Check the header and the org/agent IDs in the URL; connect a new agent if the key was lost. |
402 | Free Cloud account's monthly limit reached | Self-host for free (no limit on any tier), or wait for the monthly reset. |
403 | An action policy or a key's service switch stopped the call. The message names the policy and, for a lookup limit, the numbers it read | Nothing ran. Change the call, or the policy. A lookup limit that couldn't read its number (no amount, no id, the provider's read failed) also lands here, on purpose. |
409 | An identical request is still being processed, or an earlier identical call timed out with an unknown outcome | Still processing: retry shortly and you get the first call's result. "Outcome is unknown": see A call timed out below; retrying won't run it. |
422 | The payload failed a validation rule | Each entry in validation_errors names a field and the fix; apply them and retry (nothing ran), or change the org's rule. |
424 | No key saved for that service in this org | Nothing was sent. Add the key (Connect a service, or Credentials), then retry. Retrying before that can't help. |
429 | This agent's per-minute rate limit, or a spend cap (the message says which) | Rate limit: back off and retry. Spend cap: it resets with the hour or day; raise it if the volume is legitimate. |
502 / 503 | The upstream API is failing, or its circuit breaker is open | Check the provider's status page. AgentRaaS fails closed on purpose; the breaker tries again by itself after 30 seconds. |
504 | The provider didn't answer in time, so the action may or may not have run | Not retried on purpose. See A call timed out below. |
500 | AgentRaaS itself failed | Retry with backoff. If it keeps happening, contact us with the reqId. |
A call timed out (504 "outcome unknown", then 409)
The provider never answered, so nobody knows whether the action ran. AgentRaaS stops there instead of guessing: it doesn't retry, and identical calls get a 409 until you resolve it. That is what keeps a slow provider from turning into a double charge. Work down this list:
- Check the provider. Look in its own dashboard or request log around the time of the call. Every call AgentRaaS sends carries
Idempotency-Key: agentraas-..., which you can search for (in Stripe: Developers, Logs). - It ran, and the provider supports Idempotency-Key (Stripe does, for 24 hours): open Failed Requests in the dashboard and click Replay. The provider hands back the original result instead of running it again, and your agent's identical calls start getting that result.
- It did not run: click Replay and it runs once.
- It ran, but the provider has no Idempotency-Key support (or its window has passed): click Dismiss, not Replay. The
409s are correct and stop when the dedup window ends (24 hours by default). - You're sure it did not run and want your agent's own retry to go through: send the call with a new
x-agentraas-idempotency-key. Don't do this if it might have run.curl -X POST https://agentraas.io/v1/webhook/YOUR_ORG/YOUR_AGENT \ -H "Authorization: Bearer ar_live_..." \ -H "x-agentraas-idempotency-key: order-1042-retry-1" \ -H "Content-Type: application/json" \ -d '{"service":"stripe","action":"charge.create","payload":{"amount":5000,"currency":"usd"}}' - Use Replay, not Edit & Replay, to resolve a timeout: an edited payload is a new request and goes out without the original key.
- Happens often with one provider? Self-hosted: raise
PROXY_TIMEOUT_SECONDS(default 30) in.envand restart. Cloud: contact us with areqId.
Library mode (@exactly_once)
Basic
- The function ran twice. By default a call's key is all of its arguments, so a timestamp or random ID among them makes every call unique. Pass
key=with what really identifies the action (an order ID). - It ran once per machine.
SQLiteStoreis per machine. UseRedisStorewhen several machines run the same code. TypeErrorabout JSON. Results are stored as JSON: return ids and plain dicts, not SDK objects.
Intermediate
- It ran again after the provider already did the work. The response was lost, the function raised, and a raising call is never cached. Add an
idempotency_keyparameter and send it to the provider (see Library mode). InFlightError. A duplicate waitedwaitseconds for the first call to finish. If the first process was killed mid-call, its slot stays pending untilttlexpires.- An old result keeps coming back. It's inside the
ttlwindow (default 24 hours). Lowerttl, or clear the state (below).
Advanced
# see what's stored (SQLite)
sqlite3 agentraas-dedup.db "SELECT key, substr(value,1,60), datetime(expires_at,'unixepoch') FROM dedup;"
# clear everything: delete the file (SQLite), or in Redis:
redis-cli --scan --pattern 'dedup:local:*' | xargs -r redis-cli del
MCP wrap
Basic
- The server doesn't show up, or
uvx: command not found. Install uv, orpip install agentraasand use"command": "agentraas"with"args": ["wrap", "--", ...]. - The server fails to start. Run the server's own command alone in a terminal first. Then run
uvx agentraas wrap -- <same command>: it should sit waiting for input, which is normal for a stdio MCP server.
Intermediate
- A write tool isn't deduplicated. Tools the server marks read-only, or named like
get_*,list_*,search_*, always run. Force it with--dedupe TOOL_NAME. - A tool is deduplicated when repeats are intentional. Use
--no-dedupe TOOL_NAME, or shorten the window with--ttl(seconds, default 600). - See what happened. Every tool call is logged:
tail ~/.agentraas/mcp-audit.jsonl. Theoutcomeisexecuted,deduplicated,failedorpassthrough.
Advanced
- Reset all dedup state: delete
~/.agentraas/mcp-dedup.db. - Wrapper and server errors go to stderr, which your MCP client captures in its logs (Claude Desktop: Settings, Developer).
- "Identical call still in flight" means a duplicate arrived while a slow first call was running; it's refused rather than re-run. Raise
--waitfor tools that take longer than 30 seconds.
Chaos tester
- Basic: "0 write requests". Your code didn't use
AGENTRAAS_CHAOS_URLas its API base URL. "Address already in use": pass--port. - Intermediate: a duplicate you think is fine. The same method, path and body really executed twice. If the provider dedupes by key, send an
Idempotency-Keyheader; retries that reuse it count as protected. - Intermediate: retries never flagged. If each retry's body differs (a timestamp inside it), they aren't identical requests. That also defeats payload-based dedup, so use a stable key.
- Advanced.
--fault error-after-commitfor clients that retry on 5xx rather than dropped connections;--fault nonejust records. Against real APIs, use test-mode keys.
Self-hosted server
- Basic.
podman ps(ordocker ps): are the API, Postgres and Redis containers running? Thencurl localhost:13000/health. - Intermediate: the API container exits at start. Read its logs. A missing or empty
JWT_SECRET,DATABASE_URL,REDIS_URLorCREDENTIALS_ENCRYPTION_KEYis the usual cause. - Intermediate:
relation "..." does not exist. A migration wasn't applied. Run every file ininfra/migrations/in order; they're safe to re-run. - Intermediate: approvals, SSO or DLP missing. Check that
ENTERPRISE_MODE=truereaches the container and that the image was built with--features enterprise(the defaultContainerfiledoes). - Intermediate: no verification email. Without
SMTP_*settings the link is written to the logs instead. - Advanced: rebuilt, but the old behavior remains. Recreate the container (
up -d --force-recreate) and check it runs the new image. If the build "succeeded" suspiciously fast, check disk space: a full disk can fail the compile while the wrapper still reports success. - Advanced: trace one request. Grep the API logs for the
reqIdfrom the response.
Still stuck?
Email hello@agentraas.io or open a GitHub issue with: what you ran, the full response body including the reqId, your agentraas version or server commit, and whether it's Cloud or self-hosted. Never include API keys or credentials.
API reference
Every endpoint of the server (150 in all), read from its code. The same list as an OpenAPI file, to import into Postman, Insomnia or a code generator: openapi.json. Dashboard login means the session cookie from /api/v1/auth/login (or the same token as Authorization: Bearer); Agent key is Authorization: Bearer ar_live_....
Calling services
| Call | What it does | Auth |
|---|---|---|
POST /mcp | MCP endpoint (JSON-RPC): every connected service as MCP tools, through the same checks. | Agent key |
GET /v1/hitl/:req_id | Status of a call held for approval: pending, approved (with its result) or denied. | Agent key |
GET /v1/inbound/:token | Check an inbound webhook URL is live. | Token in the URL or body |
POST /v1/inbound/:token | Receive a third-party webhook, deduplicated, and pass it on. | Token in the URL or body |
POST /v1/sdk/:service/:action | The same call, SDK style: the service and action in the path, the payload as the body. | Agent key |
GET /v1/sdk/whoami | Which org and agent an agent key belongs to. | Agent key |
POST /v1/webhook/:org_id/:agent_id | Run one action through AgentRaaS: body {service, action, payload}. The main call for webhooks, n8n, Make, Zapier and plain HTTP. | Agent key |
Approvals
| Call | What it does | Auth |
|---|---|---|
GET /api/v1/hitl-escalation-config | What happens to a call nobody decides on in time. | Dashboard login |
POST /api/v1/hitl-escalation-config | Set the escalation for undecided calls. | Dashboard login |
GET /api/v1/hitl-rules | List approval rules. | Dashboard login |
POST /api/v1/hitl-rules | Create an approval rule: {org_id, service, action, field?, operator?, threshold?}. | Dashboard login |
DELETE /api/v1/hitl-rules/:id | Delete an approval rule. | Dashboard login |
GET /api/v1/hitl-slack-config | The Slack destination for approval cards. | Dashboard login |
POST /api/v1/hitl-slack-config | Set the Slack destination for approval cards. | Dashboard login |
POST /api/v1/hitl/:req_id/decide | Approve or deny a held call: {decision: approve | deny}. | Dashboard login |
GET /api/v1/hitl/pending | Calls waiting for approval. | Dashboard login |
POST /v1/hitl/interactions/:org_id | Slack's interactive button callback (Approve/Deny in Slack). | Token in the URL or body |
POST /v1/hitl/link | Read an approval link's call (the one-time link sent to Slack, Teams, Telegram, Discord or email). | Token in the URL or body |
POST /v1/hitl/link/decide | Approve or deny through an approval link. | Token in the URL or body |
Rules
| Call | What it does | Auth |
|---|---|---|
GET /api/v1/action-policies | List action policies. | Dashboard login |
POST /api/v1/action-policies | Create an action policy: deny, or allow only within limits, domains or hours. | Dashboard login |
DELETE /api/v1/action-policies/:id | Delete an action policy. | Dashboard login |
GET /api/v1/chaos | Chaos mode settings (inject failures to test an agent). | Dashboard login |
PUT /api/v1/chaos | Change chaos mode settings. | Dashboard login |
GET /api/v1/dedup-rules | List dedup rules (which fields make two calls the same). | Dashboard login |
POST /api/v1/dedup-rules | Create a dedup rule: {org_id, service, action, fields}. | Dashboard login |
DELETE /api/v1/dedup-rules/:id | Delete a dedup rule. | Dashboard login |
GET /api/v1/output-pruning | Whether tool output pruning is on for an org. | Dashboard login |
PUT /api/v1/output-pruning | Turn tool output pruning on or off. | Dashboard login |
GET /api/v1/output-sanitization | Whether output sanitization is on for an org. | Dashboard login |
PUT /api/v1/output-sanitization | Turn output sanitization on or off. | Dashboard login |
GET /api/v1/record-locks | Whether automatic record locks are on, and which actions lock. | Dashboard login |
PUT /api/v1/record-locks | Turn automatic record locks on or off for an org. | Dashboard login |
GET /api/v1/shadow-rules | Agents in shadow mode (writes recorded, never sent). | Dashboard login |
POST /api/v1/shadow-rules | Put an agent, service or action in shadow mode. | Dashboard login |
DELETE /api/v1/shadow-rules/:id | Remove one shadow rule. | Dashboard login |
POST /api/v1/shadow-rules/promote | Take an agent out of shadow mode; nothing is replayed. | Dashboard login |
GET /api/v1/spend-cap-rules | List spend caps. | Dashboard login |
POST /api/v1/spend-cap-rules | Create a spend cap: calls or a money total per hour or day, then block or ask a person. | Dashboard login |
DELETE /api/v1/spend-cap-rules/:id | Delete a spend cap. | Dashboard login |
GET /api/v1/validation-rules | List validation rules. | Dashboard login |
POST /api/v1/validation-rules | Create or replace a validation rule for a service action. | Dashboard login |
DELETE /api/v1/validation-rules/:id | Delete a validation rule. | Dashboard login |
POST /api/v1/validation-rules/test | Test a rule against a sample payload without saving it. | Dashboard login |
Agents and services
| Call | What it does | Auth |
|---|---|---|
GET /api/v1/agent-identity-tokens | List agent identity tokens. | Dashboard login |
POST /api/v1/agent-identity-tokens | Issue an agent identity token. | Dashboard login |
DELETE /api/v1/agent-identity-tokens/:id | Revoke an agent identity token. | Dashboard login |
GET /api/v1/agentgateway-targets | List agentgateway targets. | Dashboard login |
POST /api/v1/agentgateway-targets | Add an agentgateway target. | Dashboard login |
DELETE /api/v1/agentgateway-targets/:id | Delete an agentgateway target. | Dashboard login |
GET /api/v1/agents | Agents seen recently. | Dashboard login |
POST /api/v1/agents/connect | Connect an agent: returns its key (shown once) and its URLs. | Dashboard login |
GET /api/v1/agents/keys | List agent keys. | Dashboard login |
DELETE /api/v1/agents/keys/:id | Revoke an agent key. | Dashboard login |
POST /api/v1/agents/keys/:id/regenerate | Replace an agent key; the old one stops working. | Dashboard login |
PUT /api/v1/agents/keys/:id/services | Choose which connected services an agent key may use. | Dashboard login |
GET /api/v1/credentials | List saved service keys (never the values). | Dashboard login |
POST /api/v1/credentials | Save a service key, encrypted. | Dashboard login |
DELETE /api/v1/credentials/:id | Delete a saved service key. | Dashboard login |
GET /api/v1/custom-actions | List custom actions (your own HTTP APIs). | Dashboard login |
POST /api/v1/custom-actions | Register a custom action. | Dashboard login |
DELETE /api/v1/custom-actions/:id | Delete a custom action. | Dashboard login |
GET /api/v1/inbound-webhooks | List inbound webhook URLs. | Dashboard login |
POST /api/v1/inbound-webhooks | Create an inbound webhook URL. | Dashboard login |
DELETE /api/v1/inbound-webhooks/:id | Delete an inbound webhook URL. | Dashboard login |
GET /api/v1/mcp-servers | List registered MCP servers. | Dashboard login |
POST /api/v1/mcp-servers | Register an MCP server. | Dashboard login |
DELETE /api/v1/mcp-servers/:id | Delete an MCP server. | Dashboard login |
GET /api/v1/services | The service catalog: every service and action AgentRaaS knows. | Dashboard login |
GET /api/v1/tunnel/connect/:org_id/:agent_id | Dev tunnel: the CLI's WebSocket to a local service. | Agent key |
GET /api/v1/tunnel/stream | Dev tunnel: live requests (server-sent events). | Dashboard login |
Monitoring
| Call | What it does | Auth |
|---|---|---|
GET /api/v1/dashboard/by-service | Calls per service for a range. | Dashboard login |
GET /api/v1/dashboard/runs/:run_id | One run, as the dashboard shows it. | Dashboard login |
GET /api/v1/dashboard/stats | Totals for a range (24h, 7d, 30d, 90d), optionally one org. | Dashboard login |
GET /api/v1/dashboard/timeseries | Calls over time for a range. | Dashboard login |
GET /api/v1/dead-letter-queue | Failed requests (the service itself refused them). | Dashboard login |
DELETE /api/v1/dead-letter-queue/:id | Dismiss a failed request. | Dashboard login |
POST /api/v1/dead-letter-queue/:id/replay | Send a failed request again, optionally with an edited payload. | Dashboard login |
GET /api/v1/export/csv | Download the audit log as CSV. | Dashboard login |
GET /api/v1/health-checks | Services under active monitoring. | Dashboard login |
POST /api/v1/health-checks | Turn on active monitoring for a service. | Dashboard login |
DELETE /api/v1/health-checks/:org_id/:service | Turn off active monitoring for a service. | Dashboard login |
GET /api/v1/notification-webhooks | Alert channels (Slack, Discord, Telegram, Teams, email, Jira). | Dashboard login |
POST /api/v1/notification-webhooks | Add an alert channel. | Dashboard login |
DELETE /api/v1/notification-webhooks/:id | Delete an alert channel. | Dashboard login |
POST /api/v1/notification-webhooks/:id/test | Send a test alert. | Dashboard login |
GET /api/v1/public/execution-ledger | Public, hash-chained count of executions. | None |
GET /api/v1/public/status | Public status of each service (the status page). | None |
GET /api/v1/recent | Recent calls with their outcome (the dashboard's activity list). | Dashboard login |
GET /api/v1/recordings | Recorded runs (X-AgentRaaS-Record). | Dashboard login |
DELETE /api/v1/recordings/:name | Delete a recording. | Dashboard login |
GET /api/v1/recordings/:name | Every call and answer in one recording. | Dashboard login |
GET /api/v1/reliability-report | Uptime, success rate and duplicates prevented per service. | Dashboard login |
GET /api/v1/reports | Savings and reliability report as JSON, CSV or HTML. | Dashboard login |
GET /api/v1/reports/settings | Monthly report email settings. | Dashboard login |
PUT /api/v1/reports/settings | Turn the monthly report email on or off. | Dashboard login |
GET /api/v1/runs/:run_id | Every step of one agent run. | Dashboard login |
GET /api/v1/schema-drift-events | Times a service's response shape changed. | Dashboard login |
GET /api/v1/shadow-calls | Writes agents in shadow mode would have made, with the decision. | Dashboard login |
GET /api/v1/stats | Overall counts. | Dashboard login |
GET /api/v1/undo-log | Actions that can be undone, for 30 days. | Dashboard login |
POST /api/v1/undo-log/:id/undo | Undo an action once (for example refund a charge). | Dashboard login |
GET /api/v1/usage | Actions used this month. | Dashboard login |
GET /api/v1/usage/stream | Live usage (server-sent events). | Dashboard login |
GET /health | Server health: Postgres, Redis and the version. | None |
GET /metrics | Prometheus metrics. | METRICS_TOKEN |
Account
| Call | What it does | Auth |
|---|---|---|
POST /api/v1/auth/forgot-password | Email a reset link. | None |
POST /api/v1/auth/invites/accept | Accept an invite to an org. | None |
POST /api/v1/auth/login | Log in; sets the session cookie. | None |
POST /api/v1/auth/logout | Log out this session. | Dashboard login |
POST /api/v1/auth/logout-all | Log out every session. | Dashboard login |
GET /api/v1/auth/me | The logged-in user, org and plan. | Dashboard login |
POST /api/v1/auth/password | Change the password. | Dashboard login |
POST /api/v1/auth/register | Create an account. | None |
POST /api/v1/auth/resend-verification | Send the email confirmation link again. | None |
POST /api/v1/auth/reset-password | Set a new password from a reset link. | None |
GET /api/v1/auth/verify-email | Confirm an email address (link from the email). | None |
GET /api/v1/billing/checkout-info | What the checkout needs (Cloud). | Dashboard login |
GET /api/v1/billing/usage | Cloud usage and the monthly cap. | Dashboard login |
PUT /api/v1/billing/usage | Change the monthly cap. | Dashboard login |
GET /api/v1/download/self-host | Download the self-host bundle. | Dashboard login |
POST /api/v1/download/self-host/request | Ask for a self-host download link. | Dashboard login |
GET /api/v1/licensing/token | The self-host license key for a paid plan. | Dashboard login |
GET /api/v1/org-branding/:org_id | An org's branding (name, logo) for approval pages. | Dashboard login |
PUT /api/v1/org-branding/:org_id | Change an org's branding. | Dashboard login |
POST /api/v1/webhooks/paddle | Paddle's billing webhook (signed by Paddle). | Token in the URL or body |
Single sign-on and members
| Call | What it does | Auth |
|---|---|---|
GET /api/v1/auth/sso/:org_id/configs | List identity providers. | Dashboard login |
POST /api/v1/auth/sso/:org_id/configs | Add an identity provider (OIDC). | Dashboard login |
DELETE /api/v1/auth/sso/:org_id/configs/:config_id | Remove an identity provider. | Dashboard login |
PUT /api/v1/auth/sso/:org_id/configs/:config_id | Change an identity provider. | Dashboard login |
GET /api/v1/auth/sso/:org_id/invites | List open invites. | Dashboard login |
POST /api/v1/auth/sso/:org_id/invites | Invite someone by email, with a role. | Dashboard login |
DELETE /api/v1/auth/sso/:org_id/invites/:id | Cancel an invite. | Dashboard login |
GET /api/v1/auth/sso/:org_id/login | Start SSO login for an org. | None |
GET /api/v1/auth/sso/:org_id/members | List members and roles. | Dashboard login |
DELETE /api/v1/auth/sso/:org_id/members/:user_id | Remove a member. | Dashboard login |
PUT /api/v1/auth/sso/:org_id/members/:user_id | Change a member's role. | Dashboard login |
GET /api/v1/auth/sso/callback | SSO callback from the identity provider. | None |
Server admin
| Call | What it does | Auth |
|---|---|---|
GET /api/v1/admin/audit/siem-export | Export the audit log for a SIEM. | Dashboard login |
GET /api/v1/admin/audit/verify-integrity | Check the audit log's hash chain. | Dashboard login |
GET /api/v1/admin/maintenance | Whether maintenance mode is on. | Dashboard login |
POST /api/v1/admin/maintenance/pause | Pause forwarding (calls are held, not lost). | Dashboard login |
POST /api/v1/admin/maintenance/resume | Resume forwarding. | Dashboard login |
GET /api/v1/admin/overview | Server-wide overview. | Dashboard login |
GET /api/v1/admin/users | All users. | Dashboard login |
Demo and tools
| Call | What it does | Auth |
|---|---|---|
POST /api/v1/demo/live-test | Send 8 identical calls to the sandbox and show that 1 ran. | Dashboard login |
POST /api/v1/demo/reset | Clear an org's demo data. | Dashboard login |
POST /api/v1/demo/seed | Fill an org with demo data. | Dashboard login |
GET /api/v1/demo/session | Open the read-only demo dashboard. | None |
POST /api/v1/tools/webhook-audit | The free webhook audit tool. | None |
Support
Questions this page didn't answer: hello@agentraas.io. Bug or feature request: open an issue on GitHub. General product questions are also covered in the FAQ on the homepage.