The staff detail row for a customer’s payment showed a bare number with no currency symbol. PayPal’s confirmation email to that customer showed a different, larger number in CAD. The two reconcile if you already know the exchange rate, but the staff interface had nothing to reconcile against: no currency code, no conversion note, just a figure.

At first pass we read it as a display bug. It wasn’t. The cause was in what we stored at write time.

What the capture payload carries

PayPal’s capture response has two amounts. The purchase_units[].payments.captures[].amount field is the merchant-currency settlement, which for us is USD. The payload also carries seller_receivable_breakdown, which holds the original buyer-side amount and currency, plus the exchange rate PayPal applied at capture time. For a buyer whose card is CAD, the breakdown says exactly what they paid in CAD, at that day’s rate, and what it yielded in USD to us.

Our capture handler read the settlement field and ignored the breakdown. It stored the USD amount in dbo.transactions, left currencyCode as NULL, and moved on.

The money moved correctly, because PayPal handled the conversion. What we got wrong was throwing away the buyer’s version of the transaction at write time. Nothing in the record let us recover it afterward.

What staff saw

The detail view gave staff a number with no label and no hint that the payment was foreign-currency at all. Unless you already knew to check the PayPal confirmation email, you were looking at a figure with no unit.

The support path from there means pulling the PayPal transaction ID, opening a browser, logging into the PayPal dashboard, and finding the original capture. That is three or four minutes per ticket for a routine question. For a billing dispute it is worse, because you are rebuilding the event from a source the customer cannot see and you cannot link them to.

The customer saw a CAD figure. Staff saw an unlabeled USD figure. Those are different invoices, and the record said nothing about the gap.

The fix

The change touched two layers.

On the write side, the PayPal capture handler now pulls buyerCurrencyCode, buyerAmount, and exchangeRate out of the seller receivable breakdown and writes them next to the settlement amount. For USD buyers the new columns are redundant and harmless. For CAD buyers the record is now complete.

On the render side, the staff detail view checks buyerCurrencyCode. If it is non-null and differs from the settlement currency, it shows both figures side by side, each labeled with its own currency code. If the two match, or the column is null, it renders the old way, so no existing rows break.

After the code shipped, we backfilled the customer’s row by hand. PayPal’s transaction API still had the original capture data, so the source was recoverable. We updated currencyCode, buyerAmount, and exchangeRate directly and reloaded the staff view. That step turns a code fix into a support fix. The fix does not fully exist until the affected row renders correctly.

The same assumption, still in auto-renewal

We did not fix auto-renewal in this ticket. Some organizations on the platform set a billing currency at the org level. The renewal job reads the invoice amount from dbo.transactions in USD, because that is what the original capture stored, and submits it to PayPal without reading the org-level currency flag.

So a CAD org renewing today pays in USD, silently. PayPal converts at current rates, the payment succeeds, and the record looks normal. There is no error message and no reconciliation flag. Depending on that day’s rate, the customer may pay slightly more or slightly less than they expect.

We opened a follow-up ticket and left it alone here, since the scope of this one was clear and testable. It still deserves a plain statement: fixing the capture handler did not remove the bad assumption from the codebase. The capture handler stopped holding it, and the renewal logic keeps its own copy.

Why nobody had seen it

CAD payments are a minority of platform traffic. Most organizations are US-based and pay in USD, where buyer currency and settlement currency are identical. A NULL currencyCode never mattered for them, because the stored USD amount was the right number.

The bug only surfaced when a Canadian org made a payment, noticed something off, and contacted support. In a legacy application, a low-frequency edge case can sit undetected for a long time if nobody flags it. That customer’s ticket was the flag.

We had not looked carefully at what the capture payload holds beyond the settlement amount, and that was wrong. The buyer-side breakdown is documented, it is present in every cross-currency capture, and we were not reading it.

This shipped in a platform release.

What to check in your own payments code

If you store only settlement currency, your support team ends up debugging a version of the transaction the customer never saw. Check whether your foreign-currency payments keep the buyer’s amount, currency and exchange rate, and whether recurring billing reads the customer’s billing currency. Both the data model and the staff display need the fix, because having the data and being able to act on it are different problems.