AgentRaaS

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

  1. Register and log in at the dashboard (Cloud), or self-host with ./install.sh. See Self-hosting below.
  2. 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.)
  3. 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):
    ...
OptionDefaultWhat it does
storeSQLite fileSQLiteStore(path) for one machine, RedisStore(url) for several (needs pip install redis).
keyall argumentsFunction returning what counts as "the same call", e.g. an order ID.
ttl86400Seconds a completed result is remembered.
wait30Seconds 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"]
    }
  }
}
OptionDefaultWhat it does
--ttl600Seconds an identical write call is deduplicated.
--dedupe TOOLAlways dedupe this tool, even if it looks read-only. Repeatable.
--no-dedupe TOOLNever dedupe this tool. Repeatable.
--db~/.agentraas/mcp-dedup.dbSQLite file for dedup state.
--log~/.agentraas/mcp-audit.jsonlJSON-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.

FlagWhat it does
--config PATHScan the servers in this config file (mcpServers). Repeatable. Without it: Claude Desktop, ~/.cursor/mcp.json, ~/.claude.json, ./.mcp.json.
--fail-onhigh or medium: exit 1 when a tool at that level or above is found.
--jsonPrint the result as JSON.
--timeoutSeconds 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.

FlagWhat it does
--mockAnswer every call with a fake 200. Nothing real is called.
--upstream URLForward to a real API base URL.
--faultlost-response (default) drops the connection; error-after-commit returns a 502 instead; none only records.
--portLocal 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.

LimitValue
Tunnels per org1 (opening a second closes the first)
Session length2 hours, then re-run the command
Local response timeout30 seconds
Requests through the public URL120/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 styleWhere the key goes
WebhookAuthorization: Bearer <key>
SDK-style RESTx-agentraas-key: <key> header
MCPx-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.

POST /v1/webhook/:org_id/:agent_id
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" }
  }'
FieldRequiredDescription
serviceYesWhich connector. See Supported services, or a Custom Action's name.
actionYesWhich operation on that service.
payloadNoFields the action needs, matching the real API's own shape.
end_user_idNoYour own identifier for who this action was on behalf of. Shows up in the audit trail.
resource_idNoYour 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.

POST /v1/sdk/:service/:action
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.

GET /v1/sdk/whoami

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_..."]
    }
  }
}
POST /mcp

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:

HeaderPurpose
x-agentraas-idempotency-keyYour 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-idCorrelates every call from one agent "run" together in the audit trail.
x-agentraas-step-idIdentifies which step within a run this call was, for multi-step agent workflows.
x-agentraas-end-userSDK-style equivalent of the webhook body's end_user_id.
x-agentraas-resource-idSDK-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.

StatusMeaning
401Missing or invalid API key.
402Monthly usage limit reached (Cloud only; self-hosted has no enforced limit on any tier).
403Authenticated, but not permitted for this org/action, or blocked by an action policy (the message names it).
422Request validation failed. Nothing was sent. validation_errors lists every failing field with a hint saying what to send instead (example below).
424The 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.
429Rate limit exceeded. See Plans & limits.
409An identical call is still in flight, or an earlier identical call has an unknown outcome (see Idempotency & tracing).
502/503The upstream service (Stripe, Twilio, etc.) was unreachable or errored; AgentRaaS fails closed rather than silently succeeding un-deduplicated.
504The 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" }
FieldWhat it does
agent_idOptional. Leave it out to apply to every agent in the org.
actionA service action, or * for every action of the service.
effectdeny never allows the call; require allows it only when the payload passes fields.
fieldsSame 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_violationblock (default), or hitl to send the call for approval instead. A block always wins over an approval.
hoursOptional, 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" }
KeyWhat it does
actionThe read call to make, such as order.get (Cashfree) or payment.get (Razorpay).
withWhich fields of the call feed the read: {"order_id": "order_id"} sends the call's order_id as the read's order_id.
fieldWhere the number is in the answer, a dotted path such as order_amount or data.total.
factorOptional. 0.5 allows half of it; the default allows all of it.
minusOptional. Subtracts a second number from the answer, such as Razorpay's amount_refunded, so earlier partial refunds count.
serviceOptional. 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
KindSame asOne rule per
DedupRuleDedup rulesservice + action
ValidationRuleValidation rulesservice + action
ActionPolicyAction policiesagent + service + action + effect
SpendCapSpend capsagent + 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.

