---
title: "Every write made to this store, with who, on whose approval, before, after, revert and cost."
method: POST
path: "/api/v1/receipts"
tags: ["read"]
---

# Every write made to this store, with who, on whose approval, before, after, revert and cost.

`POST /api/v1/receipts`

## Request

- Base URL: `https://app.magistry.io`
- URL: `https://app.magistry.io/api/v1/receipts`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://app.magistry.io/api/oauth/authorize`, token URL `https://app.magistry.io/api/oauth/token`, scopes: `automation:read`)

One row per recorded write, newest first, across every place a write is recorded. `action_id` joins the rows that belong to one action. `actor` is `<kind>:<name>` with kind one of user, agent, app, key, operator or system; `unattributed` marks a row written before actors were recorded. `before` is the value the write replaced, or an object naming why it is absent (`_unavailable`) or where it will be captured (`_pending`). `action_cost_credits` is the credits the whole action cost, repeated on each of its rows, and null when nothing was attributed to it — not zero. Pages are keyset: pass `next_cursor` back as `cursor`. A json page answers `{ ok, result: { receipts, page, scope } }`; a csv or jsonl page carries its cursor in the `x-next-cursor` header. `all=true` streams the whole history as CSV or JSONL and ends with a manifest line (`rows`, `sha256` of every line before it, `complete`, `next_cursor`); when `complete` is false the export stopped after 50 seconds and `next_cursor` resumes it.

## Request body

- object
  - `store_id` string, uuid: One store this credential covers. Defaults to all of them; never widens.
  - `kinds` string[]: Sources to include. Defaults to all six.
  - `action_id` string, uuid: Only the rows of this action.
  - `since` string, date-time: Inclusive lower bound on occurred_at.
  - `until` string, date-time: Exclusive upper bound on occurred_at.
  - `limit` integer: Rows per page.
  - `cursor` string: The `next_cursor` of the previous page or export manifest.
  - `format` 'json' | 'csv' | 'jsonl'
  - `all` boolean: Stream every page as csv or jsonl, ending with a manifest line.
  - `subjects` string[]: Only rows about one of these: a variant or product uuid, a Shopify numeric id, a gid or a theme handle, matched against the key each source records its subject under.

## Response `200`

Result

- object
  - `ok` boolean, required
  - `result` unknown, required

## Other responses

