Skip to main content
Pay with Coinbase lets Coinbase customers pay with the USDC in their Coinbase account. The customer links their Coinbase account once through OAuth. You then use their stored token to authorize each payment.

Prerequisites

Pay with Coinbase requires Payment Acceptance onboarding, where you receive an OAuth client (client_id, client_secret) and register your redirect_uri. Contact our team to get started. Examples on this page use production hosts. In Sandbox, use these hosts instead:

How it works

Linking uses OAuth 2.0 with PKCE and returns the access and refresh tokens you need for every payment.
Keep client_secret and the PKCE code_verifier on your backend. Never send them to a browser or mobile app.

Required scopes

Request all nine scopes: A scope your client isn’t allowed to request returns invalid_scope. A missing required scope makes authorization fail with 403 forbidden.

Start a linking session

POST https://login.coinbase.com/oauth2/init-oauth-session Authentication: CDP API key, Authorization: Bearer <JWT> (generate a JWT)

Send the customer to Coinbase

Open link_url. It opens the Coinbase app when installed (Universal Link on iOS, App Link on Android), and an in-app browser otherwise. After the customer signs in and consents, Coinbase redirects them to your redirect_uri. After the redirect, exchange the session for tokens: POST https://login.coinbase.com/oauth2/confirm-link Authentication: CDP API key, Authorization: Bearer <JWT>
Store both tokens, user_id, and your external_reference, all mapped to your customer. oauth.sign_in.completed and oauth.token.revoked identify the customer by externalReferenceId, which holds the external_reference you sent. Identity events use coinbaseUserId.

Refresh tokens

An expired access token returns 401 Unauthorized. Exchange the refresh token for new tokens: POST https://login.coinbase.com/oauth2/token Authentication: OAuth client credentials, client_id and client_secret in the form body
Each refresh invalidates the previous refresh_token, so store the new one before you use the new access token. Sending an invalidated refresh token ends the link. Run one refresh at a time per customer. invalid_grant means the link was revoked or expired. The customer must link again. To disconnect the account, for example from a Disconnect Coinbase button, revoke either token: POST https://login.coinbase.com/oauth2/revoke Authentication: OAuth client credentials, client_id and client_secret in the form body
Revoking one token revokes both. Delete the stored tokens after a 200 response. Later calls return 401 until the customer links again.

2. Check the customer’s USDC balance

Check that the customer can cover the payment before you create it. This call needs the coinbase:stablecoins:balance-read scope. GET https://api.coinbase.com/v2/accounts/USDC Authentication: Customer’s OAuth token, Authorization: Bearer <access_token>
Use available_to_send, not balance. balance includes funds on hold. Amounts are decimal strings with six decimal places. Compare them as decimals, not as strings or floats.
The balance check is production only. In Sandbox, skip it and rely on the asynchronous coinbase_insufficient_balance error.

3. Create a payment session

POST https://api.cdp.coinbase.com/platform/v2/payment-sessions Authentication: CDP API key, Authorization: Bearer <JWT>
See Payment Sessions for all session fields.

4. Authorize the payment

Start a challenge, complete 2FA if Coinbase requires it, then submit the authorization. Every call uses the customer’s access token.

Start an authorization challenge

Call this before every authorization. Coinbase decides whether 2FA is required. POST https://login.coinbase.com/api/v1/authorization-challenges Authentication: Customer’s OAuth token, Authorization: Bearer <access_token>

Complete 2FA if required

  1. Redirect the customer to challengeUrl to complete 2FA on a Coinbase page.
  2. Poll GET https://login.coinbase.com/api/v1/authorization-challenges?authorizationId={authorizationId} with the customer’s access token until authorizationStatus is AUTHORIZATION_STATUS_AUTHORIZED, or wait for the oauth.mfa.completed webhook.
The challenge expires after expiryTtl seconds.

Submit the authorization

This places a hold on the customer’s USDC balance. POST https://api.cdp.coinbase.com/platform/v2/payment-sessions/{paymentSessionId}/authorizations/coinbase Authentication: Customer’s OAuth token, Authorization: Bearer <access_token>, not your CDP API key
A body is required. Send {} if you have no metadata, externalReferenceId, or customerDisplay.referenceCode.
The authorization starts as pending and moves to succeeded or failed. Track it with the acceptance.payment_session.authorization_succeeded and acceptance.payment_session.authorization_failed webhooks, or poll GET /v2/payment-sessions/{id}/authorizations/{authorizationId}. The authorizationId in this response (authorization_…) identifies the payment authorization. It’s different from the challenge authorizationId you sent in cb-authz-id. Use this one to poll.
This endpoint doesn’t check the balance. A customer without enough USDC still gets 200 with status: "pending", then the authorization fails with coinbase_insufficient_balance. Run the balance check first.

5. Capture, void, or refund

These calls use your CDP API key, as for any session. With autoCapture: true, funds are captured after authorization succeeds. Otherwise, capture after fulfillment or void to release the hold. Refunds return funds to the customer’s Coinbase account. See Payment Sessions.

Charge without the customer present

For recurring or usage-based charges while the customer is away, start the challenge with initiator: "merchant" and enforce2fa: false:
Never set enforce2fa: true when the customer is away. Nobody can complete the 2FA redirect, so the challenge stays in AUTHORIZATION_STATUS_CHALLENGE_PENDING.
For the full recurring setup, including token refresh in billing jobs, see Recurring Payments.

Errors

Account linking

Errors return an error field. The message is in error_message (init-oauth-session, confirm-link) or error_description (token, revoke).

Authorization challenge

Coinbase authorization

Errors specific to POST .../authorizations/coinbase. See the API Reference for the full list.

Asynchronous authorization failures

The acceptance.payment_session.authorization_failed webhook gives the reason in data.authorization.error.code: These failures are final. To retry, create a new payment session. Match on code, not message. For timed_out, internal_error, and invalid_parameters, contact support with the correlationId and session ID.

Webhooks

Besides payment session events, subscribe to OAuth and identity events to track each linked account:
  • oauth.token.revoked fires when the customer disconnects your app in Coinbase, not when you call /oauth2/revoke. Don’t rely on it alone: also treat invalid_grant on refresh as a revoked link. A 401 on authorization can mean the access token expired, so refresh once and retry before you treat the link as revoked.
  • On identity.user.deleted, stop processing payments for that customer.
See example payloads.

Payment Sessions

Expiries, balances, auto-capture, and target types

Pay with a Wallet

Let customers pay from a self-custody wallet

Recurring Payments

Charge a linked account on your schedule

Webhooks

Subscribe to payment and OAuth events

API Reference - Coinbase Auth

Coinbase authorization API