{
  "page": "docs/budgets-and-approvals",
  "title": "Budgets and approvals",
  "slug": "budgets-and-approvals",
  "description": "Per-agent budgets and approval thresholds, enforced on our side: what happens when a purchase needs a person.",
  "section": "Buying results",
  "updated": "2026-09-30",
  "blocks": [
    {
      "type": "p",
      "text": "An agent spends your money, so every agent has limits it can't change: a monthly budget and an approval threshold. Both are checked on our side before anything is held, on every request. Going over either doesn't fail the purchase: it asks an owner, and the agent carries on once they say yes."
    },
    {
      "type": "h2",
      "id": "limits",
      "text": "The limits"
    },
    {
      "type": "table",
      "caption": "Per-agent limits",
      "head": [
        "Limit",
        "Default",
        "What it does"
      ],
      "rows": [
        [
          "`monthly_budget_credits`",
          "50,000 credits ($50) per UTC calendar month",
          "A live purchase that would take the agent's spend this month over its budget needs approval (`over_budget`). At 80% the owners get a `budget.80pct` event and email."
        ],
        [
          "`approval_threshold_credits`",
          "20,000 credits ($20)",
          "A single request whose price × items is above it needs approval (`over_threshold`). `null` means never ask. The dashboard offers $5, $20, $50 or never."
        ],
        [
          "`ip_allowlist`",
          "empty (any IP)",
          "IP addresses or CIDR ranges the key may be used from. Anything else gets `403 ip_not_allowed`."
        ]
      ]
    },
    {
      "type": "p",
      "text": "Set them on the Agents page, or with `PATCH https://api.arettic.com/v1/orgs/{orgId}/agents/{agentId}` (a member, or a write org key). An agent key can read its own limits (`GET /v1/agent`, `GET /v1/balance`) but never change them."
    },
    {
      "type": "code",
      "title": "curl",
      "lang": "bash",
      "code": "curl -X PATCH https://api.arettic.com/v1/orgs/$ORG_ID/agents/$AGENT_ID \\\n  -H \"Authorization: Bearer $ARETTIC_ORG_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{ \"monthly_budget_credits\": 200000, \"approval_threshold_credits\": 50000 }'"
    },
    {
      "type": "h2",
      "id": "order",
      "text": "What is checked, in order"
    },
    {
      "type": "list",
      "ordered": true,
      "items": [
        "**Balance.** The org can't pay for price × items: declined, `402 insufficient_credits`. No approval can fix that; top up.",
        "**Trial limit.** An org that has never topped up can spend at most 50 trial credits an hour: declined, `429 trial_limit`.",
        "**Approval threshold,** then **monthly budget**: over either, the request becomes an approval (below)."
      ]
    },
    {
      "type": "p",
      "text": "The hold step checks the budget again under a lock, so two requests sent at the same moment can't both slip under it."
    },
    {
      "type": "h2",
      "id": "approval-flow",
      "text": "When a purchase needs approval"
    },
    {
      "type": "p",
      "text": "The request is not run and nothing is held. The answer is HTTP 202:"
    },
    {
      "type": "code",
      "title": "Response",
      "lang": "json",
      "code": "{\n  \"status\": \"approval_required\",\n  \"approval_id\": \"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d\",\n  \"reason\": \"over_threshold\",\n  \"amount\": { \"credits\": \"24000\", \"usd\": \"24.000\" },\n  \"expires_at\": \"2026-10-01T11:10:00.000Z\",\n  \"message\": \"This is above the agent's approval threshold. An owner has been asked to approve it; retry with approval_id once approved.\"\n}"
    },
    {
      "type": "list",
      "ordered": true,
      "items": [
        "Every owner gets an email with a one-click link to approve or decline, no sign-in needed. They can also decide on the Approvals page, or with `POST /v1/orgs/{orgId}/approvals/{id}/decision` and `{ \"decision\": \"approve\" }` or `\"reject\"` (optional `note`). Webhook endpoints get `approval.requested` and then `approval.decided`.",
        "The agent checks `GET /v1/approvals/{id}`: `status` is `pending`, `approved`, `rejected`, `expired` or `used`, with `reason`, `agent`, `tool`, `items`, `amount`, `expires_at`, `decided_at` and `decision_note`.",
        "Once `approved`, the agent sends **the same request again** with `approval_id`. It runs without the threshold and budget checks, for exactly the tool, inputs and amount that were approved."
      ]
    },
    {
      "type": "list",
      "items": [
        "Approvals expire after 24 hours; using one after that gets `410 approval_expired`. Send the request again for a new one.",
        "A declined request gets `403 approval_rejected`.",
        "Changing the tool or the inputs, or a price that went up since, gets `409 approval_mismatch`. So does using an approval twice, or another agent's.",
        "Sending the same request again while its approval is pending returns the same approval: owners aren't emailed twice."
      ]
    },
    {
      "type": "code",
      "title": "TypeScript",
      "lang": "ts",
      "code": "import { Arettic } from \"@arettic/sdk\";\n\nconst arettic = new Arettic();\nconst request = { tool_id: \"peopledatalabs-enrich-company\", inputs };\nlet result = await arettic.execute(request);\nif (result.status === \"approval_required\") {\n  const approval = await arettic.waitForApproval(result.approval_id); // polls every 5 s, up to 24 h\n  if (approval.status !== \"approved\") throw new Error(`Not approved: ${approval.status}`);\n  result = await arettic.execute({ ...request, approval_id: result.approval_id });\n}"
    },
    {
      "type": "code",
      "title": "Python",
      "lang": "python",
      "code": "import time\n\nfrom arettic import Arettic\n\nclient = Arettic()\nrequest = {\"tool_id\": \"peopledatalabs-enrich-company\", \"inputs\": inputs}\nresult = client.execute(request)\nif result[\"status\"] == \"approval_required\":\n    approval_id = result[\"approval_id\"]\n    while (approval := client.approval(approval_id))[\"status\"] == \"pending\":\n        time.sleep(5)\n    if approval[\"status\"] != \"approved\":\n        raise RuntimeError(f\"Not approved: {approval['status']}\")\n    result = client.execute(request, approval_id=approval_id)"
    },
    {
      "type": "p",
      "text": "Over MCP, `execute` returns the same `approval_required` answer and `get_approval` checks it; see [MCP](/docs/mcp)."
    },
    {
      "type": "h2",
      "id": "other-guards",
      "text": "Other guards"
    },
    {
      "type": "list",
      "items": [
        "**Velocity alerts.** If an agent spends more than 3× its usual hourly rate in an hour (and at least 1,000 credits, $1), the owners get an email, at most once a day per agent.",
        "**Revoke at once.** Revoking an agent's key on the Agents page (or `POST /v1/orgs/{orgId}/agents/{agentId}/revoke-key`) stops it on the next request. Rotating issues a new key and revokes the old one in the same step.",
        "**Plan limits.** Each plan has a number of agents and seats; going past it gets `402 plan_limit`, with a link to upgrade in `message`."
      ]
    },
    {
      "type": "table",
      "caption": "Agents and seats per plan",
      "head": [
        "Plan",
        "Agents",
        "Seats"
      ],
      "rows": [
        [
          "Pay as you go",
          "2",
          "1"
        ],
        [
          "Pro",
          "10",
          "5"
        ],
        [
          "Max",
          "no limit",
          "no limit"
        ]
      ]
    }
  ]
}