Engineering

How a Pocket transfer settles in seconds

Most Pocket transfers arrive in seconds, in 160 countries. A look at the double-entry ledger, pre-funded local accounts and idempotency keys that make that fast and keep every cent accounted for.

Jun Seo

Staff Engineer, Payments

Share

Last month, 71% of Pocket transfers to another country arrived in under 20 seconds, and transfers between two Pocket accounts took a median of 400 milliseconds. People notice speed first. What they never see, and what took us most of the work, is making sure that every one of those transfers is recorded exactly once, balances to the cent, and can be retried safely when a phone loses signal halfway through.

This is a tour of how a transfer moves through Pocket, from the tap in the app to the money landing.

Not crossing borders

The fastest way to move money across a border is not to. A traditional international transfer hops from your bank through one or more correspondent banks to the recipient's bank, and each hop adds a day and sometimes a fee.

Instead, Pocket keeps accounts with partner banks in the countries we send to, funded in the local currency. When you send euros to someone in Mexico, the euros leave your balance and land in our euro pool, and pesos leave our Mexican account through the local instant payment system to the recipient. Nothing crosses a border at that moment. We rebalance those pools in bulk later, at the mid-market rate, with large and predictable transfers between our own accounts.

That is why speed depends on the destination's local systems. Where they are instant, like in the eurozone, the UK, Brazil, India and Mexico, so are we. Where they are not, the app tells you the expected arrival time before you confirm.

A ledger that always balances

Every balance in Pocket is the sum of entries in a double-entry ledger. Nothing updates a balance directly. A movement of money is written as a set of entries that, within each currency, add up to zero.

Here are the entries for sending €500 to a US dollar account, with a fee of €2.35 and the mid-market rate at 1.0842:

Account

Currency

Entry

Customer balance

EUR

−500.00

Fee revenue

EUR

+2.35

FX pool

EUR

+497.65

FX pool

USD

−539.55

Outbound payments, US

USD

+539.55

The euro entries sum to zero, and so do the dollar ones. If they ever did not, the write would be rejected. That one rule catches whole classes of bugs before they become someone's missing money, and it means any balance at any moment can be rebuilt by replaying the entries behind it.

Entries are never edited or deleted. If a payment is returned by the receiving bank, we write new entries that reverse it, so the history shows both what happened and how it was undone.

Idempotency keys

Mobile networks are unreliable. You tap Send on a train, the request leaves your phone, the tunnel arrives, and the app never hears back. Did the transfer go through? The app does not know, so it tries again. Without care, that retry sends the money twice.

Every request that moves money carries an idempotency key, a unique ID the client generates once per intended transfer. The app, our public API and our internal services all follow the same rule:

const transfer = await pocket.transfers.create(
  {
    source: { balance: "EUR" },
    target: { currency: "USD", recipientId: "rcp_5Kd81vQe" },
    amount: { value: "500.00", currency: "EUR" },
    reference: "Rent October",
  },
  {
    // Generated once when the user taps Send, reused on every retry.
    idempotencyKey: "a3f6c2e0-7b1d-4c58-9e2a-0d4f8b61c7aa",
  },
);
const transfer = await pocket.transfers.create(
  {
    source: { balance: "EUR" },
    target: { currency: "USD", recipientId: "rcp_5Kd81vQe" },
    amount: { value: "500.00", currency: "EUR" },
    reference: "Rent October",
  },
  {
    // Generated once when the user taps Send, reused on every retry.
    idempotencyKey: "a3f6c2e0-7b1d-4c58-9e2a-0d4f8b61c7aa",
  },
);
const transfer = await pocket.transfers.create(
  {
    source: { balance: "EUR" },
    target: { currency: "USD", recipientId: "rcp_5Kd81vQe" },
    amount: { value: "500.00", currency: "EUR" },
    reference: "Rent October",
  },
  {
    // Generated once when the user taps Send, reused on every retry.
    idempotencyKey: "a3f6c2e0-7b1d-4c58-9e2a-0d4f8b61c7aa",
  },
);
const transfer = await pocket.transfers.create(
  {
    source: { balance: "EUR" },
    target: { currency: "USD", recipientId: "rcp_5Kd81vQe" },
    amount: { value: "500.00", currency: "EUR" },
    reference: "Rent October",
  },
  {
    // Generated once when the user taps Send, reused on every retry.
    idempotencyKey: "a3f6c2e0-7b1d-4c58-9e2a-0d4f8b61c7aa",
  },
);

The first time we see a key, we store it with a fingerprint of the request body in the same database transaction that writes the ledger entries. A retry with the same key and the same body returns the original result, including the original transfer ID, without doing anything again. A retry with the same key and a different body is rejected, because it almost always means a bug in the client.

Keys are kept for 30 days. Developers using our API get exactly the same behaviour, which matters a lot when a payroll job crashes halfway through a batch of 400 payouts and is restarted.

From hold to settled

A transfer moves through a small number of states, and each change writes ledger entries:

  1. Held. The amount and fee leave your available balance and sit in a hold. This happens before anything else, so the money cannot be spent twice.

  2. Sent. The payout instruction goes to the local payment system. Most instant systems confirm within a few seconds.

  3. Settled. The receiving bank confirms the credit. The hold is released into the entries above, and you get a notification.

  4. Returned. Rarely, a receiving bank sends the money back, usually because an account is closed. We reverse the entries and refund you in full, fee included.

Checking our own work

Fast systems can fail fast too, so we check the ledger against reality continuously. Every partner bank account we hold has a statement feed, and a reconciliation service matches each line against our ledger within minutes. Anything unmatched after an hour pages an engineer on call.

Last quarter we processed a little over 210 million ledger entries, and reconciliation flagged 63 items for a human to look at. Every one was explained, mostly by partner banks posting late, and none involved a customer's money going missing. You can read more about how we protect accounts on our security page.

#ledger

#payments

#infrastructure

Open a Pocket account in three minutes

Free to open, free to hold 38 currencies, and your card is ready to use the moment you're approved.

Open a Pocket account in three minutes

Free to open, free to hold 38 currencies, and your card is ready to use the moment you're approved.

Written by

Jun Seo

Staff Engineer, Payments

Keep reading

Money, explained monthly

Rate moves, product drops and one useful money idea. No spam, unsubscribe in a tap.

© 2026 Pocket. All rights reserved.

All systems normal

Pocket is a fictional company used for demonstration only. Pocket Payments Ltd is not a real e-money institution and holds no licence; nothing on this site is an offer of financial services.

In a real deployment, state here who issues your cards, who regulates you and how customer money is safeguarded.

Buy template

Create a free website with Framer, the website builder loved by startups, designers and agencies.