@get-asset/sdk

Transaction Match

Headless compound for settling an Invoice or a Bill against the bank. It fetches the transactions around the record's due date, keeps only the ones moving money in the right direction, ranks them by how close they come to the outstanding balance, and attaches or detaches one per click — each pick is its own request, so what the list shows is what the ledger has. kind picks both the endpoints and the sign a candidate must carry; see Guardrails. It also renders the payments already recorded, so the same surface is where a match is undone, and takes host-supplied creditsas a second payment source. A fully settled record fetches nothing — see Settled records.

Anatomy

Import the primitive and compose the parts you need.

<TransactionMatch.Root
kind="invoice"
objectId={invoice.id}
outstanding={invoice.totalDue}
dueAt={invoice.dueAt}
matched={payments}
>
{/* The record being settled */}
<TransactionMatch.Outstanding />
<TransactionMatch.Total />
<TransactionMatch.ActionError />
{/* The candidate transactions, ranked. Put them in a picker. */}
<TransactionMatch.Success>
<TransactionMatch.Candidates>
<TransactionMatch.CandidateDate />
<TransactionMatch.CandidateDescription />
<TransactionMatch.CandidateAccount />
<TransactionMatch.CandidateAmount />
<TransactionMatch.CandidateDifference />
<TransactionMatch.CandidateExact>
{/* "settles this exactly" badge */}
</TransactionMatch.CandidateExact>
<TransactionMatch.CandidateMatch />
</TransactionMatch.Candidates>
</TransactionMatch.Success>
{/* Credits the party already holds, when the host supplies them */}
<TransactionMatch.HasCredits>
<TransactionMatch.Credits>
<TransactionMatch.CreditReference />
<TransactionMatch.CreditDate />
<TransactionMatch.CreditRemaining />
<TransactionMatch.CreditDraw />
</TransactionMatch.Credits>
</TransactionMatch.HasCredits>
{/* What is matched right now */}
<TransactionMatch.HasMatches>
<TransactionMatch.MatchedPayments>
<TransactionMatch.MatchedDescription />
<TransactionMatch.MatchedAccount />
<TransactionMatch.MatchedDate />
<TransactionMatch.MatchedAmount />
<TransactionMatch.Unmatch />
</TransactionMatch.MatchedPayments>
</TransactionMatch.HasMatches>
<TransactionMatch.NoMatches>
{/* nothing matched yet */}
</TransactionMatch.NoMatches>
<TransactionMatch.Loading>
{/* skeleton */}
</TransactionMatch.Loading>
<TransactionMatch.Error>
<TransactionMatch.ErrorMessage />
<TransactionMatch.Retry />
</TransactionMatch.Error>
<TransactionMatch.Empty>
{/* nothing in the window to offer */}
</TransactionMatch.Empty>
{/* Nothing outstanding: nothing was fetched and nothing is offered. */}
<TransactionMatch.Settled>
{/* settled copy */}
</TransactionMatch.Settled>
</TransactionMatch.Root>

Required scopes

The access token used by AssetProvider must include these scopes for this primitive to fetch data successfully:

  • invoice:create
  • invoice:delete
  • bill:create
  • bill:delete
  • transaction:read
  • external_account:read

API reference

Root props