- `400`: `invalid_request` — The request is malformed or an argument is unusable. The message names which. `invalid_body` — The body is not a JSON object. `invalid_expiry` — An approval request must end: `expires_at` lies at least 5 minutes and at most 7 days after it is filed. A request with no decision by then closes as expired, which means not approved. `invalid_rule` — The rule could not bind as written: a fact or action outside the vocabulary, an operator that cannot compare that fact, or a limit with no bound. Nothing was saved.
- `401`: `unauthorized` — No credential was presented, or the one presented is not valid. This deliberately does not say whether it was unknown or forged: a 401 that tells those apart is an oracle for guessing tokens. A credential that VERIFIED and is refused answers with its own code (expired, grant_revoked, app_disabled, address_not_allowed). `expired` — The credential verified and its expiry has passed. `grant_revoked` — The OAuth token verified, and the merchant revoked this connection after it was issued. `app_disabled` — The OAuth token verified, and Magistry has disabled this application for every merchant. No token of the application is admitted and none is issued. `address_not_allowed` — The credential verified and carries an IP allowlist that this request did not come from. `credential_rejected` — Diagnose could not resolve the credential at all. `no_credential` — Diagnose was called with no Authorization header, so there is nothing to report on. `invalid_client` — The client authentication on this request did not verify.
- `403`: `forbidden` — The credential is valid and does not hold a scope this tool requires. `insufficient_scope` — As `forbidden`, and the body names `required_scopes`. A webhook subscription needs both `automation:write` to configure the store and the read scope for every type it filters — a webhook is a read. `entity_not_granted` — The credential holds a finance scope, and the entity's owner has not given this app those books (or has taken them back). A finance scope says what an app may do; the owner's grant says which entities. A natural person's books need the person's own explicit delegate grant.
- `404`: `not_found` — No tool of that name is exposed to this credential. Note that a tool hidden by a missing scope answers `forbidden`, not this — the two are told apart deliberately.
- `409`: `idempotency_conflict` — This idempotency key was already used with DIFFERENT arguments. Replaying it would be a second, different write under a key that promises one. `idempotency_in_flight` — A request with this key is still running. This is not an error — it is "ask again", and the response carries `Retry-After`. `no_policy` — The app has no grant for this field on this store. Owning a field is a separate, larger grant than a scope: the reconciler pushes what the owner declares, and nobody else may touch the field meanwhile. `held_by_other` — Another owner holds this field, named in `held_by`. A field has at most one owner, enforced by a unique index, and an app never takes a field over from a live holder. `raced` — Another owner took this field in the same instant as this declaration. The database let exactly one of the two through. `merchant_kept` — The merchant kept an outside change to this field. Once kept, the database refuses every new declaration on it unless a store rule allows declaring it again. `too_many_pending` — This app already has the maximum number of approval requests pending on this store (the body carries `pending` and `limit`). The bound keeps one app from burying the merchant's screen. `already_closed` — The approval request was decided, expired or withdrawn before this call; `status` says which. A closed request never reopens. `store_unavailable` — The store is not accepting approval requests because Magistry is not installed on it. `too_many_rules` — The store already holds the maximum number of rules. `in_flight` — The declaration is being written to the platform at this moment, so it cannot be reverted underneath that write. `overwritten_externally` — Someone other than this app changed the field on the platform after the app's value was applied, and the merchant has not yet decided whether that change stays. A revert now would overwrite it, so nothing was reverted. `finance_proposal_refused` — The books refused the proposal in their current state: the entity's owner has not switched app proposals on (Books → Assistant), or the period it would land in is closed. Nothing was recorded and nothing was posted. `reason` names which. `export_not_ready` — The export job has not produced its file (yet): `status` in the body says whether it is still `queued`/`running` or `failed`, with the failure's own code.
- `410`: `finance_cursor_gone` — The change-feed cursor is older than the feed keeps (guarantees.finance_change_feed_retention_days in /api/v1/changelog.json) and events after it have been removed. The body carries `oldest_cursor`. `export_expired` — The export's file was downloadable until `expires_at` and is no longer kept.
- `413`: `payload_too_large` — The body exceeds the endpoint’s size bound.
- `429`: `rate_limited` — This credential exceeded its per-minute limit. The body carries the limit and the count, and the response carries `Retry-After` and the `RateLimit-*` headers.
- `500`: `simulation_unverified` — The simulation could not prove that its transaction was aborted, so no verdict is given: an answer from a run that may have kept something would read as a clean one. Reported on our side. `server_error` — Something on our side failed. The request may or may not have taken effect.
- `502`: `sandbox_failed` — Creating or seeding the sandbox failed. Provisioning is one transaction, so nothing was created and the key shown to nobody was never stored.
- `503`: `idempotency_unavailable` — The idempotency claim could not be taken, so the write was NOT attempted. This fails closed on purpose: running an unguarded write because the guard was unreadable is how a customer gets refunded twice. `approvals_unavailable` — Approval requests are not enabled on this server yet. Nothing was written. `receipts_unavailable` — Receipts are not enabled on this server yet. Nothing was exported. `policies_unavailable` — Store rules are not enabled on this server yet. Nothing was read or written. `sandbox_unavailable` — Developer sandboxes are not open: the database fence that keeps a sandbox key on demo data is not in place, or the provisioning worker is not configured. Nothing was created. `unavailable` — A read could not be served just now. Nothing was written or exported. `overwrites_unreadable` — We could not confirm that nobody else has changed this field since it was applied, and a revert that cannot check could overwrite a merchant's change. Nothing was reverted. `rls_scoped_execution_unavailable` — This deployment is not currently running the row-level-scoped execution mode the headless surface requires, so it refuses rather than executing with wider reach than the grant. `finance_grants_unavailable` — The entities this app may read could not be established for this request, so nothing was read — an unreadable grant is never treated as a wider one, nor as "no entities". `finance_unavailable` — The books could not be read or a figure could not be computed from them just now, so nothing was returned — never a partial list or a zero that would read as the ledger. `export_unavailable` — The export job could not be started, read or served just now — including a stored file whose bytes do not match the SHA-256 the job recorded, which is never sent. Nothing was started or sent. `corpus_unavailable` — The corpus this endpoint reads could not be loaded.

---

[API](https://skmtc.dev/magistry/apis/magistry-api.md) · [All operations](https://skmtc.dev/magistry/apis/magistry-api/llms.txt) · [OpenAPI document](https://skmtc.dev/magistry/apis/magistry-api/revisions/13dae20d49cb?raw)
