MCP

Veriticity's Model Context Protocol endpoint lets an AI assistant check, request and follow up on purchases within the spending rules a person set. Nine tools, three scopes, one OAuth connection.

Veriticity exposes a Model Context Protocol endpoint so an AI assistant can check, request and follow up on purchases — within the spending rules a person set, and acting as an agent that already exists.

Endpoint
POST https://app.veriticity.com/mcp

Streamable HTTP, stateless. Nothing is remembered between calls, which is correct for a multi-instance deployment and means one caller’s session can never be served to another.

How an assistant connects#

MCP uses the same OAuth boundary as the rest of the API — not a second authentication model. An access token from a connection a person authorised is the only credential accepted; an API key is refused outright.

The discovery chain is standard, so most clients do it for you:

1. Unauthenticated call
# An unauthenticated call tells a client where to authenticate.
curl -i -X POST https://app.veriticity.com/mcp

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="veriticity", resource_metadata="https://app.veriticity.com/.well-known/oauth-protected-resource"
2. Protected resource metadata
{
  "resource": "https://app.veriticity.com/mcp",
  "authorization_servers": ["https://app.veriticity.com"],
  "scopes_supported": ["purchase:check", "purchase:request", "purchase:read"],
  "bearer_methods_supported": ["header"]
}

From there the client fetches the authorization server’s own metadata and runs Authorization Code with PKCE. A person signs in, chooses the organisation and the agent, and approves the scopes. See Authentication for the full flow.

Live and Sandbox#

Each environment has its own endpoint, its own sign-in and its own connections. A token issued by one is not accepted by the other.

EnvironmentEndpoint
Livehttps://app.veriticity.com/mcp
Sandboxhttps://sandbox.veriticity.com/mcp

Every tool behaves the same in both. In Sandbox the agents, policies, budgets, purchases and approvals are Sandbox’s own, and nothing decided there authorises a real purchase. get_agent returns environment as live or sandbox, so an assistant can tell which it is connected to. Connecting in Sandbox does not connect Live: add the Live endpoint separately. See Sandbox.

The eleven tools#

ToolScopeWhat it does
get_agentpurchase:checkWho this connection acts as, in which environment, currency and calendar, and which scopes it was granted. Takes no arguments.
get_spending_rulespurchase:checkThe rules in force for that agent.
get_budget_statuspurchase:checkWhat the budget windows currently hold.
check_purchasepurchase:checkWould this be allowed, right now? Changes nothing.
request_purchasepurchase:requestSubmits it, and the Trust Engine decides. An approval is permission to buy; nothing is bought.
cancel_purchasepurchase:requestTakes back a request of this connection’s that is still waiting on a person.
request_paymentpurchase:requestAsks Veriticity to pay for an approved purchase of this connection’s, on its approved terms. Answers unavailable where paying isn’t set up.
get_purchasepurchase:readWhat happened to one purchase this connection submitted.
list_purchasespurchase:readEverything this connection has submitted.
get_pending_approvalspurchase:readThe ones a person still has to answer.
A tool call
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "check_purchase",
    "arguments": {
      "merchant": "Acme Cloud",
      "merchantDomain": "acme-cloud.example.com",
      "amountMinor": "7500",
      "currency": "GBP",
      "category": "software"
    }
  }
}

check_purchase answers with simulatedDecision (outcome, reasonCodes, explanation, advance and deferredRules), findings for each rule checked, budgets for each window the purchase would touch, and policiesConsidered, where each policy’s participation is PARTICIPATING or NOT_APPLICABLE. It is the body POST /v1/simulations/current-policies returns.

Every answer that carries a purchase’s amountMinor, the known amount, carries maximumMinor beside it: the most the purchase can cost with every variable charge at its maximum, which is what rules and budgets judged, or null when no charge can vary. On request_purchase, get_purchase and list_purchases, an approved decision’s authority is WITHIN_POLICY when the rules allowed it with no person deciding, and AUTHORIZED when a rule asked for approval and a person gave it. Read it, not reasonCodes, to tell the two apart.

Three scopes across eleven tools is not a gap. The vocabulary describes what a person consented to, not how many functions the transport exposes — reading what is left of a budget is the same grant as asking whether a purchase would be allowed, because it is that question with the purchase left out.

Paging through history#

