Privy enables programmable account funding via bank transfer by natively integrating Bridge directly within Privy’s API. This recipe details how your app can use Privy to provide a seamless, customizable fiat/crypto conversion experience for your users.
Using Privy’s APIs, you can:
- Link a Privy user with a Bridge customer account and their external bank accounts
- Onramp (convert fiat to crypto)
- Offramp (convert crypto to fiat)
- Switch between Bridge sandbox and production environments
Setup
If you don’t already have a Bridge account, request access at Bridge and get Bridge API keys.
Then, turn on the bank transfer method on the Account Funding page in the Privy dashboard.
Enter your Bridge API keys when prompted, and save.
Register a new user for onramp or offramp
When a user is onramping or offramping via Bridge for the first time, they must agree to their terms of service, as well as go through the KYC process.
Provide a terms of service agreement to the user
First, the user must agree to the onramp provider’s terms of service.
Request a new terms of service url for a user:
curl --request POST \
--url https://api.privy.io/v1/users/{user_id}/fiat/tos \
--header 'Authorization: Basic <encoded-value>' \
--header 'Content-Type: application/json' \
--header 'privy-app-id: <privy-app-id>' \
--data '{
"provider": "bridge-sandbox"
}'
curl --request POST \
--url https://api.privy.io/v1/users/{user_id}/fiat/tos \
--header 'Authorization: Basic <encoded-value>' \
--header 'Content-Type: application/json' \
--header 'privy-app-id: <privy-app-id>' \
--data '{
"provider": "bridge-sandbox"
}'
Then, add the query param redirect_uri
to the url, so that after the user goes to that link and signs the terms of service, the user is redirected to your app. Upon redirect, a signed_agreement_id
parameter will be in the url, which you will use for the next KYC step.
In order to onramp or offramp, the user must first be KYC’d. You can check their KYC status like so:
curl --request GET \
--url https://api.privy.io/v1/users/{user_id}/fiat/kyc \
--header 'Authorization: Basic <encoded-value>' \
--header 'privy-app-id: <privy-app-id>'
curl --request GET \
--url https://api.privy.io/v1/users/{user_id}/fiat/kyc \
--header 'Authorization: Basic <encoded-value>' \
--header 'privy-app-id: <privy-app-id>'
import os
from privy import PrivyAPI
client = PrivyAPI(
app_id=os.environ.get("PRIVY_APP_ID"), # This is the default and can be omitted
app_secret=os.environ.get("PRIVY_APP_SECRET"), # This is the default and can be omitted
)
kyc = client.fiat.kyc.get(
user_id="user_id",
provider="bridge",
)
print(kyc.user_id)
If the user is not KYC’d yet, gather and submit their KYC information:
curl --request POST \
--url https://api.privy.io/v1/users/{user_id}/fiat/kyc \
--header 'Authorization: Basic <encoded-value>' \
--header 'Content-Type: application/json' \
--header 'privy-app-id: <privy-app-id>' \
--data '{
"provider": "bridge-sandbox",
"data": {
"type": "individual",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone": "+59898222122",
"residential_address": {
"street_line_1": "1234 Lombard Street",
"street_line_2": "Apt 2F",
"city": "San Francisco",
"subdivision": "CA",
"postal_code": "94109",
"country": "USA"
},
"signed_agreement_id": "123",
"birth_date": "1989-09-09",
"identifying_information": [
{
"type": "ssn",
"number": "111-11-1111",
"issuing_country": "USA",
"image_front": "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
"image_back": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."
}
]
}
}'
curl --request POST \
--url https://api.privy.io/v1/users/{user_id}/fiat/kyc \
--header 'Authorization: Basic <encoded-value>' \
--header 'Content-Type: application/json' \
--header 'privy-app-id: <privy-app-id>' \
--data '{
"provider": "bridge-sandbox",
"data": {
"type": "individual",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone": "+59898222122",
"residential_address": {
"street_line_1": "1234 Lombard Street",
"street_line_2": "Apt 2F",
"city": "San Francisco",
"subdivision": "CA",
"postal_code": "94109",
"country": "USA"
},
"signed_agreement_id": "123",
"birth_date": "1989-09-09",
"identifying_information": [
{
"type": "ssn",
"number": "111-11-1111",
"issuing_country": "USA",
"image_front": "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
"image_back": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."
}
]
}
}'
import os
from privy import PrivyAPI
client = PrivyAPI(
app_id=os.environ.get("PRIVY_APP_ID"), # This is the default and can be omitted
app_secret=os.environ.get("PRIVY_APP_SECRET"), # This is the default and can be omitted
)
kyc = client.fiat.kyc.create(
user_id="user_id",
data={
"birth_date": "1989-09-09",
"email": "[email protected]",
"first_name": "John",
"identifying_information": [{
"issuing_country": "USA",
"type": "ssn",
"number": "111-11-1111",
"image_front": "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
"image_back": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."
}],
"last_name": "Doe",
"phone": "+59898222122",
"residential_address": {
"city": "San Francisco",
"country": "USA",
"street_line_1": "1234 Lombard Street",
"street_line_2": "Apt 2F",
"subdivision": "CA",
"postal_code": "94109"
},
"signed_agreement_id": "123",
"type": "individual",
},
provider="bridge-sandbox",
)
print(kyc.user_id)
Note: After submission, the KYC process takes about 1-2 minutes to undergo review. Sometimes, a manual review is required, which can take longer (a few hours).
Onramp funds
This assumes that all the above steps have been completed.
Trigger the onramp flow
Once the user is KYC’d, you can trigger the onramp flow.
curl --request POST \
--url https://api.privy.io/v1/users/{user_id}/fiat/onramp \
--header 'Authorization: Basic <encoded-value>' \
--header 'Content-Type: application/json' \
--header 'privy-app-id: <privy-app-id>' \
--data '{
"amount": "100.00",
"provider": "bridge-sandbox",
"source": {
"currency": "usd",
"payment_rail": "ach_push"
},
"destination": {
"currency": "usdc",
"chain": "base",
"to_address": "0x38Bc05d7b69F63D05337829fA5Dc4896F179B5fA"
}
}'
curl --request POST \
--url https://api.privy.io/v1/users/{user_id}/fiat/onramp \
--header 'Authorization: Basic <encoded-value>' \
--header 'Content-Type: application/json' \
--header 'privy-app-id: <privy-app-id>' \
--data '{
"amount": "100.00",
"provider": "bridge-sandbox",
"source": {
"currency": "usd",
"payment_rail": "ach_push"
},
"destination": {
"currency": "usdc",
"chain": "base",
"to_address": "0x38Bc05d7b69F63D05337829fA5Dc4896F179B5fA"
}
}'
import os
from privy import PrivyAPI
from privy.lib.stablecoins.usdc import deposit_usdc_from_bank
client = PrivyAPI(
app_id=os.environ.get("PRIVY_APP_ID"), # This is the default and can be omitted
app_secret=os.environ.get("PRIVY_APP_SECRET"), # This is the default and can be omitted
)
onramp = deposit_usdc_from_bank(
client=client,
user_id="user_id",
amount_in_usdc=100.00,
onramp_provider="bridge-sandbox",
onramp_source={
"currency": "usd",
"payment_rail": "ach_push",
},
chain_id=8453,
)
print(onramp.id)
This will return deposit instructions for the user to complete, since they will need to send fiat funds to the onramp provider in order to receive stablecoins in their wallet. For example:
{
"id": "3a61a69a-1f20-4113-85f5-997078166729",
"deposit_instructions": {
"amount": "100.0",
"currency": "usd",
"payment_rail": "ach_push",
"account_holder_name": "account_holder_name",
"bank_account_number": "11223344556677",
"bank_address": "1800 North Pole St., Orlando, FL 32801",
"bank_beneficiary_address": "1234 Elm St, Springfield, IL 12345",
"bank_beneficiary_name": "Bridge Ventures Inc",
"bank_name": "Bank of Nowhere",
"bank_routing_number": "123456789",
"bic": "bic",
"deposit_message": "BRGFU2Z9TJPJXCS7ZZK2",
"iban": "iban"
},
"status": "awaiting_funds"
}
Follow bank deposit instructions
If the onramp method is a bank transfer, the user will need to follow the specific instructions to send a bank deposit with the specified deposit message. If these instructions are not followed correctly, the onramp will fail.
Offramp
This assumes that that the Terms of Service and KYC process have been completed for the user.
Register a fiat account to offramp to
In order for Bridge to offramp and send fiat funds to the user, the user must first register a fiat account (e.g. bank account) with them.
curl --request POST \
--url https://api.privy.io/v1/users/{user_id}/fiat/accounts \
--header 'Authorization: Basic <encoded-value>' \
--header 'Content-Type: application/json' \
--header 'privy-app-id: <privy-app-id>' \
--data '{
"provider": "bridge-sandbox",
"account_owner_name": "John Doe",
"currency": "usd",
"bank_name": "Chase",
"account": {
"account_number": "1234567899",
"routing_number": "121212121",
"checking_or_savings": "checking"
},
"address": {
"street_line_1": "123 Washington St",
"street_line_2": "Apt 2F",
"city": "New York",
"state": "NY",
"postal_code": "10001",
"country": "USA"
},
"first_name": "John",
"last_name": "Doe"
}'
curl --request POST \
--url https://api.privy.io/v1/users/{user_id}/fiat/accounts \
--header 'Authorization: Basic <encoded-value>' \
--header 'Content-Type: application/json' \
--header 'privy-app-id: <privy-app-id>' \
--data '{
"provider": "bridge-sandbox",
"account_owner_name": "John Doe",
"currency": "usd",
"bank_name": "Chase",
"account": {
"account_number": "1234567899",
"routing_number": "121212121",
"checking_or_savings": "checking"
},
"address": {
"street_line_1": "123 Washington St",
"street_line_2": "Apt 2F",
"city": "New York",
"state": "NY",
"postal_code": "10001",
"country": "USA"
},
"first_name": "John",
"last_name": "Doe"
}'
import os
from privy import PrivyAPI
impor
client = PrivyAPI(
app_id=os.environ.get("PRIVY_APP_ID"), # This is the default and can be omitted
app_secret=os.environ.get("PRIVY_APP_SECRET"), # This is the default and can be omitted
)
account = client.fiat.accounts.create(
user_id="user_id",
account_owner_name="John Doe",
currency="usd",
provider="bridge-sandbox",
account={
"account_number": "1234567899",
"routing_number": "121212121",
"checking_or_savings": "checking"
},
address={
"street_line_1": "123 Washington St",
"street_line_2": "Apt 2F",
"city": "New York",
"state": "NY",
"postal_code": "10001",
"country": "USA"
},
first_name="John",
last_name="Doe"
)
print(account.id)
Trigger the offramp flow
Once the user has a fiat account registered, you can trigger the offramp flow. In this flow, stablecoins must be sent from the user’s wallet to the onramp provider’s on-chain address.
curl --request POST \
--url https://api.privy.io/v1/users/{user_id}/fiat/offramp \
--header 'Authorization: Basic <encoded-value>' \
--header 'Content-Type: application/json' \
--header 'privy-app-id: <privy-app-id>' \
--data '{
"provider": "bridge-sandbox",
"amount": "100.00",
"source": {
"currency": "usdc",
"chain": "base",
"from_address": "0xc24272abc794b973b896715db40a72714a030323"
},
"destination": {
"currency": "usd",
"payment_rail": "ach_push",
"external_account_id": "a068d2dd-743a-4011-9b62-8ad33cc7a7be"
}
}'
This will return deposit instructions for the user to complete, since they will need to send their stablecoins to the onramp provider’s on-chain address in order to receive fiat funds in their account.
Use Privy to complete this send. As an example of how to do this, see the Send USDC recipe.
curl --request POST \
--url https://api.privy.io/v1/users/{user_id}/fiat/offramp \
--header 'Authorization: Basic <encoded-value>' \
--header 'Content-Type: application/json' \
--header 'privy-app-id: <privy-app-id>' \
--data '{
"provider": "bridge-sandbox",
"amount": "100.00",
"source": {
"currency": "usdc",
"chain": "base",
"from_address": "0xc24272abc794b973b896715db40a72714a030323"
},
"destination": {
"currency": "usd",
"payment_rail": "ach_push",
"external_account_id": "a068d2dd-743a-4011-9b62-8ad33cc7a7be"
}
}'
This will return deposit instructions for the user to complete, since they will need to send their stablecoins to the onramp provider’s on-chain address in order to receive fiat funds in their account.
Use Privy to complete this send. As an example of how to do this, see the Send USDC recipe.
Note: This helper function will both trigger the offramp flow and also send the crypto from the user’s wallet to the onramp provider’s on-chain address.
import os
from privy import PrivyAPI
from privy.lib.stablecoins.usdc import withdraw_usdc_to_bank
client = PrivyAPI(
app_id=os.environ.get("PRIVY_APP_ID"), # This is the default and can be omitted
app_secret=os.environ.get("PRIVY_APP_SECRET"), # This is the default and can be omitted
)
offramp = withdraw_usdc_to_bank(
client=client,
user_id="user_receiving_fiat",
wallet_id="wallet_id_sending_usdc",
amount_in_usdc=100.00,
offramp_provider="bridge-sandbox",
offramp_destination={
"currency": "usd",
"payment_rail": "ach_push",
"external_account_id": "account.id from previous step",
},
chain_id=8453,
)
print(offramp.id)
To check on a status of an onramp or offramp, you can go to the Wallets
page in the Privy dashboard, and click
on the wallet. On the sidebar, go to the Fiat transactions
tab.