GET / POST /v1/inbound/:token

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):

VariablePurpose
JWT_SECRETSigns dashboard sessions. Required; the server won't start without it.
CREDENTIALS_ENCRYPTION_KEYEncrypts stored service credentials at rest. Required, 32 bytes.
PUBLIC_URLUsed for email links and the Connect Agent webhook/MCP URLs. Set this to your real domain.
AGENT_RATE_LIMIT_PER_MINPer-key rate limit across webhook/SDK/MCP routes. Default 300.
CLIENT_IP_HEADERBehind 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_SECONDSHow long one forwarded call may take (default 30). A call that hits it is held as outcome unknown, not retried.
OTEL_EXPORTER_OTLP_ENDPOINTOptional. 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_URLSet 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_NETWORKSOptional, 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
ValueWhat it does
publicUrlThe URL people reach the dashboard at (email links, Connect Agent URLs).
existingSecretA 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.
clientIpHeaderThe header your ingress writes with the client IP (see CLIENT_IP_HEADER in Self-hosting).
metricsTokenTurns on /metrics.
extraEnvAny other environment variable, e.g. SMTP_HOST, OTEL_EXPORTER_OTLP_ENDPOINT.
replicaCountKeep 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.

PlatformHowTested by us
Nutanix AHV, VMware vSphere, Proxmox VE, Hyper-V, bare metalA Linux VM (or host) with Docker or Podman, then ./install.shNo
Nutanix Kubernetes Platform, VMware Tanzu, RancherHelm chartNo
Red Hat OpenShiftHelm chart, with the changes belowNo
Air-gapped networksThe published image, mirrored to your registryNo

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.sh relabels the project folder for SELinux and asks for sudo to do it.
  • The API listens on 127.0.0.1:13000 only. Put your reverse proxy or load balancer in front of it with TLS, set PUBLIC_URL in .env to the address people use, and set CLIENT_IP_HEADER to the header that proxy writes (x-real-ip for nginx). See Self-hosting.
  • A VM snapshot is not a database backup. Back up Postgres with pg_dump, and keep a copy of .env somewhere safe: it holds CREDENTIALS_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:

PartExpectedWhat may need changing
API containerWorks (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 settingsNeeds one valueChart 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 RedisMost likely to failThey 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 containerWorksIt 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.
IngressWorksOpenShift 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_TOKEN is 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 in ALLOWED_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 managerReference to paste as the API key
Vault / OpenBao KVvault://secret/data/stripe#api_key
AWS Secrets Manageraws-sm://arn:aws:secretsmanager:us-east-1:111122223333:secret:stripe-AbCd
Google Secret Managergcp-sm://projects/my-project/secrets/stripe (latest version, or add /versions/3)
Azure Key Vaultazure-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]
MetricTypeLabels
agentraas_calls_totalcounterservice, status (success, deduplicated, blocked, error, ...)
agentraas_call_duration_mshistogram (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):

p50p95
Added by AgentRaaS to a forwarded call2.1 to 2.4 ms2.9 to 3.7 ms
Retried duplicate (answered from cache, upstream never called)2.1 to 2.3 ms total3.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 secondp50p99Errors
5002.5 ms24 ms0
8002.7 ms101 ms0
10003.0 ms142 ms0
300 refunds with a lookup limit (two upstream calls each)2.3 ms3.4 ms0

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

CommunityTeamEnterprise
Self-hostedFree, unlimited actions, no license keyLicense key: talk to us
CloudFree trial: 1,000 actions once, every feature
FeaturesExactly-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
Seats3Unlimited

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

  1. Is the server up? Cloud: /status, or curl https://agentraas.io/health should return {"ok":true,"postgres":true,"redis":true}. Self-hosted: curl http://localhost:13000/health.
  2. Read the response body. Every error has a message and a reqId; the table below says what each status means.
  3. Upgrade the package: pip install -U agentraas. Check what you have with pip show agentraas.

API responses (webhook, SDK-style REST, MCP)

