Skip to main content
The Bank Links API connects a user’s bank accounts to their PWS account through a hosted linking widget. Your user authorizes their bank once; you get back stable, opaque identifiers for the connection and each shared account. Money movement — payouts today, with conversions referencing the same identifiers — uses those identifiers instead of account or routing numbers.
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. Create a bank link for a linkable funding method with Create bank link. The response is pending 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 to widget_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. Get bank link returns one link. List bank links returns an account’s links, newest first. List is cursor-paginated. Use page_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 example ach, 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 example name_mismatch when 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 is ineligible too). unknown: not established on this read — at least one account could not be fully evaluated. Treat unknown as “not verified yet”, never as ineligible.

Reconnecting

When a link reports expired — 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.
The link comes back in 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 unlinked and every account it had shared becomes removed. 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 404 with bank_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.
Unlinking deletes the connection at the provider, not just our record of it, so there is nothing left to reconnect to. Linking the same bank account again means creating a new bank link, and it comes back with a new 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 stable error.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.

API reference