Skip to main content

Submitting wallet authorizations

This page is an optional implementation reference with ethers examples. For the complete multi-destination payment lifecycle, use the consolidated Multi-destination payments guide.

When you configure a business or partner multi-destination wallet through the API, Bitpayway saves the desired configuration and returns an authorization. Your application must submit that authorization to the wallet registry on the token's blockchain; an API call alone does not spend gas or send a blockchain transaction.

For the client's multi-destination wallet for a given token, the Bitpayway business dashboard already performs this complete flow: it saves the wallet, obtains the authorization and submits the transaction after the user approves it in the connected browser wallet. Use the examples below when integrating directly with the API, operating a relayer or building your own interface. Partner wallets always use this API flow because they are not editable in the Bitpayway dashboard.

The response is self-contained. It carries the network the transaction belongs to, the registry address and abi, the functionName to call and its args already in call order:

{
"walletRegisterAddress": "0x2222222222222222222222222222222222222222",
"abi": [],
"network": { "name": "Ethereum", "chainId": "1", "isTestnet": false },
"functionName": "setSplitWalletWithAuth",
"args": [
"12345",
"7",
"0x3333333333333333333333333333333333333333",
"0x1111111111111111111111111111111111111111",
true,
"0",
"1789834200",
27,
"0x...",
"0x..."
],
"expiresAt": "2026-09-20T12:10:00.000Z",
"signatureParams": {
"clientId": "12345",
"splitId": "7",
"token": "0x3333333333333333333333333333333333333333",
"wallet": "0x1111111111111111111111111111111111111111",
"active": true,
"nonce": "0",
"deadline": "1789834200",
"v": 27,
"r": "0x...",
"s": "0x..."
}
}

functionName is the name of the registry contract method to call (it is part of the abi): setClientWalletWithAuth for the business wallet and setSplitWalletWithAuth for a partner wallet; you do not need to pick it from the presence of splitId. signatureParams holds the same values in named form for integrations that build the call themselves, and expiresAt is the ISO 8601 form of signatureParams.deadline.

The account that submits the transaction is only a relayer: it pays the gas but does not need to be the registered wallet. The signed parameters prevent it from changing the client, token, wallet, partner state or expiration.

The following helper works with both a server signer and a browser wallet. The examples use ethers v6:

import { Contract } from "ethers";

async function submitWalletAuthorization(authorization, signer) {
const { walletRegisterAddress, abi, functionName, args } = authorization;

if (new Date(authorization.expiresAt) <= new Date()) {
throw new Error("The authorization has expired; request a new one");
}

const registry = new Contract(walletRegisterAddress, abi, signer);
const transaction = await registry[functionName](...args);

const receipt = await transaction.wait();
if (!receipt || receipt.status !== 1) {
throw new Error("The registry transaction failed");
}
return receipt;
}

Send the transaction on the network in network.chainId. GET /api/currency-token reports the same chainId per token if you need it before requesting an authorization. An authorization is valid for ten minutes and can be consumed only once.

From a server or relayer​

Keep the API key and relayer private key in environment variables or a secret manager. Use a dedicated, funded relayer account with only enough native token to pay transaction fees.

import { JsonRpcProvider, Wallet } from "ethers";

const apiResponse = await fetch(
`${process.env.BITPAYWAY_API_URL}/api/split/wallet`,
{
method: "PATCH",
headers: {
Authorization: `ApiKey ${process.env.BITPAYWAY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
tokenContractId: "28a555aa-d6bb-4a0b-9266-0b6668f824b5",
wallet: "0x1111111111111111111111111111111111111111",
active: true,
}),
},
);

if (!apiResponse.ok) {
throw new Error(`Could not configure wallet: ${apiResponse.status}`);
}

const authorization = await apiResponse.json();
const provider = new JsonRpcProvider(process.env.BLOCKCHAIN_RPC_URL);

// Fail fast when the relayer RPC points at a different network than the one the
// authorization was signed for.
const connected = (await provider.getNetwork()).chainId;
if (connected !== BigInt(authorization.network.chainId)) {
throw new Error(
`The RPC is on chain ${connected}; use ${authorization.network.name} (${authorization.network.chainId})`,
);
}

const relayer = new Wallet(process.env.RELAYER_PRIVATE_KEY, provider);
const receipt = await submitWalletAuthorization(authorization, relayer);

console.log("Wallet registered in transaction", receipt.hash);

For a partner, use PATCH /api/split/partners/{partnerId}/wallet; the transaction helper remains the same.

From a frontend with MetaMask​

The Bitpayway dashboard already provides this browser-wallet flow for the client's multi-destination wallet for the selected token. If you build a custom frontend, keep the API call in your backend as shown below.

Never put the Bitpayway API key in frontend code. Create an authenticated route in your own backend that calls the wallet PATCH endpoint and returns only its authorization response. In this example, /your-backend/wallet-authorization represents that application route.

import { BrowserProvider } from "ethers";

if (!window.ethereum) {
throw new Error("MetaMask is not installed");
}

const provider = new BrowserProvider(window.ethereum);
await provider.send("eth_requestAccounts", []);
const signer = await provider.getSigner();
const selectedWallet = await signer.getAddress();

// This calls your own backend. That backend keeps the API key secret and calls
// PATCH /api/split/wallet (or the partner wallet PATCH) at Bitpayway.
const response = await fetch("/your-backend/wallet-authorization", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
tokenContractId: "28a555aa-d6bb-4a0b-9266-0b6668f824b5",
wallet: selectedWallet,
active: true,
}),
});

if (!response.ok) {
throw new Error(`Could not obtain authorization: ${response.status}`);
}

const authorization = await response.json();

// The authorization says which network it belongs to, so MetaMask can be switched
// to it instead of asking the user to do it.
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: `0x${BigInt(authorization.network.chainId).toString(16)}` }],
});

// Take a signer again: the previous one is bound to the network from before the switch.
const networkSigner = await new BrowserProvider(window.ethereum).getSigner();
const receipt = await submitWalletAuthorization(authorization, networkSigner);
console.log("Wallet registered in transaction", receipt.hash);

MetaMask asks the user to approve the transaction and displays the gas cost. After the receipt succeeds, check the corresponding /status route from your backend until isEnabled is true. If the transaction is not sent before the deadline, or the same nonce was already consumed, call the authorization endpoint to obtain a fresh response instead of resubmitting the old one.

Client-wallet registration is required separately for each token because the on-chain key includes the client and token. A partner-wallet registration is also token-specific and additionally includes the partner link.