StatusMeaningWhat to do
400Unknown service.action, missing fields, or no Custom Action with that name for this orgCheck spelling against Supported services, or register it as a Custom Action.
401API key missing, wrong, or revoked for this org and agentCheck the header and the org/agent IDs in the URL; connect a new agent if the key was lost.
402Free Cloud account's monthly limit reachedSelf-host for free (no limit on any tier), or wait for the monthly reset.
403An action policy or a key's service switch stopped the call. The message names the policy and, for a lookup limit, the numbers it readNothing 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.
409An identical request is still being processed, or an earlier identical call timed out with an unknown outcomeStill 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.
422The payload failed a validation ruleEach entry in validation_errors names a field and the fix; apply them and retry (nothing ran), or change the org's rule.
424No key saved for that service in this orgNothing was sent. Add the key (Connect a service, or Credentials), then retry. Retrying before that can't help.
429This 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 / 503The upstream API is failing, or its circuit breaker is openCheck the provider's status page. AgentRaaS fails closed on purpose; the breaker tries again by itself after 30 seconds.
504The provider didn't answer in time, so the action may or may not have runNot retried on purpose. See A call timed out below.
500AgentRaaS itself failedRetry 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:

  1. 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).
  2. 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.
  3. It did not run: click Replay and it runs once.
  4. 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).
  5. 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"}}'
  6. Use Replay, not Edit & Replay, to resolve a timeout: an edited payload is a new request and goes out without the original key.
  7. Happens often with one provider? Self-hosted: raise PROXY_TIMEOUT_SECONDS (default 30) in .env and restart. Cloud: contact us with a reqId.

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. SQLiteStore is per machine. Use RedisStore when several machines run the same code.
  • TypeError about 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_key parameter and send it to the provider (see Library mode).
  • InFlightError. A duplicate waited wait seconds for the first call to finish. If the first process was killed mid-call, its slot stays pending until ttl expires.
  • An old result keeps coming back. It's inside the ttl window (default 24 hours). Lower ttl, 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, or pip install agentraas and 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. The outcome is executed, deduplicated, failed or passthrough.

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 --wait for tools that take longer than 30 seconds.

Chaos tester

  • Basic: "0 write requests". Your code didn't use AGENTRAAS_CHAOS_URL as 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-Key header; 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-commit for clients that retry on 5xx rather than dropped connections; --fault none just records. Against real APIs, use test-mode keys.

Self-hosted server

  • Basic. podman ps (or docker ps): are the API, Postgres and Redis containers running? Then curl localhost:13000/health.
  • Intermediate: the API container exits at start. Read its logs. A missing or empty JWT_SECRET, DATABASE_URL, REDIS_URL or CREDENTIALS_ENCRYPTION_KEY is the usual cause.
  • Intermediate: relation "..." does not exist. A migration wasn't applied. Run every file in infra/migrations/ in order; they're safe to re-run.
  • Intermediate: approvals, SSO or DLP missing. Check that ENTERPRISE_MODE=true reaches the container and that the image was built with --features enterprise (the default Containerfile does).
  • 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 reqId from 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

CallWhat it doesAuth
POST /mcpMCP endpoint (JSON-RPC): every connected service as MCP tools, through the same checks.Agent key
GET /v1/hitl/:req_idStatus of a call held for approval: pending, approved (with its result) or denied.Agent key
GET /v1/inbound/:tokenCheck an inbound webhook URL is live.Token in the URL or body
POST /v1/inbound/:tokenReceive a third-party webhook, deduplicated, and pass it on.Token in the URL or body
POST /v1/sdk/:service/:actionThe same call, SDK style: the service and action in the path, the payload as the body.Agent key
GET /v1/sdk/whoamiWhich org and agent an agent key belongs to.Agent key
POST /v1/webhook/:org_id/:agent_idRun one action through AgentRaaS: body {service, action, payload}. The main call for webhooks, n8n, Make, Zapier and plain HTTP.Agent key

Approvals

