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

# Structured financial data

> Read accounts, balances, and transaction records through the SDK, MCP, or HTTP API.

Use structured reads when your app or agent needs individual financial records
for its own storage and analysis. Results preserve provider IDs and account
attribution, without passing the records through a language model.

<Note>
  This guide describes the financial-data release. It requires a compatible SDK
  or MCP build and an API environment with structured reads enabled. Availability
  on the hosted endpoint is controlled separately from this documentation.
</Note>

## Request permission

Configure your server's `PaymanConnect` instance with
`scopes: ["financial_data:read"]`, then use the normal
[authorization and callback flow](/quickstart). The customer's consent screen
shows the data permission. Use `payman.forUser(customerId).startAuthorization()`
to request new consent for an existing customer.

The default scope remains `interact`. Request both scopes only when your app
needs money tasks and data reads. Changing client configuration does not upgrade
an existing grant: the customer must consent again on the existing primary
connection, or revoke and reconnect. Adding another provider cannot broaden
permissions. Device-code pairing currently supports only `interact`; use the
browser authorization flow for data access.

## Read through the SDK

After authorization, discover the connections the customer has shared with your
app, then use exact account IDs from that connection:

```ts theme={null}
const user = payman.forUser(customerId);
const connections = await user.data.connections();

// Let the customer select one of these connection IDs in your app.
const connectionId = selectedConnectionId;
const readOptions = {
  intent: {
    purpose: "Prepare October bookkeeping",
    taskId: "bookkeeping-2026-10",
    requestedBy: "bookkeeping-agent",
  },
};
const { accounts } = await user.data.accounts(connectionId, readOptions);

// Let the customer select an account returned above.
const { transactions, result } = await user.data.transactions({
  connectionId,
  accountId: selectedAccountId,
  startDate: "2026-10-01",
  endDate: "2026-10-31",
}, readOptions);
```

The example assumes an authorized `payman` instance and IDs selected from the
returned lists. Do not substitute account labels, last-four digits, or an ID
from another connection. Account balances may be cached; a missing available
balance means unknown, not zero.

| Operation | Arguments | Result |
| - | - | - |
| `list_accounts` | Optional `cursor`, `limit` | Accounts and supplied balances |
| `get_account` | Required `accountId` | One account |
| `list_transactions` | Required `accountId`, `startDate`, `endDate`; optional `cursor`, `limit` | A transaction snapshot |
| `sync_transactions` | Required `accountId`; optional `cursor`, `limit` | Native changes, currently Plaid only |

Use `user.data.read({ connectionId, read: { operation, arguments } })` for any
operation. Snapshots depend on each provider's configured read tools. A
connection's `transactionSync` describes implementation support; it does not
prove that its runtime catalog is published or that the bank is healthy.
Investments, liabilities, and statement PDFs are outside this release.

## Record why a read was requested

Include optional `intent` whenever the caller's purpose is known. Hosted and local
MCP advertise it by default and ask the bot to include the known purpose without
asking the user just to populate metadata. Omitting it or sending `null` keeps
existing integrations working. Payman does not invent a reason when none is supplied.

| Field | Meaning |
| - | - |
| `purpose` | Why the user wants the records; required when supplying an intent object, up to 1,000 characters |
| `taskId` | Optional shared ID linking related reads, up to 128 characters; each read still has its own `requestId` |
| `requestedBy` | Optional caller-reported bot/application label, up to 128 characters; authentication determines the actual caller |

String values are trimmed and must not be blank. Keep the purpose concise; do not
include credentials or the full conversation. Intent is descriptive context and
does not change permissions, tool arguments, or trigger a model call.

The SDK accepts `intent` on `data.start()` / `data.read()` requests, or in the
options passed to those methods and `accounts()`, `transactions()`, and
`syncTransactions()`. An explicit request value overrides the options value;
`intent: null` explicitly omits shared context. Keep the same intent when retrying
the same `requestId`.

Submission responses and JSON results always include `intent` (or `null`). It is
stored encrypted with the read request and expires with the job after 24 hours.
Save `runId`, `createdAt`, `intent`, and the final status in your own records for
longer retention. CSV exports contain only financial rows; retain the JSON metadata
separately.

On compatible runtimes, each provider-page attempt also creates a Studio execution
trace under the read job's session. It shows purpose, connection/account/date scope,
tool name, result counts, timing, and outcome with zero model calls. Response rows
and cursor contents are excluded. Each saved page includes `executionId` and
`sessionId` for correlation. In Studio, open the connection's conversations and
the read's trace; failed attempts are recorded there too.

These runtime traces follow existing execution retention and survive the Cloud
job's 24-hour expiry. The agent's trace storage setting still applies: DETAILED
captures this context by default, METRICS retains timing only, and NONE disables
the trace blob. Older runtimes may omit the page trace IDs until upgraded.

## Keep Plaid transactions in sync

Start without a cursor. On subsequent imports, supply the checkpoint from the
previous committed batch:

```ts theme={null}
const batch = await user.data.syncTransactions({
  connectionId,
  accountId,
  cursor: savedCheckpoint ?? undefined,
  requestId: persistedImportRequestId,
}, {
  intent: { purpose: "Keep bookkeeping transactions up to date", taskId: "bookkeeping-sync" },
});

// Application database pseudocode. Commit the records and checkpoint together.
await database.transaction(async tx => {
  await tx.upsertTransactions([...batch.added, ...batch.modified]);
  await tx.removeTransactions(batch.removed);
  await tx.saveCheckpoint(connectionId, accountId, batch.checkpoint);
});
```

Key records by `(connectionId, accountId, id)`. Keep one checkpoint per grant,
connection, and account. Serialize imports for that stream, or compare and swap
the stored checkpoint when committing. A new grant or replaced bank connection
requires a full sync. Reuse the same request ID for an identical submission
retry; give each new import a fresh request ID.

Posted records can replace pending records under a different ID. Use
`pendingTransactionId` when supplied, and apply removals. Plaid's exact
`amountDecimal` is an absolute amount in major currency units; `type` supplies
the credit/debit direction. Prefer this over rounded `amountCents` for precision.
Use the source `date`/`datetime` fields; legacy `createdAt` may be synthesized
from a date. These extra source fields are not certified across every provider.

The server stages the whole sync batch before returning it and restarts
pagination if the source changes during the read. Incomplete sync jobs expose
neither staged pages nor a checkpoint. Apply every returned page and persist
the final checkpoint in one database transaction.

## Use MCP

Request OAuth scope `financial_data:read` and reauthorize your hosted MCP
connection. Read-only grants expose `payman_connection_status`,
`payman_read_data`, and `payman_read_result`, with money and approval tools
omitted. The API feature flag must also be enabled.

1. Call `payman_connection_status` to discover a `deploymentId`.
2. Call `payman_read_data` with `list_accounts` to discover exact account IDs.
3. Poll `payman_read_result` with its `runId`.
4. Submit a transaction read using the same deployment and chosen account.

```json theme={null}
{
  "deploymentId": "<returned deploymentId>",
  "requestId": "october-bookkeeping-1",
  "intent": {
    "purpose": "Prepare October bookkeeping",
    "taskId": "bookkeeping-2026-10",
    "requestedBy": "bookkeeping-agent"
  },
  "allPages": true,
  "read": {
    "operation": "list_transactions",
    "arguments": {
      "accountId": "<returned account id>",
      "startDate": "2026-10-01",
      "endDate": "2026-10-31",
      "limit": 500
    }
  }
}
```

Tools return `structuredContent` as well as text. `payman_read_result` returns
one saved page: `data.data` contains its records and `data.fetchedAt` its retrieval
time. Follow `nextPage` by passing `page` on the next call. Retrieve every saved
page before committing a sync checkpoint, even when the job is already completed.

For the local stdio package, set `PAYMAN_SCOPES=financial_data:read`, restart it,
and reconnect through `payman_connect`. Local discovery uses
`payman_data_connections` and local read input uses `connectionId` instead of
`deploymentId`. These identify the same connection. The SDK uses `connectionId`
too. See [MCP setup](/mcp-server) for building and configuring the local package.

If a hosted client always requests only `interact` and does not allow scope
selection, use the configurable local MCP build. Asking for raw data in chat
does not change an OAuth grant's permissions.

## Jobs, limits, and recovery

For manual polling, call `data.start(request)`, then `data.result(runId)` or
`data.wait(runId)`. The HTTP equivalents are:

| Endpoint | Purpose |
| - | - |
| `GET /connect/data/connections` | Discover consented connections |
| `POST /connect/reads` | Submit a typed read; returns `202` and a durable `runId` |
| `GET /connect/reads/{runId}` | Retrieve status and saved pages |

HTTP read input uses `deploymentId`. Confidential clients send
`x-paygent-connect-app-key` and `x-paygent-connect-grant`. Public OAuth clients
send their token in `x-paygent-connect-grant` without an app key. The hosted
`/mcp` endpoint uses `Authorization: Bearer` instead. See the API reference for
the request schemas. A completed snapshot can also be requested with
`?format=csv`; check `x-payman-read-state` and `x-payman-rows-complete` before
using it. Change feeds require JSON.

| Limit | Value |
| - | - |
| Snapshot date window | 94 days inclusive; use separate windows for longer history |
| Requested page size | Default 200; maximum 500 |
| Job size | 20 pages, 2 MB per upstream page, 10 MB stored total |
| Saved job lifetime | 24 hours |
| SDK convenience wait | 120 seconds by default; configurable with `timeoutMs` |

`data.read()` and the convenience methods throw `FinancialDataError` when a job
is incomplete. Inspect `error.result`; do not treat a partial export as complete.
`read_timeout` and `read_aborted` carry `runId` so you can resume polling without
submitting a new job. `read_submission_unknown` carries `requestId`; retry the
same input with that ID. These data methods throw, unlike money-task `run()`'s
result union.

If a sync batch exceeds the bounds, do not save a partial cursor. Try a larger
page size up to 500 through `data.read()`. If it still exceeds the limit, the
integration needs a larger export path before that batch can be committed.

`rowsComplete=true` means the requested provider pagination finished. It does
not certify that the bank supplied all historical records. `historyComplete`
remains `false`; `sourceUpdatedAt` remains `null` until independently verified.
`fetchedAt` is retrieval time, not the bank's refresh time. Reconcile against
statements before relying on an import as complete books.


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