Skip to main content

Wallets and transfers

A Hubpay account holds one wallet per currency after onboarding. You can open further wallets in those currencies to keep separate balances: one per end-customer, project, channel or purpose. Each wallet has its own balance, its own entries feed and its own settlement details, so incoming funds identify who paid, and transfers move funds between wallets without leaving Hubpay.

The model​

  • Primary wallet. When your account is onboarded for a currency, it receives a wallet in that currency. It has no name or externalId, it cannot be renamed, and it stays open for as long as the account is open.
  • Wallets you open. Any further wallet in a currency the account holds, opened with POST /v1/wallets. It carries a name and externalId of your choosing and can be closed when it is no longer needed.
  • Transfer. A same-currency move of funds between two wallets of the account. It appears on both wallets' entries feeds as a TRANSFER entry.

Every wallet is held in the legal name of the account. name is a label for your own use, for example your end-customer's name or reference. It is not the account holder name on the settlement details, which is always the account's legal name.

A wallet holds one currency. Open wallets in any currency your account is onboarded for; use Exchange to move between currencies.

Routes​

RouteUse
POST /v1/walletsOpen a wallet. Returns 202 with the new walletId and status OPENING.
GET /v1/walletsList the account's wallets and balances, optionally by currency.
GET /v1/wallets/{walletId}Read a wallet, its balances and its labels.
PUT /v1/wallets/{walletId}Replace the name of a wallet you opened.
POST /v1/wallets/{walletId}/closeClose a wallet you opened.
GET /v1/funding-accounts?walletId=Settlement details that credit one wallet. With currency instead, those of the primary wallet in that currency.
GET /v1/wallets/{walletId}/entriesThe wallet's entries feed, which is also its statement.
POST /v1/transfersMove funds between two wallets of the account.
GET /v1/transfersList transfers, optionally filtered by walletId on either side.
GET /v1/transfers/{transferId}Read one transfer.

All routes take the AccountId header, as the other wallet routes do. Amounts are exact decimal strings and timestamps are UTC in ISO 8601 format with a Z suffix.

Wallet lifecycle​

statusMeaning
OPENINGThe open request was accepted and is being completed. The wallet is not yet usable.
OPENThe wallet accepts funds and transfers.
FAILEDThe open request could not be completed, for example because the bank partner declined the account. The wallet never becomes OPEN; open a new one.
CLOSEDThe wallet was closed. Its entries feed stays readable; it accepts no funds or transfers.

Opening is asynchronous. POST /v1/wallets returns 202 at once; subscribe to v1.wallet.opened and v1.wallet.failed, or poll GET /v1/wallets/{walletId} until the status is OPEN or FAILED.

Settlement details are a separate step. GET /v1/funding-accounts?walletId= returns an empty list until the bank partner has issued details for the wallet, and each item names the walletId it credits. v1.wallet.updated is sent when details are issued.

externalId is your own reference and your idempotency key, and it cannot change after the wallet is opened. Repeating an open request with the same externalId returns the existing wallet, whatever its status, instead of opening another; the same externalId with different fields is rejected with 409 conflict. The comparison uses the original open request, so a later change of name does not affect it. Omit externalId to open a new wallet on every request; a blank value is rejected. To retry after a FAILED open, use a new externalId. name is at most 140 characters and externalId at most 36.

A wallet can be closed only when its balance is zero and it has no open reservations. Otherwise the close request returns 409 with the code conflict; transfer the balance to another wallet first. The primary wallet of a currency cannot be closed or renamed.

Transfers​

A transfer debits fromWalletId and credits toWalletId with amount in currency. Both wallets must belong to the account in the AccountId header and share the same currency; a transfer between currencies is rejected with validation_failed. The source wallet's available balance must cover the amount.

statusMeaning
PENDINGThe transfer is held for review, for example when it exceeds a limit on the account. It becomes EXECUTED or REJECTED.
EXECUTEDBoth wallets are posted. The entries carry the transfer's reference.
REJECTEDReview declined the transfer. Nothing moved.
FAILEDPosting did not complete. Nothing moved.

A transfer that is not held for review is EXECUTED in the 201 response. completedAt is set when the transfer reaches EXECUTED, REJECTED or FAILED. reference is at most 100 characters and is shown on both wallets' entries. externalId is your idempotency key: repeating a request with the same externalId returns the existing transfer; the same externalId with different fields is rejected with 409 conflict. Omit it to create a new transfer on every request; a blank value is rejected.

Every transfer sends v1.transfer.created, then v1.transfer.settled or v1.transfer.failed, including a transfer that was already EXECUTED in the 201 response, so you receive the outcome even if that response was lost.

Webhooks​

The wallet and transfer event families are listed in the event catalogue.

Connected apps​

Connected apps need the scopes WALLET_BALANCE_READ and FUNDING_ACCOUNT_READ to read wallets and settlement details, and TRANSACTION_FEED_READ for the entries feed.