list_purchases uses keyset cursor pagination: limit between 1 and 50, defaulting to 20, and a nextCursor to carry forward. This is deliberately not the page-and-size model GET /v1/policies uses; see Conventions.

Naming the merchant#

A merchant is identified by its website. Send merchantDomain, the host the purchase would be made on, with every check_purchase and request_purchase. A full URL is accepted and only its host is used.

  • merchant is required and is a label for people. It is never looked up, and a familiar name establishes nothing.
  • example.com and www.example.com are the same website. Any other subdomain is a different one unless the organisation has recorded it for that merchant.
  • merchantReference supports the website and never replaces it. Sent without merchantDomain it establishes no merchant.

Without a website on record, the reason code is MERCHANT_IDENTITY_UNESTABLISHED, and a policy that restricts merchants refuses the purchase or holds it for a person. That is a different answer from a merchant that was identified and is not allowed: sending the website fixes the first, and only a change of policy fixes the second.

Naming the category#

Both purchase tools take category: exactly one value from Veriticity’s list, which the tool’s input schema sets out with a definition of each. The assistant chooses it from what the product is. other is for a product it can identify that no category covers, never for one it can’t. Any other value is refused.

A purchase holding different kinds of product, or a product that involves more than one restricted kind, is sent as items instead, each with its own category and a risk tag for every other restricted kind it involves, with charges and taxIncluded. An item whose category the assistant can’t establish is sent as uncertain, with a reason, rather than given a guessed category. Asking again for the same items in another order, within fifteen minutes, is the same purchase.

A category is recorded as the assistant stated it. It can make a purchase fail a category rule, and it counts in a purchase’s favour only where the rule accepts the category the agent gives.

What an assistant cannot do#

Each absence is a design decision rather than a missing feature.

  • No tool approves, rejects, resolves or overrides an approval. When the engine escalates, a person answers in the dashboard and the assistant finds out by calling get_purchase. An assistant that could answer its own escalation would make the human half of the trust layer decorative. It can take its own waiting request back with cancel_purchase, which only removes authority.
  • No tool writes, activates or archives a policy. The rules an agent is judged against are not editable by the thing being judged.
  • No tool creates, renames, disables or archives an agent, and none issues, reads or revokes a credential or a connection.
  • No tool enumerates the organisation’s agents. An agent handed a roster of its organisation’s other agents has been handed a map of the identities it might impersonate. get_agent describes the one agent the caller already is.
  • No tool reads the authoring model — no drafts, no version history, no assignment lists. get_spending_rules reports what is in force, not how somebody arrived at it.
  • No tool reads merchant names. get_spending_rules says that a policy has a merchant list and how long it is, and never what is on it. A merchant is identified by the website a purchase states, never by its name, and what an assistant states is not verified. Under such a policy a purchase is refused or waits for a person unless its merchant can be established.
  • No generic passthrough. There is no tool that forwards an arbitrary request to /v1.
  • No tool moves money. Veriticity governs purchase decisions; it does not execute payments. A check, an approval and an authorization are each permission at most. None is a record that anything was bought, and no tool uses an authorization.

Identity comes from the connection#

No tool accepts organizationId, agentId or connectionId. They are absent from every schema, so a value an assistant sends for one is dropped before any tool reads it and changes nothing.

Both come from the authenticated connection. An assistant that could choose its own agent would be choosing whose money it was about to spend.

Every read is narrowed on the submitting connection inside the query. Another connection’s purchases — even another connection to the same agent — are indistinguishable from purchases that do not exist.

Approval#

A request that needs approval is decided by a person in the dashboard. Who may decide depends on that person’s role in the organisation, and on nothing else. An assistant never approves or rejects anything, whoever connected it.

RoleConnect an assistantApprove or reject its requests
OwnerYesYes, including requests from an assistant they connected
AdminYesYes, including requests from an assistant they connected
UserYesNo. An owner or admin decides
ViewerNoNo

On an individual account you are the owner, so you approve your own assistant’s requests. An approval is permission for that purchase and nothing more: no checkout is run and no payment is taken. A purchase your rules refuse outright can’t be approved by anybody.

Disconnecting an assistant stops its token at once. A request it left waiting stays in the approval queue for a person to approve or reject, and the disconnected assistant cannot read the result. Removing Veriticity inside an assistant does not always disconnect it: Claude keeps the connection open, and ChatGPT ends it. Disconnect it in Veriticity’s settings to be sure it has ended.