CallWhat it doesAuth
GET /api/v1/hitl-escalation-configWhat happens to a call nobody decides on in time.Dashboard login
POST /api/v1/hitl-escalation-configSet the escalation for undecided calls.Dashboard login
GET /api/v1/hitl-rulesList approval rules.Dashboard login
POST /api/v1/hitl-rulesCreate an approval rule: {org_id, service, action, field?, operator?, threshold?}.Dashboard login
DELETE /api/v1/hitl-rules/:idDelete an approval rule.Dashboard login
GET /api/v1/hitl-slack-configThe Slack destination for approval cards.Dashboard login
POST /api/v1/hitl-slack-configSet the Slack destination for approval cards.Dashboard login
POST /api/v1/hitl/:req_id/decideApprove or deny a held call: {decision: approve | deny}.Dashboard login
GET /api/v1/hitl/pendingCalls waiting for approval.Dashboard login
POST /v1/hitl/interactions/:org_idSlack's interactive button callback (Approve/Deny in Slack).Token in the URL or body
POST /v1/hitl/linkRead 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/decideApprove or deny through an approval link.Token in the URL or body

Rules

CallWhat it doesAuth
GET /api/v1/action-policiesList action policies.Dashboard login
POST /api/v1/action-policiesCreate an action policy: deny, or allow only within limits, domains or hours.Dashboard login
DELETE /api/v1/action-policies/:idDelete an action policy.Dashboard login
GET /api/v1/chaosChaos mode settings (inject failures to test an agent).Dashboard login
PUT /api/v1/chaosChange chaos mode settings.Dashboard login
GET /api/v1/dedup-rulesList dedup rules (which fields make two calls the same).Dashboard login
POST /api/v1/dedup-rulesCreate a dedup rule: {org_id, service, action, fields}.Dashboard login
DELETE /api/v1/dedup-rules/:idDelete a dedup rule.Dashboard login
GET /api/v1/output-pruningWhether tool output pruning is on for an org.Dashboard login
PUT /api/v1/output-pruningTurn tool output pruning on or off.Dashboard login
GET /api/v1/output-sanitizationWhether output sanitization is on for an org.Dashboard login
PUT /api/v1/output-sanitizationTurn output sanitization on or off.Dashboard login
GET /api/v1/record-locksWhether automatic record locks are on, and which actions lock.Dashboard login
PUT /api/v1/record-locksTurn automatic record locks on or off for an org.Dashboard login
GET /api/v1/shadow-rulesAgents in shadow mode (writes recorded, never sent).Dashboard login
POST /api/v1/shadow-rulesPut an agent, service or action in shadow mode.Dashboard login
DELETE /api/v1/shadow-rules/:idRemove one shadow rule.Dashboard login
POST /api/v1/shadow-rules/promoteTake an agent out of shadow mode; nothing is replayed.Dashboard login
GET /api/v1/spend-cap-rulesList spend caps.Dashboard login
POST /api/v1/spend-cap-rulesCreate 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/:idDelete a spend cap.Dashboard login
GET /api/v1/validation-rulesList validation rules.Dashboard login
POST /api/v1/validation-rulesCreate or replace a validation rule for a service action.Dashboard login
DELETE /api/v1/validation-rules/:idDelete a validation rule.Dashboard login
POST /api/v1/validation-rules/testTest a rule against a sample payload without saving it.Dashboard login

Agents and services

