Indexing and reconciliation

List inbound transfers, paginate, dedupe, and reconcile against your product ledger.

GET /v1/payments/transfers is the workhorse for reconciliation. By default, it returns paginated persisted transfer records across your organization's custody wallets, with filters for direction, status, token, exact custodyWalletId, and date range. Use it to mirror SDP transfers locally, match inbound payments to orders, and drive a delta-polling worker. To include recent on-chain observations for one exact wallet, set custodyWalletId and includeObserved=true.

Listing inbound transfers

The minimal inbound-only request:

Terminal
curl "https://api.solana.com/v1/payments/transfers?direction=inbound&pageSize=50" \
  -H "Authorization: Bearer sk_test_..."

Query parameters

ParameterTypeNotes
directioninbound | outboundCommon filter. Inbound = payments to wallets you control.
statuspending | processing | confirmed | finalized | failedScope to a single state.
custodyWalletIdstringExact SDP Wallet ID (id from GET /v1/wallets).
includeObservedbooleanDefault false. When true, adds recent on-chain activity for the exact custodyWalletId; it cannot be used without that filter. Synthetic observed rows have custodyWalletId: null.
tokenstringFilter by token symbol or on-chain mint (exact match against the stored transfer's token value).
from, toISO 8601 datetimeTime-window filter on createdAt. Use with offset (2026-05-14T00:00:00Z).
pageintegerDefault 1.
pageSizeintegerDefault 20, max 100.

Deduplication

Two natural dedup keys:

  1. Transfer.id — SDP-internal, immutable, present on every transfer (including pending ones with no signature yet). Use this as your local primary key.
  2. Transfer.signature — the on-chain signature, populated once the transaction has been built and observed on-chain (typically at confirmed/finalized, but treat its presence rather than the status enum as the trigger — processing and even pending records can still have a null signature). UNIQUE across SDP's transfers table wherever it is set; two records cannot share a signature. Use this if you need to dedupe across multiple data sources (SDP + a parallel chain indexer, say) — only once signature is present.

In practice: store transfers in your reconciliation table keyed by SDP Transfer.id, and assert signature uniqueness once it is populated.

Reconciling an order

Customer-initiated inbound transfers do not populate Transfer.memo (memo-program instructions aren't extracted into the field), and the inbound record doesn't expose a reference. The reliable on-record correlation key is the inbound transfer's destination — match it against an order whose pre-issued receiving address is that same destination.

The typical inbound match cycle:

  1. Read recent persisted inbound transfers for the relevant token:
    Request
    GET /v1/payments/transfers
      ?direction=inbound
      &token=<mint or SOL>
      &from=<since last reconciled>
      &to=<now>
      &pageSize=100
  2. For each transfer, look up an open order keyed by the transfer's destination (findOrderByDestination(tr.destination)).
  3. If matched, assert the amount and token match the expected values on the order row, then mark the order paid, record the SDP Transfer.id, and dispatch downstream side effects (email, shipment, ledger entry).
  4. If unmatched, leave the transfer for the next pass (it may be an early-arrival for an order you haven't staged yet) or flag it for manual review after a grace window.

If you need memo- or Solana-Pay-reference-based correlation (e.g., a single-wallet flow where the destination address is shared across orders), read the on-chain transaction directly via Solana RPC using the transfer's signature. SDP does not surface either field on inbound transfer records today.

Solana Pay reference accounts (roadmap)

SDP does not attach a Solana Pay reference pubkey on-chain, and inbound transfer records do not expose a reference field today. The realistic correlation options are: (a) per-order destination addresses — pre-issue a fresh receiving wallet per order and match by Transfer.destination (the recommended pattern; see Reconciling an order above); (b) on-chain reads via Solana RPC keyed off the transfer's signature, used to recover the memo-program instruction or Solana Pay reference that SDP itself doesn't surface for inbound transfers.

A delta-poll worker

A reconciliation worker that wakes on a tick and asks "what is new since last time":

reconcile-tick.ts
const OVERLAP_MS = 5_000; // re-query the last 5s on each tick to catch boundary arrivals

async function reconcileTick(state: WorkerState) {
  // Nudge `from` backwards by the overlap window; `alreadySeen` dedupes the
  // resulting duplicates. Always advance the high-water mark to `to` regardless,
  // otherwise the window grows without bound.
  const fromMs = state.lastSeenIso
    ? Date.parse(state.lastSeenIso) - OVERLAP_MS
    : Date.now() - 60_000;
  const from = new Date(fromMs).toISOString();
  const to = new Date().toISOString();

  // This lists persisted records. To discover additional recent chain activity,
  // run the same window per exact wallet with custodyWalletId + includeObserved.
  for await (const tr of listInbound({ from, to })) {
    if (await alreadySeen(tr.id)) continue;

    // Match by destination — the on-record correlation key for inbound transfers.
    // `tr.memo` is omitted (undefined) for customer-initiated inbound payments.
    const order = await findOrderByDestination(tr.destination);
    if (!order) {
      await persistUnmatched(tr);
      continue;
    }

    if (tr.status === "finalized") {
      await markOrderPaid(order, tr);
    }
    await persistSeen(tr.id, tr.status);
  }

  state.lastSeenIso = to;
}

Notes:

  • The overlap window (5s above) re-queries the trailing edge of the last tick to catch transfers that arrive at the boundary; alreadySeen collapses the resulting duplicates.
  • The high-water mark advances to to on every pass even though from is rolled back — otherwise the query window grows unbounded.
  • Always re-fetch a transfer to upgrade its status: a transfer seen as confirmed in one tick will appear as finalized in a later tick.
  • Use pageSize=100 and the async iterator pattern (see Payouts and disbursements) to handle large windows.
Is this page helpful?