Retries and asking again#

A tool call carries no Idempotency-Key convention, so request_purchase derives one from the connection and what is being bought: merchant, merchantDomain, amountMinor, currency, category and any references. A host that retries a timed-out call gets the original decision instead of making a second purchase.

Calling request_purchase again for the same purchase within fifteen minutes is one purchase, whatever was decided, and the reply says replayed: true. reason is not part of the key, so rewording it does not make a second purchase. After fifteen minutes the same purchase is a new request.

A refusal is therefore repeated for fifteen minutes. To have the same purchase decided again sooner, pass the earlier purchase’s id as previousPurchaseRequestId:

Asking again
{
  "name": "request_purchase",
  "arguments": {
    "merchant": "Acme Cloud",
    "merchantDomain": "acme-cloud.example.com",
    "amountMinor": "7500",
    "category": "software",
    "reason": "Renew the build pipeline's compute credits",
    "previousPurchaseRequestId": "0199c0de-0000-7000-8000-000000000001"
  }
}
  • The earlier purchase must be this connection’s, and must not be waiting for approval. Otherwise the call is invalid_arguments and nothing is submitted.
  • If it was approved and its authorization has not been used, that authorization is withdrawn. It can’t be used afterwards, whatever the new request is decided, and its purchase stops counting towards budgets.
  • It is a new request, decided against the rules and budgets as they stand, and counted as a new attempt. The earlier one is kept as it was.
  • Retrying that call returns the same new purchase. To ask a third time, name the second.

Cancelling a request#

cancel_purchase takes a purchaseRequestId and an optional reason. It ends a request that is still waiting on a person and releases the budget it held. It is the operation POST /v1/purchase-requests/{id}/cancel performs.

cancel_purchase result
{
  "purchaseRequest": { "id": "0199c0de-0000-7000-8000-000000000001", "status": "CANCELLED" },
  "replayed": false
}
  • A second call for the same request answers the same way with replayed: true.
  • A request that was approved, refused or expired, or already decided by a person, is not cancelled: the tool answers not_cancellable and nothing is changed. It does not end an authorization.
  • A purchase another connection submitted is not_found.

Paying for an approved purchase#

request_payment takes a purchaseRequestId for an approved purchase this connection requested, and variableFinal only when the purchase was approved with charges that could still vary. It asks Veriticity to pay exactly what was approved: the amount, merchant and currency are the purchase’s own and cannot be changed. There is no HTTP equivalent.

The person the purchase was made for approves paying it in the dashboard first; until then the answer is not_payable and nothing is reserved. Their approval covers one payment attempt. If that attempt charged nothing, they approve again before the next.

Where Veriticity pays by issuing a one-time card for the purchase, the answer to the call that issued it carries the card in credential, once. Your assistant’s model can read those details; use them only to check out that purchase at its merchant. Every other answer has credential set to null. An issued card is not a payment.

Before a card is issued, the person confirms it in the dashboard, for that one payment. Until they do, payment.status is verification_required, with verificationExpiresAt, and nothing is reserved or paid. They have five minutes to confirm, and the confirmation can be used for five minutes after that. Call request_payment again once they have. Your assistant can’t confirm it, and no code is sent to it.

request_payment result, payments not set up
{
  "purchaseRequest": { "id": "0199c0de-0000-7000-8000-000000000001" },
  "payment": {
    "status": "unavailable",
    "paymentId": null,
    "amountMinor": null,
    "currency": null,
    "capturedMinor": null
  },
  "replayed": false,
  "verificationExpiresAt": null,
  "credential": null
}
  • Paying through Veriticity is not yet available. Until it is, every call answers payment.status unavailable, and nothing is reserved or paid.
  • Where it is available, payment.status is the payment’s state, or verification_required before a card is issued: in_progress, outcome_unknown, authorized, captured, declined, failed or cancelled. None says a purchase was made: a payment is not an order. An unknown outcome stays unknown until the payment provider says.
  • A second call for the same purchase answers with the same payment and replayed: true. Only one payment can hold a purchase’s authorization at a time.
  • A purchase that wasn’t approved, has expired, was withdrawn, voided or used, or was already reported on is not_payable, and nothing is paid. A purchase another connection submitted, or one requested in the dashboard, is not_found.
  • The person you work for must approve paying for the purchase in Veriticity first. Until they do, the answer is not_payable and nothing is reserved. You can’t approve it yourself.
  • A purchase paid this way takes its outcome from the payment provider. report_purchase_outcome refuses it.

