Reconciliation
Match funds arriving in your account against the payments you initiated
Plaid initiates a payment from your end user's bank account, but the funds arrive in your own account, on the payment rails. Plaid has no visibility into the payment after the payer's bank accepts it, so if you are not using Virtual Accounts, matching an arriving credit back to the payment you created is your responsibility.
This guide covers which identifiers to match on, when each becomes available, and an approach to matching that works across UK and European institutions. Use it as a starting point rather than a complete specification: the right design depends on your ledger, your volumes and the institutions your end users pay from, and no matching logic will attribute every credit automatically.
If you use Virtual Accounts, Plaid performs this reconciliation for you and reports the result as PAYMENT_STATUS_SETTLED. See Payment Confirmation. The rest of this guide applies to payments arriving directly in your own bank account.
The three identifiers
A payment carries three identifiers, and they serve different purposes. Using the wrong one for reconciliation is the most common source of unmatched payments.
payment_ididentifies the payment to the Plaid API. Use it to call/payment_initiation/payment/getand to look up your own records when aPAYMENT_STATUS_UPDATEwebhook arrives. It is never transmitted on the payment rails, so it will not appear on the credit that lands in your account.end_to_end_idis an identifier Plaid generates for every payment and asks the payer's bank to carry on the underlying payment message. Where the bank does so, it arrives on the credit as the ISO 20022EndToEndIdand is usually the most precise key available. Plaid generates a unique value for each payment, but the value that arrives on a credit is not guaranteed to be unique: some banks overwrite it, send a default placeholder value, or carry it over onto a later payment. Treat a match on it as strong evidence to confirm, not as proof.referenceis the value you supply when creating the payment. It is presented to the payer during authorisation and normally appears on the credit as the remittance information.
When each identifier becomes available
end_to_end_id is assigned by Plaid during the /payment_initiation/payment/create call itself, before your end user has selected an institution. Calling /payment_initiation/payment/get immediately after creating a payment will return it, and its value never changes afterwards. There is no need to poll for it, and no need to wait until after the Link flow completes.
This matters because a payment can settle while your end user is still in the Link session. Store end_to_end_id and reference as pending keys before you launch Link, and a credit that arrives early is already matchable.
adjusted_reference is the exception. If Plaid has to modify your reference to satisfy the payer's bank, the adjusted value is written when the bank acknowledges the initiation, and it is null until then. It is settled once the payment reports PAYMENT_STATUS_INITIATED or PAYMENT_STATUS_EXECUTED, and it is never populated after that. Key this check on either status rather than on INITIATED alone: some payments report EXECUTED without ever reporting INITIATED, and others terminate at INITIATED without reaching EXECUTED. See Payment Status for how the statuses behave across markets.
Once a payment has reported either status, a null adjusted_reference means your reference was not adjusted, not that the value has yet to arrive.
A suggested matching approach
Create the payment with a reference that is unique per payment. This does more for match rates than any other step: a unique reference is what makes the fallback path reliable, and it also avoids Plaid having to rewrite a reference that collides with one you have used before.
Call
/payment_initiation/payment/getimmediately, and storepayment_id,end_to_end_idandreference. Index the last two, because they are what you will match on.Launch Link. From this point a matching credit may arrive at any time.
On the
PAYMENT_STATUS_UPDATEwebhook forPAYMENT_STATUS_INITIATEDorPAYMENT_STATUS_EXECUTED, storeadjusted_referenceif it is populated. The webhook also carriesoriginal_referenceandadjusted_referencedirectly, so you can record it without an additional API call.When a credit arrives in your account, a reliable order to attempt matches is:
Compare the incoming
EndToEndIdagainst your storedend_to_end_id, case-insensitively. Not all banks preserve the original case, so normalise both values before comparing. Confirm the match with the amount and currency, and if they differ, treat the ID as not matching and continue to the reference comparison.If the credit carries no
EndToEndId, or it does not match, compare the incoming remittance information againstadjusted_referenceif it is set, otherwisereference. Confirm the match with the amount and currency.
Compare on end_to_end_id first because it is precise when present and costs nothing to attempt. Build the reference comparison to carry real volume rather than treating it as an error branch — for a significant share of European payments it is the only key that will match.
Reference requirements
Your reference must be 1–18 characters and alphanumeric. Individual institutions impose tighter rules than that, and where your reference does not satisfy them Plaid adjusts it before sending and returns the adjusted value as adjusted_reference. Both the original and the adjusted value are returned by /payment_initiation/payment/get, and you should store both.
The most common adjustment is truncation, where an institution accepts fewer characters than you supplied. It is not the only one: some institutions enforce a minimum length or disallow leading spaces, in which case the reference is padded or rewritten rather than shortened.
Because the adjusted reference is what the payer's bank actually sends, always match on adjusted_reference when it is set. Keeping your reference short, unique, and free of spaces and punctuation reduces how often any adjustment is needed.
When a credit does not match
However good your matching logic is, some credits will not match a pending payment, some will match a payment they do not belong to, and some payments will never be matched by a credit at all. Plan for all three before you go live, because the first occurrence usually arrives without warning.
Credits that match no payment
A credit can arrive that matches nothing you are expecting. Common causes are an institution changing the reference in a way that adjusted_reference does not reflect, an end user who set you up as a payee manually and never used the Plaid flow, or a payment made to you outside Plaid altogether. Route these to an unmatched queue rather than discarding them or attributing them on a best guess, and keep the raw remittance information, amount and payer details so they can be resolved later.
Repeated payments
Once an end user has paid you, most banking apps save you as a payee and offer to repeat a previous transfer. A repeat can arrive carrying the same reference, the same amount and the same payer account as a payment you have already matched. No Plaid flow ran, so there is no new payment_id and no PAYMENT_STATUS_UPDATE webhook.
Matching on end_to_end_id does not protect you here. Depending on the payer's bank, a repeat may carry a freshly generated EndToEndId, none at all, or a copy of the one Plaid issued for the original payment — in which case it will match your stored end_to_end_id exactly, even though it is a different payment.
Guard against this by treating each payment as matchable only once, on both match paths. After a pending payment has been matched and acted on, a later credit carrying the same values is a new, unattributed credit rather than a re-delivery of the old one. When an incoming EndToEndId resolves to a payment you have already reconciled, do not fall back to reference matching for that credit either — doing so would attribute it to a different payment.
Payments whose credit never arrives
Neither PAYMENT_STATUS_INITIATED nor PAYMENT_STATUS_EXECUTED confirms that funds have reached your account. If a credit for a pending payment has not arrived within the window you would expect for its scheme — typically seconds to minutes on Faster Payments and SEPA Instant, and up to a few business days on SEPA Credit Transfer — flag it for investigation rather than leaving it pending indefinitely. If you act on a payment before its credit is matched, decide in advance how you will handle one whose credit never arrives.
Deciding what to do
What you do with an unattributed credit is a policy decision that Plaid cannot make for you — credit the end user's balance, hold the funds for manual review, or return them to the sender. Expect a residual share of credits to need manual review, and size your operations for it rather than treating it as an exception path.
If you use Virtual Accounts, Plaid detects payments that arrive without an associated Plaid payment and can refund them automatically. See Handling Unexpected Payments.
Regional differences
The identifiers and the matching approach are the same in every market Plaid supports, but how reliably end_to_end_id reaches the creditor is not.
United Kingdom. Payments run on Faster Payments, and the identifier reaches the creditor on the large majority of payments. Reference matching is still required as a fallback.
Europe. Payments run on SEPA Credit Transfer or SEPA Instant Credit Transfer. Propagation of the identifier varies considerably by institution and by market — some banks carry it on effectively every payment, while at others it rarely arrives. In several European markets the reference is the only key that will match most payments, so the fallback path is not optional.
end_to_end_id is nullable and its presence on an arriving payment is not guaranteed. If testing shows that a particular institution carries it reliably, that is a reasonable optimisation to lean on, but treat it as an observation rather than a contract — institutions change this behaviour, in both directions, without notice. Keep the reference fallback active even for institutions that currently look reliable, and do not raise alerts on a missing EndToEndId.
Testing your matching logic
You can exercise the full flow in Sandbox by following the one-time payment integration guide. Sandbox assigns end_to_end_id at creation in the same way Production does, so a /payment_initiation/payment/get call straight after creating a payment will return it, and you can confirm that you are storing and indexing both keys before launching Link.
Sandbox cannot reproduce the behaviour of a specific bank omitting the identifier from the arriving credit. Test your fallback path by matching on the reference with the end_to_end_id comparison disabled, so you know the reference path works on its own before you go live.
