POST /v1/actions
The core action proxy. Every call your agent makes to a connected SaaS platform flows through this endpoint.
Most agents call this indirectly via the MCP
read_platform,write_platformanddelete_platformtools. Direct invocation is only needed when building a custom MCP server or integrating from a non-MCP host.
Request
POST /v1/actions HTTP/1.1
Host: api.agentvalet.ai
Authorization: Bearer <agent-rs256-jwt>
Content-Type: application/json{
"platform": "slack",
"endpoint": "/api/chat.postMessage",
"method": "POST",
"scope": "chat:write",
"data": { "channel": "#general", "text": "Hello" },
"headers": {},
"reason": "Posting the daily standup summary"
}| Field | Type | Required | Notes |
|---|---|---|---|
platform | string | yes | Platform id from list_platforms (e.g. slack, gmail, stripe) |
endpoint | string | yes | The SaaS endpoint path. Validated by the SSRF guard — no private/loopback addresses |
method | string | yes | GET / POST / PUT / PATCH / DELETE |
scope | string | yes | The scope the agent claims for this call. Must be granted in the dashboard permission matrix |
data | object | no | Request payload forwarded to the SaaS |
headers | object | no | Extra headers forwarded to the SaaS |
connection_id | string | no | Target a specific connection when the platform has several. Omit to use the default |
caller | object | no | The person this call is for, when one agent serves many people: { "assertion": "<jwt>" } (verified against the org’s trusted issuers) or { "source": "...", "id": "..." } (asserted). Also accepted as the X-AV-Caller header |
reason | string | no | Justification shown to the human approver and recorded on the approval and audit row |
Success — 2xx
The upstream response wrapped in a discovery envelope. The HTTP status mirrors the upstream status:
{
"data": { "ok": true, "channel": "C123", "ts": "1715000000.000100" },
"_meta": {
"capability": "agent.action",
"timestamp": "2026-05-18T10:40:55.123Z",
"next_actions": [
{ "capability": "audit.query", "endpoint": "GET /v1/audit" },
{ "capability": "agent.permissions", "endpoint": "GET /v1/agent/permissions" }
],
"docs": "https://api.agentvalet.ai/v1/openapi.json",
"correlation_id": "0f4c9a61-2b7d-4e3a-9c85-6d1e0a2b3f47",
"audit_id": "5d0e2f7a-3c1b-4a9e-8f6d-0b2c4e6a8d10"
}
}correlation_id is always present. audit_id is the audit row id (and
the id of its signed receipt). connection appears when
the proxy can say which connection served the call. If the audit write
failed, _meta.audit_write_failed is true — the call still happened, so
do not retry it.
A non-2xx upstream response is returned with the upstream status and body as-is, not wrapped.
If the org has PII redaction beta enabled and the agent has a per-platform
visibility map, the data body is rewritten before the response is
returned. The audit row records pii_detected, pii_categories,
pii_redacted, and pii_record_count. See PII redaction.
Pending approval — 202 Accepted
When the agent’s grant for the platform has require_approval set (it
applies to every call on that grant, reads included), or a policy or
guardrail asks for approval:
{
"status": "pending_approval",
"approval_id": "7c1e4b2a-9d3f-4e8a-b6c5-2f0a1d9e8b34",
"audit_id": "5d0e2f7a-3c1b-4a9e-8f6d-0b2c4e6a8d10",
"message": "Owner approval required"
}Your agent should poll GET /v1/approvals/:id until
status is approved (with result populated) or denied /
expired. The MCP platform tools do this long-poll for you for
up to 50 seconds; after that it returns the 202 envelope and your
agent should explain the wait to the user.
Approvals expire 10 minutes from creation. The owner is notified in the dashboard inbox, plus push (mobile) and delegate email on Team and above.
Error codes
All errors follow the common envelope. The error
identifier is stable; where it is a sentence, the machine code is in
reason or code (and in report_hint.suggested_payload.code).
| Status | error | Cause | Fix |
|---|---|---|---|
| 400 | Secrets detected in request params | Request body matched the secrets scanner (AWS key, Slack token, etc.) | Don’t embed credentials in data; use the platform’s own auth mechanism |
| 400 | Endpoint not allowed | SSRF guard rejected the endpoint (private/loopback) | Use the platform’s public API endpoint |
| 400 | invalid_caller / untrusted_caller_issuer | The caller value is malformed or its assertion can’t be verified | Fix the caller field or header |
| 401 | Missing Bearer token | No Authorization: Bearer header | Add the header |
| 401 | Invalid JWT | JWT malformed or expired, missing agent_id / owner_id, or the signature doesn’t verify against the agent’s public key | Check the claims; if the key doesn’t match the registered public key, re-register or rotate |
| 401 | Unknown agent | agent_id claim doesn’t match any row | Confirm the agent exists in the dashboard |
| 402 | plan_inactive | A paid plan is past_due or unpaid (Free is never inactive) | Update billing |
| 402 | spend_cap_reached | The org’s opt-in spend cap is reached | Raise or disable the cap in Billing |
| 403 | agent_halted (with reason: agent_suspended / agent_revoked / circuit_breaker_open and a halt block) | The agent itself can’t act | Stop and tell the user; the owner must reactivate the agent or reset the breaker |
| 403 | Permission denied (with reason, e.g. scope_not_granted, no_policy_assigned, denied_by_guardrail) | The call isn’t granted, or a policy or guardrail blocks it | Grant the scope in Agents → permissions, or change the policy |
| 403 | AuthZEN policy denied | A policy decision from the AuthZEN evaluator denied the call | Inspect the audit row’s metadata.authzen_reason |
| 403 | Response blocked by ingress policy (code: blocked_by_doorman) | The upstream response looked like a prompt-injection payload and was withheld | Inspect the audit row |
| 409 | needs_consent | MCP-server platform: this agent hasn’t been authorized on the server yet. The body carries connect_url and poll_url; the MCP tools surface it as a -32042 URL elicitation | Give the human connect_url, then retry once approved |
| 409 | platform_reauth_needed | The MCP connection has expired | The owner reconnects the platform |
| 412 | consent_required | The person in caller hasn’t connected their own account | Ask the person to connect |
| 412 | platform_not_connected | Nothing is connected for this platform | The owner connects it; access_request_hint says how to ask |
| 413 | Request body too large to hold for approval (code: approval_body_too_large) | An approval-gated call’s body exceeds the queue limit (max_bytes) | Send a smaller body |
| 429 | rate_limited | The plan’s per-minute limit for the org is exceeded (Retry-After header) | Back off |
| 502 | Upstream request failed (with detail: "Response could not be safely returned") | PII redaction failed closed and the org has the beta flag on | See troubleshooting: PII fail-closed 502 |
| 502 | Upstream request failed (generic detail) | The upstream call threw (network error or timeout). Not retried; no audit_id or receipt in the body | Check the upstream platform status; quote correlation_id |
| 502 | Upstream authentication failed (code: reauth_failed) | The platform rejected the stored credential and a token refresh didn’t fix it | The owner re-authorizes the platform |
| 502 | Platform misconfigured | A required connection field is missing | Reconnect the platform with the field filled in |
Audit
Every governed call writes one row to audit_log with:
action= the requested scoperesult=allowed/denied/pending_approval/errorrequest_id= the response’scorrelation_idmetadata.response_statusfrom the upstreampii_*columns if PII redaction ran- Forensic columns (
ip_address,user_agent,geo_country, etc.) if your org has forensic capture enabled