Quickstart

Submit your first purchase through the Veriticity API and read the decision. A complete worked example in cURL and TypeScript, from API key to explained outcome.

This walks through one complete purchase: check it, submit it, handle the answer, and follow it if a person has to decide. About five minutes.

Before you start#

You need an API key bound to an agent. In the dashboard:

  1. Create an agent, if you have not already.
  2. Write at least one policy that governs it — without one, purchases are refused with NO_APPLICABLE_POLICY. Veriticity approves nothing by default.
  3. Issue an API key for that agent. The secret is shown once.
Set it in your environment
export VERITICITY_API_KEY="cmp_live_9f2a1c4b7e30.<secret>"

1. Check before you buy#

A simulation runs the same evaluation a real purchase would and changes nothing — no purchase record, no decision, no budget reserved.

Simulate
curl -X POST https://app.veriticity.com/v1/simulations/current-policies \
  -H "Authorization: Bearer $VERITICITY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "merchant": "Acme Cloud",
    "amountMinor": "7500",
    "currency": "GBP",
    "reason": "Renew build pipeline compute credits"
  }'
200 OK
{
  "simulation": {
    "isSimulation": true,
    "mode": "CURRENT_POLICIES",
    "disclaimer": "This is a simulation, not a decision. Veriticity created no purchase request, no decision, no budget reservation and no audit evidence, and consumed no budget.",
    "simulatedAt": "2026-09-12T09:14:02.471Z"
  },
  "simulatedDecision": {
    "outcome": "APPROVED",
    "wouldRequireApproval": false,
    "reasonCodes": ["WITHIN_POLICY"],
    "explanation": "Acme Cloud is on the allow list. GBP 75.00 is within the GBP 500.00 single-purchase maximum."
  },
  "budgets": [
    {
      "periodKind": "DAY",
      "currency": "GBP",
      "simulated": { "requestedMinor": "7500", "projectedMinor": "12500" },
      "wouldExceed": false
    }
  ]
}

Note there is no id anywhere in a simulation response. The fields a real decision is identified by are absent rather than null, so a simulation cannot be mistaken for one.

2. Submit the purchase#

Submit
curl -X POST https://app.veriticity.com/v1/purchase-requests \
  -H "Authorization: Bearer $VERITICITY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: build-credits-2026-09-12" \
  -d '{
    "merchant": "Acme Cloud",
    "merchantDomain": "acme-cloud.example.com",
    "amountMinor": "7500",
    "currency": "GBP",
    "category": "cloud-infrastructure",
    "reason": "Renew build pipeline compute credits"
  }'
201 Created
{
  "purchaseRequest": {
    "id": "0199a3f1-8c2e-7b40-9f31-6d2e5a1c0b77",
    "status": "APPROVED",
    "amountMinor": "7500",
    "currency": "GBP"
  },
  "decision": {
    "id": "0199a3f1-91b4-7c02-a55d-3e7f1d9c4a20",
    "outcome": "APPROVED",
    "reasonCodes": ["WITHIN_POLICY"],
    "explanation": "Acme Cloud is on the allow list. GBP 75.00 is within the GBP 500.00 single-purchase maximum, and the daily budget has GBP 420.00 remaining.",
    "evaluatedAt": "2026-09-12T09:14:07.882Z",
    "engineVersion": "veriticity-trust-engine@0.1.0"
  },
  "approval": { "required": false, "expiresAt": null },
  "ruleEvaluations": [ /* every rule considered, including the ones that passed */ ],
  "replayed": false
}

The same call in TypeScript, using the official client. Install it with npm install @veriticity/sdk; the SDK page covers the rest.

With the SDK
import { Veriticity } from "@veriticity/sdk";

const veriticity = new Veriticity({ apiKey: process.env.VERITICITY_API_KEY! });

const result = await veriticity.purchases.submit({
  merchant: "Acme Cloud",
  amountMinor: 42000n,
  currency: "GBP",
  reason: "Annual renewal of the build pipeline plan",
});

console.log(result.decision.outcome, result.decision.explanation);

Idempotency-Key makes a retry safe. Send the same key with the same body and you get the original decision back with replayed: true and a 200, with no new budget reserved. Send it with a different body and you get 409 idempotency_key_reuse.

3. Handle all three outcomes#

Handling a decision
type Outcome = "APPROVED" | "REJECTED" | "REQUIRES_APPROVAL";

const response = await fetch(
  "https://app.veriticity.com/v1/purchase-requests",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.VERITICITY_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify({
      merchant: "Acme Cloud",
      amountMinor: "7500",
      currency: "GBP",
      reason: "Renew build pipeline compute credits",
    }),
  },
);

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message}`);
}

const result = await response.json();

// Branch on the decision, never on the status code. All three outcomes
// arrive as 201 (or 200 for an idempotent replay).
switch (result.decision.outcome as Outcome) {
  case "APPROVED":
    await completeCheckout(result.purchaseRequest.id);
    break;

  case "REJECTED":
    // result.decision.explanation says why, in a sentence you can log or
    // show. result.decision.reasonCodes is the machine-readable half.
    await recordRefusal(result.decision);
    break;

  case "REQUIRES_APPROVAL":
    // A person has been asked, and the budget is held until this instant.
    await waitForHuman(
      result.purchaseRequest.id,
      result.approval.expiresAt,
    );
    break;
}

4. Follow an escalation#

When the answer is REQUIRES_APPROVAL, a person has been asked and the budget is held until approval.expiresAt. If nobody answers by then, the hold is released and the purchase will not happen unless you submit it again.

Read the purchase back to find out what they decided:

Read it back
curl https://app.veriticity.com/v1/purchase-requests/0199a3f1-8c2e-7b40-9f31-6d2e5a1c0b77 \
  -H "Authorization: Bearer $VERITICITY_API_KEY"
200 OK
{
  "purchase": {
    "id": "0199a3f1-8c2e-7b40-9f31-6d2e5a1c0b77",
    "status": "APPROVED",
    "merchant": "Acme Cloud",
    "amountMinor": "42000",
    "currency": "GBP",
    "decision": {
      "outcome": "APPROVED",
      "kind": "APPROVAL_REEVALUATION",
      "explanation": "A person approved this purchase, and it was re-evaluated against the policies in force."
    },
    "approval": {
      "state": "APPROVED",
      "isPending": false,
      "resolvedBy": { "name": "Priya Raman" },
      "comment": "Signed off — this is the renewal we discussed."
    }
  }
}

decision.kind is APPROVAL_REEVALUATION once a person has answered, against INITIAL for the engine’s first decision. The purchase is re-evaluated against the policies in force at that moment, so an approval is not a bypass — if the rules changed in the meantime, the new rules apply.

Polling works, and webhooks are better: subscribe to purchase.approved and purchase.rejected and you will be told rather than having to ask.

Where to go next#

  • TypeScript SDK — the official client, with typed decisions, typed errors and webhook verification.
  • Purchases — reason codes, budget holds, and the full decision shape.
  • Policies — how the rules that decided this are written.
  • Examples — webhook verification, OAuth, and idempotent retries in full.
  • API reference — every endpoint.