PropTypeDefaultDescription
kindrequired"invoice" | "bill"Which side of the ledger this is. Picks the apply/detach endpoints and the sign a candidate must have — an invoice is settled by money in, a bill by money out.
objectIdrequiredstringThe Invoice or Bill being settled. Sent as the payment's parent.
outstandingrequirednumberWhat is still owed on the record. Candidates are ranked against it, and it is what a credit's suggested draw is capped to. Zero (to the cent) disables matching entirely.
dueAtrequiredstringThe record's due date, ISO-8601. The candidate window centres on it — a year back and a quarter forward.
matchedMatchedPayment[][]The payments already recorded against the record, from whichever list owns it. They become the matched rows, they are what a candidate's untick detaches, and their descriptions are what a settled record renders — it runs no candidate query to resolve one.
pageSizenumber50Rows per candidate request. The API's ceiling is 100.
creditsCreditSource[]Credits the customer or vendor holds, offered beside the bank transactions. No balance query is issued — the host supplies these. Omit it and the credit parts render nothing. See Credits.
applyCredit(credit: CreditSource, amount: number) => voidDraws amount from credit. The host's write, not the SDK's: without it canApplyCredit is false and a credit can be shown and priced but not committed. The amount is clamped to the credit's remaining balance, and a draw that rounds to nothing is dropped.
removeCredit(row: MatchRow) => voidRemoves an applied credit. Owned by the host for the same reason as applyCredit. Without it, a credit-sourced row detaches through the ordinary payments endpoint.
onChanged() => voidCalled after each committed change. Writes land one at a time and the surface stays open, so this is for reacting to a change rather than closing over it.
candidatesMatchCandidateOverride[]Override the candidate transactions (skips both requests). The direction filter and the ranking still apply, so an offline story behaves like the live component. An empty array is still an override — it renders the empty state without a request. Used by storybook/tests.
state"loading" | "error" | "empty"Force a specific state for testing. Skips both requests. Ignored on a settled record, which has its own state.
showCurrencybooleanForce the ISO code beside each amount on or off, skipping the business fetch (storybook/tests). By default it shows only for a business that holds money in more than one currency.
childrenrequiredReactNodeCompound parts.

Parts

Container

Always rendered. Fetches and ranks the candidates, resolves the account names behind them, owns the attach and detach writes, and provides context.

PartDescription
TransactionMatch.RootOwns the candidate window, the direction filter and the ranking, the per-row writes and their busy state, the matched rows, and the credit seam.

The record being settled

These read the root context rather than a row, so they render in every state.

PartDescription
TransactionMatch.Outstanding
asChild?, format?
What is still owed — the figure the candidates are judged against.
TransactionMatch.Total
asChild?, format?
What the matched payments add up to. Read against Outstanding: the two together are the reconciliation, which the host owns because only the host knows what the record is worth.
TransactionMatch.ActionError
asChild?
Renders when the last attach or detach failed, and nothing when all is well. Fetch failures surface on Error instead.

Candidates

The transactions offered as a settlement, closest amount first and then nearest the due date. Wrap them in Success — the list is empty in every other state.

PartDescription
TransactionMatch.SuccessState wrapper: renders when at least one candidate is on offer. Compose the picker inside.
TransactionMatch.CandidatesIterates the ranked candidates, providing each as context to its children.
TransactionMatch.CandidateDescription
asChild?
The bank's own description, e.g. "NORTHWIND TRADERS ACH". What identifies a transaction — two payments of the same value are indistinguishable by figure.
TransactionMatch.CandidateAccount
asChild?
The name or mask of the account it landed in, or an em dash when it cannot be resolved.
TransactionMatch.CandidateDate
asChild?, format?
The posting date, as a raw ISO-8601 string unless format is given. Two payments from the same payer are told apart by when they landed.
TransactionMatch.CandidateAmount
asChild?, format?
The transaction's unsigned magnitude — what is compared against the balance. The signed value is on the candidate itself, via useCandidate().
TransactionMatch.CandidateDifference
asChild?, format?
How far the candidate is from settling the balance (magnitude less outstanding), with data-fit for tone — under for a part-payment, over for more than is owed. Renders nothing on an exact match: there is no difference to report.
TransactionMatch.CandidateExactRenders children only for a candidate that settles the balance to the cent. Compared in cents, so float noise cannot make an exact match read as off by a fraction.
TransactionMatch.CandidateMatch
asChild?
Matches this candidate, or detaches it if it is already in the set — one request per click, no staged set and no save. Sets aria-pressed, names itself “Match <description>”, and disables only itself while its own write is in flight, so the rest of the list stays usable. Default label “Match” / “Selected”.