CallWhat it doesAuth
GET /api/v1/agent-identity-tokensList agent identity tokens.Dashboard login
POST /api/v1/agent-identity-tokensIssue an agent identity token.Dashboard login
DELETE /api/v1/agent-identity-tokens/:idRevoke an agent identity token.Dashboard login
GET /api/v1/agentgateway-targetsList agentgateway targets.Dashboard login
POST /api/v1/agentgateway-targetsAdd an agentgateway target.Dashboard login
DELETE /api/v1/agentgateway-targets/:idDelete an agentgateway target.Dashboard login
GET /api/v1/agentsAgents seen recently.Dashboard login
POST /api/v1/agents/connectConnect an agent: returns its key (shown once) and its URLs.Dashboard login
GET /api/v1/agents/keysList agent keys.Dashboard login
DELETE /api/v1/agents/keys/:idRevoke an agent key.Dashboard login
POST /api/v1/agents/keys/:id/regenerateReplace an agent key; the old one stops working.Dashboard login
PUT /api/v1/agents/keys/:id/servicesChoose which connected services an agent key may use.Dashboard login
GET /api/v1/credentialsList saved service keys (never the values).Dashboard login
POST /api/v1/credentialsSave a service key, encrypted.Dashboard login
DELETE /api/v1/credentials/:idDelete a saved service key.Dashboard login
GET /api/v1/custom-actionsList custom actions (your own HTTP APIs).Dashboard login
POST /api/v1/custom-actionsRegister a custom action.Dashboard login
DELETE /api/v1/custom-actions/:idDelete a custom action.Dashboard login
GET /api/v1/inbound-webhooksList inbound webhook URLs.Dashboard login
POST /api/v1/inbound-webhooksCreate an inbound webhook URL.Dashboard login
DELETE /api/v1/inbound-webhooks/:idDelete an inbound webhook URL.Dashboard login
GET /api/v1/mcp-serversList registered MCP servers.Dashboard login
POST /api/v1/mcp-serversRegister an MCP server.Dashboard login
DELETE /api/v1/mcp-servers/:idDelete an MCP server.Dashboard login
GET /api/v1/servicesThe service catalog: every service and action AgentRaaS knows.Dashboard login
GET /api/v1/tunnel/connect/:org_id/:agent_idDev tunnel: the CLI's WebSocket to a local service.Agent key
GET /api/v1/tunnel/streamDev tunnel: live requests (server-sent events).Dashboard login

Monitoring

