What you can build
Subscriptions
Bill monthly or annual plans without sending the customer back to checkout every cycle.
Usage-based billing
Bill for metered usage at the end of each billing period, for any amount within your limits.
Auto top-ups
Refill a prepaid balance or credits automatically when it runs low.
One-click repeat purchases
Let returning customers pay without going through checkout again.
Choose a source
Customers can pay from their Coinbase account or from a self-custody wallet. You can offer both.Charge a Coinbase account
Recurring charges from a Coinbase account use the Pay with Coinbase flow. After the first charge, your billing job runs it without the customer.- Link the account and take the first charge while the customer is present. Start the first charge’s challenge with
initiator: "merchant", notcustomer. See Link the customer’s Coinbase account and the scenario table. - Refresh the access token only when it has expired or is about to. Check
expires_atbefore each charge. Each refresh invalidates the previous refresh token, and sending an invalidated refresh token ends the link withinvalid_grant. Run one refresh at a time per customer, and save the new refresh token before you use the new access token. See Refresh tokens. - Check
available_to_sendbefore you create the session. See Check the customer’s USDC balance. - Authorize off-session with
initiator: "merchant"andenforce2fa: false. See Charge without the customer present. - Stop billing when the link ends. A
401on authorization can mean the access token expired, so refresh once and retry. The link has ended when the refresh returnsinvalid_grant, or when you receiveoauth.token.revokedoridentity.user.deleted. Afterinvalid_grantoroauth.token.revoked, the customer must link again.
Charge a self-custody wallet
The customer approves a mandate once: their revocable consent for you to pull payments from their wallet, within limits you set. Funds stay in their wallet until you charge. Approving and paying are gasless. Customers can approve from Base, Ethereum, Arbitrum, Optimism, Polygon, or Solana. Solana uses the network namesolana in every environment, including sandbox.
1. Create a mandate
Create one mandate per customer relationship, such as one per subscription. Set spending limits that fit your billing model.Example response
Example response
mandateId. You can’t set it yourself, so save it with your customer record. You’ll need it to authorize every payment. To attach your own identifiers, such as a customer or subscription ID, use metadata.
created status. You can’t authorize payments with it until the customer approves it.
2. Get the customer’s approval
The customer approves the mandate once from their self-custody wallet, for example during signup or checkout. Redirect the customer to the mandate’surl, or embed the approval in your page by calling render({ mandateId }) on the <coinbase-payment> web component. Coinbase connects the wallet, picks the best-funded network, and collects the signature. See Hosted Checkout for setup.
The approval returns in pending status and confirms onchain shortly after. Wait for the acceptance.mandate.approval_succeeded webhook before you authorize a payment. At that point the mandate moves to approval_succeeded and its source shows the wallet address and network the customer approved from. If approval fails, the mandate moves to approval_failed and the customer can try again.
Build your own approval UI
Build your own approval UI
You only need these endpoints if you’re not using the hosted page or embedded component. They don’t use your API key, because the customer’s wallet signature is the proof of consent, so you can call them from your frontend. The flow mirrors wallet authorization.
-
Get approval options for one to five of the customer’s wallet addresses:
Coinbase returns at most one option, choosing the address and network with the highest stablecoin balance, including Solana. Pass
networkonly when the customer must approve from one network. The Solana network name issolanain every environment, including sandbox. Addresses that can’t be used appear inineligibleAddresseswith a reason, such asinsufficient_funds. -
Sign the payload for the option’s
type: -
Submit the signature:
The example signature is an EVM hex signature. A Solana approval uses the same request body. Its
signatureis the base58 string from Solana signatures.
GET /v2/mandates/{mandateId}/revocations/wallet/options, have the customer sign the returned payload (eip2612 on EVM, solana_subscription on Solana, where the transaction is revokeDelegation), and submit the signature to POST /v2/mandates/{mandateId}/revocations/wallet.3. Authorize a payment
For each payment, create a payment session for the amount due, then authorize it with the mandate. The customer doesn’t need to take any action.Example response
Example response
pending status and moves to succeeded or failed.
An authorization is rejected before any funds move if the mandate isn’t usable or the amount breaks the spending limits. See Mandate errors. If the customer’s wallet doesn’t hold enough to cover the payment, the authorization fails with insufficient_funds. See Prevent failed mandate payments for how to catch that before billing day.
After you authorize
From here, charges from both sources follow the standard lifecycle:- Auto-capture suits most recurring charges, because you’ve already delivered the service or are about to.
- Voids and refunds return funds to the source: the customer’s Coinbase account, or the wallet and network they approved from.
- Idempotency keys let your billing jobs retry safely. Always send
X-Idempotency-Keyso a retry never charges twice. See Idempotency. - Failed authorizations can’t be retried on the same session. To try again, create a new payment session and authorize it with a new
X-Idempotency-Key.
Mandate spending limits
Every mandate has apolicy that caps what you can authorize. Coinbase checks the policy on every authorization.
All limits are optional. Any limit you omit gets a Coinbase default, and limits above Coinbase’s maximums are lowered to the maximum. The response always shows the limits that apply. An authorization over a limit is rejected with
422 mandate_policy_violation, and no funds move.
We recommend setting your own limits that match your billing model. Your business is responsible for payments authorized with its mandates, so tight limits protect your customers from misuse. Leave some headroom above your expected payment amounts for plan upgrades, taxes, or usage spikes. Mandate terms can’t be edited after you create the mandate. To change them, cancel the mandate, create a new one, and ask the customer to approve it.
Ending a mandate
There are two ways to end a mandate, and they’re independent.
When a customer ends their subscription, cancel the mandate so no further payments can be authorized, and point them to
revocationUrl if they also want to remove the wallet approval. If a customer removes the approval directly from their wallet, your next authorization fails.
Prevent failed mandate payments
A payment fails withinsufficient_funds when the customer’s wallet doesn’t hold enough of the mandate asset on the approved network. No funds move and the mandate stays usable, but the payment doesn’t go through until you retry.
Unlike a card, a wallet’s balance is public onchain, so you can spot a shortfall before you authorize. We recommend checking the balance in your billing job before every mandate payment and letting the customer know when they’re short. Coinbase doesn’t contact your customers.
Check the balance before you authorize
-
Get
source.address,source.network, andsource.assetfrom the mandate, either from theacceptance.mandate.approval_succeededwebhook orGET /v2/mandates/{mandateId}. Payments are only pulled from this wallet, on this network, in this asset. -
List the wallet’s balances on
source.networkwith the Token Balances API:Example response
The Token Balances API covers Base and Ethereum. For mandates approved from Arbitrum, Optimism, or Polygon, read the USDC balance ofsource.addresswith your own node or indexer, then continue from step 4. -
Find the USDC entry by
token.contractAddress(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913on Base,0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48on Ethereum), not bysymbol. Results are paginated: if the response includesnextPageToken, pass it aspageTokento get the next page. If USDC isn’t on any page, the balance is zero. -
Compare the balance to the amount due. The API returns
amountas an integer in USDC’s smallest unit, where 1 USDC is 1,000,000 units (decimals: 6). Payment amounts like19.99are in whole USDC, so convert the amount due before you compare:4100000is less than19990000, so the customer is 15.89 USDC short. - If the balance is below the amount due, tell the customer how much to add and the address, network, and asset to send it to. Funds on another network or in another token don’t count.
If the payment fails anyway
A passing check doesn’t guarantee the payment goes through, because the customer can move funds after you check. When an authorization fails, theacceptance.payment_session.authorization_failed webhook carries the reason in data.authorization.error.code.
If the reason is insufficient_funds, no funds moved and the mandate is still usable. To try again, create a new payment session and authorize it with the same mandateId and a new X-Idempotency-Key. Other failure reasons, such as a revoked mandate, need a different response. See Mandate errors.
Mandate statuses
status reflects the most recent action on the mandate.
To decide whether you can authorize a payment with a mandate, check its timestamps rather than relying on
status alone. A mandate is usable when approvedAt is set, canceledAt and revokedAt are empty, expiresAt is empty or in the future, and no revocation is in progress (status isn’t revocation_pending). For example, a mandate in revocation_failed is still usable because the approval was never removed.
Mandate errors
These errors mean Coinbase rejected a mandate authorization before any funds moved. Coinbase checks the mandate in this order and returns the first match.
Standard payment session errors, such as an expired authorization window, also apply. An
insufficient_funds failure arrives asynchronously through the acceptance.payment_session.authorization_failed webhook rather than as an HTTP error. See Prevent failed mandate payments.
Webhooks
Charges from both sources use the existing payment session events. For a Coinbase account, also subscribe to the OAuth and identity events to know when a link ends. For a wallet, subscribe toacceptance.mandate.* events to track approvals and cancellations:
Every mandate event’s
data contains the full mandate. Approval events also include approval, and revocation events include revocation, each with an error when the attempt fails. See example payloads and Webhooks for setup.
What to read next
Payment Sessions
Create the session you authorize with a mandate
Pay with Coinbase
Link a Coinbase account and authorize payments with OAuth
Pay with a Wallet
Connect a wallet and sign a one-time payment
Webhooks
Subscribe to payment session, mandate, and OAuth events