# 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, follow it if a person has to decide, and use the authorization before you buy. 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**

```bash
export VERITICITY_API_KEY="cmp_live_9f2a1c4b7e30.<secret>"
```

> **Nothing is allowed until a policy allows it**
>
> An agent with no applicable policy has no spending authority at all. That is deliberate: purchasing authority is granted, never assumed.

## 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**

```bash
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",
    "category": "software",
    "reason": "Renew build pipeline compute credits"
  }'
```

**200 OK**

```json
{
  "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": "Approved. GBP 75.00 is within the GBP 500.00 limit for a single purchase."
  },
  "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**

```bash
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": "software",
    "reason": "Renew build pipeline compute credits"
  }'
```

**201 Created**

```json
{
  "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"],
    "authority": "WITHIN_POLICY",
    "explanation": "Approved. GBP 75.00 is within the GBP 500.00 limit for a single purchase.",
    "evaluatedAt": "2026-09-12T09:14:07.882Z",
    "engineVersion": "veriticity-trust-engine@0.1.0"
  },
  "approval": { "required": false, "expiresAt": null },
  "findings": [ /* 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](https://docs.veriticity.com/sdk.md) covers the rest.

**With the SDK**

```typescript
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",
  category: "software",
  reason: "Annual renewal of the build pipeline plan",
});

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

`Idempotency-Key` is required, and 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**

```typescript
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",
      category: "software",
      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;
}
```

> **A refusal is a successful call**
>
> `REJECTED` comes back as `201`. Veriticity was asked a question and answered it. If you branch on the status code you will treat a correctly refused purchase as an outage.

## 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**

```bash
curl https://app.veriticity.com/v1/purchase-requests/0199a3f1-8c2e-7b40-9f31-6d2e5a1c0b77 \
  -H "Authorization: Bearer $VERITICITY_API_KEY"
```

**200 OK**

```json
{
  "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."
    },
    "authorization": {
      "id": "0199a3f2-0d1a-7e55-8c40-2b9e6f3a1d08",
      "status": "active",
      "expiresAt": "2026-09-13T10:02:41.117Z"
    }
  }
}
```

`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](https://docs.veriticity.com/webhooks.md) are better: subscribe to `purchase.approved` and `purchase.rejected` and you will be told rather than having to ask.

## 5. Use the authorization

An approved purchase carries `authorization`. Before you make the purchase, ask for an execution token for it with the same key:

**Get an execution token**

```bash
curl -X POST https://app.veriticity.com/v1/purchase-authorizations/$AUTHORIZATION_ID/execution-token \
  -H "Authorization: Bearer $VERITICITY_API_KEY"
```

The token is shown once. It works only for this authorization, and stops working when the authorization is used, withdrawn, voided or expires. Send it with the terms you are about to buy on:

**Use the authorization**

```bash
curl -X POST https://app.veriticity.com/v1/purchase-authorizations/$AUTHORIZATION_ID/use \
  -H "Authorization: Bearer $VERITICITY_API_KEY" \
  -H "Veriticity-Execution-Token: $EXECUTION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amountMinor": "42000",
    "currency": "GBP",
    "merchant": "Acme Cloud",
    "category": "software",
    "executionReference": "order-1182"
  }'
```

**200 OK**

```json
{
  "result": "authorized",
  "authorization": {
    "id": "0199a3f2-0d1a-7e55-8c40-2b9e6f3a1d08",
    "status": "used",
    "usedAt": "2026-09-12T10:05:12.604Z",
    "expiresAt": "2026-09-13T10:02:41.117Z"
  },
  "replayed": false
}
```

Go ahead only on `authorized`. A refusal is also a `200`, with a `reason`. Reading the purchase back now shows the authorization as `used`.

> **Authorized is not purchased**
>
> `authorized` means the authorization was checked and used. Veriticity does not make the purchase and is not told whether it went through. If it fails, your organisation voids the authorization from the dashboard.

## Where to go next

- [TypeScript SDK](https://docs.veriticity.com/sdk.md) — the official client, with typed decisions, typed errors and webhook verification.
- [Purchases](https://docs.veriticity.com/purchases.md) — reason codes, budget holds, and the full decision shape.
- [Policies](https://docs.veriticity.com/policies.md) — how the rules that decided this are written.
- [Examples](https://docs.veriticity.com/examples.md) — webhook verification, OAuth, and idempotent retries in full.
- [API reference](https://docs.veriticity.com/api-reference.md) — every endpoint.

---

Source: https://docs.veriticity.com/quickstart