CallWhat it doesAuth
GET /api/v1/dashboard/by-serviceCalls per service for a range.Dashboard login
GET /api/v1/dashboard/runs/:run_idOne run, as the dashboard shows it.Dashboard login
GET /api/v1/dashboard/statsTotals for a range (24h, 7d, 30d, 90d), optionally one org.Dashboard login
GET /api/v1/dashboard/timeseriesCalls over time for a range.Dashboard login
GET /api/v1/dead-letter-queueFailed requests (the service itself refused them).Dashboard login
DELETE /api/v1/dead-letter-queue/:idDismiss a failed request.Dashboard login
POST /api/v1/dead-letter-queue/:id/replaySend a failed request again, optionally with an edited payload.Dashboard login
GET /api/v1/export/csvDownload the audit log as CSV.Dashboard login
GET /api/v1/health-checksServices under active monitoring.Dashboard login
POST /api/v1/health-checksTurn on active monitoring for a service.Dashboard login
DELETE /api/v1/health-checks/:org_id/:serviceTurn off active monitoring for a service.Dashboard login
GET /api/v1/notification-webhooksAlert channels (Slack, Discord, Telegram, Teams, email, Jira).Dashboard login
POST /api/v1/notification-webhooksAdd an alert channel.Dashboard login
DELETE /api/v1/notification-webhooks/:idDelete an alert channel.Dashboard login
POST /api/v1/notification-webhooks/:id/testSend a test alert.Dashboard login
GET /api/v1/public/execution-ledgerPublic, hash-chained count of executions.None
GET /api/v1/public/statusPublic status of each service (the status page).None
GET /api/v1/recentRecent calls with their outcome (the dashboard's activity list).Dashboard login
GET /api/v1/recordingsRecorded runs (X-AgentRaaS-Record).Dashboard login
DELETE /api/v1/recordings/:nameDelete a recording.Dashboard login
GET /api/v1/recordings/:nameEvery call and answer in one recording.Dashboard login
GET /api/v1/reliability-reportUptime, success rate and duplicates prevented per service.Dashboard login
GET /api/v1/reportsSavings and reliability report as JSON, CSV or HTML.Dashboard login
GET /api/v1/reports/settingsMonthly report email settings.Dashboard login
PUT /api/v1/reports/settingsTurn the monthly report email on or off.Dashboard login
GET /api/v1/runs/:run_idEvery step of one agent run.Dashboard login
GET /api/v1/schema-drift-eventsTimes a service's response shape changed.Dashboard login
GET /api/v1/shadow-callsWrites agents in shadow mode would have made, with the decision.Dashboard login
GET /api/v1/statsOverall counts.Dashboard login
GET /api/v1/undo-logActions that can be undone, for 30 days.Dashboard login
POST /api/v1/undo-log/:id/undoUndo an action once (for example refund a charge).Dashboard login
GET /api/v1/usageActions used this month.Dashboard login
GET /api/v1/usage/streamLive usage (server-sent events).Dashboard login
GET /healthServer health: Postgres, Redis and the version.None
GET /metricsPrometheus metrics.METRICS_TOKEN

Account

CallWhat it doesAuth
POST /api/v1/auth/forgot-passwordEmail a reset link.None
POST /api/v1/auth/invites/acceptAccept an invite to an org.None
POST /api/v1/auth/loginLog in; sets the session cookie.None
POST /api/v1/auth/logoutLog out this session.Dashboard login
POST /api/v1/auth/logout-allLog out every session.Dashboard login
GET /api/v1/auth/meThe logged-in user, org and plan.Dashboard login
POST /api/v1/auth/passwordChange the password.Dashboard login
POST /api/v1/auth/registerCreate an account.None
POST /api/v1/auth/resend-verificationSend the email confirmation link again.None
POST /api/v1/auth/reset-passwordSet a new password from a reset link.None
GET /api/v1/auth/verify-emailConfirm an email address (link from the email).None
GET /api/v1/billing/checkout-infoWhat the checkout needs (Cloud).Dashboard login
GET /api/v1/billing/usageCloud usage and the monthly cap.Dashboard login
PUT /api/v1/billing/usageChange the monthly cap.Dashboard login
GET /api/v1/download/self-hostDownload the self-host bundle.Dashboard login
POST /api/v1/download/self-host/requestAsk for a self-host download link.Dashboard login
GET /api/v1/licensing/tokenThe self-host license key for a paid plan.Dashboard login
GET /api/v1/org-branding/:org_idAn org's branding (name, logo) for approval pages.Dashboard login
PUT /api/v1/org-branding/:org_idChange an org's branding.Dashboard login
POST /api/v1/webhooks/paddlePaddle's billing webhook (signed by Paddle).Token in the URL or body

Single sign-on and members

CallWhat it doesAuth
GET /api/v1/auth/sso/:org_id/configsList identity providers.Dashboard login
POST /api/v1/auth/sso/:org_id/configsAdd an identity provider (OIDC).Dashboard login
DELETE /api/v1/auth/sso/:org_id/configs/:config_idRemove an identity provider.Dashboard login
PUT /api/v1/auth/sso/:org_id/configs/:config_idChange an identity provider.Dashboard login
GET /api/v1/auth/sso/:org_id/invitesList open invites.Dashboard login
POST /api/v1/auth/sso/:org_id/invitesInvite someone by email, with a role.Dashboard login
DELETE /api/v1/auth/sso/:org_id/invites/:idCancel an invite.Dashboard login
GET /api/v1/auth/sso/:org_id/loginStart SSO login for an org.None
GET /api/v1/auth/sso/:org_id/membersList members and roles.Dashboard login
DELETE /api/v1/auth/sso/:org_id/members/:user_idRemove a member.Dashboard login
PUT /api/v1/auth/sso/:org_id/members/:user_idChange a member's role.Dashboard login
GET /api/v1/auth/sso/callbackSSO callback from the identity provider.None

Server admin

CallWhat it doesAuth
GET /api/v1/admin/audit/siem-exportExport the audit log for a SIEM.Dashboard login
GET /api/v1/admin/audit/verify-integrityCheck the audit log's hash chain.Dashboard login
GET /api/v1/admin/maintenanceWhether maintenance mode is on.Dashboard login
POST /api/v1/admin/maintenance/pausePause forwarding (calls are held, not lost).Dashboard login
POST /api/v1/admin/maintenance/resumeResume forwarding.Dashboard login
GET /api/v1/admin/overviewServer-wide overview.Dashboard login
GET /api/v1/admin/usersAll users.Dashboard login

Demo and tools

CallWhat it doesAuth
POST /api/v1/demo/live-testSend 8 identical calls to the sandbox and show that 1 ran.Dashboard login
POST /api/v1/demo/resetClear an org's demo data.Dashboard login
POST /api/v1/demo/seedFill an org with demo data.Dashboard login
GET /api/v1/demo/sessionOpen the read-only demo dashboard.None
POST /api/v1/tools/webhook-auditThe 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.