Policies

How Veriticity decides what an AI agent may buy. Spending limits, merchant allow and block lists, budget windows, approval thresholds, and why policies are authored by people.

A policy is the set of rules the Trust Engine evaluates a purchase against. Spending limits, approval thresholds, merchant lists and budget windows, attached to an organisation or to particular agents.

Policies are written by people#

There is no API for creating, editing, activating or archiving a policy, and there will not be one. Every credential of every kind is refused with 403 api_key_not_permitted.

If you receive api_key_not_permitted, no other credential will work. Do not go looking for one.

What a policy contains#

GET /v1/policies/{id}
{
  "policy": {
    "id": "0199a3e0-1f4c-7a11-b3d2-5e8c7a4f2b90",
    "name": "Company baseline",
    "scopeType": "ORGANIZATION",
    "status": "ACTIVE",
    "inForce": true,
    "agentIds": ["0199a3d1-…", "0199a3d2-…"],
    "activeVersion": {
      "id": "0199a3e0-2a7b-7c33-9f10-1b2c3d4e5f60",
      "versionNumber": 4,
      "status": "ACTIVE",
      "currencyScope": "EXPLICIT",
      "currencies": ["GBP"],
      "ruleCount": 3,
      "merchantCount": 12,
      "activatedAt": "2026-09-01T08:00:00.000Z"
    }
  }
}
FieldMeaning
scopeTypeORGANIZATION governs every agent; AGENT governs the ones in agentIds.
statusACTIVE, DISABLED or ARCHIVED.
inForceWhether the engine is actually reading it right now. Not the same as having an active version — see below.
activeVersionThe version the engine evaluates against, or null.

What a version contains#

GET /v1/policies/{id}/versions/{versionId}
{
  "version": {
    "id": "0199a3e0-2a7b-7c33-9f10-1b2c3d4e5f60",
    "versionNumber": 4,
    "status": "ACTIVE",
    "policyId": "0199a3e0-1f4c-7a11-b3d2-5e8c7a4f2b90",
    "currencies": ["GBP"],
    "rules": [
      {
        "id": "0199a3e0-…",
        "ruleType": "AMOUNT_LIMIT",
        "effect": "BLOCK",
        "periodKind": "PER_REQUEST",
        "amountMinor": "50000",
        "currency": "GBP"
      },
      {
        "id": "0199a3e0-…",
        "ruleType": "AMOUNT_LIMIT",
        "effect": "REQUIRE_APPROVAL",
        "periodKind": "PER_REQUEST",
        "amountMinor": "25000",
        "currency": "GBP"
      },
      {
        "id": "0199a3e0-…",
        "ruleType": "SPEND_LIMIT",
        "effect": "BLOCK",
        "periodKind": "DAY",
        "amountMinor": "100000",
        "currency": "GBP"
      }
    ],
    "merchants": [
      { "id": "0199a3e0-…", "listType": "ALLOW", "merchantKey": "acme-cloud", "label": "Acme Cloud" }
    ]
  }
}

Rules#

CombinationWhat it does
AMOUNT_LIMIT + BLOCKA single-purchase maximum. Anything above it is refused outright.
AMOUNT_LIMIT + REQUIRE_APPROVALAn approval threshold. Above it, a person is asked and the budget is held.
SPEND_LIMIT + BLOCK, period DAY or MONTHA budget. Total spending in the window is capped.
SPEND_LIMIT + REQUIRE_APPROVALA budget that escalates rather than refusing once crossed.

Merchant lists#

ALLOW and BLOCK. A block list refuses what is on it; an allow list refuses everything that is not on it. Merchant names are normalised into a merchantKey for matching, so “Acme Cloud” and “acme cloud” match. Domains are recorded as evidence and are never matched on.

Versions and lifecycle#

Policies are versioned, and versions are immutable once active. A decision records which version it was evaluated against, so a purchase from March can be explained against the rules that were in force in March rather than today’s.

  • DRAFT — editable, not enforced. A policy may have one.
  • ACTIVE — enforced. Exactly one per policy.
  • ARCHIVED — superseded, kept for evidence.

Budget windows#

A SPEND_LIMIT creates a window — a day or a month — resolved against the organisation’s timezone rather than the server’s or the caller’s. A daily limit resets at midnight where the organisation is.

A window tracks three separate quantities:

QuantityMeaning
heldMinorReserved by purchases waiting on a person.
committedMinorSpent by purchases that were approved.
openingBalanceMinorReconciled spend the window’s own reservations do not account for.

A simulation returns all three plus what the purchase would add, which is the honest way to answer “how much is left”. Subscribe to budget.threshold_reached and budget.exhausted on webhooks to be told rather than having to poll.

Currency scope#

A version declares which currencies it covers. Veriticity performs no currency conversion, so a purchase in a currency no applicable policy covers is refused with CURRENCY_NOT_COVERED rather than converted at a rate nobody agreed to.

A simulation lists the policies excluded for exactly this reason under policiesExcludedByCurrencyScope, which is what makes a NO_APPLICABLE_POLICY answer actionable.

Reading policies over the API#

Three read endpoints, all organisation-scoped API keys only:

  • GET /v1/policies — filter with ?status and ?search, page with ?page and ?pageSize.
  • GET /v1/policies/{id}
  • GET /v1/policies/{id}/versions/{versionId}

An agent-bound credential is refused. The constraints being drawn up for an agent are the authoring side’s business — an agent learns what it may spend by asking, through a simulation, rather than by reading the rules.

Testing a change before it is live#

Two endpoints help before an activation:

Validate#

Runs activation’s own check as a dry run. It answers 200 even when the draft is unacceptable — you asked a question and got an answer.

Validate a draft
curl -X POST https://app.veriticity.com/v1/policies/$POLICY_ID/versions/$VERSION_ID/validate \
  -H "Authorization: Bearer $VERITICITY_ORG_KEY"
200 OK
{
  "valid": false,
  "issues": [
    {
      "code": "APPROVAL_THRESHOLD_ABOVE_MAXIMUM",
      "message": "The approval threshold of GBP 600.00 is above the GBP 500.00 maximum, so nothing can ever reach it."
    }
  ],
  "warnings": [],
  "budgetImpact": [],
  "version": { "versionNumber": 5, "status": "DRAFT" }
}

budgetImpact is empty in the ordinary case. A non-empty one means activating would start a limit from a total that already includes spending nobody reserved against — worth looking at before you activate.

Simulate#

POST /v1/simulations/draft-policy asks what a draft would decide about a specific purchase. POST /v1/simulations/policy-version asks the same of an active or archived version, exactly as it was frozen — which is how you explain a decision that was made months ago.