Reporting what happened#

report_purchase_outcome tells Veriticity what became of an approved purchase the connection requested: COMPLETED with finalAmountMinor, COMPLETED_AMOUNT_UNKNOWN, PARTIAL with itemPositions, FAILED, or REFUNDED with refundMinor. It takes the fields of POST /v1/purchase-requests/{id}/outcome and records the same report.

  • The report is the assistant’s word. Depending on the organisation’s settings it is accepted as what was charged or waits for a person to confirm it. It approves nothing and makes, changes or cancels no order or payment.
  • When more than one purchase could be the one meant, the assistant is told to confirm which, by merchant, amount and when it was requested, before it reports. The answer’s purchase names the purchase the report was recorded against.
  • Send a reportId with each report. The same report again answers with replayed: true; a different report with a used reportId is report_id_reuse.
  • A purchase that was never approved is not_reportable. One another connection submitted is not_found.

Errors and refusals#

Tools return structuredContent as well as text, so a client that wants fields gets fields. There are deliberately no declared output schemas: the shapes come from the same builders the REST API uses, and a second hand-maintained description of them would drift.

A tool that cannot answer returns isError: true inside an ordinary 200 response. Its text is code: sentence, and its structuredContent is { error, message }, plus retryAfterSeconds when waiting is the answer. This is not the REST API’s { error: { code, message } } envelope.

CodeMeaningWhat to do
insufficient_scopeThe connection was not granted this tool’s scope.Don’t retry. The person reconnects the assistant with the wider grant.
invalid_argumentsThe arguments can’t be evaluated. Nothing was submitted.Correct the arguments the message names, then call again.
not_foundNo purchase with that id was submitted by this connection.Use an id from request_purchase or list_purchases.
not_cancellableThe purchase is no longer waiting on a person: it was decided or expired. Nothing was cancelled. Cancelling one already cancelled answers as before, with replayed: true.Call get_purchase to see where it stands.
not_reportableThe purchase was never approved, so it has no outcome. Nothing was recorded.Don’t retry.
report_id_reuseThat reportId was already used for a different report. Nothing was recorded.Send the new report with a new reportId.
rate_limitedToo many calls recently.Wait retryAfterSeconds, then call again.
temporarily_unavailableA limit or the plan couldn’t be checked, so nothing was submitted.Wait retryAfterSeconds, then call again.
purchase_allowance_exhausted, plan_required, plan_limit_exceeded, feature_not_included, billing_suspendedThe organisation’s plan refuses this.Don’t retry. Tell the person; an owner changes the plan.
invalid_callerThe credential doesn’t act as an agent.Connect the assistant to an agent through OAuth.
internal_errorVeriticity couldn’t complete the call.Reads can be repeated. Before repeating request_purchase, call list_purchases to see whether it was recorded.

Arguments that don’t match a tool’s input schema, and a tool name this server doesn’t have, are refused before the tool runs. That error has text only, beginning MCP error -32602, and no structuredContent. Correct the call; repeating it unchanged fails the same way.

A token that is missing, expired or revoked is refused at the HTTP layer with 401 and a WWW-Authenticate header naming the resource metadata, before any tool runs. When a token was sent, the header also says error="invalid_token". An expired access token is renewed with the refresh token. A revoked connection, or one whose person can no longer connect assistants, needs that person to connect it again. See Errors.

Every tool shares the limits its REST equivalent spends, counted against the connection: 600 reads a minute, 120 check_purchase calls a minute, 120 cancel_purchase calls a minute, and for request_purchase 120 a minute for the connection and 600 a minute for the organisation. Reaching a limit on MCP spends the same allowance as reaching it over HTTP; there is one allowance per connection, not one per transport.

Over a limit, the tool answers rate_limited; if the limit can’t be checked, purchases answer temporarily_unavailable and nothing is submitted. Both carry retryAfterSeconds in structuredContent alongside the sentence, so a host can back off without parsing English.