Not what you’re looking for? Bank links connect a user’s own bank accounts for automated money movement. For
fiat-to-crypto purchases via a hosted checkout UI, see Ramp. For
rule-driven fiat ↔ crypto conversion on deposits, see Conversion
rules.
The model
A bank link (bank_link_id, prefixed bal_) is one connection to one institution — created when your user authorizes their bank through the widget. It owns one or more account links (account_link_id, prefixed al_), one per bank account the user chose to share. Money always moves against an account link. Anything that acts on the connection — reconnecting it, unlinking it, or it expiring or being revoked — acts on the bank link and takes all of its accounts with it.
A second institution is a second, fully independent bank link.
What is never returned: provider identifiers, access tokens, account owner names, and raw account or routing numbers.
How it works
A typical Bank Links integration follows three steps:1
Create
Create a bank link for a linkable funding method. You get back a hosted-widget URL.
2
Link
Send your user to that URL. They authorize their bank and choose which accounts to share.
3
Read
Read the link to see which accounts you can move money against, and their identifiers.
Base URL
Authentication
All requests require three headers:
See Authentication for how to generate the signature.
Step 1 — Create a bank link
Create a bank link for a linkable funding method with Create bank link. The response ispending with a hosted-widget URL:
A method that does not support linking is rejected with
bank_link_method_not_linkable.
A pending link reports accounts_eligibility_status: "unknown" — it has no accounts to evaluate yet, so that is not a denial.
Step 2 — Send your user to the widget
Send them towidget_url. They pick their institution, authenticate with their bank, and choose which accounts to share. When they finish, the link becomes linked.
The widget URL works once. If you supplied a return_url, the widget returns your user to it when it is done — read the link to find out what was shared. If the session expires before they open it, or they abandon it partway, create a new bank link.
Step 3 — Read the link
Get bank link returns one link. List bank links returns an account’s links, newest first. List is cursor-paginated. Usepage_size to set the first page size (default 20, maximum 25). If a response includes next_page_token, pass that value as page_token to fetch the next page; the token carries its own page size, so a new page_size sent alongside it is ignored. An account with no links returns an empty body.
A link appears in the list once it has finished linking. Links still pending and links you have unlinked are not listed; expired ones are.
institution is present once the link has completed and the provider named the bank; it is omitted on a pending link and whenever no name is available. Only bank_link_id, method_id, status and accounts_eligibility_status are always there.
linked_at is when the connection became active, so it appears once the link has finished linking and not before. expires_at is when the connection is due to expire, and appears only when the bank has told us a date — plenty of connections never carry one, so treat its absence as “no expiry reported” rather than “no expiry”.
account_links lists the accounts currently shared with you, each with a status of active. An account your user stops sharing is dropped from the list on the next read rather than turning up as removed, so compare the ids you hold against what a read returns — an id you no longer see is gone. There are no webhooks for any of this; reading the link is how you find out.
not_linked_accounts lists accounts your user picked at their bank that could not be linked. They have no account_link_id, are excluded from account_links and from accounts_eligibility_status, and cannot move money. They are reported so you can tell your user an account they chose was left out; if a later reconnect links one, it becomes an ordinary account link. Each entry carries an account_last4 and a reason: missing_identity when the provider could not verify the account holder, missing_balance when it could not read the balance, or unknown. Treat any reason you do not recognize as unknown — new ones may be added. The field is absent or empty when every selected account linked.
Four fields carry the eligibility story. Eligibility is computed fresh on every read — nothing is cached, so the answer can change between reads as ownership or rail capability changes.
eligibility_status— this account’s own verdict, always present.eligible: its owner is validated and at least one rail is known.ineligible: a check ruled it out, or the account is no longer linked.unknown: a check could not answer or has not run.eligible_rails— the transfer rails this account can use right now (for exampleach,rtp). Omitted when there are none, so read it as absent-or-listed rather than expecting an empty array.ineligibility_reasons— why no rail is eligible, for examplename_mismatchwhen the account holder does not match the PWS account’s verified identity. Omitted when there are none. An account that could not be evaluated carries no reasons, so absence is not proof of eligibility.accounts_eligibility_status— the link-level fold of the accounts’ own statuses, always present.eligible: at least one account has a validated owner and a usable rail.ineligible: every account is conclusively ruled out (a link that finished linking with no accounts isineligibletoo).unknown: not established on this read — at least one account could not be fully evaluated. Treatunknownas “not verified yet”, never as ineligible.
Reconnecting
When a link reportsexpired — the connection lapsed, or your user or their bank revoked it — send your user back through the widget with Reconnect bank link. A link you have unlinked cannot be reconnected — link the account again instead.
pending with a widget_url. Send your user there as you did on create; they re-authorize with their bank and the widget resumes the existing connection.
That URL is the same address create returned — reconnect puts a new session behind it, so an address you have seen before works again. Calling reconnect twice before the widget is opened returns that same session with its original expires_at, so a repeat call does not buy a full session lifetime.
The bank link itself does not change: bank_link_id and method_id stay the same. Its accounts re-synchronize against whatever your user shares this time — an account they share again keeps its existing account_link_id, even one that had been dropped earlier, a newly shared account appears as a new account link, and one they leave out disappears.
Unlinking
Unlink bank link severs the whole connection:- the link becomes
unlinkedand every account it had shared becomesremoved. This response is the only place you see that status; - the response is the final snapshot, and repeating the call returns the same one — unlinking is idempotent;
- the link then disappears from both reads: Get answers
404withbank_link_not_found, and List omits it; - money movement against its accounts stops working;
- unlinking a link that never finished linking cancels it the same way.
bank_link_id and a new account_link_id for every account — the same bank account does not get its old ids back.
The provider can refuse to sever the connection — for example while transactions are still settling. That comes back as 409 with bank_link_provider_rejected, and the link keeps the status it had; retry once the condition clears. Do not treat a DELETE as unconditionally successful.
A link busy with an in-flight provider operation — an activation, a reconnect, or an earlier unlink attempt — returns 503; retry shortly.
Errors
Bank-link operations return the standard PWS error envelope with stableerror.code values. These four are specific to bank links:
Whatever your user runs into inside the widget — an authorization that expires mid-flow, an institution already linked, a connection limit, a provider outage — the widget handles, and none of it reaches your API calls. The link’s own state is the outcome.