# Receipts

Every purchase ends in an immutable receipt: what was asked, what ran, what it cost, what was refunded.

Every live `execute` request ends in one receipt. It records the tool, the fingerprint of what you sent, and for every item the provider that answered, the outcome of the published check, the version of that check, the fingerprint of the result, what was charged and what went back. A receipt never changes. A refund from an upheld dispute is added on top of it, so the receipt still shows what happened at the time. This page has every field, where to read receipts (API, SDKs, MCP, CSV, dashboard) and a full example.

> Arettic is pre-launch. Keys go to design partners first; everyone else joins the [waitlist](/waitlist). The `@arettic/sdk` and `@arettic/mcp` packages (npm) and the `arettic` package (PyPI) are published at launch. Test keys (`sk_test_…`) write no receipts: `receipt_id` is absent from a test response, and `GET /v1/receipts` lists live purchases only. See [Test mode](/docs/test-mode#not-in-test-mode).

## When a receipt is written

A receipt is written once every item of a request has settled, that is, once each one is charged or released. For one input or a batch of up to 25, that is before the `execute` answer comes back, and the answer carries `receipt_id`. For a batch of more than 25, the [job](/docs/jobs) gets its receipt when it finishes, and `GET /v1/jobs/{job_id}` then carries `receipt_id`. A retry with the same `idempotency_key` never writes a second receipt: the replayed answer carries the first `receipt_id`. See [Idempotency](/docs/execute#idempotency).

If the process dies between the last item settling and the receipt, a safety net writes the receipt a few minutes later, so a replay never hangs on a purchase that has no receipt. An item caught mid-call by the same crash is released with the reason `interrupted` and never charged.

## The receipt, field by field

Money is always `{ "credits": "7", "usd": "0.007" }`: credits as a whole number in a string (1 credit is $0.001) and the same amount in dollars with three decimals.

Top-level receipt fields

| Field | Type | Meaning |
|---|---|---|
| receipt_id | UUID | The receipt's id. It is the `receipt_id` on the execute answer. |
| created_at | ISO 8601 timestamp | When the receipt was written: after the last item settled. |
| tool_id | string or null | The tool the request ran on, by its slug (for example `hunter-verify-email`). When a paused tool was replaced before the call (`fallback: true`), this is the replacement; the execute answer named the original in `substituted_for`. |
| job_id | UUID or null | The job, when the request was a batch of more than 25. Otherwise `null`. |
| idempotency_key | string or null | The `idempotency_key` you sent, or `null` if you sent none. |
| request_hash | 64 hex characters or null | SHA-256 of the whole request: the tool and every input. See [Hashes](#hashes). |
| check_version | string or null | The version of the pass rules the items were checked with. Currently `v1`. |
| charged | money | What was captured across all items. This never changes, even after a dispute is upheld. |
| refunded | money | What went back: the held credits of items that were not charged, plus every refund from an upheld dispute on this receipt. This is the one figure that grows after the receipt is written. |
| summary | object | `items`, `passed`, `partial` and `failed`: one count per input, after any fallback. |
| items | array | One entry per item attempt, in `index` order. A fallback attempt is a second entry with the same `index`. See below. |

## Items

Each entry in `items[]` is one attempt at one input: one execution, one hold, one provider call, one check, one settlement. `reason`, `fallback_of` and `dispute` are present only when they apply.

Item fields

| Field | Type | Meaning |
|---|---|---|
| index | integer from 0 | The position of the input in `inputs` (0 for a single `input`). The original attempt and its fallback share it. |
| execution_id | UUID | The attempt's id. It is the `execution_id` on a single-input answer, and what a [dispute](/docs/disputes) can name instead of `receipt_id` + `item_index`. |
| tool_id | string | The tool that ran this attempt, by slug. A fallback attempt names its own tool. |
| provider | string | The provider behind that tool, by slug (for example `hunter`). |
| outcome | string | The settlement: `captured`, `released` or `expired`. See the table below. |
| reason | string, optional | Why the item was not a pass: a [reason code](/docs/execute#reason-codes) such as `status:unknown`, `no_result`, `provider_error`, `timeout`, `interrupted`, `cancelled` or `job_time_limit`. Absent on a pass. |
| charged | money | What was captured for this attempt. |
| refunded | money | The rest of the hold, given back: price minus `charged`. A dispute refund is not in here; it is in `dispute.refunded` and in the receipt's top-level `refunded`. |
| pricing_mode | string | `per_success` (the default: a pass is charged, a fail is not) or `per_call` (every checked call is charged, pass or fail; provider errors stay free). See [Per-call pricing](/docs/execute#per-call-pricing). |
| input_hash | 64 hex characters | SHA-256 of this input. See [Hashes](#hashes). |
| result_hash | 64 hex characters or null | SHA-256 of the provider's answer as it was checked. `null` when there was no answer (a provider error, a timeout, an interrupted or expired item). |
| check_version | string or null | The pass-rule version this attempt was checked with (`v1`). |
| fallback_of | UUID, optional | On a fallback attempt: the `execution_id` of the first attempt. See [Fallback](/docs/execute#fallback). |
| settled_at | ISO 8601 timestamp or null | When the attempt was captured or released. |
| dispute | object, optional | When this attempt has been disputed: `dispute_id`, `status` (`open`, `upheld` or `rejected`) and `refunded` (money; 0 until upheld). |

Item outcomes

| outcome | Money | When |
|---|---|---|
| captured | `charged` is above 0 | The result passed its check, or was a partial pass (web search, charged pro rata), or the org is on per-call pricing for the tool and the call was checked. |
| released | `charged` is 0, `refunded` is the full price | The check failed, the provider errored or timed out, the checker itself threw (`check_error`), a job cancelled the item before it ran (`cancelled`), or a crash caught it mid-call (`interrupted`). |
| expired | `charged` is 0, `refunded` is the full price | A job hit its 2-hour limit before this item ran (`job_time_limit`). Only on receipts with a `job_id`. |

The receipt's `charged` is the sum of the items' `charged`; its `refunded` is the sum of the items' `refunded` plus the sum of every `dispute.refunded`. The CSV export adds up the same way, row by row.

## Hashes

The three hashes let you prove, later, what you asked and what you got, without Arettic keeping the data. Inputs and results themselves are stored encrypted for 7 days (longer while a dispute on them is open); the hashes stay on the receipt.

Every hash is SHA-256, as 64 lowercase hex characters, of the value's **canonical JSON**: object keys sorted at every level, arrays in their order, no whitespace, strings and numbers encoded as JSON. The values hashed are:

- `request_hash`: the object `{ "tool": <the tool's UUID>, "items": [ <every input, in order> ] }`. A single `input` is a one-item list. The same key with a different `request_hash` is what makes a replay refuse with `idempotency_conflict`.
- `input_hash`: the input object of that item, exactly as you sent it after the free pre-call check.
- `result_hash`: the provider's answer after Arettic's normalisation, which is the `result` a passed item returns. For `find_email` it includes the `verification_status` Arettic's own verifier added before the check. `null` when there was no answer.

To check a hash yourself, canonicalise and hash the value you hold. In Node this is the same code the API runs:

**TypeScript (Node)**

```ts
import { createHash } from "node:crypto";

// Canonical JSON: keys sorted at every level, no whitespace.
function canonical(v: unknown): string {
  if (Array.isArray(v)) return `[${v.map(canonical).join(",")}]`;
  if (v && typeof v === "object")
    return `{${Object.keys(v as Record<string, unknown>)
      .sort()
      .map((k) => `${JSON.stringify(k)}:${canonical((v as Record<string, unknown>)[k])}`)
      .join(",")}}`;
  return JSON.stringify(v);
}
const sha256 = (s: string) => createHash("sha256").update(s).digest("hex");

// input_hash of the input you sent; result_hash of the result a passed item returned.
console.log(sha256(canonical({ email: "jason@acme.com" })));
console.log(sha256(canonical(bought.result)));
```

**Python**

```python
import hashlib
import json

# The same canonical form for values that came off the wire as JSON:
# strings, whole numbers, booleans, null, lists and objects.
def canonical(value):
    return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)

def sha256(text):
    return hashlib.sha256(text.encode("utf-8")).hexdigest()

print(sha256(canonical({"email": "jason@acme.com"})))  # compare with input_hash
print(sha256(canonical(bought["result"])))  # compare with result_hash
```

## Read one receipt

`GET /v1/receipts/{receipt_id}` with a live agent key. Any agent of the org can read any of the org's receipts, not only its own. An id that is not a UUID, or that belongs to another org, answers `404 not_found`; the message never says which.

**curl**

```bash
curl https://api.arettic.com/v1/receipts/7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d \
  -H "Authorization: Bearer $ARETTIC_API_KEY"
```

**TypeScript**

```ts
import { Arettic } from "@arettic/sdk";

const arettic = new Arettic({ apiKey: process.env.ARETTIC_API_KEY });

const receipt = await arettic.receipt("7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d");
console.log(receipt.charged.usd, receipt.refunded.usd, receipt.summary);
for (const item of receipt.items) {
  console.log(item.index, item.provider, item.outcome, item.reason ?? "", item.charged.credits);
}
```

**Python**

```python
from arettic import Arettic

client = Arettic()  # reads ARETTIC_API_KEY (and ARETTIC_API_URL, if set)

receipt = client.receipt("7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d")
print(receipt["charged"]["usd"], receipt["refunded"]["usd"], receipt["summary"])
for item in receipt["items"]:
    print(item["index"], item["provider"], item["outcome"], item.get("reason", ""), item["charged"]["credits"])
```

Over MCP the `get_receipt` tool takes `receipt_id` and returns the same JSON as text content; a refusal comes back as a tool error whose text is the error envelope. Use it with a live key: test calls write no receipts, so there is nothing of a test agent's own to read. Client configs are on [MCP server](/docs/mcp).

**MCP tool call**

```json
{
  "name": "get_receipt",
  "arguments": { "receipt_id": "7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d" }
}
```

A full receipt for a batch of three verify-email inputs: two passed, one failed on `status:unknown`, and the last item was later disputed and upheld. Note that `charged` stays at 14 while `refunded` is the 7 not charged plus the 7 refunded by the dispute. The ids and hashes are made up.

**Response (example)**

```json
{
  "receipt_id": "7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
  "created_at": "2026-09-29T10:14:07.512Z",
  "tool_id": "hunter-verify-email",
  "job_id": null,
  "idempotency_key": "verify-batch-1",
  "request_hash": "9f2c1e8a7b6d5c4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f",
  "check_version": "v1",
  "charged": { "credits": "14", "usd": "0.014" },
  "refunded": { "credits": "14", "usd": "0.014" },
  "summary": { "items": 3, "passed": 2, "partial": 0, "failed": 1 },
  "items": [
    {
      "index": 0,
      "execution_id": "c02576cf-b3ce-4b0f-a30c-6e8aad4328ce",
      "tool_id": "hunter-verify-email",
      "provider": "hunter",
      "outcome": "captured",
      "charged": { "credits": "7", "usd": "0.007" },
      "refunded": { "credits": "0", "usd": "0.000" },
      "pricing_mode": "per_success",
      "input_hash": "1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809",
      "result_hash": "d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3",
      "check_version": "v1",
      "settled_at": "2026-09-29T10:14:06.201Z"
    },
    {
      "index": 1,
      "execution_id": "9b1f0d2e-4c6a-4e8b-9f3d-2a7c5e1b8d40",
      "tool_id": "hunter-verify-email",
      "provider": "hunter",
      "outcome": "released",
      "reason": "status:unknown",
      "charged": { "credits": "0", "usd": "0.000" },
      "refunded": { "credits": "7", "usd": "0.007" },
      "pricing_mode": "per_success",
      "input_hash": "2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a",
      "result_hash": "e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4",
      "check_version": "v1",
      "settled_at": "2026-09-29T10:14:06.744Z"
    },
    {
      "index": 2,
      "execution_id": "5e8c1a7b-2d3f-4a6e-8b9c-1f2e3d4c5b6a",
      "tool_id": "hunter-verify-email",
      "provider": "hunter",
      "outcome": "captured",
      "charged": { "credits": "7", "usd": "0.007" },
      "refunded": { "credits": "0", "usd": "0.000" },
      "pricing_mode": "per_success",
      "input_hash": "3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b",
      "result_hash": "f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b8a7f6e5",
      "check_version": "v1",
      "settled_at": "2026-09-29T10:14:07.030Z",
      "dispute": {
        "dispute_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
        "status": "upheld",
        "refunded": { "credits": "7", "usd": "0.007" }
      }
    }
  ]
}
```

With `fallback: true`, an input that failed on the first tool and passed on the next appears twice, with the same `index`. Only the passing attempt is charged:

**Response (example): the items of a fallback receipt**

```json
"items": [
  {
    "index": 0,
    "execution_id": "0d2e7f31-8a4b-4c9d-b1e6-5f7a9c3d2e10",
    "tool_id": "hunter-verify-email",
    "provider": "hunter",
    "outcome": "released",
    "reason": "status:unknown",
    "charged": { "credits": "0", "usd": "0.000" },
    "refunded": { "credits": "7", "usd": "0.007" },
    "pricing_mode": "per_success",
    "input_hash": "1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809",
    "result_hash": "e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4",
    "check_version": "v1",
    "settled_at": "2026-09-29T11:02:15.118Z"
  },
  {
    "index": 0,
    "execution_id": "3f9c2c1e-6d7a-4b8f-a2e1-9c0b5d4e3f21",
    "tool_id": "zerobounce-verify-email",
    "provider": "zerobounce",
    "outcome": "captured",
    "charged": { "credits": "6", "usd": "0.006" },
    "refunded": { "credits": "0", "usd": "0.000" },
    "pricing_mode": "per_success",
    "input_hash": "1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809",
    "result_hash": "0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b",
    "check_version": "v1",
    "fallback_of": "0d2e7f31-8a4b-4c9d-b1e6-5f7a9c3d2e10",
    "settled_at": "2026-09-29T11:02:16.402Z"
  }
]
```

## List receipts

`GET /v1/receipts` with a live agent key lists the org's receipts, newest first, one line per receipt. It is the same list the dashboard's receipt explorer shows. Filters go in the query string; all are optional.

Query parameters of GET /v1/receipts

| Parameter | Format | Meaning |
|---|---|---|
| from | YYYY-MM-DD | First day to include (UTC). Without it, 30 days before `to` (or before now). |
| to | YYYY-MM-DD | Last day to include, inclusive (UTC). Without it, now. |
| agent_id | agent UUID | Only this agent's receipts. A value that is not a UUID is ignored. |
| tool_id | tool slug | Only receipts for this tool, for example `hunter-verify-email`. |
| before | the `next` value of the previous page | Cursor: receipts older than this timestamp. Anything else is `invalid_input`. |
| limit | 1 to 200 | Receipts per page. The default is 50; values outside the range are clamped. |

The range may cover at most 366 days, and `from` must be before `to`; otherwise the answer is `400 invalid_input`. Each page carries `next`: pass it as `before` for the next page, and stop when it is `null`.

**curl**

```bash
curl "https://api.arettic.com/v1/receipts?from=2026-09-01&to=2026-09-29&tool_id=hunter-verify-email&limit=50" \
  -H "Authorization: Bearer $ARETTIC_API_KEY"
```

**TypeScript**

```ts
import { Arettic } from "@arettic/sdk";

const arettic = new Arettic({ apiKey: process.env.ARETTIC_API_KEY });

// Every receipt for one tool in September, page by page.
const query = { from: "2026-09-01", to: "2026-09-30", toolId: "hunter-verify-email", limit: 200 };
let page = await arettic.receipts(query);
while (true) {
  for (const r of page.receipts) console.log(r.created_at, r.agent_name, r.charged.usd, r.net.usd);
  if (!page.next) break;
  page = await arettic.receipts({ ...query, before: page.next });
}
```

**Python**

```python
from arettic import Arettic

client = Arettic()

# `from` is a Python keyword, so the SDK takes from_ and sends it as from.
query = {"from_": "2026-09-01", "to": "2026-09-30", "tool_id": "hunter-verify-email", "limit": 200}
page = client.receipts(**query)
while True:
    for r in page["receipts"]:
        print(r["created_at"], r["agent_name"], r["charged"]["usd"], r["net"]["usd"])
    if not page["next"]:
        break
    page = client.receipts(**query, before=page["next"])
```

**Response (example)**

```json
{
  "receipts": [
    {
      "receipt_id": "7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
      "created_at": "2026-09-29T10:14:07.512Z",
      "agent_id": "4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a",
      "agent_name": "Research, EU",
      "tool_id": "hunter-verify-email",
      "job_id": null,
      "summary": { "items": 3, "passed": 2, "partial": 0, "failed": 1 },
      "charged": { "credits": "14", "usd": "0.014" },
      "dispute_refunded": { "credits": "7", "usd": "0.007" },
      "net": { "credits": "7", "usd": "0.007" },
      "open_disputes": 0
    }
  ],
  "next": null
}
```

Fields of a receipt in the list

| Field | Meaning |
|---|---|
| receipt_id, created_at, tool_id, job_id, summary, charged | As on the receipt. |
| agent_id, agent_name | The agent that bought. |
| dispute_refunded | Money refunded by upheld disputes on this receipt. |
| net | `charged` minus `dispute_refunded`: what the receipt cost in the end. Credits released at settlement are not part of either. |
| open_disputes | Disputes on this receipt not yet decided. |
| next | The cursor for the next page, or `null` on the last one. |

## The org endpoints

The same reads exist under `/v1/orgs/{orgId}/…` for people and for [org keys](/docs/authentication#org-keys) (`ok_…`, read scope is enough), with the member role or above. They take the same filters and return the same shapes. The CSV export exists only here.

Receipt endpoints

| Endpoint | Key | Returns |
|---|---|---|
| GET /v1/receipts/{id} | agent key | One receipt. |
| GET /v1/receipts | agent key | The org's receipts, newest first, with filters and paging. |
| GET /v1/orgs/{orgId}/receipts/{id} | session or org key | One receipt. |
| GET /v1/orgs/{orgId}/receipts | session or org key | The same list, same filters. |
| GET /v1/orgs/{orgId}/exports/receipts.csv | session or org key | Every item attempt in the range as CSV; filters `from` and `to`. See [CSV export](#csv-export). |

**curl**

```bash
curl "https://api.arettic.com/v1/orgs/$ARETTIC_ORG_ID/receipts?limit=200" \
  -H "Authorization: Bearer $ARETTIC_ORG_KEY"

curl https://api.arettic.com/v1/orgs/$ARETTIC_ORG_ID/receipts/7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d \
  -H "Authorization: Bearer $ARETTIC_ORG_KEY"

curl "https://api.arettic.com/v1/orgs/$ARETTIC_ORG_ID/exports/receipts.csv?from=2026-09-01&to=2026-09-30" \
  -H "Authorization: Bearer $ARETTIC_ORG_KEY" \
  -o arettic-receipts.csv
```

**TypeScript**

```ts
import { AretticOrg } from "@arettic/sdk";

const org = new AretticOrg({ apiKey: process.env.ARETTIC_ORG_KEY, orgId: "your-org-id" });

const page = await org.receipts.list({ from: "2026-09-01", to: "2026-09-30", limit: 200 });
const receipt = await org.receipts.get("7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d");
const csv = await org.receipts.exportCsv({ from: "2026-09-01", to: "2026-09-30" }); // a string
```

**Python**

```python
import os

from arettic import AretticOrg

org = AretticOrg(os.environ["ARETTIC_ORG_KEY"], "your-org-id")

page = org.receipts.list(from_="2026-09-01", to="2026-09-30", limit=200)
receipt = org.receipts.get("7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d")
csv = org.receipts.export_csv(from_="2026-09-01", to="2026-09-30")  # a string
```

## CSV export

`GET /v1/orgs/{orgId}/exports/receipts.csv` returns one row per item attempt (a fallback is its own row), oldest receipt first, for the same `from` and `to` range as the list (default the last 30 days, at most 366), up to 100,000 rows. The body is `text/csv; charset=utf-8` with CRLF line endings, sent as an attachment named `arettic-receipts.csv`. Cells with commas, quotes or line breaks are quoted; a cell that starts with `=`, `+`, `-`, `@` or a tab gets a leading `'` so a spreadsheet never runs it as a formula. Per receipt, the `charged_credits` rows add up to the receipt's `charged`, and the `net_credits` rows to the list's `net`.

CSV columns, in order

| Column | Meaning |
|---|---|
| receipt_id | The receipt. |
| receipt_created_at | When the receipt was written, ISO 8601. |
| agent_id | The agent that bought. |
| agent_name | Its name. |
| tool_id | The tool that ran this attempt, by slug. |
| provider | Its provider, by slug. |
| item_index | The input's position; the original and its fallback share it. |
| execution_id | The attempt's id. |
| attempt | `original` or `fallback`. |
| outcome | `captured`, `released` or `expired`. |
| reason | The reason code, or empty on a pass. |
| price_credits | The price held for the attempt. |
| charged_credits | What was captured. |
| charged_usd | The same in dollars, three decimals. |
| not_charged_credits | `price_credits` minus `charged_credits`: the hold given back. |
| dispute_status | `open`, `upheld`, `rejected`, or empty when never disputed. |
| dispute_refunded_credits | Credits refunded by an upheld dispute, else 0. |
| net_credits | `charged_credits` minus `dispute_refunded_credits`. |
| net_usd | The same in dollars. |
| input_hash | SHA-256 of the input. |
| result_hash | SHA-256 of the result, or empty. |
| check_version | The pass-rule version (`v1`). |
| settled_at | When the attempt settled, ISO 8601. |

**Response (example)**

```text
receipt_id,receipt_created_at,agent_id,agent_name,tool_id,provider,item_index,execution_id,attempt,outcome,reason,price_credits,charged_credits,charged_usd,not_charged_credits,dispute_status,dispute_refunded_credits,net_credits,net_usd,input_hash,result_hash,check_version,settled_at
7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d,2026-09-29T10:14:07.512Z,4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a,"Research, EU",hunter-verify-email,hunter,0,c02576cf-b3ce-4b0f-a30c-6e8aad4328ce,original,captured,,7,7,0.007,0,,0,7,0.007,1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809,d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3,v1,2026-09-29T10:14:06.201Z
7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d,2026-09-29T10:14:07.512Z,4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a,"Research, EU",hunter-verify-email,hunter,1,9b1f0d2e-4c6a-4e8b-9f3d-2a7c5e1b8d40,original,released,status:unknown,7,0,0.000,7,,0,0,0.000,2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a,e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4,v1,2026-09-29T10:14:06.744Z
7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d,2026-09-29T10:14:07.512Z,4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a,"Research, EU",hunter-verify-email,hunter,2,5e8c1a7b-2d3f-4a6e-8b9c-1f2e3d4c5b6a,original,captured,,7,7,0.007,0,upheld,7,0,0.000,3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b,f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b8a7f6e5,v1,2026-09-29T10:14:07.030Z
```

## The dashboard

Signed-in members see the same data at [https://arettic.com/app/receipts](/app/receipts). The explorer lists receipts newest first with the time (UTC), agent, tool, how many items were charged out of how many, what was charged and the net after dispute refunds, with a pill for open disputes. It filters by date, agent and tool, shows the last 30 days by default, and pages with an "Older receipts" link. "Download CSV" gives the export above for the same date range. Opening a receipt shows every item with its provider, outcome, reason and charge, and a "Dispute" form on any charged item for 7 days after it settled. Filing and following disputes is on [Disputes](/docs/disputes).

## Immutability and disputes

A receipt and its items are written once, after settlement, and never updated: `charged`, every item's `outcome`, `charged`, `refunded`, the hashes and `check_version` stay as they were. Two things are layered on top when a dispute is filed and decided:

- The item gains a `dispute` object with the dispute's `status` and its `refunded` amount, which is 0 until the dispute is upheld.
- The receipt's top-level `refunded` grows by every upheld dispute's refund. In the list, `dispute_refunded` carries the same amount and `net` is `charged` minus it.

A charged item can be disputed for 7 days after it settled; Arettic decides within 48 hours against the stored copy of the result, and a dispute not decided in time is upheld automatically. An upheld dispute returns the item's charge to the buckets it came from (trial or paid). The receipt's `charged` is not reduced, so a receipt read a year later still says what was paid on the day.

## JSON Schema

The receipt's JSON Schema (draft 2020-12) is published at [https://api.arettic.com/schemas/receipt.json](https://api.arettic.com/schemas/receipt.json) and listed with the other schemas at [https://arettic.com/schemas](/schemas). Its version is bumped only when the shape changes in a way a validator would notice. Fields marked optional above (`reason`, `fallback_of`, `dispute`) are absent, not `null`, when they do not apply.

## Errors

A refusal comes back as `{ "error": { "code", "message", "doc_url", "retryable" } }`; the SDKs raise it as `AretticApiError` with the same fields. The codes you can get from the receipt endpoints:

Error codes on the receipt endpoints

| code | HTTP | When |
|---|---|---|
| [not_found](/docs/errors#not_found) | 404 | The receipt id is not a UUID, or no receipt with that id belongs to your org. On the org endpoints, also when the person or key has no membership in that org. |
| [invalid_input](/docs/errors#invalid_input) | 400 | `from` or `to` is not `YYYY-MM-DD`, `from` is not before `to`, the range is over 366 days, or `before` is not a valid cursor. |
| [unauthenticated](/docs/errors#unauthenticated) | 401 | No key, a revoked key, or a disabled agent. A test key can read receipts but sees none of its own, since test calls write none. |
| [forbidden](/docs/errors#forbidden) | 403 | An org key used on another org's `/v1/orgs/{orgId}/…` path. |
| [rate_limited](/docs/errors#rate_limited) | 429 | Too many requests from this agent. Wait the `Retry-After` seconds and send it again; see [Rate limits](/docs/rate-limits). |

## Where next

[Disputes](/docs/disputes) to challenge a charged item. [Execute](/docs/execute) for the answer a purchase returns and every reason code. [Batches and jobs](/docs/jobs) for receipts that come from a job. [Org API](/docs/org-api) for everything an org key can read next to receipts. [Error codes](/docs/errors) for the full registry.

Updated 2026-09-29. This page as HTML: https://arettic.com/docs/receipts · Markdown: https://arettic.com/docs/receipts.md · JSON: https://arettic.com/docs/receipts.json
