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. 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.
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 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.
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.
| 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. |
| 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.
| 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 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 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. |
| input_hash | 64 hex characters | SHA-256 of this input. See 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. |
| 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). |
| 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 singleinputis a one-item list. The same key with a differentrequest_hashis what makes a replay refuse withidempotency_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 theresulta passed item returns. Forfind_emailit includes theverification_statusArettic's own verifier added before the check.nullwhen 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:
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: "[email protected]" })));
console.log(sha256(canonical(bought.result)));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": "[email protected]"}))) # compare with input_hash
print(sha256(canonical(bought["result"]))) # compare with result_hashRead 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 https://api.arettic.com/v1/receipts/7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d \ -H "Authorization: Bearer $ARETTIC_API_KEY"
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);
}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.
{
"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.
{
"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:
"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.
| 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 "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"
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 });
}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"]){
"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
}| 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 (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.
| 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. |
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
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 stringimport 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 stringCSV 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.
| 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. |
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. 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.
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
disputeobject with the dispute'sstatusand itsrefundedamount, which is 0 until the dispute is upheld. - The receipt's top-level
refundedgrows by every upheld dispute's refund. In the list,dispute_refundedcarries the same amount andnetischargedminus 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 and listed with the other schemas at https://arettic.com/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:
| code | HTTP | When |
|---|---|---|
| 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 | 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 | 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 | 403 | An org key used on another org's /v1/orgs/{orgId}/… path. |
| rate_limited | 429 | Too many requests from this agent. Wait the Retry-After seconds and send it again; see Rate limits. |
Where next
Disputes to challenge a charged item. Execute for the answer a purchase returns and every reason code. Batches and jobs for receipts that come from a job. Org API for everything an org key can read next to receipts. Error codes for the full registry.