Matched payments

What is recorded against the record right now, built from the matched prop. There is no staged layer: a write lands immediately, so these rows are the ledger's position rather than a pending one. A description and an account are joined on from the candidate set when the host does not supply them.

PartDescription
TransactionMatch.MatchedPaymentsIterates the matched rows, providing each as context.
TransactionMatch.HasMatchesRenders children only when something is matched.
TransactionMatch.NoMatchesThe inverse — renders children when nothing is matched yet.
TransactionMatch.MatchedAmount
asChild?, format?
What this payment settled. Read-only for every source: it is the allocation the API or host already recorded, so there is no write here that re-prices it. The transaction's full bank value is on transactionAmount, via useMatchedPayment().
TransactionMatch.MatchedTransactionAmount
asChild?, format?
The transaction's own full value, only when it settled the record for less than it moved — the API caps the application at what was outstanding, and the difference is worth showing beside the applied figure. Renders nothing when the two agree (compared in cents) or when the transaction's value is not known.
TransactionMatch.MatchedDate
asChild?, format?
When the payment landed, ISO-8601 unless format is given.
TransactionMatch.MatchedDescription
asChild?
The matched transaction's bank description. Renders nothing when neither the host nor the candidate set knows it — fall back to the amount rather than a blank.
TransactionMatch.MatchedAccount
asChild?
The account it came through. Renders nothing when it cannot be resolved.
TransactionMatch.Unmatch
asChild?
Detaches this row's payment, immediately. Every row gets one, credit-sourced included — a payment is detached by its own id and a row you can add but not remove would be a gap rather than a design. Disabled while its own write is in flight. Default label “Unmatch”.

Credits

A second payment source, rendered from the creditsprop. Nothing here issues a request or a write of the SDK's own — see Credits.

PartDescription
TransactionMatch.CreditsIterates the available credits, providing each as context. Renders nothing when there are none.
TransactionMatch.HasCreditsRenders children only when the party holds at least one credit — for the group's heading.
TransactionMatch.CreditReference
asChild?
What names the credit to a human: a credit memo number, usually.
TransactionMatch.CreditDate
asChild?, format?
When it was issued, ISO-8601 unless format is given.
TransactionMatch.CreditRemaining
asChild?, format?
What is left on the credit to draw against — the credit's own balance, not what applying it here would take.
TransactionMatch.CreditDraw
asChild?, format?
What this credit would settle if applied now: the smaller of its remaining balance and the outstanding amount. A $1,000 credit against a $100 invoice draws $100 and leaves $900 on the credit.

Loading

Renders while the candidate fetch is in flight.

PartDescription
TransactionMatch.LoadingState wrapper. Render a skeleton inside.

Empty

Renders when the window held nothing to offer — no transaction in range moves money in the right direction.

PartDescription
TransactionMatch.EmptyState wrapper. Render the empty copy inside.

Settled

Renders when nothing is outstanding. It takes precedence over every other gate — see Settled records.

PartDescription
TransactionMatch.SettledState wrapper. The matched rows still render outside it, so a settled record still shows what paid it and can still be unmatched.

Error

Renders on candidate-fetch error. Failed attaches and detaches surface on ActionError instead.

PartDescription
TransactionMatch.ErrorState wrapper. Compose the error UI inside.
TransactionMatch.ErrorMessage
asChild?, format?
Renders the error message string.
TransactionMatch.Retry
asChild?
Refetches the candidates and the account names. Default label "Try again".

Hooks

PartDescription
useTransactionMatch()Hook returning the full context: kind, outstanding, canMatch, candidates, hasData, the loading/error flags and retry, matchedTransactionIds and toggle, rows/remove/matchedTotal, busy/working/actionError, and the credit seam (credits, canApplyCredit, applyCredit, suggestedCreditDraw). A picker needs the arrays and callbacks rather than slots, which is what this is for.
useCandidate()Hook returning the current candidate inside Candidates — including the signed amount, the difference, isExact and exceedsOutstanding.
useMatchedPayment()Hook returning the current matched row inside MatchedPayments — the payment id, the transaction id (null for a credit-sourced payment), the amount, the transaction's own full value, the date, the description and the account.
useCredit()Hook returning the current credit inside Credits.

