How to Sync Bank Transactions with Open Banking APIs (2026 Guide)
Most open banking tutorials stop at "make a GET request. " In production, you need consent flows, token refresh, pagination across thousands of transactions, deduplication, and rate-limit handling. This guide. . .

Most open banking tutorials stop at "make a GET request. " In production, you need consent flows, token refresh, pagination across thousands of transactions, deduplication, and rate-limit handling. This guide walks through the full sync pipeline — what actually breaks at 3am when your cron job fires. The sync lifecycleA real bank-data sync has four phases:Onboard: User consents to read-only access → you get a consent tokenInitial fetch: Pull all historical transactions (can be thousands)Incremental sync: Poll for new transactions on a scheduleToken refresh: Consent expires (90 days default under PSD2) → re-authorizeChoosing a providerProviderCert needed?Pricing modelBank coveragePlaidPer-regionPer-item ($0. 20-0. 50/mo)12,000+ globallyTrueLayerYes (eIDAS QWAC)Volume tiers1,500+ EU/UKTinkYes (eIDAS QWAC)Custom3,400+ EU/UKGoCardless (Bank Account Data)No (they hold the cert)Free tier → paid1,500+ EU/UKopen-banking. ioNo~€3/mo flat2,500+ EU/UKDisclosure: I maintain open-banking. io. The rest of this guide is vendor-neutral — pick whichever fits your stack. The eIDAS QWAC certificate is the hidden gatekeeper. It costs €3,000-15,000/year, requires an annual audit, and takes weeks to obtain. If you're a solo developer or small team, providers that absorb the certificate (GoCardless, open-banking. io) save you that entire process. Step 1: The consent flowAll PSD2 Account Information Service (AIS) providers use a similar OAuth-style redirect:// 1. Redirect user to the bank's consent pageconstauthUrl=provider. getAuthUrl({redirect_uri:'https://yourapp. com/callback',scope:'accounts transactions',access_valid_for_days:90});// User clicks → bank asks "Allow YourApp to read your transactions?"// User approves → redirects back with a codeEnter fullscreen modeExit fullscreen modeThe consent token you receive istime-limited(PSD2 mandates max 180 days, most default to 90). Store the expiry date — you'll need it. Step 2: Fetch accountsasyncfunctiongetAccounts(consentToken){constres=awaitfetch(`${API_BASE}/accounts`,{headers:{'Authorization':`Bearer${consentToken}`}});constdata=awaitres. json();returndata. accounts;// [{ id, iban, name, currency, balance }]}Enter fullscreen modeExit fullscreen modeStep 3: Sync transactions (the hard part)This is where tutorials end and production begins:asyncfunctionsyncTransactions(accountId,consentToken,lastSyncCursor){letallTransactions=[];letcursor=lastSyncCursor;lethasMore=true;while(hasMore){consturl=`${API_BASE}/accounts/${accountId}/transactions?cursor=${cursor}`;constres=awaitfetch(url,{headers:{'Authorization':`Bearer${consentToken}`}});if(res. status===429){// Rate limited — back offconstretryAfter=res. headers. get('Retry-After')||60;awaitsleep(retryAfter*1000);continue;}constpage=awaitres. json();allTransactions. push(. . . page. transactions);cursor=page. next_cursor;hasMore=page. has_more;// Respect rate limits between pagesawaitsleep(200);}return{transactions:allTransactions,nextCursor:cursor};}Enter fullscreen modeExit fullscreen modeDeduplicationBanks will return overlapping transactions between syncs. The cursor handles most of this, but edge cases exist:A pending transaction settles (bookingDate changes, transactionId may change)A transaction is reversed and re-bookedThe bank back-dates a transactionStore a composite key:{bankTransactionId, bookingDate, amount}. If the bank provides atransactionIdorentryReference, prefer that. If not, hash the three fields above. functiondedupeKey(tx){constid=tx. transactionId||tx. entryReference;if(id)returnid;return`${tx. bookingDate}:${tx. amount}:${tx. counterpartyIban||''}`;}Enter fullscreen modeExit fullscreen modeStep 4: Token refreshPSD2 consent expires. You cannot silently refresh it — the user must re-authorize. Design for this:asyncfunctioncheckConsent(consentToken){constres=awaitfetch(`${API_BASE}/accounts`,{headers:{'Authorization':`Bearer${consentToken}`}});if(res. status===401||res. status===403){// Consent expired or revokednotifyUser('Your bank connection needs reauthorization');return{valid:false,needsReauth:true};}return{valid:true};}Enter fullscreen modeExit fullscreen modeSend the notification early (7 days before expiry). Users who see "reconnect your bank" at the moment of need will do it. Users who see it when they're not using your app will ignore it. Production checklistRate limits: Most providers allow 4-15 requests/minute per consent. Batch where possible. Idempotency: Use the bank'stransactionIdas your dedup key, not your own generated ID. Pagination: Some banks return 100/page, others 500. Always respect thehas_moreflag. Webhooks: Some providers offer transaction webhooks (push instead of poll). Use them if available — they reduce API calls by 90%+. Error handling: Banks go down. The PSD2 network is 6,000+ banks — some will be unavailable. Retry with exponential backoff, but don't block the entire sync on one failing account. Data freshness: Transactions can appear 1-3 days after the actual payment (pending → booked). Don't display "real-time" if you're polling daily. What breaks in productionThe three things that caused the most production incidents in my experience:Consent expiry cascade: Multiple users' consents expire on the same day (if you onboarded them in a batch). Your reauth notification queue overflows. Stagger onboarding or add jitter to expiry notifications. Transaction ID instability: Some banks change thetransactionIdwhen a pending transaction settles. Your dedup fails and you get duplicates. Always store both pending and booked versions, and reconcile by matching onamount + date + counterparty. Rate limit headers: Some providers returnX-RateLimit-Remainingin headers, not the response body. If you ignore headers, you'll get 429s that seem random. The open banking APIs are powerful, but the production details — consent lifecycle, deduplication, rate limiting — are where most integrations stall. The providers above all expose similar REST APIs, so the patterns in this guide apply regardless of which one you choose. If you're building something with bank data and want a certificate-free starting point, open-banking. io (which I maintain) offers a simple server-to-server API at around €3/month — but the architecture patterns here work with any PSD2 provider.