> ## Documentation Index
> Fetch the complete documentation index at: https://docs.grainledger.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Transactions

> Create bank and card transactions, code them to accounts, funds, and dimensions, attach receipts, and approve them.

A transaction moves through three coding states, shown in `coding.status`:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
UNCATEGORIZED ──PUT /coding──> CATEGORIZED ──POST /approve──> APPROVED
      ^                            │  ^                          │
      └────── DELETE /coding ──────┘  └──── POST /unapprove ─────┘
```

* **UNCATEGORIZED**: no journal entry. Grain's rules and AI may add a `suggestion`.
* **CATEGORIZED**: a draft journal entry exists. It does not affect reports until approved.
* **APPROVED**: the journal entry is posted to the ledger.

## Look up coding IDs

| Need | Endpoint | Scope |
| - | - | - |
| Account IDs | `GET /api/v1/accounts?accountType=EXPENSE` | `accounts:read` |
| Fund IDs | `GET /api/v1/funds` | `funds:read` |
| Dimension and value IDs | `GET /api/v1/dimensions` | `accounts:read` |
| Vendor IDs | `GET /api/v1/vendors` | `vendors:read` |

Only dimension values with `isAssignable: true` can be assigned. Parent values roll up for reporting. Dimensions with `isRequired: true` must be set before a transaction can be approved.

## Create a transaction without duplicates

Send your system's ID as `externalId` and a stable `Idempotency-Key` header. Reuse the same key if a request times out.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --fail-with-body "https://api.grainledger.com/api/v1/transactions" \
  -H "Authorization: Bearer $GRAIN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: card-txn-8812" \
  --data '{"description":"Office Depot","amount":184.32,"date":"2026-07-01","transactionType":"WITHDRAWAL","accountId":"YOUR_CARD_ACCOUNT_ID","externalId":"card-txn-8812"}'
```

* A retry with the same key and body returns `201` with the original transaction. A different body returns `409`.
* A second transaction with the same `externalId` returns `409`, even with a new key. Find the existing one with `GET /api/v1/transactions?externalId=card-txn-8812`.
* `amount` is always positive. `DEPOSIT` records money in; `WITHDRAWAL`, `TRANSFER`, and `ADJUSTMENT` record money out. The response's `direction` is `INFLOW` or `OUTFLOW`.
* `accountId` is the ledger cash, bank, or card account the transaction clears through. Transactions from a connected feed use `bankAccountId` instead.

Unknown fields are rejected with `400`.

## Code or split a transaction

`PUT /api/v1/transactions/{id}/coding` replaces the coding. Line amounts must add up to the transaction amount.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --fail-with-body -X PUT "https://api.grainledger.com/api/v1/transactions/TXN_ID/coding" \
  -H "Authorization: Bearer $GRAIN_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "defaultFundId": "GENERAL_FUND_ID",
    "lines": [
      { "accountId": "SUPPLIES_ID", "amount": 120, "dimensions": [{ "dimensionId": "DEPT_ID", "dimensionValueId": "ADMIN_ID" }] },
      { "accountId": "HOSPITALITY_ID", "fundId": "YOUTH_FUND_ID", "amount": 64.32, "memo": "Volunteer lunch" }
    ]
  }'
```

* One line codes the transaction to a single account. Several lines split it across accounts, funds, or dimensions.
* A line's fund falls back to `defaultFundId`, then to the transaction's fund, then to the organization's default fund.
* A line without `dimensions` inherits the transaction's dimensions.
* Positive amounts allocate the transaction. A negative amount offsets another line, for example a refund inside a split. Response `coding.lines` use the same convention.
* Recoding is allowed until the transaction is approved. Unapprove first to change approved coding. Reconciled transactions cannot be recoded, and transactions in an open reconciliation return `409`.

Because `PUT` replaces the coding, retrying it is safe.

`DELETE /api/v1/transactions/{id}/coding` removes the draft journal entry and returns the transaction to `UNCATEGORIZED`. An approved transaction returns `409`; unapprove it first.

## Set dimensions

`PUT /api/v1/transactions/{id}/dimensions` replaces the transaction's dimensions. On a single-account transaction they are copied to the coded line. Split lines keep their own dimensions; set those in the coding request. Send `{"dimensions": []}` to clear them.

## Approve

`POST /api/v1/transactions/{id}/approve` posts the journal entry. It needs the `transactions:approve` scope. With OAuth, the signed-in user must also be allowed to post journal entries.

* Approval fails if `coding.missingRequiredDimensions` is not empty (`400`) or the accounting period is locked.
* Approving an approved transaction returns it unchanged, so retries are safe. `POST /unapprove` works the same way in reverse.

## Receipts

Upload a PDF or image (JPEG, PNG, GIF, WebP, or HEIC) of up to 5 MB as `multipart/form-data`:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --fail-with-body "https://api.grainledger.com/api/v1/transactions/TXN_ID/attachments" \
  -H "Authorization: Bearer $GRAIN_API_KEY" \
  -F "file=@receipt.pdf;type=application/pdf"
```

* `GET /transactions/{id}/attachments` lists attachments, including receipts synced from card platforms.
* `GET /transactions/{id}/attachments/{fileId}/download` returns a signed URL that expires in 15 minutes. Request a new one each time you need the file; don't store it.
* `DELETE /transactions/{id}/attachments/{fileId}` removes an attachment.

## Changes in API version 1.3.0

Transaction responses now use a documented, stable shape. Internal fields that earlier responses passed through, such as model reasoning, audit logs, and bank-feed metadata, are no longer returned. The `fundId` and `vendorId` fields are replaced by `fund` and `vendor` summaries, and the new `coding` object carries the journal entry and its lines.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.