Guardrails

The risk here is not that the transaction cannot be found. It is that the wrong one is picked: a payment recorded against the wrong record misstates both records and the ledger behind them. Four decisions follow from that.

Direction is a hard filter, not a warning.An invoice is settled by money coming in and a bill by money going out, so a candidate with the wrong sign is never in the list. That removes the single most damaging mistake — a vendor payment booked against a customer invoice — rather than asking the user to notice it. kind is what sets the sign, alongside the endpoints.

Amount proximity is ranked, never filtered.Part-payments and over-payments are legitimate, so a transaction whose amount does not match is still offered — sorted by how far off it is, then by how near the due date it landed, and labelled with the difference on CandidateDifference. Filtering these out would hide real matches. The window itself is a bounded lookback: a year back and a quarter forward of the due date, which a searchable picker can carry without a wall of rows to mis-click.

The API owns the allocation. No payment_amount is sent, so the API applies what the record still needs. A transaction worth more than the balance is still offered and exposes exceedsOutstanding, but matching it settles the record and posts the excess to customer or vendor credit instead of making the balance negative.

A pick writes immediately, one row at a time.Ticking a transaction attaches it and unticking detaches it, each as its own request, and only the row being written is marked busy. There is no staged set and no save, so the matched rows are the ledger's position rather than a pending one. A successful write invalidates the candidate list and whichever list the record came from, since a match moves its balance.

Settled records

canMatchis false once the outstanding balance rounds to zero. Nothing is fetched — neither the candidates nor the account names — and nothing is offered, because there is nothing left to pay. Mounting the compound on a settled record costs no requests at all.

Settled is where that is rendered, and it takes precedence: Loading, Error, Success and Empty are all gated on canMatch as well, so a settled record fires Settled alone. Without that, Emptywould read as “we looked and found nothing” when the truth is “we did not look”. The state override is subject to the same gate.

The matched rows sit outside those gates, so a settled record still names what paid it and can still be unmatched — which is the way back if the wrong transaction was attached. Their descriptions come from matchedfor exactly this reason: with no candidate query to join against, a settled record's payments would otherwise show as a date and a figure with no name against them.

Credits

A credit the party already holds is a second source of payment, and it is a host-supplied seam end to end: credits comes from the host, and applyCredit and removeCreditare the host's writes. The compound issues no balance query and posts no credit application of its own. Supply no applyCredit and canApplyCreditis false — the credits can be shown and priced, but a pick commits nothing.

The distinction that shapes the parts: a bank transaction is assigned whole, because the bank moved exactly that much money, while a credit is a pool that someone draws a figure from. That is why a credit carries CreditRemaining and CreditDraw— the balance on it, and what applying it here would take — and why suggestedCreditDraw caps the proposal at the outstanding amount. Drawing more than the balance would put the record back into credit, which is the thing the credit is settling.

Once applied, a credit is an ordinary matched row with no transaction behind it ( transactionId is null ). Its Unmatch calls removeCredit when the host supplied one, and otherwise detaches through the ordinary payments endpoint.

Where it is mounted

This compound settles one record, so it lives inside that record's detail rather than on a surface of its own. Invoices mounts it in the expanded row and in the full view, and its MatchTrigger is what opens a row at it; AssetTransactionMatch is the styled panel, and AssetInvoicesis that composition. The reconciliation around it — the total, the matched sum, what that leaves — belongs to the host, because only the host knows what the record is worth.

matched comes from the same list: the payments are already on the record the host is rendering, and passing them in is what lets the surface offer to detach one, reconcile against Total, and name a settled record's payments without a request.