Skip to main content
A transaction moves through three coding states, shown in coding.status:
  • 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

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.
  • 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.
  • 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:
  • 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.
Last modified on October 10, 2026