# Global Support
Source: https://docs.zbdpay.com/earn/coverage
List of regions where ZBD App & Earn SDK is supported.
> After accessing the ZBD login portal URL and successfully authenticating, the user will be redirected back to your application with `state` and `code` query parameters included. These two properties allow you to hit the `token` endpoint, and get the user's `accessToken`.
### Getting the Token
* Token: `POST` [https://api.zbdpay.com/v1/oauth2/token](https://api.zbdpay.com/v1/oauth2/token)
* Once you have the `code` returned from the previous `authorization` endpoint, you must now make a `POST` request to the token endpoint, with the following properties:
```json theme={null}
{
"client_id": "{{client_id}}",
"client_secret": "{{client_secret}}",
"code": "{{code}}",
"code_verifier": "{{code_verifier}}",
"grant_type": "authorization_code",
"redirect_uri": "{{your_url_callback}}"
}
```
* And, as response, you should receive the user's `accessToken`:
```json theme={null}
{
"accessToken": "{{userAccessToken}}"
}
```
### Getting ZBD User Profile Data
* Profile: `GET` [https://api.zbdpay.com/v1/oauth2/user](https://api.zbdpay.com/v1/oauth2/user)
* You can now call the ZBD API endpoints passing the provided `accessToken` from the previous endpoint on the request authorization header.
```json theme={null}
Response: {
"data": {
"email": "testzebedee2@gmail.com",
"gamertag": "testzebedee",
"id": "6109484f-08e6-479a-a044-be930d89b42a",
"image": "https://lh3.googleusercontent.com/a/AATXAJwNvpB49NrxF41KVrEFX9k1v93WoMAMBb4wBF7Q=s96-c",
"isVerified": true,
"lightningAddress": "testzebedee@zbd.gg",
"publicBio": "Welcome to my zbd.gg page!",
"publicStaticCharge": "lnurl1dp68gurn8ghj7ar9wd6zu7n9vfjkget99e5k7tmkxqhhyet3w4jhxapdwd6xzarfvvkkx6rpwfnk2ue0x56nxvrpxuervtfkxa3rgtf5xu6k2ttzxy6nstt9xsurgvejx5exydpjvc9v9tk7",
"social": {
"twitter": "gamertag",
"website": "https://example.com",
},
},
"success": true,
}
```
### Getting ZBD User Wallet Data
* Profile: `GET` [https://api.zbdpay.com/v1/oauth2/wallet](https://api.zbdpay.com/v1/oauth2/wallet)
* You can now call the ZBD API endpoints passing the provided `accessToken` from the previous endpoints on the request authorization header.
```json theme={null}
Response: {
"data": {
"balance": "98251000",
"remainingAmountLimits": {
"daily": "1000000000",
"maxCredit": "4901749000",
"monthly": "25000000000",
"weekly": "5000000000",
},
},
"message": "Successfully fetched user wallet.",
"success": true,
}
```
It is common for software libraries and prebuilt packages to handle a lot of
this heavy-lifting for you, except for the `Getting the Token` part, which is
a true secret and should be handled in your secure backend system services.
## Integration
### Get Credentials from ZBD Developer Dashboard
In order to be able to implement `ZBD Login` functionality, you must first get your application/game's OAuth2 `Client ID` and `Client Secret`, and set up proper redirect URLs.
### Getting Keys
After creating a game inside of the ZBD Developer Dashboard, head on over to the `OAuth2` tab inside of that game's single details view.
You can now get your `Client ID` (first field) and `Client Secret` (second field).
Keep your secrets secure and safe. Don't share secrets with others. Don't
commit secrets into your code.
### Setting Redirect URLs
Now its time to set up your OAuth2 redirect URLs. Head on over to the OAuth2 settings modal through the `View Settings` button.
> It is a requirement that the same redirect URL be used in the first step of the OAuth2 flow (`authorization endpoint`) - if you set a redirect URL different from the one being used when building the authorization URL, or if you don't set a redirect URL at all, the Authorization redirect will not work.
From the opened OAuth2 Settings modal, you can add up to 3 redirect URLs (whether native mobile URIs or HTTPS-based URLs). To get started use the `+` button.
Once you've added your redirect URLs / app schemas, you can save by clicking `Update`. You're now all set up for using `ZBD Login` on your application. For more detailed examples [check out the OAuth2 Code Walkthrough for source code and LOC explanation](/earn/oauth2/walkthrough).
# Implementation Walkthrough
Source: https://docs.zbdpay.com/earn/oauth2/walkthrough
Detailed walkthrough of an implementation of ZBD Login.
## Client Side
When setting up OAuth2 from client side, you have two options:
* Build the entire flow yourself
* Make a request for building the [authorization URL](/earn/oauth2/walkthrough#building-authorization-url)
* Handling the Webview with the created URL
* Getting the `code` after authenticating with ZEBEDE
* Making another request for [getting the token](/earn/oauth2/walkthrough#getting-the-token-and-accessing-zbd-api)
* Make use of some of the available OAuth libraries available for the language you are working on, which would handle the initial steps of the flow
* Install and set up configs accordingly to the library
* If PKCE is available on the library you might receive the `code_verifier` from the response, after authenticating.
* If PKCE is not available, you might need to [build the code\_challenge and code\_verifier](/earn/oauth2/walkthrough#building-authorization-url) yourself.
* Getting `code` as response
* Making the request for [getting the token](/earn/oauth2/walkthrough#getting-the-token-and-accessing-zbd-api)
If you chose the second option, there are open-source libraries for handling OAuth2 on Mobile using AppAuth ([iOS](https://github.com/openid/AppAuth-iOS) and [Android](https://github.com/openid/AppAuth-Android)), as well as for React Native using [react-native-app-auth](https://github.com/FormidableLabs/react-native-app-auth).
### iOS Webview Configuration
If you are calling the OAuth flow from an iOS webview integration, you need to add the following to your `Info.plist` file in Xcode:
```xml theme={null}
LSApplicationQueriesSchemes
zebedee.oauth2
zbd
```
This allows your iOS app to query and open the ZBD app URL schemes required for the OAuth flow. Without this configuration, the OAuth redirect may not work properly on iOS devices.
## Server Side
### Node.js
This flow go over all the steps for Logging in with ZBD, from Building the authorization URL, with proper PKCE Keys, to getting the accessToken and calling ZBD API.
If you are using a library that provides PKCE guidelines, it will most likely be able to handle the initial flow (building auth URL and getting the code) just fine. In this case, feel free to jump to the [Token Section](/earn/oauth2/walkthrough#building-authorization-url).
### Building Authorization URL
In case you are setting up the PKCE flow yourself (no usage of libraries), we will need to setup the `code_challenge` from a `code_verifier` which will be used later on, on token request.
```js theme={null}
const crypto = require('crypto');
import OAuth from 'oauth';
function sha256(buffer) {
return crypto.createHash('sha256').update(buffer).digest();
}
function base64URLEncode(str) {
return str
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
export function GeneratePKCE() {
const verifier = base64URLEncode(crypto.randomBytes(32));
if (verifier) {
const challenge = base64URLEncode(sha256(verifier));
return { challenge, verifier };
}
}
const createZBDOauth = () => {
const { OAuth2 } = OAuth;
const authorizeUrl = 'oauth2/authorize';
const tokenPath = 'oauth2/token';
const oauth2 = new OAuth2(
ZBD_CONSUMER_KEY, // CLient ID
ZBD_CONSUMER_SECRET, // Client Secret
ZEBEDEE_API_URL, // ZBD API URL: https://api.zbdpay.com/v0/
authorizeUrl, // Authorization URL: oauth2/authorize/
tokenPath, // Token Path: oauth2/token
null,
);
return oauth2;
};
// Called by the app to get a url it will open in a webview/or browser
export const getZBDLoginUrl = async (userId: string) => {
// generate the PKCE key
const { verifier, challenge } = GeneratePKCE();
// save the verifier/key to the user object so we can access it when validating in the callback
await User.findOneAndUpdate({ userId }, { oauthVerifier: verifier });
const scope = 'user';
const state = userId;
const suffix = `&response_type=code&code_challenge=${challenge}&code_challenge_method=S256&state=${state}`;
const oauth2 = createZBDOauth();
// use NPM module to create the url
const res = await oauth2.getAuthorizeUrl({
redirect_uri: ZBD_REDIRECT_URL,
scope,
});
// npm module doesnt support PKCE so append the url with the code_challenge info
const url = res + suffix;
return url;
};
```
**Code explanation**
* `getZBDLoginUrl` function
* In this function, we initially Generate PKCE keys: `verifier` and `code_challenge`
* Then, `findOneAndUpdate` represents an generic ORM call for updating the User model, on the database. This way we can, afterwards, retrieve that value for getting the token.
* We then Build the URL suffix
* Url prefix and generated from `createZBDOauth`, which uses `oauth` library to do so
* `generatePKCE`
* This is where we setup PKCE Keys.
* initially we set the code verifier:
* `const verifier = base64URLEncode(crypto.randomBytes(32));`
* From the code verifier, the code\_challenge is created:
* `const challenge = base64URLEncode(sha256(verifier));`
When logging in using the provided URL, you will get a response including `state` and `code` which should be user now for getting the token, and user data:
### Getting the token and accessing ZBD API
```js theme={null}
// called by ZBD oauth on login
export const zbdLoginCallback = async (payload: Object) => {
const { code, state } = payload;
try {
const user = await User.findOne({ userId: state });
const { oauthVerifier } = user;
// get the verifier generated by the PKCE script
const res = await getAccessToken({
code,
client_secret: ZBD_CONSUMER_SECRET,
client_id: ZBD_CONSUMER_KEY,
code_verifier: oauthVerifier,
grant_type: 'authorization_code',
redirect_uri: ZBD_REDIRECT_URL,
});
const { access_token } = res.data;
// get user data now we have the access token
const response = await getUserData(access_token);
const { data } = response;
const userData = data.data;
const { gamertag, email, isVerified, id } = userData;
if (!gamertag || !id || !email) {
throw new Error('gamer tag not found');
}
// save the details to the user object so we now know their email, verification etc
user.gamerTag = gamertag;
user.gamerTagId = id;
user.email = email;
user.isZBDVerified = isVerified;
user.oauthLoginDate = new Date();
await user.save();
// show a webpage that will redirect back to the app app by calling the app schema url
return {
html: path.resolve(`${__dirname}/../callback/oauth-callback.html`),
};
} catch (e) {
console.error(e);
return {
html: path.resolve(`${__dirname}/../callback/oauth-callback-error.html`),
};
}
};
```
**Code explanation**
Once the user is successfully authenticated on ZBD Client, there is a redirect back to your app, sending `code` and `state` as query params.
Those values are send as payload on this next request:
* First of all, remember we registered the verifier on the User (generic ORM update)? now we retrieve that value:
* `const user = await User.findOne({ userId: state });`
* With the proper verifier, we can now make a post request to the token endpoint, and we should have the token returned.
* `const res = await getAccessToken({โฆbody})`
```js theme={null}
export const getAccessToken = async (payload: Object) => {
const response = await axios({
method: 'POST',
data: payload,
url: `https://api.zbdpay.com/v0/oauth2/token`,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
},
});
return response;
};
```
* Now, by adding the accessToken to request headers, we can fetch data from ZBD API
* `const response = await getUserData(access_token);`
```js theme={null}
export const getUserData = async (accessToken: string) => {
const response = await axios({
method: 'GET',
url: `https://api.zbdpay.com/v0/oauth2/user`,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
Authorization: `Bearer ${accessToken}`,
},
});
return response;
};
```
You are now able to proceed with your login logic and send the user data accordingly โก
## Code Examples
See the full source code.
See the full source code.
# ZBD Earn SDK
Source: https://docs.zbdpay.com/earn/sdk
Add real-money rewards to your Unity mobile game. No backend required to get started.
The ZBD Earn SDK lets you reward players with real money at meaningful moments in your mobile game (level completions, daily missions, milestone unlocks). Done well, it lifts retention and LTV by making progress feel tangible.
Unlike third-party rewards networks, rewards live natively inside your game โ branded to your experience, triggered by your gameplay moments, and controlled entirely by you. Players never leave your app to collect rewards from an aggregator. The habit loop stays yours.
You control when rewards fire and how much players can earn. ZBD handles account creation, identity verification, and payouts through an in-app modal that requires no UI work on your end.
Drop a prefab into your scene. No backend required.
Reward players in your own virtual currency units or in satoshis. Most studios use the in-game currency model.
Players cash out through the ZBD modal. You open it with one method call; ZBD handles the rest.
Average revenue per daily active user increase seen by ZBD partners.
Long-term retention lift seen by partners with well-integrated reward economies.
Typical reward budget. Reinvesting a slice of revenue generates significantly more in return.
## What makes it work
Before anything else, know what a successful integration depends on. **Three things drive conversion โ if one is missing, your rewards may underperform:**
1. Your **UI/UX shows and explains** how rewards work and how to claim them โ native game elements first, plus creatives and notifications.
2. You're **rewarding enough** to be worth coming back for. Nobody changes their behavior for a few cents โ size rewards off actual revenue by [connecting your MMP](/earn/sdk/mmp-postbacks).
3. You **know your audience**. Acquiring players who want to earn โ or who'd be interested in earning โ improves conversion rates.
What to do for each one, and the pages that implement them. Read this before you design your rewards UX.
## How it works
Players accumulate a **rewards balance** at triggers you define. The balance is denominated in your chosen reward currency: virtual units in in-game currency mode, or satoshis in Bitcoin mode. When players are ready to cash out, they tap the rewards entry point in your game, pick how they want to be paid, and withdraw. ZBD handles the rest.
Two values govern every player's rewards state:
| Value | What it is |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Rewards balance** | The total amount the player has earned and not yet withdrawn. You add to this by calling `SendReward`. |
| **Withdrawal limit** | How much of that balance the player can withdraw right now. It grows automatically as they play, on the reward schedule configured for your app, and you can also raise it from your server at any time. |
The withdrawal limit exists mainly to protect against fraud, but it's also a retention mechanic: how much the limit can grow in a day is capped, and that daily allowance resets every 24 hours โ so players come back tomorrow to unlock more of what they've earned.
A player can earn faster than their limit grows. The excess stays in their balance and becomes withdrawable as the limit rises, either through continued play or a server-side increase.
You set the reward amounts and configure the withdrawal limits. ZBD enforces those limits on the player side. This separation means you can be generous with rewards without taking on uncapped financial exposure.
## The player experience
Here's what a player sees from first launch through first cashout โ the journey you're building toward.
At a gameplay moment you define (level complete, daily mission chest, milestone hit), a reward notification fires: "You earned 500 coins (\$0.05)!" They didn't have to do anything differently โ it just happened. Fire this early in the first session, so the player never sees the rewards counter sitting at zero: discovering a balance that already exists is what makes the feature feel real.
The player notices a balance counter in your game UI alongside your other currencies โ and there's already something in it. It's branded to your game; they have no idea what's powering it.
Right after the first reward lands, a one-screen [creative](/earn/sdk/creatives) shows the balance they already have, paired with the reason it's real: *"game studios share their ad revenue with players."* The modal's built-in first-time screen doesn't carry that line, so this is where players learn why the rewards aren't a scam. See the [Player Communication Guide](/earn/sdk/player-comms).
They tap the counter (or your rewards button). The modal opens instantly โ no extra screens โ showing their balance and the cashout options you've configured with your Customer Success Manager: gift cards, Cash App, ZBD, and others depending on their region.
They pick a payout option and withdraw. The modal handles the full flow, and they're deep-linked back to your game when it's done.
Most options need nothing more than that. Cashing out **to ZBD** is the one that requires the player to have a linked ZBD account โ worth knowing because ZBD runs partner incentive programmes for studios whose players cash out to ZBD, so you may want to promote it as an option. It's never a requirement for withdrawing.
Their withdrawal limit doesn't reset โ what resets is how much more of it they can unlock. Each day brings a fresh allowance, so any balance they couldn't withdraw yet becomes withdrawable as they keep playing. The habit loop is set: play, earn, come back.
For guidance on what to say to players at each of these moments, see the [Player Communication Guide](/earn/sdk/player-comms).
## Key decisions
Before writing any code, you'll need to choose your reward currency (in-game units or Bitcoin), your delivery method (client-side or server-side), and confirm your target regions are covered. These decisions shape your integration.
Reward currency, delivery method, and payout options โ with a comparison table for each. Read this before starting the setup.
## How the economics work
The reward budget comes from you โ typically a small percentage of the revenue your players are already generating. You're not buying ad impressions or paying for external traffic. You're redirecting a portion of existing player LTV back to the players who generate it, at a fraction of what acquisition channels cost.
ZBD's platform fees are low, and the total cost of running a rewards program is designed to be well within the value it returns in retention and engagement. Your Customer Success Manager will walk you through the specifics when you get set up.
## Compliance and KYC
Payments involve compliance requirements. Here's what falls on ZBD and what falls on you.
**ZBD handles:**
* Player identity verification โ players go through a light KYC step the first time they cash out, inside the ZBD modal. You don't build or manage this.
* Regulatory compliance โ ZBD is a licensed payment service provider in the US and EU.
* Fraud tooling โ withdrawal limits, device attestation, geolocation, and VPN detection are built into the SDK.
**You are responsible for:**
* Your reward exposure โ you set the budget and limits. ZBD's built-in protections reduce fraud risk, but you ultimately own your reward economics. Moving to [server-side reward delivery](/earn/sdk/send-rewards-server) is the most effective step you can take. See [Security](/earn/sdk/security) for the full picture.
* App store compliance โ ZBD rewards must be implemented correctly to comply with Apple App Store and Google Play policies. See [iOS App Store Guidelines](/earn/sdk/ios-app-store) and [Android App Store Guidelines](/earn/sdk/android-app-store).
* How you communicate rewards to players โ see the [Player Communication Guide](/earn/sdk/player-comms) for what to say and what to avoid.
You do not take on KYC obligations for your players. ZBD holds that relationship.
## Before you start
A few decisions should be locked in before you write any code. The reward budget, daily withdrawal limit, and reward currency are all tunable after launch, but getting them roughly right at the start saves significant iteration.
Recommended starting values for reward budget, withdrawal limits, and trigger ceilings. Start here.
What you need to know to pass App Store and Google Play review with ZBD integrated.
## Get started
ZBD Earn is available to approved partners. [Talk to our team](https://zbdpay.com/contact) to discuss your use case and get access to the Unity SDK. Most studios are set up within a few days.
Lock in your reward budget, withdrawal limit, and reward currency before integrating. [Reward Economics โ](/earn/sdk/reward-economics)
Your ZBD Customer Success Manager will provide the Unity package and help configure your project. [Download โ](/earn/sdk/download)
Import the package, drop the prefab, and call `Init` on app launch. [Integration guide โ](/earn/sdk/integration)
Call `SendReward` at a gameplay moment. [Client-side โ](/earn/sdk/send-rewards-client) or [Server-side โ](/earn/sdk/send-rewards-server)
Run through the go-live checklist before pushing to production. [Go-live checklist โ](/earn/sdk/go-live-checklist)
# Google Play Store Guidelines
Source: https://docs.zbdpay.com/earn/sdk/android-app-store
Instructions for successfully submitting your game to the Google Play Store.
**It is very important to follow the below guidelines to ensure your game does not run into issues with app reviewers on the Google Play Store.**
# Accessing App Content
In the left sidebar, go to **Monitor and improve** โ **Policy and programs** โ **App content**. Select the **Actioned** tab.
After accessing **Data Safety**, click **Next** until you reach the **Data Types** section.
Expand the **App Activity** section and select **Installed Apps**.
Expand the **Device or other IDs** section and select **Device or other IDs**.
Click **Next** to proceed to **Data Usage and Handling**.
### 4. Data Usage and Handling
1. In the **Data Usage and Handling** section, expand **App Activity** and click **Start**.
2. Choose **Collected**
3. This is **NOT** processed ephemerally
4. Select **Data collection is required** (users canโt turn off this data collection) if applicable.
5. Under the reasons for data collection, select **Fraud prevention, security, and compliance**.
| Data Types | Collected | Shared | Processed ephemerally | Required or Optional | Purposes |
| ---------------------- | --------- | ------ | --------------------- | -------------------- | ---------------- |
| Installed applications | Yes | No | No | Required | Fraud Prevention |
| User IDs | Yes | No | No | Required | Fraud Prevention |
| Device or other IDs | Yes | No | No | Required | Fraud Prevention |
### 7. Finalizing and Saving
# Attestation Set Up
Source: https://docs.zbdpay.com/earn/sdk/attestation-setup
Configure attestation to ensure your app has not been tampered with using required information for Android and iOS.
In order to ensure we have everything we need to detect whether your app has not been tampered with as part of our attestation check, we require a couple of pieces of information.
Here is a handy video guide:
## Android
For Android, we need your **Bundle ID** or **package name**, i.e.`com.domain.appname`, which you set in your Unity build settings.
This is NOT the appId ZBD provides you.
We also need the **SHA-256 hash** of your signing key.
To find your app's SHA-256 hash key for Google Play:
Sign in to your Google Play Console account.
Choose the app for which you need the SHA-256 hash.
In the left-hand menu, select Protected with Play.
During development, you are likely using a custom keystore in Unity, so we will need that **SHA-256 hash** as well. You can generate the SHA-256 hash key for your keystore using the following command:
```bash theme={null}
keytool -list -v -keystore path/to/your.keystore -alias your_alias -storepass your_keystore_password -keypass your_key_password
```
It should look something like this :
EA:A1:69:AA:2F:FC:6D:16:84:ED:06:11:E0:65:2B:E0:BB:9C:5B:E3:41:96:06:E2:3D:85:8F:2C:22:2A:E9:86
## iOS
### App Attest
For iOS builds, you must have **App Attest** enabled on your provisioning profile. The Earn SDK backend uses App Attest to validate the integrity of your game. Without it, attestation checks will fail.
To enable App Attest:
Make sure to regenerate and download your provisioning profile after enabling App Attest, then update it in Xcode.
### Bundle ID & Team ID
For iOS we need your **Bundle ID** i.e.`app id`, e.g.`com.domain.appname` and your **Team ID**.
To find your app's Team ID for Apple Developer:
Sign in to your Apple Developer account
This is NOT the appId ZBD provides you.
# Best Practices
Source: https://docs.zbdpay.com/earn/sdk/best-practices
How to balance earn rates with withdrawal limits, handle SDK failures gracefully, and keep the player experience smooth.
## Balancing earn rate with withdrawal limit
A player's rewards balance and their withdrawal limit are two separate values. It's possible to credit a player more than they can currently withdraw โ and if that gap gets too large, it creates a frustrating experience.
Keep the earn rate roughly aligned with the player's withdrawal capacity. Occasionally letting players earn ahead of their limit is fine and can drive return visits. Consistently crediting far more than they can access is not โ a player who earns 1,000 units but can only withdraw 100 will disengage.
Avoid giving out rewards too often or at passive moments. Rewards tied to skill or effort feel earned; passive rewards feel arbitrary and train players to expect them without engagement.
Make early rewards easy to earn, then gradually slow down progression as the session continues. This mirrors good game economy design and keeps withdrawal limits in sync with balance growth.
Use `ZBDController.Instance.GetBalance` to retrieve the player's current balance and daily withdrawal cap before issuing a reward. Adjust the reward amount based on how much headroom they have.
### Example
In a game with a daily task chest mechanic, rewards are balanced dynamically:
* If the player's balance is already above their daily limit, the chest reward is reduced or deferred until tomorrow.
* Early in a session, rewards are more generous. Later in the session, as the balance builds, rewards taper off.
This pattern keeps the balance/limit relationship healthy and avoids the frustration of a player who can see they've earned rewards but can't withdraw them.
## Handling SDK initialization failures
Call `ZBDController.Instance.Init` as early as possible โ ideally on game launch. Keep the instance alive with a singleton or `DontDestroyOnLoad` so it persists across scenes.
`Init` reports its result on the `completion` object. Handle each outcome distinctly โ never fail silently. Note that **a failed init means the modal is unavailable**: `ShowModal()` has nothing to display until `Init` succeeds, so the player-facing message is yours to show, based on the failure reason. For **maintenance** and **unsupported region**, hide the rewards UI instead (message only players who already have a balance).
[Error Handling](/earn/sdk/error-handling) is the canonical reference โ it has the full init handler code, every failure cause, and the complete scenario matrix.
# Building
Source: https://docs.zbdpay.com/earn/sdk/building
Comprehensive video guides for mastering Bitcoin apps and games with ZBD.
# Building for Android
### Minimum API Level
Make sure that the minimum API Level is set to **Android 11 API Level 30** in your settings. See [Compatibility](/earn/sdk/compatibility) for the current version requirements.
### Dependencies
The ZBDSDK in Android requires a few dependenices to be installed. To make this as smooth as possible we included the `External Dependency Manager` in the package. This should automatically install the dependencies and deal with any duplicate classes.
However, you may need to manually resolve the dependencies by going to:
If during the build process you get build errors, specifically mentioning duplicate classes you may need to go to
# Building for iOS
### Target Minimum iOS Version
Make sure that the `Target Minimum iOS Version` is set to at least 13 in your settings.
# Compatibility
Source: https://docs.zbdpay.com/earn/sdk/compatibility
SDK compatibility with Unity and mobile platforms.
The ZBD Earn SDK for Unity is only compatible with **iOS** and **Android** mobile environments.
# Requirements
### Unity Version
```bash theme={null}
2022 and above
```
### iOS Version
```bash theme={null}
Deployment target 13 and above
```
### Android Version
```bash theme={null}
Minimum Api Level OS 11/API 30 and Target API Level 34
Build settings: Stripping level to low or minimum
```
# Upgrading from Time Based SDK
If you are upgrading from the ZBD Time-based Earn SDK version, please follow the steps outlined in this video walkthrough.
# Create an API Key
Source: https://docs.zbdpay.com/earn/sdk/create-api-key
POST https://api.zbdpay.com/api/v1/rewards/app/{rewardsAppId}/api-key
Generate a new API key for your Rewards App to authenticate server-side reward requests.
Generate an API key for your Rewards App to authenticate server-side reward requests.\
This key authorizes your backend to send rewards securely without exposing credentials to the client.
Store this key securely, it will not be displayed again after creation. Save it immediately in your environment variables or secrets manager.
## Configuration
### Header Parameters
Client identifier (use "developer-dashboard" )
Bearer token for authentication
Format: `Bearer {JWT_TOKEN}`
### Path Parameters
Your Rewards App ID.
```json 201 - Success theme={null}
{
"success": true,
"message": "API key created successfully. Please save this key securely as it will not be shown again.",
"data": {
"id": "78b411d8-1f61-4824-97c6-e3c3a571f1c5",
"apiKey": "sdk_live_cbe66d6f0a60e2a8c0b2ac2831db8909d736409cadb78bce9ad94954259d8c88",
"createdAt": "2025-10-29T13:58:11.602Z"
}
}
```
## Response Fields
| Field | Type | Description |
| ----------- | ------- | ------------------------------------------------------------------------ |
| `success` | boolean | Whether the request was successful |
| `message` | string | Description of the result |
| `data` | object | Contains API key details |
| `id` | string | Unique identifier for this API key |
| `apiKey` | string | The actual API key to use for server authentication. **Store securely!** |
| `createdAt` | string | ISO 8601 timestamp of when the key was created |
***
### Response Status Codes
| Code | Description |
| ----- | ------------------------------------------- |
| `201` | API key created successfully |
| `400` | Bad request - invalid UUID |
| `401` | Unauthorized - authentication required |
| `403` | Forbidden - developer does not own this app |
| `409` | Conflict - race condition detected |
| `500` | Internal server error |
```json 400 - Bad Request theme={null}
{
"success": false,
"message": "Invalid UUID"
}
```
```json 401 - Unauthorized theme={null}
{
"success": false,
"message": "Authentication required"
}
```
```json 403 - Forbidden theme={null}
{
"success": false,
"message": "Developer does not own this app"
}
```
```json 409 - Conflict theme={null}
{
"success": false,
"message": "Race condition detected"
}
```
```json 500 - Server Error theme={null}
{
"success": false,
"message": "Internal server error"
}
```
This API key will only be shown once in the response. If you lose it, you'll need to create a new key and revoke the old one.
## Code Examples
```javascript Node.js theme={null}
const rewardsAppId = 'YOUR_REWARDS_APP_ID';
const jwtToken = 'YOUR_JWT_TOKEN';
const response = await fetch(`https://api.zbdpay.com/api/v1/rewards/app/${rewardsAppId}/api-key`, {
method: 'POST',
headers: {
'z-client': 'developer-dashboard',
'Authorization': `Bearer ${jwtToken}`
}
});
const data = await response.json();
if (data.success) {
console.log('API Key created:', data.data.apiKey);
// IMPORTANT: Store this key in your environment variables
// It will not be shown again!
} else {
console.error('Error:', data.message);
}
```
```bash cURL theme={null}
curl --location --request POST 'https://api.zbdpay.com/api/v1/rewards/app/{rewardsAppId}/api-key' \
--header 'z-client: developer-dashboard' \
--header 'Authorization: Bearer {JWT_TOKEN}' \
--data ''
```
```python Python theme={null}
import requests
rewards_app_id = "YOUR_REWARDS_APP_ID"
jwt_token = "YOUR_JWT_TOKEN"
url = f"https://api.zbdpay.com/api/v1/rewards/app/{rewards_app_id}/api-key"
headers = {
"z-client": "developer-dashboard",
"Authorization": f"Bearer {jwt_token}"
}
response = requests.post(url, headers=headers)
data = response.json()
if data["success"]:
print(f"API Key created: {data['data']['apiKey']}")
# IMPORTANT: Store this key in your environment variables
else:
print(f"Error: {data['message']}")
```
## Try It Out
Ready to create your first API key? Use our API playground on the right to test with your JWT token.
## What's Next?
Now that youโve created an API key, your backend can authenticate securely with the ZBD Earn API.\
In the next step, youโll learn how to **send rewards directly from your server** using the **v2 API** endpoint.
# Creative Modals
Source: https://docs.zbdpay.com/earn/sdk/creatives
Show full-screen image modals over gameplay, configured by ZBD without an app store release.
Creatives are full-screen image modals your game can show over gameplay. ZBD sets up the creative and hosts the artwork; your game shows it with one line of code. Changing artwork, copy, or behavior later is a ZBD-side change โ **no SDK update and no app store release**.
Typical uses:
* An onboarding carousel explaining how rewards work
* A reminder when a player has an unclaimed balance
* A seasonal or promotional announcement
* A/B testing two versions of the same message
Requires Unity SDK v1.1.7 or above.
## Quick start
```csharp theme={null}
ZBDController.Instance.ShowCreative("cr_onboarding_tour");
```
That's the whole integration. The SDK fetches your app's creatives during `Init()` and preloads their images, so the modal appears instantly.
To react to what the player did:
```csharp theme={null}
ZBDController.Instance.ShowCreative("cr_onboarding_tour", result =>
{
if (!result.shown)
{
Debug.Log($"Nothing shown: {result.error}");
return;
}
if (result.dismissReason == "cta")
Debug.Log("Player tapped through to rewards");
else
Debug.Log($"Closed on page {result.lastPage + 1} of {result.pageCount}");
});
```
## Setting up a creative
Creatives can't be configured self-serve yet. To set one up, reach out to your Customer Success Manager โ they'll configure it with you and host your artwork.
Tell your Customer Success Manager the following, and send them your images:
| What to provide | Notes |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| **Creative key** | The ID your game will pass to `ShowCreative()`. Choose it carefully โ it can't be changed later, because analytics reference it. |
| **Number of pages** | One page = a simple modal. Several = a swipeable carousel with page dots. |
| **Locale** | Only if you need it. Leave it out for a creative that should show to everyone (see [Localization](#localization)). |
| **Transition** | How the modal appears: `popup`, `fade`, or `slide`. Defaults to `fade`. |
| **Tap opens rewards** | Whether tapping the **last** page opens the ZBD rewards interface. Off = the creative is purely informational. |
Two further properties are set on the ZBD side and worth knowing about:
| Property | What it does |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Backdrop dismisses** | Whether tapping the dimmed area outside the image closes the modal. The close button always works regardless. |
| **Active** | Inactive creatives are not served to the SDK at all. This is how a creative is retired โ preferred over deleting, so historical analytics keep resolving. |
### Pages and navigation
Each page needs **at least one image** โ portrait, landscape, or both. The SDK picks whichever matches the device's current orientation, and swaps automatically when the player rotates. If you supply only one, it's used in both orientations.
Portrait art is **not** required. If your game is landscape-only, send landscape art alone โ there's no need to supply portrait images that would never be shown. The same applies in reverse for portrait-only games.
On a multi-page creative the player can:
* **Tap** the image to advance to the next page
* **Swipe** left or right to move between pages
* Use the **next arrow** (hidden on the last page)
* See their position via **page dots**
On the final page, tapping either opens the rewards interface or does nothing, depending on the **Tap opens rewards** setting.
### Image requirements
**Images must be HTTPS PNG or JPEG.**
* **HTTPS only** โ plain HTTP is blocked by iOS App Transport Security and Android's cleartext policy.
* **PNG or JPEG only** โ WebP, SVG, GIF, and AVIF cannot be decoded on device, even though they preview fine in a browser.
* Serve images with a `Content-Type` of `image/png` or `image/jpeg` and **no `Content-Encoding` header**. An image that displays correctly in a browser can still fail to decode on device if it's served with an encoding header.
**Maximum 5 pages** per creative. Every page is downloaded before the modal appears, so keep image dimensions reasonable โ target the device resolution you actually need, not the largest possible.
### Localization
The **Locale** field controls who sees a creative:
* **Not set** โ served to everyone. This is what you want for most creatives.
* **Set** (e.g. `en`, `en-GB`, `pt-BR`) โ served only to players whose locale matches.
If a creative key has *only* localized versions and none without a locale, players whose locale doesn't match will see **nothing**. When you localize, always keep one version with no locale as the fallback.
## API reference
All methods are on `ZBDController.Instance`.
### ShowCreative
```csharp theme={null}
ShowCreative(string creativeId, Action onClosed = null)
```
Shows the creative with the given key. The callback fires when the modal closes โ or immediately, with `shown == false`, if it couldn't be shown.
Your game decides *when* a creative appears. The SDK does no scheduling or frequency capping, so you're free to build your own rules (once per session, after level 3, only when a balance is unclaimed, and so on). See [Best practices](#best-practices).
### CloseCreative
```csharp theme={null}
CloseCreative()
```
Closes the visible creative, or cancels one that is still loading. The `ShowCreative` callback fires with `dismissReason == "programmatic"`.
## The result object
`ZBDCreativeResult`, passed to your `ShowCreative` callback:
| Field | Type | Description |
| --------------- | -------- | ------------------------------------------------------ |
| `shown` | `bool` | Whether a creative actually displayed. |
| `creativeId` | `string` | The key you requested. |
| `dismissReason` | `string` | How it closed โ see below. |
| `pageCount` | `int` | Total pages in the creative. |
| `lastPage` | `int` | Furthest page the player reached (0-based). |
| `error` | `string` | Why nothing was shown. Only set when `shown == false`. |
### Dismiss reasons
| Value | Meaning |
| ------------------ | --------------------------------------------------------------------------------------------------- |
| `"cta"` | **The player tapped the creative.** If *Tap opens rewards* is on, the rewards interface is opening. |
| `"close"` | The player used the close button. |
| `"backdrop"` | The player tapped outside the image. |
| `"programmatic"` | Your game called `CloseCreative()`. |
| `"safety-timeout"` | An internal safeguard closed the modal. Rare. |
| `"none"` | Placeholder value; not seen on a normal close. |
Together, `lastPage` and `pageCount` give you a drop-off funnel: you can tell whether a player read all three pages and tapped through, or bailed on page one.
### When `shown` is false
| `error` | Cause |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Creative not found: ''` | No **active** creative with that key for this app. Check spelling with your Customer Success Manager, and that your build is pointed at the right environment. |
| `A creative is already showing or pending` | One creative at a time. Wait for the previous callback. |
| `Creative images could not be loaded` | An image failed to download or decode โ usually the format or `Content-Type` rules above. |
| `Creative has no usable image` | The creative has no valid page images configured. |
| `A creative ID is required` | Empty key passed. |
| `Cancelled before display` | `CloseCreative()` was called while it was still loading. |
Every one of these is a clean no-op from the player's perspective: nothing renders and your callback runs, so it's safe to call `ShowCreative` optimistically.
## Best practices
### Reveal rewards after the first reward lands
A one- to three-page reveal explaining how rewards work is the highest-value creative you can ship. Show it once โ **after the player's first reward has landed**, not at cold launch. A pitch before the player has earned anything is a cold offer and triggers suspicion; a reveal of a balance they already have converts. Fire the first reward early in normal play, let the toast land, then show the creative.
Three pages is usually enough to answer the only three questions a new player has:
1. **The reveal** โ *this game pays real rewards, and you've already started earning.* Lead with the balance the player already has โ "you've already earned 240 coins" converts better than an announcement.
2. **The reason** โ *here's why, so it doesn't feel like a scam.* This page carries the most important sentence in your rewards copy โ *"game studios share their ad revenue with players"* โ which the modal's built-in first-time screen doesn't show. The creative is how you ship it. See the [Player Communication Guide](/earn/sdk/player-comms).
3. **The portal** โ *here's where your progress lives and how you claim.*
#### Example flows by cashout option
What page 3 says depends on how your players actually get paid, so the flow differs. Each example below is a **six-step journey spanning several creatives** โ the three reveal pages above, then separate creatives for the first-earn, stacking and payout-confirmation moments (a single creative is capped at five pages, and you wouldn't want all six at once anyway):
Gift cards have a minimum to reach, so the flow is about **saving up toward a named card** โ the goal is what motivates.
| Page | What it says |
| -------------- | ---------------------------------------------------------------- |
| 1 ยท Reveal | This game pays real rewards โ and you've already started earning |
| 2 ยท Reason why | No catch โ we share a slice of this game's revenue with players |
| 3 ยท The portal | Where your progress lives, and how you claim |
| 4 ยท First earn | You've earned your first \[Currency] |
| 5 ยท Stacking | You're X% of the way to a \$5 gift card |
| 6 ยท Claimed | Your gift card is ready |
With Cash App there's **no minimum**, so lead on immediacy instead of a savings goal: a few cents can leave straight away. Page 3 explains the mechanic โ paid in bitcoin, landing in their Cash App.
| Page | What it says |
| ------------------- | --------------------------------------------------------------------------------------------------------- |
| 1 ยท Reveal | This game pays you real money โ straight to Cash App. *No \$5 minimum. Cash out a few cents if you want.* |
| 2 ยท Reason why | No catch โ we share a slice of this game's revenue with players |
| 3 ยท How you're paid | You're paid in bitcoin, straight to your Cash App. *Free to receive. Keep it, or turn it into dollars.* |
| 4 ยท First cents | You've earned 6c. *There's no minimum โ cash out whenever you like.* |
| 5 ยท Stacking | You're at \$1. *Cash out now, or keep stacking.* |
| 6 ยท Sent | Sent โ \$1 landed in your Cash App. *That took about four seconds.* |
The "no minimum" line is doing the work here: it removes the objection a player brings from other rewards apps, where earnings are stuck behind a threshold they'll never reach.
**Claim "free" only for the receive.** Cash App charges nothing to receive bitcoin, but *selling* it for dollars carries their standard fee (around 2% under \$500, plus a spread). Page 3 above says "free to receive" and then "turn it into dollars" without pricing it โ deliberately. Don't write copy implying the conversion is free.
Removing the threshold also removes your motivational engine โ there's no goal to climb toward when the whole pitch is that you don't have to climb. That's the trade: instant liquidity rewards checking in, where a goal pushes players on. If retention is the aim, consider the mixed set instead.
Offering several payout options? Make **choice** the pitch โ small amounts out instantly, or save toward something bigger. Page 3 shows the options rather than one mechanic.
| Page | What it says |
| ---------------- | -------------------------------------------------------------------------------------------------------- |
| 1 ยท Reveal | This game pays you real money โ however you want it. *Amazon cards, Cash App, your ZBD wallet and more.* |
| 2 ยท Reason why | No catch โ we share a slice of this game's revenue with players |
| 3 ยท Your options | Cash out your way. *A few cents to Cash App or ZBD, or save up for a gift card.* |
| 4 ยท First cents | You've earned 6c. *Take it now, or keep going for something bigger.* |
| 5 ยท Stacking | You're at \$1. *Take it now, or reach \$5 for an Amazon card.* |
| 6 ยท Sent | Sent โ your \$1 is on its way. *Switch payout any time.* |
Naming the brands ("Amazon cards, Cash App") converts better than "multiple payout options" โ concrete beats abstract.
**"No minimum" is not a blanket claim once gift cards are in the mix.** It's true of Cash App and the ZBD wallet, and false of gift cards, which only come in fixed denominations. Split it the way page 3 does โ *"a few cents to Cash App or ZBD, or save up for a gift card"* โ rather than promising no minimum across the board.
This is the only set that gets both motivational engines at once: instant rails let a player take six cents now, and gift-card denominations still leave something ahead worth reaching. Page 5 carries both โ a live balance above a meter that ends at a named card.
**The Amazon and Cash App marks in these examples are stylised stand-ins.** Swap them for licensed assets before shipping anything player-facing โ Amazon in particular has specific rules about the smile and its clear space, which usually arrive with your gift-card agreement.
Don't ship all six steps as one creative. The first three belong together as the reveal; steps 4โ6 are separate creatives triggered at those moments โ a player who has just earned 6c doesn't need to be told what happens at \$5 yet.
Trigger it from your first-reward moment, with `PlayerPrefs` so it only ever shows once:
```csharp theme={null}
private const string RevealShownKey = "zbd_reveal_shown";
// Call this from the gameplay moment where the player's first reward lands
void OnFirstRewardLanded()
{
// Already seen it โ don't show it again
if (PlayerPrefs.GetInt(RevealShownKey, 0) == 1) return;
ZBDController.Instance.ShowCreative("cr_onboarding_tour", result =>
{
// Only record it if it actually displayed, so a failed
// image load doesn't suppress the reveal forever
if (!result.shown) return;
PlayerPrefs.SetInt(RevealShownKey, 1);
PlayerPrefs.Save();
});
}
```
Set your "already shown" flag inside the callback and only when `result.shown` is `true`. If you set it before calling `ShowCreative`, a network or image failure would permanently prevent the player from ever seeing the reveal.
### Remind players as they make progress
A creative that reflects the player's actual progress converts far better than a generic announcement. Once a player is earning, show reminder creatives at milestones on the way to their first reward.
Three milestones map naturally onto the player's journey:
1. **Started earning** โ their first reward has landed. Reinforce that it's real.
2. **Halfway** โ they're about halfway to a specific gift card. Name the card to make it concrete.
3. **Ready to claim** โ they can redeem now. This is the one that should open the rewards interface.
[`GetGiftCards`](/earn/sdk/gift-cards) tells you exactly where the player is: `unlockedPercent` is a 0โ100 progress value toward each card, so you can pick the right creative without doing any conversion yourself.
Ask your Customer Success Manager to enable **Tap opens rewards** on the "ready to claim" creative, so tapping it opens the rewards interface directly.
```csharp theme={null}
private const string MilestoneKey = "zbd_reward_milestone";
void MaybeShowProgressReminder()
{
ZBDController.Instance.GetGiftCards(response =>
{
if (!response.success) return;
if (response.giftCards == null || response.giftCards.Length == 0) return;
// The cheapest card is the one the player will reach first
ZBDGiftCard target = response.giftCards[0];
foreach (ZBDGiftCard card in response.giftCards)
{
if (card.priceInSats < target.priceInSats) target = card;
}
// -1 means progress couldn't be calculated โ skip rather than guess
if (target.unlockedPercent < 0) return;
string creativeId = null;
int milestone = 0;
if (target.unlockedPercent >= 100)
{
creativeId = "cr_ready_to_claim";
milestone = 3;
}
else if (target.unlockedPercent >= 50)
{
creativeId = "cr_halfway";
milestone = 2;
}
else if (response.availableBalanceInSats > 0)
{
creativeId = "cr_started_earning";
milestone = 1;
}
if (creativeId == null) return;
// Show each milestone at most once, and never step backwards
if (PlayerPrefs.GetInt(MilestoneKey, 0) >= milestone) return;
ZBDController.Instance.ShowCreative(creativeId, result =>
{
if (!result.shown) return;
PlayerPrefs.SetInt(MilestoneKey, milestone);
PlayerPrefs.Save();
});
});
}
```
Storing a single monotonic milestone number rather than a flag per creative means a player who jumps straight past 50% only sees the milestone they actually reached, and never sees an earlier one afterwards.
If you'd rather remind periodically than only once per milestone, store a timestamp alongside the milestone and re-show after a cooldown you're comfortable with.
### Bring players back with local notifications
Creatives only reach a player who is already in your game. To win back a player who left with an uncashed balance, pair them with [local notifications](/earn/sdk/local-notifications) โ OS-scheduled reminders that need no push infrastructure, and can be driven from the same `GetGiftCards` progress value.
## Styling
The SDK draws its own close button, next arrow, and page dots. Any of them can be replaced with your own art or hidden entirely, so the modal fits your game.
**You don't need to call `SetCreativeStyle` at all** โ the SDK's built-in defaults apply automatically. Only call it if you want to change something:
```csharp theme={null}
SetCreativeStyle(ZBDCreativeStyle style)
```
Call it once after `Init()`, before your first `ShowCreative`.
```csharp theme={null}
ZBDController.Instance.SetCreativeStyle(new ZBDCreativeStyle
{
closeButtonSprite = myCloseSprite,
nextButtonSprite = myArrowSprite,
pageDotActiveSprite = myDotOn,
pageDotInactiveSprite = myDotOff,
backdropColor = new Color(0.05f, 0f, 0.15f, 0.8f),
showNextButton = false, // the artwork already draws its own NEXT button
});
```
Every field is optional โ omit one and the SDK's built-in control is used. If you've set a custom style and want to go back, pass `null` to reset to defaults.
| Group | Fields |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| **Sprites** | `closeButtonSprite`, `nextButtonSprite`, `pageDotActiveSprite`, `pageDotInactiveSprite` |
| **Visibility** | `showCloseButton`, `showNextButton`, `showPageDots` |
| **Colors** | `backdropColor`, `closeButtonColor`, `nextButtonColor`, `pageDotActiveColor`, `pageDotInactiveColor`, `iconColor` |
| **Size & position** | `closeButtonSize`, `closeButtonOffset`, `nextButtonSize`, `nextButtonOffset`, `pageDotSize`, `pageDotSpacing`, `pageDotBottomOffset` |
Sizes and offsets are in the overlay's reference space (1080ร1920, scaled to fit), not raw pixels, so they stay consistent across devices.
If you set `showCloseButton = false` on a creative that also has *Backdrop dismisses* turned off, the SDK re-enables the close button anyway โ otherwise the player would have no way to close the modal.
## Refreshing the creative list
```csharp theme={null}
FetchCreatives(Action callback = null)
```
Optional. Refreshes the cached creative list. The SDK already fetches during `Init()`, and `ShowCreative` fetches lazily if needed, so most games never call this.
## Behavior notes
**Preloading.** Creatives and their images are fetched during `Init()` so the first `ShowCreative` is instant. If the fetch fails (no network, for example), `Init()` still completes normally โ creatives simply retry on the next attempt.
**One at a time.** Calling `ShowCreative` while another creative is showing or loading returns `shown = false` rather than stacking modals.
**Rotation.** The modal handles rotation while visible, swapping to the other orientation's artwork where one is provided.
**Input.** While a creative is visible, its backdrop absorbs taps so the game underneath doesn't receive them.
**A/B testing.** Ask for two creatives with different keys, assign players to a variant in your own code, and call `ShowCreative` with the matching key. Your game keeps full control of the split.
## Troubleshooting
**"Creative not found" but it was just set up.** Confirm the creative key spelling with your Customer Success Manager, check that it's been made active, and make sure your build is pointed at the same environment it was configured in. The **App ID** must match your game's token exactly โ it's case-sensitive.
**The modal doesn't appear and I get "Creative images could not be loaded".** Almost always the image: confirm it's HTTPS, PNG or JPEG, and served with an image `Content-Type` and no `Content-Encoding` header. A browser preview succeeding does not guarantee the device can decode it.
**Nothing shows for some players only.** Check the locale โ a creative with a locale set is only served to matching players. Ask for a version with no locale as a fallback.
**A newly updated creative isn't picked up.** The creative list is cached for the session. Restart the app, or call `FetchCreatives()` to refresh.
# Modal Customization
Source: https://docs.zbdpay.com/earn/sdk/customization
Customize the colors, graphics, and copy of the in-game rewards modal to match your game.
The ZBD rewards modal is a WebView delivered inside your game. You control the visual branding โ colors, background, graphics, and logo โ while ZBD controls the layout and functionality.
Customization assets are submitted to your ZBD Customer Success Manager before go-live. The modal is not self-serve configurable from the dashboard.
You can customize colors and graphics. You cannot customize layouts, screen structure, or the cashout flows.
## Colors
Eight color parameters control the look of the modal. All accept hex values.
| Parameter | What it affects |
| ------------------------------ | ------------------------------------------------------- |
| **Background** | Main background of the modal. Accepts a color or image. |
| **Primary Font Color** | Main body text color. |
| **Primary CTA Color** | Background color of primary buttons (e.g. "Cash Out"). |
| **Primary CTA Font Color** | Text color on primary buttons. |
| **Secondary Background Color** | Background color of cards and panels within the modal. |
| **Secondary Font Color** | Secondary and label text color. |
| **Secondary CTA Color** | Background color of secondary buttons. |
| **Secondary CTA Font Color** | Text color on secondary buttons. |
## Graphics
Four image assets can be provided. Supply these at the specified dimensions for best results.
| Asset | Dimensions | Notes |
| -------------------------------- | ------------- | ------------------------------------------------------------------------- |
| **Background Image** | 1200 ร 1200px | Used as the modal background. Overrides the background color if provided. |
| **Tutorial Image** | 500 ร 500px | Shown in the first-time user experience (FTUE) screen. |
| **Current Balance Coin Graphic** | 280 ร 400px | Displayed on the balance screen alongside the player's current balance. |
| **Coin Graphic** | 100 ร 100px | Used inline throughout the modal to represent your reward currency. |
## Copy
| Element | Limit | Notes |
| ----------------- | ------------- | ---------------------------------------------------------------------------- |
| **Tutorial copy** | 50 characters | Text shown on the FTUE screen introducing the rewards feature to the player. |
50 characters is too short for the most important line in your rewards copy โ the reason rewards are real (*"Game studios share their ad revenue with playersโฆ"*). Ship that line with a [first-open creative](/earn/sdk/creatives) instead, and keep the FTUE copy to a short hook. The modal's built-in button copy (e.g. "Cash Out") is part of the fixed layout and can't be customized.
## Other
| Element | Notes |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Logo** | Square format. Displayed within the modal header. |
| **Deep link URL** | Your game's URL scheme (e.g. `mygame://`). Used to redirect players back to the game after account linking. See [URL Scheme](/earn/sdk/url-scheme). |
## How to submit
Send your assets to your ZBD Customer Success Manager using the template they provide. Allow time for the modal to be configured and tested in sandbox before your go-live date.
If you haven't been assigned a Customer Success Manager yet, [contact us](https://zbdpay.com/contact).
# Delete Blocklist Entry
Source: https://docs.zbdpay.com/earn/sdk/delete-blocklist-entry
DELETE https://api.zbdpay.com/api/v1/rewards/app/{rewardsAppId}/api-version-blocklist/{blocklistId}
Remove a blocklist entry to restore client-side reward sending for your Rewards App.
This operation re-enables client SDK access for the specified API route that was previously blocked.
Deleting a blocklist entry immediately re-enables client-side reward sending for that API route. Make sure you understand the security implications.
Reversible Action. You can always [add the blocklist entry back](/earn/sdk/restrict-client-rewards) if you need to restrict client-side rewards again.
## Configuration
### Header Parameters
Client identifier (use "developer-dashboard" )
Bearer token for authentication
Format: `Bearer {JWT_TOKEN}`
### Path Parameters
Your Rewards App ID
The ID of the blocklist entry to delete
Get this ID from the "Get Blocklist Entries" endpoint
```json 200 - Success theme={null}
{
"success": true,
"message": "API version blocklist deleted successfully."
}
```
```json 400 - Bad Request theme={null}
{
"success": false,
"message": "Invalid UUID"
}
```
```json 401 - Unauthorized theme={null}
{
"success": false,
"message": "Authentication required"
}
```
```json 403 - Forbidden theme={null}
{
"success": false,
"message": "Developer does not own this app"
}
```
```json 404 - Not Found theme={null}
{
"success": false,
"message": "Blocklist entry not found"
}
```
```json 500 - Server Error theme={null}
{
"success": false,
"message": "Internal server error"
}
```
## Response Fields
| Field | Type | Description |
| --------- | ------- | ----------------------------------- |
| `success` | boolean | Whether the deletion was successful |
| `message` | string | Confirmation message |
***
### Response Status Codes
| Code | Description |
| ----- | ------------------------------------------- |
| `200` | Blocklist entry deleted successfully |
| `400` | Bad request - invalid UUID |
| `401` | Unauthorized - authentication required |
| `403` | Forbidden - developer does not own this app |
| `404` | Blocklist entry not found |
| `500` | Internal server error |
## Code Examples
```javascript Node.js theme={null}
const rewardsAppId = 'YOUR_REWARDS_APP_ID';
const blocklistId = 'YOUR_BLOCKLIST_ID';
const jwtToken = 'YOUR_JWT_TOKEN';
async function deleteBlocklistEntry(rewardsAppId, blocklistId) {
const response = await fetch(
`https://api.zbdpay.com/api/v1/rewards/app/${rewardsAppId}/api-version-blocklist/${blocklistId}`,
{
method: 'DELETE',
headers: {
'z-client': 'developer-dashboard',
'Authorization': `Bearer ${jwtToken}`
}
}
);
const data = await response.json();
if (data.success) {
console.log('โ
Blocklist entry deleted successfully!');
console.log('Client-side rewards are now enabled.');
return data;
} else {
throw new Error(`Failed: ${data.message}`);
}
}
// Delete entry
await deleteBlocklistEntry(
'b28e0306-2c06-4092-8d56-a1623d6b97fb',
'bb6178d1-4fb5-4b35-93d6-31fe052b73dd'
);
```
```bash cURL theme={null}
curl --location --request DELETE 'https://api.zbdpay.com/api/v1/rewards/app/{rewardsAppId}/api-version-blocklist/{blocklistId}' \
--header 'z-client: developer-dashboard' \
--header 'Authorization: Bearer {JWT_TOKEN}'
```
```python Python theme={null}
import requests
import os
jwt_token = os.getenv('JWT_TOKEN')
def delete_blocklist_entry(rewards_app_id, blocklist_id):
"""Delete a blocklist entry to re-enable client rewards"""
url = f"https://api.zbdpay.com/api/v1/rewards/app/{rewards_app_id}/api-version-blocklist/{blocklist_id}"
headers = {
"z-client": "developer-dashboard",
"Authorization": f"Bearer {jwt_token}"
}
response = requests.delete(url, headers=headers)
data = response.json()
if data["success"]:
print("โ
Blocklist entry deleted successfully!")
print("Client-side rewards are now enabled.")
return data
else:
raise Exception(f"Failed: {data['message']}")
# Delete entry
delete_blocklist_entry(
"b28e0306-2c06-4092-8d56-a1623d6b97fb",
"bb6178d1-4fb5-4b35-93d6-31fe052b73dd"
)
```
## What Happens After Deletion?
Client cannot send rewards
Client can send rewards again
## Try It Out
Ready to delete a blocklist entry? Use our API playground on the right to test with your JWT token.
## What's Next?
Now that youโve managed your blocklist entries, you can move on to managing your API keys:
* **[List API Keys](/earn/sdk/list-api-keys)**. View and manage the API keys used for backend authentication.
* **[Restrict Client-Side Rewards](/earn/sdk/restrict-client-rewards)**. Create a blocklist entry to disable client-side rewards again.
# Download SDK
Source: https://docs.zbdpay.com/earn/sdk/download
Setup your Unity project to use the ZBD Earn SDK
Contact your ZBD Customer Success Manager to get access to the ZBD Earn SDK for your Unity project.
# Error Handling
Source: https://docs.zbdpay.com/earn/sdk/error-handling
How to handle every failure state the ZBD Earn SDK can return, and what your game should do in each case.
The SDK surfaces three types of failure: initialization failures, reward delivery failures, and maintenance mode. Each requires a different response. The guiding principle is the same in all cases: **never let a ZBD failure break your game.**
## Initialization failures
`Init` can fail for several reasons, and **when it fails the modal cannot be shown** โ the rewards web app never loaded, so `ShowModal()` has nothing to display. That means the player-facing messaging is yours: read the failure reason off the `completion` object and surface your own message (or hide the rewards UI). Never fail silently, and never leave the player staring at a rewards button that does nothing.
```csharp theme={null}
ZBDController.Instance.Init(completion =>
{
if (completion.success)
{
// SDK ready, proceed normally
}
else if (completion.maintenance)
{
// Not an error โ ZBD is in maintenance. See Maintenance Mode section below.
ShowMaintenanceMessage();
}
else if (completion.type == "region")
{
// Unsupported region โ no retry path.
// Hide all Earn UI/functionality, or show a "not available in your region" message.
HideEarnUI();
}
else
{
// Init failed โ the modal is unavailable, so show your own message
// based on completion.error and let gameplay continue as normal.
ShowRewardsUnavailableMessage(completion.error);
Debug.LogWarning("ZBD init failed: " + completion.error);
}
});
```
### Common init failure causes
| Cause | What happens | What to do |
| ----------------------------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| VPN / ad blocker / private DNS blocking device verification | `completion.success = false` | Show your own actionable message โ ask the player to disable the blocker or VPN and try again. Note: VPN *policy* detection does not fail init โ it places the player in limited earnings mode instead. See [Security & Fraud Prevention](/earn/sdk/security). |
| No network connection | `completion.success = false` | Show your own message with a retry affordance, and retry `Init` when connectivity returns. Do not prevent gameplay. |
| Unsupported region | `completion.success = false`, `completion.type = "region"` | No retry path. Hide all Earn-related UI and functionality, or show a message that Earn isn't available in their region. See [Global Support](/earn/coverage). |
| Attestation failure | `completion.success = false` | Show your own message. Indicates a potentially modified device or app โ keep the wording generic rather than accusatory. |
| ZBD maintenance | `completion.maintenance = true` | See [Maintenance Mode](#maintenance-mode) below. |
Do not tie SDK initialization to any core gameplay logic. If `Init` fails, the game must continue working normally. Players who cannot earn rewards should not be blocked from playing.
`ShowModal()` is only useful once `Init` has succeeded. After a failed init the rewards web app isn't loaded, so calling it does nothing โ which is why every init-failure row above puts the message in your hands.
## Maintenance mode
The ZBD platform occasionally enters maintenance mode. This is surfaced as a `maintenance` flag on SDK responses. It is not an error.
```csharp theme={null}
public class ZBDInitResponse
{
public bool success;
public bool maintenance;
public string userId;
public string error;
public string type;
}
```
Maintenance mode can begin at any time, including mid-session. Handle it on every SDK response, not just during initialization.
| Scenario | Correct behavior |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Init` returns `maintenance = true` | If the player has not started earning yet: hide all rewards-related UI and do not mention ZBD. If the player has already been earning: surface a message that rewards are temporarily unavailable. |
| `SendReward` returns `maintenance = true` | Silently skip the reward. Do not show an error to the player. |
| `GetBalance` returns `maintenance = true` | Do not update the displayed balance. Retry when the next natural trigger occurs. |
Maintenance periods are temporary. Do not log them as errors or alert your on-call. They are expected operational events.
## Reward delivery failures
Reward delivery can fail independently of initialization. The most common causes are network interruptions and maintenance mode.
```csharp theme={null}
ZBDController.Instance.SendReward(amount, completion =>
{
if (completion.success)
{
// Reward delivered, update UI
}
else if (completion.maintenance)
{
// Silently skip, do not surface to player
}
else
{
// Delivery failed, silently skip and optionally log
Debug.LogWarning("Reward delivery failed: " + completion.error);
}
});
```
Do not show reward failure messages to players. A failed reward is invisible to the player. The gameplay moment should feel the same whether the reward was delivered or not.
## Full error scenario matrix
| Scenario | SDK response | Correct behavior |
| ----------------------------------- | ------------------------------------ | ---------------------------------------------------------------------------- |
| Init โ VPN/ad-blocker interference | `success = false` | Your own message asking them to disable it and retry. Game continues. |
| Init โ no network | `success = false` | Your own message; retry when connectivity returns. Game continues. |
| Init โ unsupported region | `success = false`, `type = "region"` | Hide rewards UI (or show "not available in your region"). No retry path. |
| Init โ maintenance | `maintenance = true` | Hide rewards UI for new players. Message existing earners. |
| Init โ attestation failure | `success = false` | Your own generic message. Game continues. |
| App backgrounded mid-reward | n/a | Handle gracefully on foreground return. Do not retry automatically. |
| No internet during reward trigger | `success = false` | Skip or queue the reward. Do not crash. |
| `SendReward` โ maintenance | `maintenance = true` | Silently skip. No player-facing message. |
| `SendReward` โ network error | `success = false` | Silently skip. Log internally. |
| `GetBalance` โ maintenance | `maintenance = true` | Do not update balance display. |
| iOS vs. Android behavior difference | Varies | Always test both platforms separately. SDK behavior can differ between them. |
## Platform differences
Test iOS and Android separately. The SDK's attestation behavior, WebView rendering, and modal presentation can differ between platforms in ways that are not always obvious during development.
Pay particular attention to:
* **Android back button**: Handle the back button to close the modal if it's open. See [Integration](/earn/sdk/integration#android-back-button).
* **iOS foreground transitions**: The modal may need explicit handling when the app returns from background.
* **VPN detection**: VPN behavior differs between iOS and Android network stacks. A VPN that passes on one platform may be flagged on the other.
# Get Blocklist Entries
Source: https://docs.zbdpay.com/earn/sdk/get-blocklist-entries
GET https://api.zbdpay.com/api/v1/rewards/app/{rewardsAppId}/api-version-blocklist
Retrieve all API version blocklist entries for your Rewards App
Retrieve all blocklist entries for your Rewards App to see which API routes are currently restricted for client-side calls.
## Configuration
### Header Parameters
Client identifier (use "developer-dashboard" )
Bearer token for authentication
Format: `Bearer {JWT_TOKEN}`
Filter by API version (optional)
Example: `1`
Filter by API route (optional)
Example: `/earn/limited-achievement/reward`
### Path Parameters
Your Rewards App ID
```json 200 - Success theme={null}
{
"success": true,
"message": "Success.",
"data": [
{
"id": "bb6178d1-4fb5-4b35-93d6-31fe052b73dd",
"rewardsAppId": "b28e0306-2c06-4092-8d56-a1623d6b97fb",
"version": 1,
"apiRoute": "/earn/limited-achievement/reward",
"createdAt": "2025-10-30T22:56:28.666Z"
}
]
}
```
### Response Status Codes
| Code | Description |
| ----- | ------------------------------------------- |
| `200` | Blocklist entries retrieved successfully |
| `401` | Unauthorized - authentication required |
| `403` | Forbidden - developer does not own this app |
| `500` | Internal server error |
```json 401 - Unauthorized theme={null}
{
"success": false,
"message": "Authentication required"
}
```
```json 403 - Forbidden theme={null}
{
"success": false,
"message": "Developer does not own this app"
}
```
```json 500 - Server Error theme={null}
{
"success": false,
"message": "Internal server error"
}
```
## Response Fields
| Field | Type | Description |
| -------------- | ------- | -------------------------------------------- |
| `success` | boolean | Whether the request was successful |
| `message` | string | Response message |
| `data` | array | Array of blocklist entries |
| `id` | string | Unique identifier for the blocklist entry |
| `rewardsAppId` | string | Your Rewards App ID |
| `version` | number | Blocked API version |
| `apiRoute` | string | Blocked API route |
| `createdAt` | string | ISO 8601 timestamp of when entry was created |
***
## Code Examples
```javascript Node.js theme={null}
const rewardsAppId = 'YOUR_REWARDS_APP_ID';
const jwtToken = 'YOUR_JWT_TOKEN';
async function getBlocklistEntries(rewardsAppId) {
const response = await fetch(
`https://api.zbdpay.com/api/v1/rewards/app/${rewardsAppId}/api-version-blocklist`,
{
method: 'GET',
headers: {
'z-client': 'developer-dashboard',
'Authorization': `Bearer ${jwtToken}`,
'version': '1',
'apiRoute': '/earn/limited-achievement/reward'
}
}
);
const data = await response.json();
if (data.success) {
console.log(`Found ${data.data.length} blocklist entries`);
data.data.forEach(entry => {
console.log(`- Route: ${entry.apiRoute}, Version: ${entry.version}`);
});
return data.data;
} else {
throw new Error(`Failed: ${data.message}`);
}
}
// Get all entries
const entries = await getBlocklistEntries('b28e0306-2c06-4092-8d56-a1623d6b97fb');
```
```bash cURL theme={null}
curl --location 'https://api.zbdpay.com/api/v1/rewards/app/b28e0306-2c06-4092-8d56-a1623d6b97fb/api-version-blocklist' \
--header 'z-client: developer-dashboard' \
--header 'version: 1' \
--header 'apiRoute: /earn/limited-achievement/reward' \
--header 'Authorization: Bearer {JWT_TOKEN}'
```
```python Python theme={null}
import requests
import os
jwt_token = os.getenv('JWT_TOKEN')
rewards_app_id = "YOUR_REWARDS_APP_ID"
def get_blocklist_entries(rewards_app_id):
"""Get all blocklist entries for a Rewards App"""
url = f"https://api.zbdpay.com/api/v1/rewards/app/{rewards_app_id}/api-version-blocklist"
headers = {
"z-client": "developer-dashboard",
"Authorization": f"Bearer {jwt_token}",
"version": "1",
"apiRoute": "/earn/limited-achievement/reward"
}
response = requests.get(url, headers=headers)
data = response.json()
if data["success"]:
print(f"Found {len(data['data'])} blocklist entries")
for entry in data["data"]:
print(f"- Route: {entry['apiRoute']}, Version: {entry['version']}")
return data["data"]
else:
raise Exception(f"Failed: {data['message']}")
# Get entries
entries = get_blocklist_entries("b28e0306-2c06-4092-8d56-a1623d6b97fb")
```
Empty Response. If `data` is an empty array `[]`, it means no blocklist entries exist and client-side reward sending is currently enabled.
## Try It Out
Ready to view your blocklist entries? Use our API playground on the right to test with your JWT token.
## What's Next?
After viewing your blocklist entries:
* **[Delete Blocklist Entry](/earn/sdk/delete-blocklist-entry)**. Remove a restriction to re-enable client-side rewards.
* **[Restrict Client-Side Rewards](/earn/sdk/restrict-client-rewards)**. Create a new blocklist entry.
# Gift Cards
Source: https://docs.zbdpay.com/earn/sdk/gift-cards
Retrieve the gift cards available to a player and how close they are to unlocking each one.
Use `GetGiftCards` to retrieve the top gift cards available to a player in the ZBD rewards web app, along with each card's price in satoshis and the player's progress toward unlocking it. This lets you show players how close they are to earning a specific gift card.
```csharp theme={null}
ZBDController.Instance.GetGiftCards(response =>
{
if (!response.success)
{
Debug.LogError("GetGiftCards failed: " + response.message);
return;
}
if (response.giftCards == null || response.giftCards.Length == 0)
{
// No gift cards available for this player's region
return;
}
Debug.Log($"Available balance: {response.availableBalanceInSats} sats");
foreach (ZBDGiftCard card in response.giftCards)
{
// e.g. "Amazon.com United States: 1.7% toward a 5 USD card"
Debug.Log($"{card.name}: {card.unlockedPercent}% toward " +
$"{card.denomination} {card.denominationCurrency} ({card.priceInSats} sats)");
}
});
```
## Prerequisites
* **Requires Unity SDK v1.1.7 or above.** `GetGiftCards` isn't available in earlier SDK versions.
* **The SDK must be initialized.** `GetGiftCards` relies on the gift-card web view, which loads during `Init` โ so it works even while the modal is closed, but `Init` must have completed. Check `ZBDController.Instance.initialized` before calling.
* **Gift cards must be an available cashout option.** If gift-card cashout isn't enabled for your app or isn't available in the player's region, `giftCards` comes back empty (with `success = true`).
* **Requests made too early are queued.** If the web app is still loading when you call, the request is held and sent automatically once it's ready.
## Response
`GetGiftCards` returns a `ZBDGiftCardsResponse`:
| Field | Type | Description |
| ------------------------ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `success` | `bool` | Whether the request succeeded. |
| `message` | `string` | Status or error message. |
| `availableBalanceInSats` | `int` | The player's available (withdrawable) balance, in satoshis. |
| `giftCards` | `ZBDGiftCard[]` | The top gift cards available to the player. An empty array with `success = true` means no cards are available (gift-card cashout isn't enabled, or none are offered in the player's region). |
Each `ZBDGiftCard`:
| Field | Type | Description |
| ---------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | `string` | Gift card identifier, e.g. `amazon_com-usa`. |
| `name` | `string` | Display name, e.g. `Amazon.com United States`. |
| `logoUrl` | `string` | URL of the card's logo image. |
| `priceInSats` | `int` | Price of the cheapest denomination, in satoshis. |
| `denomination` | `float` | Fiat value of that denomination, e.g. `5.0`. |
| `denominationCurrency` | `string` | Currency of the denomination, e.g. `USD`. |
| `unlockedPercent` | `float` | The player's progress toward `priceInSats`, from `0` to `100`. Returns `-1` when it can't be computed (for example, a missing exchange rate). |
### Example response
```json theme={null}
{
"success": true,
"message": "Success",
"availableBalanceInSats": 125,
"giftCards": [
{
"id": "amazon_com-usa",
"name": "Amazon.com United States",
"logoUrl": "https://cdn.bitrefill.com/primg/w320h180/amazon_com-usa.png",
"priceInSats": 7150,
"denomination": 5.0,
"denominationCurrency": "USD",
"unlockedPercent": 1.7482517
},
{
"id": "walmart-usa",
"name": "Walmart United States",
"logoUrl": "https://cdn.bitrefill.com/primg/w320h180/walmart-usa.png",
"priceInSats": 14300,
"denomination": 10.0,
"denominationCurrency": "USD",
"unlockedPercent": 0.8741259
},
{
"id": "target-usa",
"name": "Target United States",
"logoUrl": "https://cdn.bitrefill.com/primg/w320h180/target-usa.png",
"priceInSats": 35750,
"denomination": 25.0,
"denominationCurrency": "USD",
"unlockedPercent": 0.34965035
}
]
}
```
In this example the player has 125 sats and the cheapest Amazon US card costs 7,150 sats, so `unlockedPercent` is roughly 1.75% (125 รท 7,150).
## Showing progress to players
Because `unlockedPercent` is a straight 0โ100 percentage, you can bind it directly to a progress bar to show how close a player is to unlocking each card. Combine it with `logoUrl`, `name`, and `denomination` / `denominationCurrency` to build a "gift card progress" panel that motivates players to keep earning.
**Prices are always returned in satoshis, even when your game uses points and a fiat value like USD.** `priceInSats` and `availableBalanceInSats` are denominated in sats because ZBD's gift-card provider uses Bitcoin as its base currency โ this is independent of the currency your game rewards or displays in.
You do **not** need to show sats to your players, and we recommend you don't. Show **`unlockedPercent`** โ a percentage toward the card โ rather than a sats amount. A progress percentage reads naturally regardless of your game's currency; a raw sats price does not.
Handle the edge cases: an empty `giftCards` array means no cards are available to the player, and any card with `unlockedPercent = -1` should be skipped (or shown without a progress value), since that means progress couldn't be calculated.
# Go-Live Checklist
Source: https://docs.zbdpay.com/earn/sdk/go-live-checklist
Make sure you have completed these steps before going live with the ZBD Earn SDK.
Before releasing your game to production, work through both lists below. The first decides whether your rewards will actually convert; the second decides whether the integration works.
## Conversion readiness
An integration can be technically perfect and still underperform. Confirm all three of the [things that drive conversion](/earn/sdk/player-comms#what-makes-rewards-convert) are in place before you ship.
Players can see their balance and progress in your own game UI, and know what they've earned and why it's real. See the [Player Communication Guide](/earn/sdk/player-comms).
Rewards are sized off actual revenue, with a route to raise withdrawal limits from real monetization signals โ and your player IDs are linked in both directions if you're using MMP data. See [Increasing Limits with MMP Data](/earn/sdk/mmp-postbacks).
Your UA is aimed at players who want to earn, or would be interested in earning.
## Technical readiness
Ensure attestation is fully configured for both Android and iOS. For iOS, this includes enabling App Attest on your provisioning profile. See the [Attestation Set Up](/earn/sdk/attestation-setup) guide.
Your game should gracefully handle ZBD platform maintenance periods by checking the `maintenance` flag on **every** SDK response. See [Maintenance Mode](/earn/sdk/error-handling#maintenance-mode).
Confirm your game handles initialization failures gracefully when a player is in an unsupported region (`completion.type == "region"`). Hide all Earn-related UI and functionality, or show a message that Earn isn't available in their region โ do not show the retry modal, as there is no retry path. See [Error Handling](/earn/sdk/error-handling).
Complete the privacy declarations both stores require for the data the SDK collects. See the [iOS App Store](/earn/sdk/ios-app-store) and [Android App Store](/earn/sdk/android-app-store) guidelines.
Make sure you have thoroughly tested your game before going live. At a minimum, verify the following:
* **Test init** โ confirm the SDK initializes successfully.
* **Test modal shows** โ confirm the ZBD modal appears correctly.
* **Test cashout options** โ confirm the payout options configured for your app appear in the modal.
* **Test linking ZBD** โ if ZBD is one of your cashout options, walk through account linking end to end. This is a common source of go-live bugs: linking sends the player out and back again, so a misconfigured [URL scheme](/earn/sdk/url-scheme) or tracking link will leave them stranded instead of returning them to your game. Verify they land back in the game with the account linked.
* **Test earning** โ confirm rewards are sent and received.
* **Test withdrawal limit increases** โ confirm your withdrawal limit increases as you play.
* **Test withdrawal** โ confirm the withdrawal flow completes successfully.
* **Test attestation** โ confirm attestation is working correctly. To do this, verify with your Customer Success Manager that attestation is enabled for your app, then check that your withdrawal limit increases as you play. If attestation is failing, the withdrawal limit will not increase and you will see errors in Android Logcat or the iOS Console.
# Integrating Earn SDK
Source: https://docs.zbdpay.com/earn/sdk/integration
How to import and initialize the ZBD Earn SDK.
# Import the SDK
Reach out to your ZBD Customer Success Manager to get access to the Unity SDK package.
To import a package, right-click the assets folder in Unity Editor and chooseย `"Import Package..."`.
Navigate to the newly imported ZBD folder `Prefabs` and drag and drop the ZBDSDK prefab into your scene.
Due to your specific scene resolution settings and orientation you may need to resize/adjust the included UI elements. We will explain how to do this in more detail later in this documentation, but for now we are assuming the UI element has imported correctly.
# Add your App ID
You should have received your `App ID` from your ZBD Customer Success Manager. Youโll need to set this in the `ZBDController` script which is attached to the `ZBDSDK` game object in the ZBDSDK prefab.
If you have not been given an App ID, reach out to your Customer Success Manager.
# Initialize the SDK
Pass a `userId` during initialization โ your own internal identifier for the player. The SDK works without it, but it's the key that joins your players to ZBD, so it's required for analytics, user linking, and for raising withdrawal limits from MMP revenue data. See [Increasing Limits with MMP Data](/earn/sdk/mmp-postbacks). If you can't supply it at `Init`, set it later with `SetUserId`.
Please reach out to your ZBD Customer Success Manager if you face any issues.
Once the ZBDSDK prefab is added you can initiate the SDK from another script, such as your main controller, via the following call:
```csharp theme={null}
// Initialize the SDK with your internal userId
using ZBD;
ZBDController.Instance.Init(userId, completion =>
{
if (completion.success) {
Debug.Log("success");
} else if (completion.maintenance) {
// Platform is in maintenance mode โ display a message to the user
Debug.Log("Rewards are temporarily unavailable. Please try again later.");
} else {
Debug.LogError(completion.error);
}
}
);
```
### Initializing SDK without `userId`
You can also set the `userId` at any time after initialization by calling:
```csharp theme={null}
ZBDController.Instance.SetUserId("user_id");
```
```csharp theme={null}
using ZBD;
ZBDController.Instance.Init(completion =>
{
if (completion.success) {
Debug.Log("success");
} else if (completion.maintenance) {
Debug.Log("Rewards are temporarily unavailable. Please try again later.");
} else {
Debug.LogError(completion.error);
}
}
);
```
This will allow the user to start earning. To withdraw, they open the ZBD modal, where they see their balance and the cashout options configured for your app โ gift cards, Cash App, ZBD, and others depending on their region. A linked ZBD account is only needed if they choose to cash out **to ZBD**; the other options don't require one.
**Timing matters:** present the modal when the player taps their rewards balance or a claim prompt โ once there's a balance worth claiming โ never at cold launch or before they've earned anything. See the [Player Communication Guide](/earn/sdk/player-comms).
### Handling Maintenance Mode
The ZBD platform may occasionally enter maintenance mode. When this happens, the SDK response will include a `maintenance` flag set to `true`. Check for this flag during initialization and respond based on the player: hide rewards UI entirely for players who haven't started earning, and show a brief, ZBD-free message (e.g. *"Rewards are temporarily unavailable"*) for players who already have a balance. See [Error Handling](/earn/sdk/error-handling#maintenance-mode) for the full guidance.
The `Init` response object contains the following fields:
```csharp theme={null}
public class ZBDInitResponse
{
public bool success;
public bool maintenance;
public string userId;
public string error;
public string type;
}
```
When `maintenance` is `true`:
* The SDK will not be fully operational
* Reward sending will be unavailable
* You should display a user-friendly message (e.g. *"Rewards are temporarily unavailable. Please try again later."*)
* You should **not** treat this as an error โ maintenance periods are temporary
Maintenance mode can begin at any time, including while players are actively in a session. This means `Init` may succeed normally, but subsequent calls to `SendReward` or `GetBalance` can return `maintenance = true` during gameplay. You must handle the maintenance flag on **every** SDK response, not just during initialization.
The following SDK methods can return the `maintenance` flag:
| Method | Response Class |
| ------------ | ----------------------- |
| `Init` | `ZBDInitResponse` |
| `SendReward` | `ZBDSendRewardResponse` |
| `GetBalance` | `ZBDBalanceResponse` |
### Presenting SDK Modal
```csharp theme={null}
ZBDModalController.Instance.ShowModal();
```
`ShowModal()` presents the ZBD modal, showing the player their balance and the cashout options configured for your app.
Users can **view their balance** and **initiate a withdrawal** from this modal. Cashing out to ZBD requires a linked ZBD account, and the modal guides the player through that if they pick it; other payout options don't need one.
### Detecting Modal Visibility
You may want to stop the gameplay or trigger some other event when the SDK modal becomes visible. You can **detect when the modal is visible** via 2 methods:
**Subscribe to the `ModalIsVisible` event**
```csharp theme={null}
private void Start()
{
ZBDController.Instance.ModalIsVisible += WebViewVisible;
}
void WebViewVisible(bool visible)
{
// handle event here
}
private void OnDestroy()
{
ZBDController.Instance.ModalIsVisible -= WebViewVisible;
}
```
**Invoke `IsModalVisible()`**
You can also call `IsModalVisible()` to check the status of the modal.
```csharp theme={null}
ZBDModalController.Instance.IsModalVisible();
```
### Android Back Button
In Android you can detect whether the modal is open and close it with the following code snippet.
```csharp theme={null}
if (Input.GetKeyDown(KeyCode.Escape))
{
if (ZBDModalController.Instance.IsModalVisible())
{
ZBDModalController.Instance.CloseModal();
}
}
```
# Apple App Store Guidelines
Source: https://docs.zbdpay.com/earn/sdk/ios-app-store
Instructions for successfully submitting your game to the Apple App Store.
**It is very important to follow the below guidelines to ensure your game does not run into issues with app reviewers on the Apple App Store.**
# Privacy Policies
Apple announced that startingย [Spring 2024](https://developer.apple.com/news/?id=3d8a9yyh), it will require its developers to declare the reasons for using certain APIs (aka Required Reason APIs) that could be misused to collect data about users' devices within privacy manifest files.
**This needs to be done when integrating the ZBD Earn SDK.**
Once your Xcode project has successfully built, follow these steps:
Right click the new `PrivacyInfo` file and select `Open As -> Source Code` then copy and paste the following.
Add or change any values below based on what your game is doing!
More information on this Apple policy can be found here: [Apple Developer Documentation](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files).
```xml theme={null}
NSPrivacyTracking
NSPrivacyTrackingDomains
NSPrivacyCollectedDataTypes
NSPrivacyCollectedDataType
NSPrivacyCollectedDataTypeDeviceID
NSPrivacyCollectedDataTypeLinked
NSPrivacyCollectedDataTypeTracking
NSPrivacyCollectedDataTypePurposes
NSPrivacyCollectedDataTypePurposeAppFunctionality
NSPrivacyAccessedAPITypes
NSPrivacyAccessedAPIType
NSPrivacyAccessedAPICategoryFileTimestamp
NSPrivacyAccessedAPITypeReasons
C617.1
```
Your game should now be able to be submitted to the Apple App Store!
If you have any questions, don't hesitate to reach out to your ZBD Customer Success Manager.
# List API Keys
Source: https://docs.zbdpay.com/earn/sdk/list-api-keys
GET https://api.zbdpay.com/api/v1/rewards/app/{rewardsAppId}/api-keys
Fetch all existing API keys associated with your Rewards App.
Retrieve all API keys for your Rewards App to see which keys are active and when they were created.
## Configuration
### Header Parameters
Client identifier (use "developer-dashboard" )
Bearer token for authentication
Format: `Bearer {JWT_TOKEN}`
### Path Parameters
Your Rewards App ID
```json 200 - Success theme={null}
{
"success": true,
"message": "Success.",
"data": [
{
"id": "46e64511-4efd-486c-92b7-b5a4f7e5f9d8",
"isActive": true,
"createdAt": "2025-10-30T21:55:34.742Z",
"revokedAt": null
}
]
}
```
### Response Status Codes
| Code | Description |
| ----- | ------------------------------------------- |
| `200` | API keys retrieved successfully |
| `400` | Bad request - invalid UUID |
| `401` | Unauthorized - authentication required |
| `403` | Forbidden - developer does not own this app |
| `404` | Rewards app not found |
| `500` | Internal server error |
```json 400 - Bad Request theme={null}
{
"success": false,
"message": "Invalid UUID"
}
```
```json 401 - Unauthorized theme={null}
{
"success": false,
"message": "Authentication required"
}
```
```json 403 - Forbidden theme={null}
{
"success": false,
"message": "Developer does not own this app"
}
```
```json 404 - Not Found theme={null}
{
"success": false,
"message": "Rewards app not found"
}
```
```json 500 - Server Error theme={null}
{
"success": false,
"message": "Internal server error"
}
```
## Response Fields
| Field | Type | Description |
| ----------- | -------------- | ------------------------------------------------------------- |
| `success` | boolean | Whether the request was successful |
| `message` | string | Response message |
| `data` | array | Array of API keys |
| `id` | string | Unique identifier for the API key |
| `isActive` | boolean | Whether the API key is currently active |
| `createdAt` | string | ISO 8601 timestamp of when key was created |
| `revokedAt` | string \| null | ISO 8601 timestamp of when key was revoked, or null if active |
***
## Code Examples
```javascript Node.js theme={null}
const rewardsAppId = 'YOUR_REWARDS_APP_ID';
const jwtToken = 'YOUR_JWT_TOKEN';
async function listApiKeys(rewardsAppId) {
const response = await fetch(
`https://api.zbdpay.com/api/v1/rewards/app/${rewardsAppId}/api-keys`,
{
method: 'GET',
headers: {
'z-client': 'developer-dashboard',
'Authorization': `Bearer ${jwtToken}`
}
}
);
const data = await response.json();
if (data.success) {
console.log(`Found ${data.data.length} API keys`);
data.data.forEach(key => {
console.log(`- Key ID: ${key.id}, Active: ${key.isActive}`);
});
return data.data;
} else {
throw new Error(`Failed: ${data.message}`);
}
}
// Get all API keys
const keys = await listApiKeys('b28e0306-2c06-4092-8d56-a1623d6b97fb');
```
```bash cURL theme={null}
curl --location 'https://api.zbdpay.com/api/v1/rewards/app/b28e0306-2c06-4092-8d56-a1623d6b97fb/api-keys' \
--header 'z-client: developer-dashboard' \
--header 'Authorization: Bearer {JWT_TOKEN}'
```
```python Python theme={null}
import requests
import os
jwt_token = os.getenv('JWT_TOKEN')
rewards_app_id = "YOUR_REWARDS_APP_ID"
def list_api_keys(rewards_app_id):
"""List all API keys for a Rewards App"""
url = f"https://api.zbdpay.com/api/v1/rewards/app/{rewards_app_id}/api-keys"
headers = {
"z-client": "developer-dashboard",
"Authorization": f"Bearer {jwt_token}"
}
response = requests.get(url, headers=headers)
data = response.json()
if data["success"]:
print(f"Found {len(data['data'])} API keys")
for key in data["data"]:
print(f"- Key ID: {key['id']}, Active: {key['isActive']}")
return data["data"]
else:
raise Exception(f"Failed: {data['message']}")
# List keys
keys = list_api_keys("b28e0306-2c06-4092-8d56-a1623d6b97fb")
```
Empty Response. If `data` is an empty array `[]`, it means no API keys exist for this Rewards App.
## Try It Out
Ready to view your API keys? Use our API playground on the right to test with your JWT token.
## What's Next?
After viewing your API keys:
**Revoke API Key**. Remove an API key to prevent it from being used for authentication
# Local Notifications
Source: https://docs.zbdpay.com/earn/sdk/local-notifications
Bring players back to keep earning or claim their rewards, with SDK-scheduled local reminders โ no push infrastructure required.
A player who leaves with an unclaimed balance is the easiest player to win back. Local reminders let you reach them โ *"you have rewards to claim"*, *"you're 80% of the way to an Amazon card"* โ without any push infrastructure.
Because the OS holds the scheduled notification, there is **no server, no device tokens, and no backend work**. The SDK schedules reminders when the player leaves and cancels them when they return, so a reminder only ever fires if they genuinely stayed away.
**You supply the conditions and the copy; the SDK owns the plumbing.** It caches the player's reward state during play, schedules on background, cancels on return, and handles the platform hygiene that makes local notifications fiddly to get right.
Requires Unity SDK v1.1.7 or above.
## Opting in
**Already have a notification system? Use that instead.** This feature exists to make reminders easier and quicker to ship for games that don't have anything built. If your game already schedules its own notifications, we recommend adding reward reminders there โ pull the reward data from [`GetGiftCards`](/earn/sdk/gift-cards) or [`GetBalance`](/earn/sdk/user-balance) โ and skipping this feature entirely.
Reminders are **off by default** and stay off until you do both of the following:
1. **Add Unity's Mobile Notifications package** (`com.unity.mobile.notifications`, version 2.3.0 or above) to your project. The SDK does not add it for you.
2. **Call `ConfigureReminders`** with your rules.
If you skip either, nothing happens โ no notification channel is registered, no permission is requested, nothing is scheduled, and there are **no build errors**. Without the package, `ConfigureReminders` logs a hint and does nothing. A game using its own notification manager can ignore this feature completely, even with the package installed.
## Quick start
Call once, any time after `Init`. Your rules depend on which cashout options your game offers โ gift cards have a minimum to reach, while Cash App and ZBD don't, so the milestones differ:
Gift cards have a fixed price, so `giftCardPercent` gives you a concrete goal to aim the player at โ the strongest motivator you have.
```csharp theme={null}
ZBDController.Instance.ConfigureReminders(new ZBDReminderConfig
{
currencyLabel = "coins", // whatever your players call it
defaultSchedule = new[] { 24f, 72f, 168f }, // hours after they leave: 1 day, 3 days, 1 week
rules = new[]
{
// Unlocked something concrete โ chase sooner, they can claim right now
new ZBDReminderRule
{
when = s => s.giftCardPercent >= 100f,
title = "Your gift card is ready",
body = "You've unlocked a {card} gift card โ come and claim it.",
schedule = new[] { 6f, 24f }, // per-rule override of defaultSchedule
},
// A concrete goal beats a bare number
new ZBDReminderRule
{
when = s => s.giftCardPercent >= 50f,
title = "Almost there!",
body = "You're {percent}% of the way to a {card} gift card.",
},
new ZBDReminderRule
{
when = s => s.balance > 0,
title = "You have rewards to claim",
body = "You've got {balance} {currency} waiting โ come and claim them.",
},
}
});
```
Cash App has **no minimum cashout**, so there's no gift-card goal to progress toward. Use a balance threshold instead: we recommend around **\$1 worth** before you tell a player they can cash out โ enough to feel worth claiming.
```csharp theme={null}
// $1 worth of your currency. At the recommended $0.0001 per unit that's
// 10,000 units โ set this from your own exchange rate.
const int CashoutThreshold = 10000;
ZBDController.Instance.ConfigureReminders(new ZBDReminderConfig
{
currencyLabel = "coins",
defaultSchedule = new[] { 24f, 72f, 168f },
rules = new[]
{
// Enough to be worth cashing out โ chase sooner
new ZBDReminderRule
{
when = s => s.balance >= CashoutThreshold,
title = "You can cash out",
body = "You've earned {balance} {currency} โ cash out to Cash App.",
schedule = new[] { 6f, 24f },
},
// Earning, but not there yet โ reinforce that it's real
new ZBDReminderRule
{
when = s => s.balance > 0,
title = "You're earning",
body = "You've got {balance} {currency} so far. Keep playing to cash out.",
},
}
});
```
ZBD cashout has **no minimum**, so the same two-step model as Cash App applies: tell the player they're earning, then tell them once there's enough to be worth claiming (**\$1 worth** is a good threshold).
```csharp theme={null}
// $1 worth of your currency. At the recommended $0.0001 per unit that's
// 10,000 units โ set this from your own exchange rate.
const int CashoutThreshold = 10000;
ZBDController.Instance.ConfigureReminders(new ZBDReminderConfig
{
currencyLabel = "coins",
defaultSchedule = new[] { 24f, 72f, 168f },
rules = new[]
{
new ZBDReminderRule
{
when = s => s.balance >= CashoutThreshold,
title = "You can cash out",
body = "You've earned {balance} {currency} โ come and claim it.",
schedule = new[] { 6f, 24f },
},
new ZBDReminderRule
{
when = s => s.balance > 0,
title = "You're earning",
body = "You've got {balance} {currency} so far. Keep playing to cash out.",
},
}
});
```
Offering several cashout options? Combine the rules in one config โ put the gift-card rules first (a named card is the stronger pull) and let the balance-threshold rules catch everyone else. First match wins, so order them most specific to least.
Then ask for permission at a sensible moment (see [Asking for permission](#asking-for-permission)):
```csharp theme={null}
ZBDController.Instance.RequestNotificationPermission();
```
## How rules work
When the player backgrounds the app, the SDK evaluates your rules **in order** against a snapshot of the player's reward state. The **first match wins** and its reminders are scheduled; if **no rule matches, nothing is scheduled**. List the most specific rule first.
**Never tell a player who has nothing waiting that rewards are waiting for them.** Gate any "you have rewards to claim" rule on `s.balance > 0` โ no-match-means-no-reminder is how a player who already cashed out stays unbothered. The one exception is a deliberate win-back rule that *invites* a lapsed player to earn again rather than claiming they have earnings; see [Winning back lapsed players](#winning-back-lapsed-players).
Each rule has:
| Field | What it does |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `when` | A predicate deciding whether the rule applies. Runs as the app suspends, so it must be a cheap in-memory check โ no network calls. |
| `title` / `body` | The notification copy. Both support tokens: `{balance}`, `{currency}`, `{card}`, and `{percent}`, substituted from the snapshot. |
| `bodyFor` | Optional. Builds the body from arbitrary state when the tokens can't express it. Takes precedence over `body`. |
| `schedule` | Optional per-rule timings (hours after leaving). Falls back to the config's `defaultSchedule`. |
### The snapshot
Rules receive a `ZBDRewardSnapshot` with the player's withdrawable `balance`, the `giftCardName` they're closest to unlocking, their `giftCardPercent` progress toward it (0โ100, or -1 when unknown), and `canWithdraw`.
The snapshot is **as of the player's last session, not a live read** โ reminders are scheduled while the app is suspending, which is not a safe place for network calls, so the SDK caches this during play: after `Init`, after each reward it sends, and after every withdrawal. You never refresh it yourself.
## Winning back lapsed players
Because the SDK cancels every pending reminder the moment a player returns, **a long-dated schedule entry can only ever reach someone who genuinely stayed away that long**. That's the mechanism for lapsed-player win-back: extend the schedule rather than trying to detect absence yourself.
```csharp theme={null}
ZBDController.Instance.ConfigureReminders(new ZBDReminderConfig
{
currencyLabel = "coins",
// 1 day, 3 days, 1 week, 2 weeks, 30 days โ the last two only ever
// reach players who never came back
defaultSchedule = new[] { 24f, 72f, 168f, 336f, 720f },
rules = new[]
{
// Has something waiting: name the payouts, not just the balance
new ZBDReminderRule
{
when = s => s.balance > 0,
title = "Your rewards are waiting",
body = "You've got {balance} {currency} to claim โ cash out to a gift card, Cash App and more.",
},
// Cashed out and drifted away: nothing waiting, so invite them to earn again
new ZBDReminderRule
{
when = s => s.balance == 0,
title = "Come back and earn",
body = "Keep playing to earn real rewards โ gift cards, Cash App and more.",
schedule = new[] { 336f, 720f }, // lapsed-only: don't nag a fresh player
},
}
});
```
**One rule sends one message, repeated at each of its schedule entries.** The title and body are built once from the matching rule, so a reminder at 30 days says exactly what the reminder at 1 day said. Write copy that reads well at any distance โ *"your rewards are waiting"* works at both; *"you just earned!"* does not.
If you need genuinely different copy at day 1 versus week 4, use your own notification system and schedule the messages yourself.
The zero-balance rule above is the **one** case where a reminder to a player with nothing waiting is appropriate โ because it invites them to earn rather than claiming they have earnings. Keep that distinction: never tell a player who cashed out that rewards are waiting for them. Give it a lapsed-only schedule so a player who simply hasn't earned yet in their first session isn't nagged the next day.
## What the SDK handles for you
* **Scheduling on background, cancelling on return** โ using `OnApplicationPause`, which fires reliably on both platforms (unlike `OnApplicationQuit`, which Android frequently skips when killing a backgrounded process).
* **Only its own notifications.** Cancellation is scoped to reminders the SDK scheduled, tracked by ID and persisted across process kills. Your game's own scheduled notifications are never touched.
* **The Android notification channel** (required on API 26+), configurable via `androidChannelId` / `androidChannelName` / `androidChannelDescription` on the config.
* **Platform hygiene** โ a 60-second floor (both platforms batch shorter timers), and reminders never show while the player is in the foreground of your game.
* **Stale-state protection** โ pending reminders are cancelled and rebuilt on every background, and the snapshot is re-cached after withdrawals so copy is never built from pre-cashout state.
## Asking for permission
**Never ask on a cold launch.** iOS shows the system permission prompt only once per install โ if the player declines, the only way back is the Settings app. The SDK never requests permission itself; you choose the moment.
Call `RequestNotificationPermission()` when the value is obvious to the player โ after their first reward, or once they're a signed-in rewards user. It only ever prompts once per install.
On **Android 13 (API 33) and above** the `POST_NOTIFICATIONS` runtime permission is required โ without it nothing is shown and nothing errors. The same call handles it.
## Cancelling programmatically
```csharp theme={null}
ZBDController.Instance.CancelReminders();
```
Cancels the SDK's own pending reminders โ for example if the player turns reminders off in your settings menu. Your game's notifications are unaffected.
## Testing
Notifications are **device-only** โ they don't fire in the Unity Editor.
To test without waiting a day, set `debugFastReminders = true` on the config: reminders fire about a minute after backgrounding instead of hours (interval tunable via `debugIntervalSeconds`, floored at 60 seconds).
Never ship with `debugFastReminders` enabled.
## Reference example
The SDK demo project's `ZBDDemoNotifications.cs` is a complete working example of configuring reminders โ rules, permission timing, and the debug flag โ and is a good starting point to copy from.
## Pairing with creatives
Local reminders bring the player back; a [creative modal](/earn/sdk/creatives) tells them what to do next. Showing a *"you're halfway to a gift card"* creative on the session that a reminder brought them back to is a natural pairing โ both are driven by the same gift-card progress data.
# Earn Controls (Dashboard)
Source: https://docs.zbdpay.com/earn/sdk/manage-controls-dashboard
Override a player's detected country and manage your project whitelist from the ZBD Developer Dashboard.
ZBD detects a player's country automatically from their IP address. This determines which payout options they see and what withdrawal limits apply. In most cases this works correctly โ but there are situations where you'll need to override it manually:
* A player is using a VPN and their detected country is wrong
* A player is traveling and their IP doesn't match their home country
* A player is in an unsupported region but you want to whitelist them for testing
* Your QA team needs to test from a region other than their own
Use this page for those cases. Access it via [dashboard.zbdpay.com](https://dashboard.zbdpay.com) under **Earn User Support**.
## Find a player
Search by any of the following:
* **`rewardsUserId`** โ the player's SDK identifier, returned by `GetUserStatus`. Loads the player directly.
* **Support ID** โ a player-facing identifier shown in the ZBD modal, used when a player contacts your support team.
* **Gamertag** โ the player's ZBD gamertag.
Click **Load Data** to search. The User Data card shows the player's withdrawal limit, rewards balance, detected country, and transaction history.
## Change country code
Override a player's detected country โ for example if their IP is flagged incorrectly due to a VPN or proxy, or if they're traveling.
1. Click **Change** next to the country code field
2. Search for the correct country
3. Click **Save**
Changing a player's country code affects their payout options and withdrawal limits immediately. Only override when you're confident the detected country is incorrect.
## Whitelist a player
Whitelisting allows a specific player to access your rewards program from a region that would otherwise be blocked โ for example, a member of your QA team based in an unsupported country.
1. Go to the **Whitelist User** card below the Earn User ID card
2. Enter the player's **Support ID**
3. Select the **Rewards App Name** tied to that Support ID
4. Click **Whitelist**
Whitelisting bypasses regional restrictions for that player only. Use it for testing and support purposes, not as a general workaround for regional coverage. See the [coverage page](/earn/coverage) for the full list of supported regions.
# Adjust Player Balance (Dashboard)
Source: https://docs.zbdpay.com/earn/sdk/manage-rewards-balance
Manually increase or decrease a player's rewards balance from the ZBD Developer Dashboard.
Use the ZBD Developer Dashboard to manually adjust a player's rewards balance โ for example, when resolving a support issue or correcting a reward that failed to deliver.
## Find the player
1. Go to [dashboard.zbdpay.com](https://dashboard.zbdpay.com)
2. Navigate to **Earn User Support** in the sidebar
3. Search by one of the following:
* **`rewardsUserId`** โ the player's SDK identifier, returned by `GetUserStatus`. Loads the player directly.
* **Support ID** โ a player-facing identifier shown in the ZBD modal, used when a player contacts your support team. Returns a list of matching players.
* **Gamertag** โ the player's ZBD gamertag. Returns a list of matching players.
4. Click **Load Data**
## Adjust the balance
Once the player's data loads:
1. Click the **edit button** next to the Rewards Balance field
2. Enter the new balance value โ higher to increase, lower to decrease
3. Click **Save**
The system calculates and applies the difference automatically.
Balance values are displayed in **SATS** if your project rewards players in Bitcoin, or **POINTS** if you are using in-game currency. This is set when your project is configured โ if you're unsure which applies, check your project settings in the dashboard or ask your ZBD Customer Success Manager.
# Manage User Linking via Dashboard
Source: https://docs.zbdpay.com/earn/sdk/manage-user-linking
Link or unlink users to ZBD gamertags via the ZBD Dashboard
Manage user linking to ZBD gamertags through the ZBD Dashboard.
## Access
1. Go to [dashboard.zbdpay.com](https://dashboard.zbdpay.com)
2. Navigate to **Earn User Support** in the sidebar
## Search Users
Enter one of the following in the search field:
* **Earn User ID** (the `rewardsUserId` returned by the SDK) โ Loads user data directly
* **Support ID** โ Shows list of users with that linked ID โ select one
* **Gamertag** โ Shows list of users with that gamertag โ select one
Click **"Load Data"** to search.
***
## Link User to Gamertag
When no user is linked, you'll see **N/A** and a **Link button**.
1. Click the **Link button**
2. An input field appears
3. Enter the ZBD gamertag
4. Click **save** (green checkmark โ) or **X** to cancel
After saving, the gamertag shows (instead of N/A) and the button changes to **Unlink**.
***
## Unlink User from Gamertag
When a user is linked, you'll see the **gamertag** and an **Unlink button**.
1. Click the **Unlink button**
2. The gamertag changes back to **N/A**
3. The button changes back to **Link**
# Manage via Dashboard
Source: https://docs.zbdpay.com/earn/sdk/manage-via-dashboard
Create API keys and control reward settings from the ZBD Dashboard
Manage your Rewards App settings directly from the ZBD Dashboard.
## Access Your Project's Earn Settings
1. Go to [dashboard.zbdpay.com](https://dashboard.zbdpay.com)
2. Navigate to **Projects**
3. Select your project
4. Click the **Earn** tab
5. Click the **Controls** sub-tab
Your Rewards App settings will load automatically under **API Key Control** and **Client Send Control** sections.
If you see "No Rewards App found for this project", contact ZBD support to enable Rewards for your project by adding your project ID to the database.
***
## Create an API Key (Required)
To send rewards from your server, you need an API key for authentication.
### How to Create
1. In the Earn tab โ Controls sub-tab, find the **API Key Control** section
2. Check the **Status** badge:
* ๐ด **Disabled** (red) with text "No active API key found" = No API key exists
* ๐ข **Enabled** (green) = API key is active
3. Click **"Create API Key"** button
4. **Click the copy button** next to the API key to copy it
5. **Save the API key in a secure place immediately** (environment variables, secrets manager, etc.)
The API key is shown only once after creation. If you lose it, you'll need to revoke it and create a new one.
**What you'll use it for:**
* Authenticating server-side reward sending requests
* Required for the [Send Rewards API](/earn/sdk/send-rewards-api)
***
## Control Client-Side Sending (Recommended)
After creating your API key, you can disable client-side reward sending to enforce server-only rewards.
**Important**: You must create an API key first before disabling client-side sending. Without an API key, disabling client-side means no rewards can be sent at all.
### How to Disable Client-Side Sending
1. In the Earn tab, find the **Client Send Control** section
2. Check the **Status** badge:
* ๐ข **Enabled** (green) with text "Client sending is enabled" = Client can send rewards
* ๐ด **Disabled** (red) with text "Client sending is disabled" = Only server can send
3. Click **"Disable Client Send"** button to block client-side rewards
**What this does:**
* Blocks client SDK reward calls
* Only your server (with the API key) can send rewards
* Prevents client-side abuse
### How to Enable Client-Side Again
If you need to allow client-side rewards:
1. In the **Client Send Control** section, click **"Enable Client Send"**
2. Status will change to ๐ข **Enabled** (green)
3. Both client SDK and server can send rewards again
***
## Revoke an API Key
If your API key is compromised or no longer needed, revoke it immediately.
### How to Revoke
1. In the Earn tab, find the **API Key Control** section
2. Make sure the Status is ๐ข **Enabled** (green)
3. Click **"Revoke Key"** button
4. Confirm the action in the dialog
**After revoking:**
* The key is immediately deactivated
* Status changes to ๐ด **Disabled** (red) with "No active API key found"
* Server requests using this key will fail
* Create a new API key if you need to continue sending rewards
***
## Next Steps
Now that you've created your API key and configured your settings, you're ready to send rewards from your server:
* **[Send Rewards from Server](/earn/sdk/send-rewards-api)** - Use your API key to send rewards securely
# Manage Withdrawal Limits via Dashboard
Source: https://docs.zbdpay.com/earn/sdk/manage-withdrawal-limits
Increase or decrease user withdrawal limits via the ZBD Dashboard
Manage user withdrawal limits through the ZBD Dashboard.
## Access
1. Go to [dashboard.zbdpay.com](https://dashboard.zbdpay.com)
2. Navigate to **Earn User Support** in the sidebar
## Search Users
Enter one of the following in the search field:
* **Earn User ID** (the `rewardsUserId` returned by the SDK) โ Loads user data directly
* **Support ID** โ Shows list of users with that linked ID โ select one
* **Gamertag** โ Shows list of users with that gamertag โ select one
Click **"Load Data"** to search.
***
## Update Withdrawal Limit
Once the User Data card loads:
1. Click the **edit button** next to the Withdrawal Limit field
2. Enter a new value:
* **Higher value** = increases the limit
* **Lower value** = decreases the limit
3. Click **save**
The system automatically calculates and applies the change.
Values display in **SATS** (for Bitcoin) or **POINTS** (for point-based currencies) depending on your project configuration.
***
## Related
* **[Player Balance & Limits](/earn/sdk/user-balance)** - Retrieve limits via SDK
# Increasing Limits with MMP Data
Source: https://docs.zbdpay.com/earn/sdk/mmp-postbacks
Grow per-player withdrawal limits from real monetization signals in your MMP โ with or without a backend of your own.
The withdrawal limit is the ceiling on what a player can cash out, so it's also the ceiling on how engaging your rewards can be. The time-based accrual described in [Withdrawal Limits (Server)](/earn/sdk/revenue-based-earning) is a rough proxy for revenue; tying limits to **actual monetization signals from your MMP** (Singular, AppsFlyer, Adjust, and similar) is how you raise them meaningfully.
Done this way the pool is self-funding: players who generate revenue earn the right to withdraw more, and you never give away more than that player has produced.
It also makes rewards easy to size. If you know a player has plenty of headroom, you can confidently issue larger rewards for completing tasks, while players with a lower limit get smaller ones. This is one pattern rather than a requirement, but it keeps rewards self-balancing against revenue.
## Raise the limit, not the balance
**Never credit a player's rewards balance directly in response to a monetization event.** "+100 points for watching an ad" is effectively cash-for-task, and it's the pattern that runs into app store review problems.
Use the monetization event to raise the **withdrawal limit** โ the ceiling โ and let players earn their **rewards balance** through genuine play: winning a match, completing a level, hitting a streak.
| Trigger | What it should change |
| --------------------------------- | --------------------------------------------------- |
| Monetization event (ad view, IAP) | **Withdrawal limit** โ how much they *can* cash out |
| Genuine gameplay | **Rewards balance** โ the points they actually earn |
This keeps your reward economy backed by real revenue while keeping the player-facing earning experience tied to play, not ads.
## Prerequisite: link the two IDs
Every approach below joins an MMP event to a ZBD player, so the two systems have to know about each other's identifiers. Do it in **both directions** โ it's cheap, and it means the mapping works whichever ID ends up on the postback.
### 1. Give ZBD your player ID
Pass your own internal player identifier when you initialize the SDK, or set it later with `SetUserId`:
```csharp theme={null}
ZBDController.Instance.Init(userId, completion => { /* ... */ });
```
See [Integration](/earn/sdk/integration#initialize-the-sdk).
### 2. Give your MMP the ZBD player ID
Read the ZBD-side identifier with `Utils.GetUserId()` and set it on your MMP's SDK as a customer/user identifier or custom property, so it comes back on their postbacks:
```csharp theme={null}
using ZBD;
// After Init has succeeded โ the ID isn't available before the player is registered
string zbdUserId = Utils.GetUserId();
if (!string.IsNullOrEmpty(zbdUserId))
{
// Set it on your MMP's SDK โ the exact call depends on the MMP.
// AppsFlyer, Adjust and Singular each expose a customer user ID
// and/or custom-data field for exactly this purpose.
MyMmp.SetCustomerUserId(zbdUserId);
}
```
`Utils.GetUserId()` returns an empty string until the SDK has registered the player, so call it after `Init` succeeds rather than at startup. Check the exact field name against your MMP's own documentation โ the concept is the same everywhere, but the method isn't.
Without at least one of these links there's nothing to match an MMP event against, and none of the options below will work.
## Choose your integration
Three ways to get monetization data to ZBD, depending on what you have to build with:
The most capable option, and the only one that gives you full control over which events count and how much they're worth.
When a player monetizes โ a rewarded ad view, a completed IAP โ your MMP fires a server-to-server postback to your backend with the player identifier and the revenue amount.
Look up the SDK user from the identifier in the postback, using the internal player ID you set at `Init`.
*Example: MMP user `1234` maps to ZBD player `1234`, who just monetized \$1.00 from a rewarded ad.*
Pick a share of the revenue to convert into withdrawal headroom โ 10โ20% is the usual starting range. So \$1.00 monetized at 10% โ \$0.10 of withdrawal limit.
Withdrawal limits are denominated in your reward currency, so convert the dollar figure using your app's configured point value. There's no live conversion endpoint yet, but the value is stable enough to hardcode.
For example, at a point value of \$0.001, \$0.10 becomes **100 points**.
The point value is configured per app โ confirm yours with your ZBD Customer Success Manager rather than assuming a rate. See [Reward Economics](/earn/sdk/reward-economics).
Call the withdrawal limit endpoint with the converted amount. See [Withdrawal Limits (Server)](/earn/sdk/revenue-based-earning#increasing-limits-based-on-actual-revenue) for the full request.
```bash theme={null}
curl --request POST \
--url https://api.zbdpay.com/api/v1/rewards/user//withdrawal-limit \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"amount": 100,
"currency": "POINT"
}'
```
No backend? Your MMP can post directly to ZBD instead. Your Customer Success Manager provides a postback URL that you add to your MMP's configuration, and ZBD raises the limit when events arrive.
**Coverage varies by MMP, and it's rarely complete.** Not every MMP supports custom server-to-server postbacks to a third party, and those that do often won't send every event type. With AppsFlyer, for example, we've been able to receive in-app purchase events but **not** ad-revenue events โ which is usually the larger share of a rewarded game's revenue.
Check what your MMP can actually send before relying on this. If ad revenue can't be forwarded, you'll be raising limits from a fraction of the real signal.
Ask your Customer Success Manager for a postback URL and share which MMP you use โ the event coverage and the exact setup depend on it.
The fallback when you have neither a backend nor usable MMP postbacks: send ZBD a **CSV or JSON file** of players who monetized, and we raise their withdrawal limits in batch on a schedule that suits you.
Each row needs the player's internal ID (the one you set at `Init`) and the amount of revenue โ or the limit increase you want applied. Agree the format and cadence with your Customer Success Manager.
It's the least responsive option, since limits move on your upload schedule rather than in real time, but it's a legitimate way to keep limits tied to real revenue without building anything.
## Sizing rewards to available headroom
Once limits track revenue, you can check a player's state before issuing a reward and scale it to what they can actually withdraw. Read it wherever you send rewards from:
Most games send rewards client-side, so check the player's state with `GetBalance` in Unity:
```csharp theme={null}
ZBDController.Instance.GetBalance(callback =>
{
if (!callback.success) return;
long balance = callback.balance; // what they've earned
long headroom = callback.withdrawalLimit; // what they can withdraw right now
// Size the reward to the headroom available
int reward = headroom > balance ? largeReward : smallReward;
ZBDController.Instance.SendReward(reward, result => { /* ... */ });
});
```
See [Player Balance & Limits](/earn/sdk/user-balance) for the full call, including maintenance handling.
If you deliver rewards server-side, read the same two values from the API before issuing one:
```bash theme={null}
curl --request GET \
--url https://api.zbdpay.com/api/v2/rewards/users//balance \
--header 'x-api-key: '
```
```json theme={null}
{
"success": true,
"message": "Success.",
"data": {
"withdrawableLimit": 6835,
"rewardsBalance": 9324,
"currency": "POINT"
}
}
```
The field names differ between the two: the client returns `balance` and `withdrawalLimit`, the API returns `rewardsBalance` and `withdrawableLimit`. Same two values.
Either way you learn two things: what the player currently has, and what they're allowed to cash out. Players with more headroom can be given larger rewards, because you know the revenue is there to back them.
Keeping the two roughly aligned matters โ a player who can see a large balance they can't withdraw disengages. See [Best Practices](/earn/sdk/best-practices).
# Before You Build
Source: https://docs.zbdpay.com/earn/sdk/planning
Three decisions to make before writing any code: reward currency, delivery method, and payout options.
These are the decisions that shape your integration. Most are configurable after launch, but getting them right upfront means less iteration once you're live.
## Reward currency
The first decision is which currency you reward players in. This is set at the project level and affects how rewards are displayed, what payout options are available, and how you configure amounts.
You define a virtual currency unit (coins, gems, tokens) that fits your game. Players see familiar numbers ("2,500 coins") and cash out via bank transfer, gift cards, or Bitcoin. Most studios use this model.
Rewards are denominated in satoshis. Players see their balance in sats and cash out to a Bitcoin Lightning wallet. Best suited for Bitcoin-native games or audiences already familiar with crypto.
Both modes use the same SDK methods (`SendReward`, `GetBalance`, `ShowModal`). The difference is the payout options available to players and โ if you deliver rewards **server-side** โ the `currency` field in your API calls. Client-side integrations never set a currency; the mode configured for your app determines it.
| | In-game currency | Bitcoin |
| ---------------------------------------- | ------------------------------------------------------------ | --------------------------------------------- |
| Reward unit | Virtual units you define | Millisatoshis (MSATS) |
| Balance display | "12,400 coins" | "12,400 sats" (UI in sats; API unit is MSATS) |
| Player payout options | Bank transfer (ACH), gift cards, Cash App, Speed Wallet, ZBD | Gift cards, Cash App, Speed Wallet, Bitcoin |
| Audience fit | Mainstream mobile, casual | Bitcoin-native, crypto-native |
| `currency` field (server-side API calls) | `POINT` | `MSATS` |
The rest of the Earn SDK documentation uses in-game currency as the default. Where behavior differs for Bitcoin rewards, it is called out explicitly.
For display guidelines and UX rules for each mode, see [Displaying Rewards](/earn/sdk/soft-currency).
## Reward delivery
You have two options for how rewards reach players. Most studios start with client-side and migrate to server-side for production.
Call `SendReward` directly from Unity. No backend required. Withdrawal limits act as the fraud layer. Best for early development or studios without a backend.
Your backend calls the Earn API. Required for LTV-based reward sizing. Disables client-side abuse entirely. Recommended for production.
See [Reward Economics](/earn/sdk/reward-economics) for guidance on how delivery method affects your reward budget and fraud exposure.
## Payout options
Players cash out through the ZBD modal โ a WebView you surface with a single method call. You don't build any payout UI.
Available methods depend on the player's region and your reward currency:
* Bank transfer (ACH)
* Gift cards
* Cash App
* Speed Wallet
* ZBD
ZBD is available in 40+ countries. Check the [coverage page](/earn/coverage) to confirm your target regions are supported before integrating.
# Player Communication Guide
Source: https://docs.zbdpay.com/earn/sdk/player-comms
What to say to players at each stage of their rewards journey โ and the one principle that makes all of it work.
Getting the integration right technically is half the job. How you introduce and talk about rewards determines whether players engage, claim, build a daily habit, and tell their friends. This page is for game designers and marketing โ the people writing copy, designing flows, and sending notifications.
## What makes rewards convert
**Three things drive conversion. If one of them is missing, your rewards are likely to underperform:**
1. Your UI/UX **shows and explains** how rewards work and how to claim them
2. You're **rewarding enough** to be worth coming back for
3. You **know your audience** โ acquiring players who want to earn improves conversion
Most integrations that underperform are missing one of these three โ not a copy problem.
### 1. Show and explain how rewards work
Players won't claim what they don't understand. Use every surface you have, not just one:
* **Native game elements** do the most work, because they're always present: a persistent balance counter alongside your other currencies, reward toasts at the moment of earning, and a progress bar toward the next reward. Build these into your own UI โ don't rely on the ZBD modal to carry the message.
* **[Creatives](/earn/sdk/creatives)** handle the set-piece moments: the reveal after the first reward lands, and milestone nudges. Dashboard-configured, in your art style, changeable without an app release.
* **[Local notifications](/earn/sdk/local-notifications)** (or your own push stack) bring players back when they have something unclaimed.
Two specifics that matter most: **give the first reward early**, within the first minutes of normal play, so players discover a balance that already exists rather than a counter at zero; and **show progress toward a near, named reward** โ [`GetGiftCards`](/earn/sdk/gift-cards) gives you the percentage for exactly that.
### 2. Reward enough to be worth it
Nobody changes their behavior for a few cents. If a player's realistic earnings are trivial, no amount of good UX will produce engagement โ and they'll tell other players it isn't worth it.
The way to fund meaningful rewards without hurting your margin is to size them off actual revenue rather than a flat guess:
* **Connect your MMP.** When an ad impression or completed view is confirmed, raise that player's withdrawal limit so players who generate more can earn more. This is the highest-leverage change you can make here, and there are options that work without a backend. See [Increasing Limits with MMP Data](/earn/sdk/mmp-postbacks).
* **Start from a revenue share, not a fixed amount.** 10โ20% of ad-generated revenue is the usual starting range. See [Reward Economics](/earn/sdk/reward-economics) for recommended values.
### 3. Know your audience
Acquisition shapes the ceiling on everything else here. UA aimed at players who want to earn โ or who'd be interested in earning โ will improve your conversion rates: rewarded-play audiences, earning-focused creatives and channels, messaging that mentions real rewards.
It's worth being deliberate about it. Players who arrive with no interest in earning are harder to convert, so the same rewards UX will read very differently depending on who you brought in.
The rest of this page is the detail behind point 1: the principle all reward copy follows, then what to say at each moment.
## The one principle: reveal, don't offer
Players installed your game to play, not to earn. A rewards pitch they haven't earned anything from yet is a cold offer โ and cold offers trigger suspicion, not desire. An unexplained reward reads as a hidden cost: *what's the catch?*
So don't *offer* rewards. **Reveal them.** The player should discover they've already earned something by doing the thing they came to do. Every screen and sentence should reinforce: this is part of the game you already like, it's already working, and here's why it's real.
Two rules follow from this:
1. **Never show a zero balance as the player's first exposure.** Fire the first reward within the first minutes of normal play, so by the time the player notices the counter, something is already in it. "You've already earned 240 coins" converts; "Start earning!" doesn't.
2. **Always pair the reward with the honest reason.** An unexplained reward feels like a scam. One sentence kills the suspicion:
**The reason line โ the single highest-leverage sentence in your rewards copy:**
*"Game studios share their ad revenue with players. The more you play, the more you earn."*
The modal's built-in first-time screen doesn't carry this line, so ship it yourself: a [first-open creative](/earn/sdk/creatives) is the ideal vehicle โ it shows in your game's art style and you can update it without an app release. Never present the reward without the reason.
## Core principles
**Speak in your game's voice, and keep ZBD invisible until it adds credibility.** The player trusts the game they chose to install; the rewards layer should inherit that trust, not spend it. Your fonts, your art, your currency, your tone. ZBD appears at the payout step, where a real payment processor makes the money credible โ never in headers or gameplay copy.
**Pair your currency with its real-money value.** Lead with your themed currency โ it keeps rewards native to your game and lets you be generous with the numbers โ and show the dollar value alongside it: players are ultimately motivated by real money. "You earned 2,500 coins (\$0.25)" gives them both. Concrete dollar milestones remain your strongest motivator: *"Next reward: \$5 gift card โ 80% there."*
**Show progress to the *next* milestone, never a far summit.** A close goal accelerates play; a distant one demotivates. [`GetGiftCards`](/earn/sdk/gift-cards) gives you `unlockedPercent` for exactly this โ bind it to a progress bar toward a named, concrete reward.
**Say "claim", not banking verbs, at first touch.** "Claim your reward" feels like winning; "withdraw" and "cash out" feel like banking, and banking feels like risk. Reserve cash-out vocabulary for the payout flow itself. (The modal's built-in CTA currently reads "Cash Out" โ that copy is fixed product UI inside the claim flow; the vocabulary *you* control on the way there should say claim.)
**Send players to the modal when there's something worth claiming.** The modal opens on their balance and cashout options, so it lands best once there's a balance worth acting on โ a player sent there with nothing to claim just sees an empty wallet. Never gate the value reveal behind sign-up: players don't need any account to see what they've earned.
## The journey, in five moments
| Moment | What happens | Your tool |
| ------------ | ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| **Earn** | "+40 coins" as ambient feedback while playing โ noticed, not interrupting. Tie triggers to high-emotion gameplay moments. | Your game UI + `SendReward` |
| **Reveal** | One screen, after the first reward has landed: the balance they already have, the reason line, one CTA ("Keep playing"). | A [creative](/earn/sdk/creatives) |
| **Progress** | Balance always visible; progress bar toward the next named reward. | Your game UI + [`GetGiftCards`](/earn/sdk/gift-cards) |
| **Claim** | Player taps through with a balance worth claiming, picks a payout option, and celebrates like a level-up, not a receipt. | The ZBD modal (`ShowModal`) |
| **Return** | Milestone reminders while away; tomorrow's fresh earning allowance framed as an opportunity. | [Local notifications](/earn/sdk/local-notifications) + creatives |
## Communication touchpoints
| Touchpoint | When | Goal | Sample copy |
| ------------------ | ----------------------------------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| First reward | First reward event, session 1 | Make it feel real, ambiently | "+500 \[Currency]" toast โ no modal, no interruption |
| Reveal | Right after the first reward | Pair value with the reason | "You've already earned 500 \[Currency] (\$0.05). This game shares its ad revenue with players โ the more you play, the more you earn." |
| Progress milestone | Meaningful % toward a reward | Keep the next goal near | "You're 80% of the way to a \$5 gift card." |
| First claim | Balance worth claiming | Convert | "Your first reward is ready to claim." |
| Limit reached | Player has withdrawn all their limit currently allows | Turn a ceiling into retention | "You've maxed out today's rewards. Come back tomorrow to earn more." |
| Re-engagement | Lapsed player (3+ days) | Re-activation | "Your \[X] \[Currency] are still waiting โ come and claim them." |
## Withdrawal limit messaging
The daily earning allowance is your most powerful retention mechanic โ use it that way.
When a player has withdrawn everything their limit currently allows, the message is: **"Come back tomorrow to earn more."** Not "You've reached your limit." Not "Withdrawal limit: \$0.20." Their allowance to unlock more resets every 24 hours โ frame that as tomorrow's opportunity, not today's restriction.
## Push notifications
Same copy model as [local notifications](/earn/sdk/local-notifications) โ concrete goal over bare number, claim over cash out:
| Scenario | Sample copy |
| ------------------------ | ---------------------------------------------------------------------------- |
| Unclaimed rewards | "You've got \[X] \[Currency] waiting โ come and claim them." |
| Near a milestone | "You're \[X]% of the way to a \[card] gift card. Keep playing to unlock it." |
| Reward ready | "You've unlocked a \[card] gift card โ come and claim it." |
| New day, fresh allowance | "You can earn more rewards today. Time to play." |
| Day 7 streak milestone | "7-day streak! A special reward is in your daily chest." |
No urgency theater ("Limited time! Act now!") โ urgency reads as manipulation to someone who didn't come to shop.
## In-app messages
| Scenario | Sample copy |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Rewards feature launch (existing players) | After their next reward lands: "You've already earned \[X] \[Currency] โ \[Game Name] now shares its ad revenue with players. Keep playing to unlock real rewards." |
| Post-first-reward | "You just earned your first \[Currency]! \[X] more and your first reward is ready to claim." |
| Approaching a milestone | "Almost there โ \[X] \[Currency] until your \$5 gift card." |
| After a long session | "Great session! You earned \[X] \[Currency] today. Keep playing tomorrow for bonus rewards." |
Avoid full-screen takeovers at launch โ a pitch before play is a cold offer. Let the first reward land, then reveal.
## Say this, not that
| Avoid | Say instead | Why |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| "Earn real money!" | "Your playtime just paid out" / "You've already earned \[X] \[Currency]" | Ad-speak triggers skepticism; already-happened is a fact |
| "Withdraw" / "Cash out" (first touch) | "Claim your reward" | Banking verbs feel like risk; claiming feels like winning |
| "Sats", "Bitcoin", "wallet", "Lightning" | "Your rewards" | Crypto vocabulary spooks a casual audience โ keep it behind the payout-methods door (Bitcoin-native games excepted) |
| "Sign up to start earning" | "You've already earned \[X] \[Currency]" | Never gate the value reveal behind effort |
| A far-off summit ("Save up for a \$100 payout") | "Next reward: \$5 gift card โ 80% there" | Near milestones motivate; distant goals demotivate |
| "Limited time! Act now!" | "New bonus round on your next game" | Urgency reads as manipulation |
| "ZBD Rewards" as a header | "\[Game]'s Reward Vault" (or Loot, Treasuryโฆ), "powered by ZBD" small at the payout step | A third-party brand is a context switch |
| "You earned 1,200 \[Currency]" (value hidden) | "You earned 1,200 \[Currency] (\$0.12)" | Players are motivated by real money โ show the value with the currency |
| "Rewards unavailable" (player hasn't earned) | Hide rewards UI silently | Nothing to miss, nothing to explain |
| "ZBD is down" | "Rewards are temporarily unavailable" (players with a balance only) | Never name the platform; never alarm players who haven't started |
Tone throughout: the voice of the *game*, not of a fintech. Short, warm, second person, zero exclamation-mark salesmanship. One idea per screen.
# Restrict Client-Side Reward Sending
Source: https://docs.zbdpay.com/earn/sdk/restrict-client-rewards
POST https://api.zbdpay.com/api/v1/rewards/app/{rewardsAppId}/api-version-blocklist
Disable client-side SendReward calls by adding an API version blocklist entry to enforce server-only rewards.
Block client-side `SendReward` calls by adding an API version blocklist entry to your Rewards App.
This step is optional but highly recommended for apps with high-value rewards or when you need complete control over reward issuance.
Enabling the blocklist rejects client SDK reward calls for this app. You can revert at any time by deleting the blocklist entry.
## Configuration
### Header Parameters
Client identifier (use "developer-dashboard" )
Bearer token for authentication
Format: `Bearer {JWT_TOKEN}`
### Path Parameters
Your Rewards App ID
### Body Parameters
API version to blocklist
Use 1 to block client SDK calls (to block Send Reward v1)
The API route to blocklist
Use "/earn/limited-achievement/reward" to block client reward sending
```json 200 - Success theme={null}
{
"success": true,
"message": "API version blocklist created successfully.",
"data": {
"id": "e2fbc21e-b524-4304-aa55-b20941ede9f6",
"rewardsAppId": "b28e0306-2c06-4092-8d56-a1623d6b97fb",
"version": 1,
"apiRoute": "/earn/limited-achievement/reward",
"createdAt": "2025-10-29T14:06:50.321Z"
}
}
```
```json 400 - Bad Request theme={null}
{
"success": false,
"message": "Invalid parameters"
}
```
```json 401 - Unauthorized theme={null}
{
"success": false,
"message": "Authentication required"
}
```
```json 403 - Forbidden theme={null}
{
"success": false,
"message": "Developer does not own this app"
}
```
```json 409 - Conflict theme={null}
{
"success": false,
"message": "Blocklist entry already exists"
}
```
```json 500 - Server Error theme={null}
{
"success": false,
"message": "Internal server error"
}
```
## Response Fields
| Field | Type | Description |
| -------------- | ------- | ------------------------------------------------ |
| `success` | boolean | Whether the request was successful |
| `message` | string | Description of the result |
| `data` | object | Contains blocklist entry details |
| `id` | string | Unique identifier for this blocklist entry |
| `rewardsAppId` | string | Your Rewards App ID |
| `version` | number | API version that is blocklisted (Send Reward v1) |
| `apiRoute` | string | The API route that is blocklisted |
| `createdAt` | string | ISO 8601 timestamp of when the entry was created |
***
### Response Status Codes
| Code | Description |
| ----- | ------------------------------------------- |
| `200` | Blocklist entry created successfully |
| `400` | Bad request - invalid parameters |
| `401` | Unauthorized - authentication required |
| `403` | Forbidden - developer does not own this app |
| `409` | Conflict - blocklist entry already exists |
| `500` | Internal server error |
## What Happens After Restriction?
After applying this blocklist:
Client SDK calls to send rewards will be rejected
Server calls with API key will continue to work
### Client SDK Behavior
When a client tries to send rewards via v1 after the blocklist is applied:
```
Error: This endpoint requires API key authentication. Please use the API key endpoint instead.
```
### Server Behavior
Your backend server can continue sending rewards normally using the v2 endpoint with your API key.
## Code Examples
```javascript Node.js theme={null}
const rewardsAppId = 'YOUR_REWARDS_APP_ID';
const jwtToken = 'YOUR_JWT_TOKEN';
async function restrictClientRewards(rewardsAppId) {
const response = await fetch(
`https://api.zbdpay.com/api/v1/rewards/app/${rewardsAppId}/api-version-blocklist`,
{
method: 'POST',
headers: {
'z-client': 'developer-dashboard',
'Authorization': `Bearer ${jwtToken}`
},
body: JSON.stringify({
version: 1,
apiRoute: '/earn/limited-achievement/reward'
})
}
);
const data = await response.json();
if (data.success) {
console.log('Client rewards restricted successfully!');
console.log(`Blocklist entry ID: ${data.data.id}`);
return data.data;
} else {
throw new Error(`Failed: ${data.message}`);
}
}
// Apply restriction
await restrictClientRewards('b28e0306-2c06-4092-8d56-a1623d6b97fb');
```
```bash cURL theme={null}
curl --location 'https://api.zbdpay.com/api/v1/rewards/app/{rewardsAppId}/api-version-blocklist' \
--header 'z-client: developer-dashboard' \
--header 'Authorization: Bearer {JWT_TOKEN}' \
--data '{
"version": 1,
"apiRoute": "/earn/limited-achievement/reward"
}'
```
```python Python theme={null}
import requests
import os
jwt_token = os.getenv('JWT_TOKEN')
rewards_app_id = "YOUR_REWARDS_APP_ID"
def restrict_client_rewards(rewards_app_id):
"""Disable client-side reward sending"""
url = f"https://api.zbdpay.com/api/v1/rewards/app/{rewards_app_id}/api-version-blocklist"
headers = {
"z-client": "developer-dashboard",
"Authorization": f"Bearer {jwt_token}"
}
payload = {
"version": 1,
"apiRoute": "/earn/limited-achievement/reward"
}
response = requests.post(url, json=payload, headers=headers)
data = response.json()
if data["success"]:
print("Client rewards restricted successfully!")
print(f"Blocklist entry ID: {data['data']['id']}")
return data["data"]
else:
raise Exception(f"Failed: {data['message']}")
# Apply restriction
restrict_client_rewards("b28e0306-2c06-4092-8d56-a1623d6b97fb")
```
You only need to create a single block list entry with version 1 to disable client-side rewards. To re-enable client rewards, simply delete that block list entry.
You can remove the blocklist entry at any time to re-enable client-side reward sending by deleting the blocklist entry.
## Whatโs Next?
Youโve now restricted client-side reward sending and enforced backend-only control. Next, manage and secure your Rewards App:
* **[Manage via Dashboard](/earn/sdk/manage-via-dashboard)** โ Create, list, and revoke API keys for your Rewards App.
# Withdrawal Limits (Server)
Source: https://docs.zbdpay.com/earn/sdk/revenue-based-earning
How withdrawal limits work, and how to increase them from your server based on the revenue a player generates.
## Overview
With the ZBD Earn SDK, a user's ability to withdraw rewards is tied to the value they generate for your app. Rather than letting users earn unlimited rewards, withdrawal limits act as a cap that scales with revenue โ whether that's from ad impressions, in-app purchases, or time spent engaging with your app.
This keeps your reward economy sustainable: users only withdraw what they've helped you earn.
## How It Works Automatically
When the ZBD Earn SDK is active on the client, the device pings the server every 20 seconds. Based on the reward schedule configured for your app, the player's withdrawal limit automatically increases every minute by a set amount.
The limit itself doesn't reset โ it's the amount the player can withdraw right now, and it carries over. What resets every 24 hours is **how much more limit they can accrue**: the daily allowance is capped, which is what brings players back tomorrow. Any balance above the current limit stays in their balance until the limit rises.
To read a player's current limit from your game client, see [Player Balance & Limits](/earn/sdk/user-balance); to adjust an individual player's limit by hand, see [Manage Withdrawal Limits](/earn/sdk/manage-withdrawal-limits).
This time-based model works as a rough proxy for revenue โ the longer a user is engaged, the more ad impressions and in-app interactions they likely generate, and the more they can withdraw. However, this is a crude estimation. Engagement time doesn't always correlate directly with revenue โ a user could be active for a long time without watching ads or making purchases.
For more precise control, use the server-side API to [increase withdrawal limits based on actual revenue events](#increasing-limits-based-on-actual-revenue).
The reward schedule and per-minute increase amount are configured by your ZBD Customer Success Manager.
## Increasing Limits Based on Actual Revenue
The automatic time-based increase covers most cases, but you may want to increase a user's withdrawal limit based on confirmed revenue events from your server. For example:
* An **MMP callback** confirms an ad impression or completed ad view
* A validated **in-app purchase receipt** confirms the user spent money
* Any other server-side event where a user directly monetized for you
When you receive one of these events, call the withdrawal limit API to increase the user's limit accordingly.
Wiring this to your MMP is the highest-leverage version of this โ and there are options that don't need a backend at all. See [Increasing Limits with MMP Data](/earn/sdk/mmp-postbacks).
### Choosing the right currency
The `currency` field must match the reward currency your game uses:
| Your game | `currency` |
| ------------------------------------------- | ---------- |
| In-game points / soft currency (most games) | `POINT` |
| Bitcoin-denominated | `MSATS` |
Most games use `POINT`, so that's the default. Only set `MSATS` if your game is Bitcoin-denominated.
If you're not sure which currency applies to your game, reach out to your ZBD Customer Success Manager to clarify before setting withdrawal limits.
```bash Points (default) theme={null}
curl --request POST \
--url https://api.zbdpay.com/api/v1/rewards/user//withdrawal-limit \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"amount": 5000,
"currency": "POINT"
}'
```
```bash Bitcoin (sats) theme={null}
curl --request POST \
--url https://api.zbdpay.com/api/v1/rewards/user//withdrawal-limit \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"amount": 5000,
"currency": "MSATS"
}'
```
This gives you precise control โ users who monetize more can withdraw more.
## Recommended Revenue Share
As a starting point, we recommend giving **10-20% of your ad-generated revenue** back to players through rewards. The exact sweet spot varies by game and geography.
Some partners go deeper and set reward amounts per user based on how much that specific user is earning them. As a simpler starting point, you can take the LTV for each country you're live in and give out a percentage based on the player's location.
For more detail on structuring your reward economy, see [Reward Economics](/earn/sdk/reward-economics) and the [Introduction to Rewarded Play](/earn/knowledge-base/rewarded-play) guide.
## Best Practices
* **Keep earn rate aligned with withdrawal capacity.** Avoid situations where users earn significantly more than they can withdraw โ this leads to frustration. For example, crediting 1,000 sats but only allowing 100 sats to be withdrawn is a poor experience.
* **Scale difficulty over time.** Make early rewards easy to earn, then gradually slow down progression as the session continues.
* **Check limits before rewarding.** Use `ZBDController.Instance.GetBalance` to retrieve the user's current balance and withdrawal cap, and adjust your reward flow based on this data.
* **Use revenue events to boost limits.** When a user watches a rewarded ad or makes an in-app purchase, increase their withdrawal limit from your server to reflect the value they generated. See [Increasing Limits with MMP Data](/earn/sdk/mmp-postbacks).
* **Raise the limit, never the balance, from a monetization event.** Crediting rewards balance for watching an ad is cash-for-task and risks app store review. Monetization raises the ceiling; gameplay earns the points.
# Revoke API Key
Source: https://docs.zbdpay.com/earn/sdk/revoke-api-key
DELETE https://api.zbdpay.com/api/v1/rewards/app/{rewardsAppId}/api-key/{apiKeyId}
Revoke an API key to prevent it from being used for authentication
Use this endpoint to immediately prevent it from being used for authentication with your Rewards App.
Revoking an API key immediately prevents it from being used for authentication. Any services using this key will no longer be able to access your Rewards App.
If Send Reward v2 is using this API key, it will stop working once the key is revoked, as the key becomes invalid.
Permanent Action. Once revoked, an API key cannot be reactivated. You will need to create a new API key if needed.
## Configuration
### Header Parameters
Client identifier (use "developer-dashboard" )
Bearer token for authentication
Format: `Bearer {JWT_TOKEN}`
### Path Parameters
Your Rewards App ID
The ID of the API key to revoke
Get this ID from the "List API Keys" endpoint
```json 200 - Success theme={null}
{
"success": true,
"message": "API key revoked successfully."
}
```
```json 400 - Bad Request theme={null}
{
"success": false,
"message": "Invalid UUID"
}
```
```json 401 - Unauthorized theme={null}
{
"success": false,
"message": "Authentication required"
}
```
```json 403 - Forbidden theme={null}
{
"success": false,
"message": "Developer does not own this app"
}
```
```json 404 - Not Found theme={null}
{
"success": false,
"message": "API key not found or does not belong to this app"
}
```
```json 500 - Server Error theme={null}
{
"success": false,
"message": "Internal server error"
}
```
## Response Fields
| Field | Type | Description |
| --------- | ------- | ------------------------------------- |
| `success` | boolean | Whether the revocation was successful |
| `message` | string | Confirmation message |
***
### Response Status Codes
| Code | Description |
| ----- | ------------------------------------------------ |
| `200` | API key revoked successfully |
| `400` | Bad request - invalid UUID |
| `401` | Unauthorized - authentication required |
| `403` | Forbidden - developer does not own this app |
| `404` | API key not found or does not belong to this app |
| `500` | Internal server error |
## Code Examples
```javascript Node.js theme={null}
const rewardsAppId = 'YOUR_REWARDS_APP_ID';
const apiKeyId = 'YOUR_API_KEY_ID';
const jwtToken = 'YOUR_JWT_TOKEN';
async function revokeApiKey(rewardsAppId, apiKeyId) {
const response = await fetch(
`https://api.zbdpay.com/api/v1/rewards/app/${rewardsAppId}/api-key/${apiKeyId}`,
{
method: 'DELETE',
headers: {
'z-client': 'developer-dashboard',
'Authorization': `Bearer ${jwtToken}`
}
}
);
const data = await response.json();
if (data.success) {
console.log('โ
API key revoked successfully!');
console.log('This key can no longer be used for authentication.');
return data;
} else {
throw new Error(`Failed: ${data.message}`);
}
}
// Revoke key
await revokeApiKey(
'b28e0306-2c06-4092-8d56-a1623d6b97fb',
'78b411d8-1f61-4824-97c6-e3c3a571f1c5'
);
```
```bash cURL theme={null}
curl --location --request DELETE 'https://api.zbdpay.com/api/v1/rewards/app/{rewardsAppId}/api-key/{apiKeyId}' \
--header 'z-client: developer-dashboard' \
--header 'Authorization: Bearer {JWT_TOKEN}'
```
```python Python theme={null}
import requests
import os
jwt_token = os.getenv('JWT_TOKEN')
def revoke_api_key(rewards_app_id, api_key_id):
"""Revoke an API key to prevent authentication"""
url = f"https://api.zbdpay.com/api/v1/rewards/app/{rewards_app_id}/api-key/{api_key_id}"
headers = {
"z-client": "developer-dashboard",
"Authorization": f"Bearer {jwt_token}"
}
response = requests.delete(url, headers=headers)
data = response.json()
if data["success"]:
print("โ
API key revoked successfully!")
print("This key can no longer be used for authentication.")
return data
else:
raise Exception(f"Failed: {data['message']}")
# Revoke key
revoke_api_key(
"b28e0306-2c06-4092-8d56-a1623d6b97fb",
"78b411d8-1f61-4824-97c6-e3c3a571f1c5"
)
```
## What Happens After Revocation?
Key can be used for authentication
Key can no longer authenticate
## Try It Out
Ready to revoke an API key? Use our API playground on the right to test with your JWT token.
# Reward Economics
Source: https://docs.zbdpay.com/earn/sdk/reward-economics
Lock in these decisions before writing any code. Most are tunable server-side after launch, but getting them roughly right from the start avoids significant iteration.
This page is for the PM or tech lead handling pre-integration setup. It covers the parameters that govern how your reward program works: how much players can earn, how fast they can withdraw, and how your reward currency maps to real-world value.
The parameters on this page apply to the **in-game currency (Points)** reward model. If you are rewarding in Bitcoin (sats), the core concepts are the same but amounts are denominated in MSATS. Contact your ZBD Customer Success Manager for Bitcoin-mode starting values.
## Core parameters
| Parameter | Recommended starting value | Notes |
| ---------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Reward budget** | 10% of cumulative player LTV | Covers all revenue (IAP + ads). Adjustable server-side at any time. In practice, players earn 30โ50% of their total possible budget. Few players reach the ceiling. |
| **New player baseline** | 10% of average LTV for that GEO | Players in days 0โ7 have zero LTV. They still need a starting reward amount. Suggested range: \$0.005โ\$0.01 per trigger. |
| **Daily withdrawal limit** | 20% of average LTV for that GEO | Set a sensible value from day one (adjustable later). Caps your fraud exposure and creates a daily return habit ("come back tomorrow to earn more"). |
| **Minimum withdrawal** | \$0.01 | Minimum amount a player can withdraw in a single transaction. |
| **Reward ceiling per trigger** | \$0.10 | Maximum reward amount per single event. Caps exposure from any one gameplay moment. |
| **Virtual currency exchange rate** | \$0.0001 per unit | Determines how granular your rewards feel. At this rate, 1,000 units = \$0.10. See [Soft Currency Model](/earn/sdk/soft-currency). |
None of these values need to be perfect at launch โ the reward budget, per-trigger ceiling, and daily withdrawal limit are all adjustable after launch without a client build. Still, give the daily withdrawal limit the most thought up front: it's your most important fraud control, and lowering it later is visible to players who have grown used to the higher cap.
## Reward delivery: client-side vs. server-side
You have two options for how rewards reach players.
### Client-side (simple setup)
Call `SendReward` directly from the Unity client. No backend required. This is the fastest way to get started.
The tradeoff: reward delivery is visible and potentially exploitable on the client. ZBD's withdrawal limit system acts as the primary fraud layer. A player who abuses client-side sends can earn an unlimited balance, but they can only withdraw up to their current limit, which grows slowly.
Use client-side delivery if you're in early development or your reward amounts are low enough that client-side exposure is acceptable.
### Server-side (recommended for production)
Your backend calls the ZBD Earn API to issue rewards. The client never touches the `SendReward` path. You can disable client-side sending entirely in the dashboard.
Server-side delivery is required if you want to:
* Tie reward amounts to actual player LTV (see [LTV-Based Rewards](/earn/sdk/revenue-based-earning))
* Issue rewards based on IAP receipts or MMP revenue events
* Eliminate client-side abuse entirely
Server-side calls authenticate with an Earn API key, which you create in the developer dashboard under **Projects โ your project โ Earn โ Controls**. See [Manage via Dashboard](/earn/sdk/manage-via-dashboard).
Use the key from the **Earn** tab โ it starts with `sdk_live_`. The key in the **API** tab is for the ZBD Payments API and will not work for Earn endpoints.
```bash theme={null}
curl --request POST \
--url https://api.zbdpay.com/api/v1/rewards/user//send \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"amount": 1000,
"currency": "POINT"
}'
```
See [Send Rewards (Server)](/earn/sdk/send-rewards-server) for the full setup guide.
## Withdrawal limits in depth
Every player has two values:
1. **Rewards balance**: what they've earned and not yet withdrawn. You add to this with `SendReward`.
2. **Withdrawal limit**: how much of that balance they can withdraw right now. It grows as they play, up to a capped daily allowance that resets every 24 hours.
The withdrawal limit accrues automatically from the SDK's time-alive signal (a ping every \~20 seconds while the app is open). The accrual rate is set per country tier. A Tier 1 country (US, UK, DE) accrues faster than a Tier 2 country.
### Increasing the withdrawal limit from your server
You can increase a player's withdrawal limit at any time from your backend, for example when your MMP confirms a revenue event.
```bash theme={null}
curl --request POST \
--url https://api.zbdpay.com/api/v1/rewards/user//withdrawal-limit \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"amount": 5000,
"currency": "POINT"
}'
```
If you use server-side reward delivery and send a large reward, you must also increase the withdrawal limit by a matching amount. Otherwise the reward sits in the player's balance and they cannot withdraw it. The default time-based accrual rate is designed for small, gradual rewards; it is not sufficient for backend-driven reward events.
### Checking a player's current balance and limit
Before sending a reward, check the player's current state to avoid over-rewarding:
```bash theme={null}
curl --request GET \
--url https://api.zbdpay.com/api/v2/rewards/users//balance \
--header 'x-api-key: '
```
```json theme={null}
{
"success": true,
"data": {
"rewardsBalance": 30000,
"withdrawableLimit": 1000,
"currency": "POINT"
}
}
```
The `withdrawableLimit` field tells you how much of the balance can currently be withdrawn. If the limit is lower than the balance, the player has earned more than they can currently access.
## In-game currency: exchange rate
*This section applies to Points mode only. Bitcoin mode rewards are denominated in MSATS; no exchange rate configuration is required.*
The exchange rate you set determines how granular your rewards feel in the UI. At \$0.0001 per unit:
| Player earns | Displayed as | Real-world value |
| -------------------- | --------------------- | ---------------- |
| First trigger reward | \~50โ100 units | \~\$0.005โ\$0.01 |
| Daily session | \~500โ2,000 units | \~\$0.05โ\$0.20 |
| Monthly engagement | \~10,000โ30,000 units | \~\$1โ\$3 |
You define the name and appearance of the virtual currency unit. ZBD handles the conversion when the player withdraws. See [Soft Currency Model](/earn/sdk/soft-currency) for display guidelines.
# Security & Fraud Prevention
Source: https://docs.zbdpay.com/earn/sdk/security
How ZBD protects your reward economy from fraud and farming, the enforcement states, and the controls you can tune.
Rewarding real money attracts bad actors. No rewards system is 100% fraud-proof โ but ZBD builds multiple layers of protection into the SDK so you don't have to build them yourself. Some apply automatically; others you can tune to your game's risk tolerance. Here's how they work and where the limits are.
## Enforcement actions
When a user trips a fraud check, ZBD can place them in one of the following states. Each restricts what the user can do, without necessarily cutting them off entirely.
| State | SDK initializes | Earns rewards | Sending rewards works | Can withdraw |
| -------------------- | :-------------: | :-----------------------: | :-------------------: | :------------------: |
| **Normal** | Yes | Yes | Yes | Yes |
| **Limited earnings** | Yes | Yes (slower limit growth) | Yes | Yes (reduced) |
| **Withdrawal lock** | Yes | Yes | Yes | No โ contact support |
| **Disabled** | No | No | No | No |
* **Limited earnings** โ the user can still earn and withdraw, but their [withdrawal limit](/earn/sdk/revenue-based-earning) increases much more slowly, so they can't earn as much as a normal user over time.
* **Withdrawal lock** โ the user can initialize the SDK and earn normally, but when they try to withdraw they're prompted to contact support. ZBD (or you) can lift the lock once the user is judged not to be suspicious.
* **Disabled** โ the SDK will not initialize for this user. They cannot earn, send rewards, or withdraw.
## How the protections work
### Device attestation
[Device attestation](/earn/sdk/attestation-setup) is the foundation. ZBD uses Apple and Google attestation systems to verify that the device running your game is genuine and unmodified โ not rooted, jailbroken, or running in an emulator. When attestation is set up correctly, only users running a legitimate build of your app on a genuine device can increase their withdrawal limit and receive rewards.
Users who attempt to sign up from an emulator or a jailbroken or rooted device are **disabled** automatically โ no configuration needed.
A sophisticated attacker using genuine devices can still pass attestation, which is why Time Alive and server-side reward delivery are additional layers rather than a substitute.
Attestation only protects you when it's configured correctly for both Android and iOS. To enable it, you'll need to provide ZBD with your app's Bundle ID and signing key details. See the [Attestation Setup guide](/earn/sdk/attestation-setup).
### Time Alive
The SDK sends a ping to ZBD servers approximately every 20 seconds while your app is open. This builds up a "Time Alive" value that gates how much a player can withdraw.
A player can earn rewards faster than they can withdraw them โ but they can only access what their Time Alive allows. This means a bad actor who calls `SendReward` repeatedly can inflate their balance, but can't immediately withdraw it. The withdrawal limit accrues slowly enough to make farming unprofitable.
For example: a player might earn 10,000 units in a session, but their withdrawal limit may only allow 200 units per hour of genuine gameplay. Farming requires real time, which limits the economics of an attack.
### Bot and emulator detection
ZBD monitors network behavior and device signals to detect automated reward farming โ bots running at scale, emulators, and coordinated IP patterns. This runs server-side and requires no configuration from you.
For higher-risk integrations, additional verification steps can be enabled at the point of withdrawal, including CAPTCHAs. Talk to your Customer Success Manager if you're concerned about large-scale farming.
### Geolocation
ZBD detects a player's country from their IP address on SDK initialization. This determines:
* Which payout methods are shown to the player
* What withdrawal limits apply (Tier 1 countries like the US, UK, and DE have higher limits than Tier 2 countries)
* Whether the player is in a supported region at all
If a player's detected country is incorrect โ due to a VPN, travel, or a bad IP lookup โ you can override it from the developer dashboard. See [Earn Controls](/earn/sdk/manage-controls-dashboard).
### VPN and proxy detection
VPN and proxy use is a fraud signal: VPNs are commonly used to spoof locations and bypass regional restrictions. ZBD responds in two different ways, at two different moments โ don't confuse them:
* **Policy: limited earnings mode.** A player detected using a VPN or proxy is placed into **limited earnings mode** โ the SDK initializes normally and rewards keep working, but their withdrawal limit grows much more slowly. This is reversible: the modal directs the player to support, and their normal earning rate can be restored once it's resolved.
* **Interference: init failure.** Some VPNs, ad blockers, and private-DNS setups block the network requests device verification depends on. When that happens `Init` fails, and because the modal can't be shown after a failed init, you need to surface your own message asking the player to disable the blocker or VPN and try again. Do not fail silently. See [Error Handling](/earn/sdk/error-handling).
## Configurable protections
Some signals are more prone to false positives, so ZBD treats them conservatively by default. You can tighten them for your game if you see abuse โ just let us know and we'll adjust your configuration.
### Datacenter connections
Most real players connect from a home or mobile network. A datacenter connection is unusual for a genuine player and is far more common among bots and gaming farms.
That said, legitimate users do sometimes appear on a datacenter connection โ for example, university or airport Wi-Fi that routes through one. Because of that, **ZBD allows datacenter sign-ups by default.**
If you notice increased fraud or farming, you can ask ZBD to **disable** or **withdrawal-lock** users on datacenter connections.
**Default: off.** Datacenter users are allowed unless you tell us otherwise. Reach out if you'd like them disabled or placed into withdrawal lock.
### Factory reset
Most users never factory reset their phones. When they do, it's often legitimate โ a second-hand phone, or occasionally a major system update that sets the factory-reset flag.
But factory resets are also a common evasion tactic: a user resets the device to give it a new identity, then plays as a "new" user to escape a disable or a withdrawal limit.
We recommend placing a user into **withdrawal lock** if their device was factory reset within the **last 2 weeks**. You can adjust this threshold โ for example, loosen it if you start seeing an increase in support requests.
### Developer tools
We can detect when a device has developer tooling enabled โ developer options, debugging, and similar low-level access. Plenty of legitimate power users turn these on, but the same tooling is also what's used to inspect, hook, and manipulate an app, so it's a useful signal when weighing other risk factors.
Because developer tooling on its own isn't proof of abuse, **ZBD allows these devices by default.** If you see abuse from devices with developer tooling enabled, you can ask ZBD to **disable** or **withdrawal-lock** them.
### Device model
Fraud farms often run large numbers of the same cheap, low-cost device model. When you notice farming concentrated on a particular device model, ZBD can automatically place users on that model into **withdrawal lock** (or **disable** them).
Because a popular budget phone can also be used by many genuine players, this control is targeted and **off by default** โ you tell ZBD which device model(s) to action.
### Minimum app version
You can require a minimum supported version of your game for withdrawals. Users on anything below it won't be able to withdraw and will be prompted to upgrade โ for example, blocking withdrawals below `v1.1.2`.
This protects you when an older build has a known bug or exploit that could be used to farm rewards.
### Configuring these controls
The configurable protections above are managed by ZBD. To turn any of them on, change a threshold, or set your minimum app version, reach out to your ZBD Customer Success Manager with your preferences. They're off by default, and we can adjust them as your needs change.
## What you can do
The single most effective thing you can do is move to server-side reward delivery:
* Your server sends rewards, not the client โ so the reward path is never exposed
* Disable client-side `SendReward` entirely in the dashboard
* Provide ZBD with your server's IP address so we can allowlist it
A bad actor who reverse-engineers your game client finds nothing useful if rewards can only be issued from a trusted server. See [Send Rewards (Server)](/earn/sdk/send-rewards-server) for setup.
For a deeper education in game security โ how games get attacked and how to harden yours โ work through the [Game Security Course](/earn/knowledge-base/game-security) in the Knowledge Base.
## Honest assessment
No system is foolproof. A sophisticated operation running hundreds of genuine, attested devices with real time-alive can still farm rewards โ slowly. The economics of doing so at scale are unattractive given the low withdrawal limits, but it's worth knowing the limits of these protections. ZBD monitors for patterns at the platform level and will flag anomalies with you.
# Send Rewards from Server
Source: https://docs.zbdpay.com/earn/sdk/send-rewards-api
POST https://api.zbdpay.com/api/v2/rewards/limited-achievement/reward
Use your API key to send rewards securely from your backend using the Earn API (v2).
Send rewards securely from your backend using the **ZBD Earn API (v2)**.
This method ensures all rewards are issued from a trusted environment, preventing client-side abuse and maintaining full control over reward distribution.
Reward Unit Configuration. The reward unit (e.g., 1 satoshi or \$0.10) is defined for your game by your ZBD Customer Success Manager.
## Configuration
### Header Parameters
API key used for authentication. Obtain this from the **Create an API Key** step.
### Body Parameters
The recipient's ZBD Rewards User ID
Reward amount in configured units
Example: `10` (units defined by your ZBD Customer Success Manager)
```json 201 - Success theme={null}
{
"success": true,
"message": "Success.",
"data": [
{
"transactionId": "f7553b31-5ae2-43bf-a200-c0a966e94071"
}
]
}
```
### Response Status Codes
| Code | Description |
| ----- | ----------------------------------------- |
| `201` | Reward sent successfully |
| `400` | Bad request โ invalid parameters |
| `401` | Unauthorized โ invalid or missing API key |
| `404` | Rewards user not found |
| `500` | Internal server error |
```json 400 - Bad Request theme={null}
{
"success": false,
"message": "Invalid parameters"
}
```
```json 401 - Unauthorized theme={null}
{
"success": false,
"message": "Invalid or missing API key"
}
```
```json 404 - User Not Found theme={null}
{
"success": false,
"message": "Rewards user not found"
}
```
```json 500 - Server Error theme={null}
{
"success": false,
"message": "Internal server error"
}
```
## Response Fields
| Field | Type | Description |
| --------------- | ------- | ------------------------------------ |
| `success` | boolean | Whether the request was successful |
| `message` | string | Response message |
| `data` | array | Array containing transaction details |
| `transactionId` | string | Unique transaction identifier |
***
Security Reminder, Never expose your API key in client-side code. Always send rewards from your backend server where your API key is securely stored.
## Code Examples
```javascript Node.js theme={null}
const apiKey = process.env.REWARDS_API_KEY;
async function sendReward(rewardsUserId, amount) {
const response = await fetch('https://api.zbdpay.com/api/v2/rewards/limited-achievement/reward', {
method: 'POST',
headers: {
'x-api-key': apiKey
},
body: JSON.stringify({
rewardsUserId,
amount
})
});
const data = await response.json();
if (data.success) {
console.log(`Reward sent! Transaction ID: ${data.data[0].transactionId}`);
return data.data[0];
} else {
throw new Error(`Failed to send reward: ${data.message}`);
}
}
// Example: Send 10 reward units
await sendReward('f2154d1a-67e6-46f2-b293-b4b226948dc9', 10);
```
```bash cURL theme={null}
curl --location 'https://api.zbdpay.com/api/v2/rewards/limited-achievement/reward' \
--header 'x-api-key: ' \
--data '{
"rewardsUserId": "f2154d1a-67e6-46f2-b293-b4b226948dc9",
"amount": 10
}'
```
```python Python theme={null}
import requests
import os
api_key = os.getenv('REWARDS_API_KEY')
def send_reward(rewards_user_id, amount):
"""Send a reward to a user from your server"""
url = "https://api.zbdpay.com/api/v2/rewards/limited-achievement/reward"
headers = {
"x-api-key": api_key
}
payload = {
"rewardsUserId": rewards_user_id,
"amount": amount
}
response = requests.post(url, json=payload, headers=headers)
data = response.json()
if data["success"]:
print(f"Reward sent! TX ID: {data['data'][0]['transactionId']}")
return data["data"][0]
else:
raise Exception(f"Failed: {data['message']}")
# Example usage
send_reward("f2154d1a-67e6-46f2-b293-b4b226948dc9", 10)
```
## Try It Out
Ready to send your first reward from the server? Use our API playground on the right to test with your API key.
# Send Rewards (Client)
Source: https://docs.zbdpay.com/earn/sdk/send-rewards-client
Instructions for sending rewards to users directly from the client using the ZBD Earn SDK.
# Reward Unit
In the ZBD Earn SDK, rewards are sent as **units**. A **unit** is a **configurable value** determined by your ZBD Customer Success Manager and can represent different amounts, such as **1 satoshi** or **\$0.10**.
For example, if the game is configured so that **1 unit = 1 satoshi**, sending 1 reward will send 1 satoshi. Conversely, if the game is set up so that **1 unit = \$0.10**, sending 1 reward will send the equivalent of \$0.10 in satoshis.
**Your games config** could also be set up so that **a unit would represent a point in your game**, e.g. sending **1 unit** would send **1 "Gem"** which is worth **\$0.10**.
# Sending Rewards
To send a reward to a user you can call:
```csharp theme={null}
ZBDController.Instance.SendReward(1, callback =>
{
if (callback.success)
{
// reward sent
}
else if (callback.maintenance)
{
// Platform is in maintenance mode
Debug.Log("Rewards are temporarily unavailable. Please try again later.");
}
else
{
Debug.LogError("send reward error " + callback.message);
}
});
```
The `SendReward` response object contains the following fields:
```csharp theme={null}
public class ZBDSendRewardResponse
{
public bool success;
public string message;
public bool needsAuthentication;
public bool maintenance;
public ZBDTransactionData[] data;
}
```
Maintenance mode can start while players are actively in a session. Even if `Init` succeeded normally, `SendReward` can return `maintenance = true` at any point during gameplay. Always check the `maintenance` flag on every reward call and display a user-friendly message when it is active. Do not queue or retry rewards during maintenance.
See [Handling Maintenance Mode](/earn/sdk/integration#handling-maintenance-mode) for the full list of affected methods.
For enhanced security and validation, we recommend handling reward distribution from your backend server.
You can learn how to set this up in the **[Server Rewards Overview section](/earn/sdk/send-rewards-server)**
# Overview
Source: https://docs.zbdpay.com/earn/sdk/send-rewards-server
Learn how to securely send rewards from your server using the ZBD Earn API.
The ZBD Earn API allows developers to send in-game or app-based rewards to users securely.
By default, rewards can be issued from **both the client SDK** and your **backend server**.
For stronger security and better control, you can restrict reward sending so that **only your backend** can issue rewards.
This setup uses the **API version blocklist** feature for your Rewards App.
Server-only reward sending is recommended when:
* You perform additional gameplay or business validation before issuing rewards
* Your app offers higher-value rewards or gift card redemptions
* You want to minimize client-side abuse and enforce strict control
## Rate Limiting
The server-side endpoints are rate-limited to **60 requests per second**.
If you exceed this limit, you will receive a `429 Too Many Requests` response. Implement exponential backoff in your retry logic to handle rate limiting gracefully.
## How It Works
Go to Dashboard โ Projects โ Select Your Project โ Earn tab โ Controls sub-tab.
Your Rewards App settings will load automatically.
Click "Create API Key" in the dashboard and save it securely.
**Required** for server-side authentication. The key is shown only once.
Click "Disable Client Send" to block client-side reward sending.
Ensures only your server can send rewards. Recommended for security.
Use your API key with the send rewards endpoint to issue rewards securely.
Next, let's begin with **[managing your Rewards App via the dashboard](/earn/sdk/manage-via-dashboard)**, where you'll create your API key and configure your settings.
# Soft Currency Model
Source: https://docs.zbdpay.com/earn/sdk/soft-currency
Pair your game-themed currency with its real-money value. This page covers how to display rewards throughout your game and why it matters.
This page covers the **in-game currency (Points)** reward model. If you are rewarding players in Bitcoin (sats), the balance display and UX rules are similar โ you show sats rather than virtual units, and the payout options differ (gift cards, Cash App, Speed Wallet, Bitcoin). The critical UX rules and IAP compliance guidance on this page apply regardless of reward currency. See [Before You Build](/earn/sdk/planning#payout-options) for the full comparison.
The ZBD Earn SDK is built around a soft-currency model: players accumulate virtual units in your game's own currency (coins, gems, tokens, whatever fits your universe) and convert them to real money only at the point of withdrawal. ZBD handles the conversion.
Players are ultimately here for real money โ that's the point of the product. Your themed currency keeps rewards native to your game's world and lets you be generous with the numbers; the dollar value is what makes them real. Show both: lead with your currency, and pair it with its real-money value โ "You earned 2,500 coins (\$0.25)".
## What to show where
| Context | Show this | Not this |
| ------------------------------------------ | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| Reward notification during gameplay | "You earned 500 coins (\$0.05)!" | Coins with the real-money value hidden |
| Persistent balance display | "Your balance: 12,400 coins โ \$1.24" | Coins alone, no dollar value |
| First-open reveal (after the first reward) | "You've already earned 500 coins (\$0.05) โ this game shares its ad revenue with players" | "Earn real money while you play!" (an offer, not a reveal) |
| Milestone progress | "Next reward: \$5 gift card โ 80% there" | A far-off summit ("Save up for a \$100 payout") |
| Daily limit reached | "You've maxed out today's rewards. Come back tomorrow to earn more." | "Daily withdrawal limit: \$0.20" |
| Claim screen | ZBD modal handles this. Don't build a custom screen. | n/a |
## Creating your rewards currency
You will need to create a new in-game currency specifically for ZBD rewards. This currency has one purpose: it can be withdrawn as real money via the ZBD modal. Players cannot spend it inside the game.
Name it something that fits your game's universe. The currency name appears in your UI; ZBD never shows it externally. Some examples:
* A pirate game: Doubloons
* A space game: Credits
* A fantasy game: Gold Coins
* A generic implementation: Reward Points
You do not need to build a store or spend mechanic for this currency. Just a balance display and a button that opens the ZBD modal.
## Required UI elements
Two UI components are required:
### 1. Rewards counter (P0)
A persistent display of the player's current rewards balance. Treat it like any other in-game currency display. Always visible, on the home screen and in gameplay.
Tapping the counter opens the ZBD modal directly. Do not add an intermediate screen.
### 2. Rewards button (optional, recommended)
A secondary entry point from the main menu or store page. Some players won't realize the counter is tappable. The button makes the entry point explicit.
Both the counter and the button should open the modal with a single call:
```csharp theme={null}
ZBDModalController.Instance.ShowModal();
```
## Critical UX rules
Breaking any of these measurably reduces cashout rates and player engagement.
**1. Entry point goes directly to the claim flow.** Do not add screens between your entry point and the ZBD modal. One extra screen drops cashout rates. The modal is the product. Get players there fast.
**2. Balance is always visible.** Home screen, store page, during gameplay. Players who can't see their balance disengage. Treat it the same as your gem or coin counter.
**3. Reveal rewards in session 1 โ after the first reward lands.** Don't wait for players to discover the feature on their own, but don't pitch it cold either. Fire the first reward early in normal play, then surface a one-screen reveal: the balance they already have, plus the reason ("this game shares its ad revenue with players"). Players who engage with rewards early retain significantly better โ and a reveal converts where an offer doesn't. A [creative](/earn/sdk/creatives) is the ideal vehicle.
**4. Never fail silently on init failure.** If `Init` fails the modal can't be shown โ it has nothing loaded to display โ so handle the failure yourself: read the reason off the `completion` object and either show your own message (network, VPN/ad-blocker, attestation) or hide the rewards UI entirely (maintenance, unsupported region). A rewards button that does nothing destroys trust before any reward is delivered. See [Error Handling](/earn/sdk/error-handling).
**5. Frame the withdrawal limit as a daily return mechanic.** When a player has withdrawn everything their limit currently allows, the correct message is "Come back tomorrow to earn more," not "You've reached your limit." Their daily allowance for unlocking more resets every 24 hours. Frame it as an opportunity, not a cap.
## Reward moment design
Tie reward triggers to high-emotion gameplay moments, the places where a player already feels a rush of satisfaction. Real-money rewards amplify that feeling. A reward that arrives when a player just cleared all their daily missions feels like recognition. A reward that arrives at an arbitrary moment feels like a pop-up.
Good trigger placements:
* Level completions and milestone unlocks
* Daily mission chest rewards
* Skill combos or streak achievements
* Refer-a-friend conversions
Avoid:
* Passive or arbitrary placements (watching an ad, launching the app)
* Direct rewards tied to purchases (use time-limited boosts instead, see below)
## IAP and subscription rewards
Never tie a specific reward amount directly to a purchase. Saying "Buy this item and earn 2,000 coins" violates both Apple App Store and Google Play policies. Use time-limited boosts instead.
If you want to use ZBD rewards as part of an IAP or subscription offer, the reward must be a **time-limited earning boost**, not a fixed currency grant.
| Do this | Not this |
| ---------------------------------------------- | --------------------------------------------------- |
| "+20% Reward Coins for 24h with this purchase" | "Earn 2,000 Reward Coins when you buy this upgrade" |
| "VIP members earn 50% more rewards" | "Get \$1.00 in rewards with your subscription" |
A boost accelerates earning. It doesn't guarantee a specific withdrawal amount. This keeps your game compliant with both app stores and limits your financial exposure.
If you want to offer rewards as a subscription benefit, we recommend testing the boost percentage (typically +20โ50%) once your base integration is stable and your reward economics are well understood.
# Troubleshooting
Source: https://docs.zbdpay.com/earn/sdk/troubleshoot
General troubleshooting guides for the ZBD Earn SDK.
# Build Issues
* If you encounter build issues, it is important to check that the Android and iOS deployment targets and minimum API level are set properly. Refer to [Compatibility](/earn/sdk/compatibility) for more information.
* If you receive a sign up error, please confirm that your Bundle ID / Package Name is the same as the one used whilst setting up the SDK configuration.
* Make sure the stripping level is set to low or minimum in the Unity build settings.
If you get the following build errors on Android please check the **[Building for Android](/earn/sdk/building#building-for-android)** section.
# Performance Issues
The ZBD Earn SDK forces hardware acceleration in Android builds, this can cause some issues with games.
If you notice performance issues after integrating the ZBD Earn SDK you can delete the `ModifyUnityAndroidAppManifestSample.cs` file in the `Assets/Editor/` folder.
# Unity Input Settings Issues
To ensure maximum compatibility when working with the ZBD Earn SDK, it is recommended to adjust Unityโs input configuration.
The Unity Editorโs WebView may influence how input is handled, so setting **Active Input Handling** to **Both** helps maintain consistent support across the Old Input Manager and the New Input System.
In Unity, go to Edit โ Project Settings.
In the Project Settings window, select Player, then scroll to the Other Settings section.
Find Active Input Handling and set it to Both โ regardless of whether your project uses the Old Input Manager or the New Input System.
# Testing from an Unsupported Region or Unity Editor
If your game is configured to reward players only in specific regions, but you as a developer are located outside of those regions, or if you are testing directly in the Unity Editor, you may run into issues with the SDK's anti-fraud system.
The anti-fraud system may block sign-ups or gameplay from the Unity Editor or from unsupported regions. To work around this during development and testing:
Connect to a VPN set to one of your game's supported regions and sign up for a ZBD account from within the game. This ensures your account is created in a supported region.
Signing up through a VPN may place your account in **limited earnings mode**. This is expected behavior from the anti-fraud system.
Your Support ID is logged to the console when the SDK is initialized. Look for the following log entry:
```
ZBD: SupportId:
```
To remove the limited earnings restriction, you have two options:
* **Contact your ZBD Customer Success Manager** โ Send them your Support ID so they can whitelist your device.
* **Use the Developer Dashboard** โ If you have been granted access to the device whitelisting feature, you can whitelist your device directly from the dashboard.
This process is only necessary for development and testing purposes. Your end users in supported regions will not encounter these restrictions during normal gameplay.
# Integrity Check Failures
If you receive either of the following errors:
```
{"success":false,"message":"Failed progress validation.","errorCode":"400P4V"}
```
```
ZBD: Error sending reward: {"success":false,"message":"Failed reward validation.","errorCode":"400R5V"}
```
This means your editor or device is failing ZBD's integrity checks. This typically happens because:
* You are testing from a **non-mobile device** (e.g. Unity Editor on desktop) rather than a real mobile device.
* Your **attestation has not been set up** correctly. Refer to the [Attestation Setup](/earn/sdk/attestation-setup) guide.
To resolve this, contact your ZBD Customer Success Manager and provide your **Support ID** so they can whitelist it. You can find your Support ID in the console logs when the SDK initializes:
```
ZBD: SupportId:
```
# URL Scheme
Source: https://docs.zbdpay.com/earn/sdk/url-scheme
Handle secure user redirection back to your app after ZBD OAuth login using a custom URL scheme.
To enable the ZBD app to redirect back to your game after the user connects via the ZBD app,we need to know your game's URL scheme.
If youโve already set this up, please share it with your ZBD Customer Success Manager.
If this is your first time setting it up, donโt worry. The process is straightforward for both iOS and Android.
## In Unity:
If youโve already added the custom URL scheme to your Android Manifest (AndroidManifest.xml) or iOS plist file, leave the field empty.
# Player Balance & Limits
Source: https://docs.zbdpay.com/earn/sdk/user-balance
Retrieve a player's rewards balance and current withdrawal limit from your game client using the SDK.
Use `GetBalance` to retrieve a player's current rewards balance and withdrawal limit from your game client โ both come back in a single call.
A player's withdrawal limit is how much of their balance they can withdraw right now. It grows automatically as they play, on the reward schedule configured for your app, and you can raise it from your server based on revenue events โ see [Withdrawal Limits (Server)](/earn/sdk/revenue-based-earning).
```csharp theme={null}
ZBDController.Instance.GetBalance(callback =>
{
if (callback.success)
{
// Balance in satoshis (not millisatoshis)
Debug.Log("Balance: " + callback.balance);
// Withdrawal limit in satoshis (not millisatoshis)
Debug.Log("Withdrawal limit: " + callback.withdrawalLimit);
}
else if (callback.maintenance)
{
// Platform is in maintenance mode โ do not update the displayed balance
Debug.Log("Balance temporarily unavailable.");
}
else
{
Debug.LogError("GetBalance failed: " + callback.message);
}
});
```
Use this before sending rewards to avoid crediting more than the player can currently withdraw.
If a player's rewards balance exceeds their withdrawal limit, the excess carries forward โ they can access it as the limit grows through continued play or a server-side increase. How much the limit can grow in a day is capped, and that daily allowance resets every 24 hours. See [Reward Economics](/earn/sdk/reward-economics) for guidance on configuring withdrawal limits.
The `maintenance` flag can return `true` at any point, including mid-session. When active, do not update the displayed balance. See [Error Handling](/earn/sdk/error-handling) for full maintenance mode guidance.
# ZBD Documentation
Source: https://docs.zbdpay.com/get-started
Global Payment Service Provider for Gaming and Interactive Entertainment
The global payment infrastructure built for the speed and scale of gaming. Process thousands of transactions per second, enable true micropayments, and unlock new monetization models โ all through a single API.
Lightning-fast global settlements that keep pace with gameplay
Finally make 1 cent transactions profitable with fees under \$0.0001
One API for global payments. No banking relationships needed.
## Why Developers Choose ZBD
**Built for Gaming Scale**
* Handle millions of transactions for players
* Microtransactions that actually make economical sense
**New Monetization Models**
* Pay-per-minute gameplay
* Real-money tournaments
* Cross-game economies
* Player-to-player trading
**Modern Payment Infrastructure**
* Instant global settlements
* Multi-currency support (USD, EUR, BTC)
* Developer-first APIs
**Use Cases**
* Streaming payments
* Creator economy
* International payouts
**Increase Revenue**
* Higher conversion with lower payment friction
* Unlock previously unprofitable price points
* Expand to Tier 1 global markets compliantly instantly
**Reduce Costs**
* 90% lower fees than cards for small payments
* Eliminate chargeback losses
## Products
### ๐ฎ ZBD Earn
Drive user engagement, boost retention, and unlock new revenue streams by offering real-money rewards and incentives across your experiences.
Unity SDK for adding real-money rewards to your games. Drag, drop, monetize.
Programmatically distribute rewards. Perfect for tournaments and achievements.
OAuth2 authentication with built-in wallet creation and KYC.
ZBD app for players to manage and cash out rewards.
### ๐ณ Payments
Enable instant global payments in your apps and games with multi-currency support for Bitcoin, Lightning Network, USD, EUR and Stablecoins. We handle the complexity of payments infrastructure so you can focus on building amazing experiences.
Let users buy Bitcoin directly in your app. First widget with Lightning Address support.
Send and receive instant payments globally. Lightning, on-chain, and fiat settlements.
Production-ready SDK with TypeScript examples and starter templates.
Enable AI agents and LLMs to process payments autonomously.
Getting started with ZBD Payments is as simple as:
```bash TypeScript / Node.js theme={null}
pnpm install @zbdpay/payments-sdk
```
```bash Rust theme={null}
cargo add zebedee-rust
```
```bash HTTP theme={null}
curl -X GET https://api.zbdpay.com/v0/wallet \
-H "apikey: YOUR_API_KEY"
```
## Quick Start Guides
[Schedule a 15-minute call](https://zbd.one/sales) with our team to discuss your use case and get access
Pick the products that fit your needs - Payments, Rewards, or both
Use our comprehensive documentation and SDKs to integrate in hours, not weeks
Go live with production access and our team's support
### Start with These Guides
Send your first payment in minutes
Add fiat-to-crypto in your app
Monetize your Unity game
## Developer Resources
Complete API documentation with examples
TypeScript, Go, Rust, C# and more
## Ready to Transform Your Payment Experience?
**Schedule a Demo** - Talk to our team about your use case and get your API keys. Most developers are up and running within 24 hours.
# Add Funds to your Account
Source: https://docs.zbdpay.com/get-started/add-funds
Fund your ZBD wallet to start processing instant global payments
Your ZBD wallet is the heart of your payment operations. Add funds in seconds and start moving money at the speed of the internet. Multiple funding options ensure you can always keep your wallet topped up.
**Lightning Speed** - Deposits via Lightning Network are instant. As soon as you send funds, they're available in your wallet. No waiting for confirmations.
## Funding Options
Instant deposits from any Lightning wallet or exchange
USD to Lightning coming soon via ZBD Ramp
## Lightning Deposits (Instant)
The fastest way to add funds. Generate an invoice and pay from any Lightning-enabled service.
### How to Generate a Lightning Invoice
Go to your project's Wallet tab in the dashboard
Select "Lightning Invoice" as your deposit method
Specify how many satoshis you want to add
Scan QR code or copy invoice to pay from another Lightning-compatible wallet
## Fiat Ramp (Coming Soon)
**Direct USD Deposits** - Soon you'll be able to buy Bitcoin directly in the Developer Dashboard using ACH bank transfers. Funds will arrive instantly at your project's Lightning address.
### What to Expect
* **Bank Transfer** - Connect your business bank account
* **Instant Conversion** - USD โ Bitcoin โ Your wallet
* **Competitive Rates** - Better than most exchanges
* **No External Accounts** - Everything within ZBD
Want early access to fiat deposits? [Talk to your success manager](https://zbd.one/sales).
## Welcome Vouchers
New to ZBD? Your Customer Success Manager can provide welcome vouchers to help you get started.
### How It Works
Mention you'd like to test the platform
Get an 8-character code via email
Go to Vouchers and enter code
Use funds to test integrations
### Best Practices
Keep 20% extra for payment spikes
Monitor low balance thresholds
**Contact Support** - Our team can guide you through the funding process and recommend the best approach for your use case.
# API Keys
Source: https://docs.zbdpay.com/get-started/api-keys
Generate and secure your keys to unlock Lightning-fast payments
API keys are your passport to the ZBD platform. With a single key, you can send payments globally, create invoices, manage wallets, and build the future of digital payments.
**Security First** - API keys are server-side only and grant full access to your project's wallet. Never expose them in client-side code, mobile apps, or public repositories.
## Your First API Key
Navigate to your project's **API** tab to view and manage your API keys.
### What You Can Do With API Keys
Pay any Lightning Address or invoice instantly
Generate invoices and process incoming payments
Check balances and transaction history
Issue redeemable payment links
## Quick Start Example
Once you have your API key, you're ready to send your first payment:
```bash cURL theme={null}
curl -X POST https://api.zbdpay.com/v0/payments \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": "1000",
"description": "First payment!",
"internalId": "test-001",
"callbackUrl": "https://myapp.com/webhook"
}'
```
```javascript Node.js theme={null}
const payment = await fetch('https://api.zbdpay.com/v0/payments', {
method: 'POST',
headers: {
'apikey': process.env.ZBD_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount: '1000',
description: 'First payment!',
internalId: 'test-001'
})
});
```
## Security Best Practices
### ๐ Secure Storage
```bash theme={null}
# .env file (never commit this!)
ZBD_API_KEY=your_api_key_here
# Access in code
const apiKey = process.env.ZBD_API_KEY;
```
**Best for**: Local development and deployment platforms
Use dedicated secret management services:
* **AWS Secrets Manager**
* **Google Secret Manager**
* **HashiCorp Vault**
* **Azure Key Vault**
**Best for**: Production environments
Configure secrets in your deployment pipeline:
* **GitHub Actions**: Repository secrets
* **Vercel**: Environment variables
* **Heroku**: Config vars
* **Docker**: Build secrets
**Best for**: Automated deployments
### ๐ก๏ธ IP Allowlisting
Restrict API access to specific IP addresses for maximum security:
**How to Configure:**
1. Navigate to your project's API tab
2. Click "API Access Controls"
3. Add your server's IP addresses
4. Save changes
**Dynamic IPs?** If your infrastructure uses dynamic IPs (like serverless functions), consider using a proxy service with static IPs or contact our team for alternative security options.
## Key Rotation & Regeneration
Regular key rotation is a security best practice. Here's when and how to do it:
### When to Rotate Keys
* โฐ **Scheduled**: Every 90 days as a best practice
* ๐จ **Immediately if**:
* Key exposed in client code
* Committed to version control
* Employee with access leaves
* Suspicious activity detected
### How to Regenerate
**Important**: Regenerating creates a new key and immediately disables the old one. Update your applications before regenerating to avoid downtime.
### Zero-Downtime Rotation Strategy
Schedule during low-traffic period
Test new key in staging environment
Update production with new key
Once deployed, regenerate in dashboard
Watch logs for any failed authentications
## API Key Permissions (Coming Soon)
**Scoped Permissions** - Soon you'll be able to create keys with limited permissions:
* Read-only keys for analytics
* Payment-only keys for point-of-sale
* Receive-only keys for donation pages
## Testing Your API Key
Use this simple test to verify your key is working:
```bash cURL theme={null}
curl https://api.zbdpay.com/v0/wallet \
-H "apikey: YOUR_API_KEY"
```
```javascript Node.js theme={null}
// Quick test script
const testApiKey = async () => {
const response = await fetch('https://api.zbdpay.com/v0/wallet', {
headers: { 'apikey': process.env.ZBD_API_KEY }
});
if (response.ok) {
console.log('โ
API key is valid!');
const wallet = await response.json();
console.log('Balance:', wallet.data.balance, 'sats');
} else {
console.error('โ API key test failed:', response.status);
}
};
testApiKey();
```
## Ready to Build?
You now have everything needed to start integrating ZBD payments into your application.
Use our SDKs to send your first Lightning payment
Explore all available endpoints
# Create Account
Source: https://docs.zbdpay.com/get-started/create-account
Get started with ZBD in minutes
Creating a ZBD account gives you access to the most advanced payment infrastructure in gaming. Here's how to get started.
## Before You Begin
**Business Account Required** - ZBD is designed for businesses, game studios, and professional developers. You'll need to complete business verification (KYB) to access production features.
### What You'll Need
* Company name and website
* Business registration details
* Tax ID / EIN (US) or equivalent
* UBO (if applicable)
* Your work email address
* Phone number
* Your role at the company
## Account Creation Process
### Step 1: Schedule Your Onboarding Call
**Required First Step** - Our team will guide you through the onboarding process and ensure ZBD is the right fit for your needs. Schedule a quick 15-minute call to get started.
During the call, we'll discuss:
* Your use case and integration needs
* Pricing based on your expected volume
* Compliance requirements for your region
* Timeline to get you live
### Step 2: Create Your Account
After your sales call, you'll receive an invitation to create your account.
1. **Access the Dashboard** - Use the link provided by our team
2. **Enter Your Information** - Fill in your user details
3. **Set a Strong Password** - Must include uppercase, lowercase, numbers, and symbols
4. **Accept Terms** - Review and accept our Terms of Service
### Step 3: Verify Your Email
Check your inbox for a 6-digit verification code.
Verification codes expire after 10 minutes. If yours expires, you can request a new one.
### Step 4: Complete Business Verification
After email verification, you'll be guided through our KYB (Know Your Business) process:
Provide company registration documents
Verify business ownership structure
Describe your intended use of ZBD
**Verification Timeline**: Most businesses are verified within 24 hours. Complex cases may take 2-3 business days.
## After Account Creation
Once verified, you'll have access to:
Set up your first project and get API keys
Fund your wallet to start processing payments
Start integrating with our quickstart guides
## Ready to Transform Your Payments?
**Schedule Your Onboarding Call** - Join thousands of developers building the future of digital payments. Our team is ready to help you get started.
# Create Project
Source: https://docs.zbdpay.com/get-started/create-project
Set up your first ZBD project and unlock the power of Lightning payments
Projects are your gateway to the future of payments. Each project gets its own Lightning wallet, API keys, and analytics โ everything you need to integrate ZBD into your app or game.
**One Account, Multiple Projects** - Create separate projects for different apps, environments, or business units. Each project has isolated wallets and permissions.
## Why Projects Matter
Each project has its own Bitcoin Lightning wallet with separate balances
Get your API key per project with specific permissions
Track performance metrics per project independently
## Creating Your First Project
### Step 1: Navigate to Projects
From your dashboard, click **Projects** in the sidebar and then the **+ New Project** button.
### Step 2: Configure Your Project
Fill in your project details in the creation modal:
### Step 3: Create and Explore
Click **Create Project** and you will be redirected to your new project's wallet page.
## What Happens Next
Once your project is created, you instantly get:
A programmable Bitcoin Lightning wallet ready for instant global payments
Head to the API Keys tab to generate your first key
Use our SDKs to send your first payment in minutes
## Project Best Practices
### Environment Strategy
Create separate projects for each environment:
* **MyGame - Development**: Sandbox testing with test transactions
* **MyGame - Staging**: Pre-production testing with real money
* **MyGame - Production**: Live environment for real users
This separation ensures clean analytics and prevents accidental transactions.
Organize projects by application:
* **Mobile Game**: iOS and Android API keys sharing one wallet
* **Web Platform**: Separate wallet with frontend/backend keys
* **Tournament System**: Isolated wallet for prize pools
Each app gets its own analytics and wallet management.
### Common Use Cases
* **Separate projects per game** for isolated economies
* **Shared project for tournaments** across multiple games
* **Dev/staging/prod projects** for each game
* **API product project** for usage-based billing
* **Marketplace project** for user-to-user payments
* **Regional projects** for compliance separation
* **Client projects** with isolated wallets
* **Demo project** for POCs and sales
* **Template projects** for quick starts
## Quick Actions After Creation
Generate your first API key to start integrating
Add funds to start processing payments
Use our SDK to send your first Lightning payment
## Success! You have Created Your First Project ๐
You are now ready to revolutionize payments in your application. Your Lightning wallet is active and waiting for its first transaction.
**Generate Your API Keys** - Get your API credentials and send your first payment in under 5 minutes.
***
Quick start guides for all languages
Explore all available endpoints
# Developer Dashboard
Source: https://docs.zbdpay.com/get-started/dev-dashboard
Your command center for global payments and real-money rewards
The ZBD Developer Dashboard is where you manage everything โ from API keys to real-time analytics. One dashboard to power payments across your entire platform.
## What You Get
Create multiple projects with isolated wallets and API keys
Track transactions, volumes, and user activity live
Generate and manage API keys to control the Project Wallet
Programmable Lightning wallets with instant settlement
Sandbox environment and testing utilities
KYB verification and transaction monitoring included
## Dashboard Features
### ๐ Quick Actions
* **Send Payments** - Pay any Lightning Address instantly
* **Create Charges** - Generate payment requests with QR codes
* **Issue Vouchers** - Create redeemable payment links
* **Download Reports** - Export transaction data
### ๐ง Developer Tools
* API playground for testing endpoints
* Webhook management and logs
* SDK quick-start guides
* Code examples in multiple languages
## Who Uses the Dashboard?
**Power Your Game Economy**
* Manage in-game currencies and rewards
* Track player transactions in real-time
* Set up tournament prize pools
* Monitor cross-game economies
**Scale Global Payments**
* Multi-project support for different products
* Compliance and regulatory tools built-in
* White-label payment solutions
* Enterprise-grade reliability
Perfect for payment providers serving multiple markets
**Monetize Anything**
* Add payments to any app in minutes
* Usage-based billing for APIs
* Creator monetization tools
* Micropayment capabilities
From social apps to productivity tools
## Getting Access
ZBD requires business verification to ensure platform security and compliance. Individual developers can explore our documentation while preparing for business onboarding.
[Talk to our sales team](https://zbd.one/sales) about your use case
Quick business verification process (usually same day)
Receive your credentials and start building
## What's Next?
Step-by-step account creation guide
Set up your first project and get API keys
**Schedule a Demo** - See the dashboard in action and get your access. Most businesses are verified within 24 hours.
# Login with ZBD
Source: https://docs.zbdpay.com/get-started/login-with-zbd
# Manage Project
Source: https://docs.zbdpay.com/get-started/manage-project
Edit project settings, manage access, and control your project lifecycle
Keep your projects organized and up-to-date. From updating branding to archiving completed projects, here's everything you need to know about project management in ZBD.
**Critical Actions**: Some project management actions (like archiving) are irreversible. Always ensure you understand the implications before proceeding.
## Editing Project Details
Your project's details appear in payment flows and help organize your dashboard. Keep them current and professional.
### What You Can Edit
Appears in payment notifications to users
Internal notes for your team's reference
Your logo shown during payment flows
### How to Edit
Editing a project details is simple:
1. Navigate to **Projects** page
2. Find your project card
3. Click the project card to enter project view
4. Select **Settings** tab
5. Edit the details you want to change
6. Click **Update**
## Archiving Projects
When a project is no longer needed, archiving keeps your dashboard clean while preserving records.
**Immediate Effects of Archiving**:
* API key is permanently disabled
* No new transactions possible
* Wallet funds auto-transfer to main wallet
* Action cannot be undone
### When to Archive
* Project development is cancelled
* App/game has been sunset
* Migrating to a new project
* Cleaning up test projects
### How to Archive
Navigate to the project you want to archive
Click the Settings tab on the top
Scroll down and choose **Archive Project** from advanced options
Read the warning and click **Archive** to confirm
## Next Steps
Secure your API keys and set up IP allowlisting
Track your project's financial activity
Discuss enterprise features for larger teams
***
**Pro Tip**: Set up a project naming convention early. As you scale, having organized projects with clear names like "AppName-Prod-US" and "AppName-Dev-EU" will save hours of confusion.
# Project Wallet
Source: https://docs.zbdpay.com/get-started/project-wallet
Understand how Bitcoin Lightning wallets power your projects
Every ZBD project comes with its own programmable Bitcoin Lightning wallet. This isolated wallet gives you complete control over your project's funds, transactions, and financial operations โ all accessible via API.
**Core Architecture**: 1 Project = 1 Wallet = 1 API Key. This separation ensures clean accounting, isolated risk, and granular control.
## How Project Wallets Work
Each project has its own Bitcoin balance, completely separate from other projects
Every transaction through your API key affects only this wallet
Balance changes instantly with Lightning-speed settlements
### Wallet Architecture
```mermaid theme={null}
graph TB
A[Developer Account] --> B[Main Developer Wallet]
A --> C[Project 1]
A --> D[Project 2]
A --> E[Project 3]
C --> F[Project 1 Wallet]
D --> G[Project 2 Wallet]
E --> H[Project 3 Wallet]
F --> I[API Key 1]
G --> J[API Key 2]
H --> K[API Key 3]
style B fill:#f9f,stroke:#333,stroke-width:2px
style F fill:#bbf,stroke:#333,stroke-width:2px
style G fill:#bbf,stroke:#333,stroke-width:2px
style H fill:#bbf,stroke:#333,stroke-width:2px
```
## Wallet Dashboard
Navigate to your project's **Wallet** tab to access comprehensive financial data:
## Wallet Management Best Practices
**Buffer Recommendation**: Keep 1.5x your daily volume
* Monitor daily burn rate
* Automate top-ups if possible
* Plan for traffic spikes
**Protect Your Funds**
* Use IP allowlisting for API keys
* Enable 2FA on your account
* Regularly audit transactions
* Set up webhook notifications
**When to Use Multiple Projects**
* Different apps/games
* Staging vs production
* Regional separation
* Department budgets
**Financial Tracking**
* Export transactions monthly (talk to your Customer Success Manager)
* Use reference IDs for reconciliation
* Track by project for clean books
* Integrate with accounting software
## Common Wallet Operations
### Check Balance
```javascript Node.js theme={null}
const wallet = await zbd.getWallet();
console.log(`Balance: ${wallet.balance} sats`);
console.log(`USD Value: $${wallet.balanceUSD}`);
```
```bash cURL theme={null}
curl https://api.zbdpay.com/v0/wallet \
-H "apikey: YOUR_PROJECT_API_KEY"
```
### Transfer Between Projects
```javascript Node.js theme={null}
// Transfer funds between your projects
const result = await zbd.internalTransfer({
amount: 10000,
receiverWalletId: 'project_abc123',
reference: 'Budget reallocation'
});
```
```bash cURL theme={null}
curl -X POST https://api.zbdpay.com/v0/internal-transfer \
-H "apikey: YOUR_PROJECT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": "10000",
"receiverWalletId": "project_abc123"
}'
```
## Next Steps
Make your first Lightning payment
Monitor transaction updates in real-time
***
**Pro Tip**: Use project wallets to isolate financial risk. If one project has issues, your other projects remain unaffected. This is especially important for testing new features or running experiments.
# Identity Verification
Source: https://docs.zbdpay.com/get-started/verify-identity
Complete your profile to unlock production access and higher limits
Identity verification unlocks the full power of ZBD. As a regulated financial institution, we're required to verify all businesses before enabling production access โ but we've made it as painless as possible.
**Why Verification Matters** - ZBD is a licensed money transmitter in the US (MSB/MTL) and an Electronic Money Institution (EMI) in the EU under MiCAR. This means your funds are protected and your business is compliant.
## Why We Verify
Get LIVE API keys to process real payments
Increase transaction and wallet balance limits
Operate legally and compliantly in Tier 1 global markets (US, EU, etc)
Your money is safeguarded by regulated entities
### Regulatory Framework
As a global payment provider, ZBD maintains licenses and registrations worldwide:
**Federal Registration**
* FinCEN registered Money Services Business (MSB)
* BSA/AML program in place
**State Licenses**
* Money Transmitter Licenses (MTL) in required states
* Compliance with state-specific regulations
**MiCAR Compliance**
* Electronic Money Institution (EMI) license
* Access to all EU member states
* Segregated customer funds
* Regular audits and reporting
**International Standards**
* FATF compliance guidelines
* Sanctions screening (OFAC, UN, EU)
* SOC 2 Type II
### Individual Verification (KYC)
For authorized representatives and beneficial owners:
* Passport (preferred)
* Driver's license
* National ID card
* Quick selfie
* Automated verification
* Instant results
## Complete Your Profile
Beyond regulatory requirements, a complete profile helps us serve you better:
### 1. Verify Email Address โ
Already completed during signup, but ensure your business email is verified.
### 2. Choose Your Gamertag
Your ZBD Gamertag appears in payment notifications to users:
**Choose Wisely** - Your Gamertag is visible to anyone you pay. Use your business name or something professional like "AcmeGameStudio" rather than "xXx\_PayMaster\_xXx".
Good examples:
* `YourCompanyName`
* `StudioNameGames`
* `AppNameOfficial`
### 3. Upload Profile Image
Add your company logo for brand recognition in payment flows.
Requirements:
* Square image (1:1 ratio)
* PNG or JPG format
* Under 3MB
* Clear on small screens
### 4. Enable Two-Factor Authentication (2FA)
**Required for Production** - 2FA is mandatory for all accounts with production access. Protect your funds and API keys.
How to enable:
1. Navigate to **Account > Security**
2. Click **Enable 2FA**
3. Scan QR code with authenticator app
4. Enter verification code
5. Save backup codes securely
Recommended authenticator apps:
* Google Authenticator
* Authy
* 1Password
* Microsoft Authenticator
### 5. Complete Identity Verification
The final step to unlock production access:
Go to **Account > Verification** and click **Verify Now**
Take photo of passport or driver's license
Follow on-screen instructions for liveness check
Usually completed within minutes
## What Happens After Verification?
* โ
**Production API Keys** - Start processing real payments
* โ
**Increased Limits** - Higher transaction and balance limits
* โ
**Full Dashboard Access** - All features unlocked
* โ
**Priority Support** - Dedicated success manager
* ๐ **Transaction Monitoring** - Automated AML checks
* ๐ **Periodic Reviews** - Annual KYB refresh
* ๐ **Sanctions Screening** - Real-time checks
* ๐ **Regulatory Updates** - We handle compliance changes
## Common Verification Issues
Ensure photos are clear, uncropped, and all text is readable
Account name must match ID exactly
Utility bills must be recent (\< 3 months)
Ensure all pages are included and documents are current
## Need Help?
**Having Issues?** - Our compliance team can guide you through verification. Most issues are resolved in one call.
***
**Compliance = Trust** - Our regulatory compliance isn't red tape โ it's your protection. It means your funds are safe, your business is legitimate, and you can scale globally without worry.
# ZBD App
Source: https://docs.zbdpay.com/get-started/zbd-app
# ZBD Earn SDK
Source: https://docs.zbdpay.com/get-started/zbd-earn-sdk
# ZBD Payments API
Source: https://docs.zbdpay.com/get-started/zbd-payments-api
# ZBD Ramp
Source: https://docs.zbdpay.com/get-started/zbd-ramp
# ZBD Payments
Source: https://docs.zbdpay.com/payments
Global instant payments infrastructure for apps and games with Bitcoin, Lightning Network, and multi-currency support
Build global payment experiences that actually work. ZBD Payments provides instant, low-cost money movement across major Tier 1 markets (US, EU, etc) using Bitcoin, Lightning Network, and traditional currencies - all through a single API.
Sub-second global payments with Lightning Network. No waiting, no borders.
Accept and send USD, EUR, and BTC. One API, multiple currencies.
Built for millions of microtransactions. Handle millions of transactions with ease.
Simple APIs, comprehensive SDKs, and documentation that doesn't suck.
## Why ZBD Payments?
### The Problem with Traditional Payments
**Card Networks:**
* Minimum \~\$0.30 fee per transaction
* Can't send 0.10c when fees are 0.30c
* Chargebacks eat profits
* High decline rates
**ZBD Payments:**
* Send amounts as low as \$0.001
* Proportional fees (typically \< 1%)
* No chargebacks
* Near 100% success rate
**Legacy Providers:**
* Complex integration (weeks/months)
* Different APIs per country
* Compliance nightmares
* Poor documentation
**ZBD Payments:**
* Single API globally
* 5-minute quick start
* Compliance built-in
* Stripe-quality docs
**Traditional Systems:**
* 3-5 day settlement times
* \$25-45 fees per transaction
* Limited to banking hours
* Blocked in many countries
**ZBD Payments:**
* Instant settlement (\< 1 second)
* Fees from \$0.0001
* 24/7/365 availability
* Works in globally
## How It Works
```mermaid theme={null}
graph LR
A[Your App] -->|API Call| B[ZBD Platform]
B --> C{Payment Type}
C -->|Lightning| D[Instant Delivery]
C -->|On-chain| E[Bitcoin Network]
C -->|Fiat| F[Banking Rails]
D --> G[Recipient]
E --> G
F --> G
```
### Core Concepts
Bitcoin's Layer 2 scaling solution enabling instant, low-cost payments. Think of it as the Visa network for Bitcoin - same security, 1000x faster.
**Key Benefits:**
* Instant settlement (\< 1 second)
* Fees typically \< \$0.01
* Millions of TPS capacity
* Perfect for gaming microtransactions
Human-readable payment addresses like `user@zbd.gg`. No more copying long Bitcoin addresses. Works like email for money.
**Why It Matters:**
* Memorable usernames
* No QR codes needed
* Works across all Lightning wallets
* Perfect for in-game identities
## Products & Features
### ๐ ZBD Ramp
Let users buy Bitcoin directly in your app. First widget to support Lightning Address delivery.
Embeddable widget โข Built-in KYC โข Multiple payment methods
### โก Payments API
Send and receive payments globally via our REST API.
Instant settlements โข Global reach โข Bitcoin Lightning Network
### ๐ ๏ธ Developer SDKs
Production-ready SDKs for popular languages and frameworks. Get building in minutes, not weeks.
TypeScript โข Go โข Rust โข C# โข More coming
## Use Cases
### Gaming & Virtual Worlds
* Instant item purchases
* No minimum transaction
* Global player base
* Zero chargebacks
* P2P marketplaces
* Instant settlement
* Cross-border trades
* Low fees = more profit
* Instant winner payouts
* Any amount, any country
* No banking details needed
* Automated via API
* Pay mod creators
* Revenue sharing
* Micropayments work
* Global participation
### Apps & Platforms
Pay-per-article, pay-per-view, tips - amounts too small for cards
Instant payouts to workers globally without bank accounts
Send money home instantly with fees under \$0.01
Usage-based billing down to fractions of a cent
## Compliance & Security
### Built-in Compliance
* **KYC/AML**: Automated identity verification where required
* **Sanctions Screening**: Real-time OFAC and international lists
* **Transaction Monitoring**: AI-powered fraud detection
* **Regulatory Licenses**: Money transmission licenses in key markets
### Security First
* **End-to-end Encryption**: All data encrypted in transit and at rest
* **API Key Security**: Scoped permissions and IP allowlisting
* **Webhook Signatures**: Cryptographic verification of all events
* **SOC 2 Type II**: Annual security audits
## Pricing
**Custom Pricing** - We offer competitive rates based on your volume and use case. Schedule a call to discuss pricing that works for your business.
### What Affects Pricing
* Transaction volume
* Average transaction size
* Geographic distribution
* Settlement preferences
* Support level needed
## Start Building
[Schedule a call](https://zbd.one/sales) with our team to get access
Pick between Ramp, Payments API, or both
Use our quick start guides to integrate in minutes
Start processing real payments with production keys
Let users buy Bitcoin
TypeScript, Go, Rust & more
Complete API documentation
***
## Need Help?
Get API keys and discuss your use case
Technical help from our engineering team
# AI Ingestion
Source: https://docs.zbdpay.com/payments/ai-ingestion
Understand how to use ZBD Payments with LLMs.
[docs.zbdpay.com](/) documentation pages are content rich and can be a great source of information for LLMs for AI tools. You can use your documentation to fine-tune a chatbot, answer questions about your product, or use it to generate code.
## /llms.txt
An [industry standard](https://llmstxt.org) that helps general-purpose LLMs index more efficiently (e.g. a sitemap for AI).
Every documentation site automatically hosts a **/llms.txt** file at the root that lists all available pages in your documentation.
Open llms.txt for docs.zbdpay.com
## /llms-full.txt
A markdown file of all your content is automatically available at **/llms-full.txt**.
llms-full.txt contains the entire content of your docs, and is a great way to provide a complete context to AI tools.
Open llms-full.txt for docs.zbdpay.com
## .md extension
Add **.md** to an individual documentation page to see a markdown version of that page. Helps load individual pages into AI tools with better token efficiency & inference time.
Open get-started.md
# Payments API Reference
Source: https://docs.zbdpay.com/payments/api
Understand general API concepts and authentication strategies.
## Overview
ZBD offers a powerful REST API that supports Bitcoin Lightning Network transactions with various payout and payin methods. Our API uses JSON data structures and makes use of industry standard HTTP response codes.
## Base URL
The ZBD API is available on the following base URL:
```bash theme={null}
https://api.zbdpay.com
```
If you're looking for the Sandbox API, that's available at `https://sandbox-api.zbdpay.com`.
## Authentication
All ZBD endpoints are protected by an API Key. To make requests against these endpoints you must pass a header property called `apikey` with your [ZBD Project API Key](/get-started/api-keys).
```json theme={null}
apikey: "1PnXYRdcm13gJrHZVzctgCAUIEniVomQR"
```
## SSL / HTTPS Access
**ZBD only provides a secure interface over HTTPS with SSL certificate support**. Any requests that attempt to reach the ZBD API in an insecure fashion (plain-text over HTTP requests) will be rejected.
Secure Sockets Layer (SSL) is the standard security technology for establishing an encrypted link between a server and a client. SSL allows sensitive information such as credit card numbers, social security numbers, and login credentials to be transmitted securely.
## Resources
Links to help you get the most out of your ZBD development experience:
# Callbacks
Source: https://docs.zbdpay.com/payments/api/callbacks
Receive secure payment status updates from ZBD servers.
**Every payment in the Bitcoin Lightning Network is asynchronous.**
**Therefore, every payment inside of ZBD Payments API is asynchronous.**
## Payment Lifecycle
Although it may be that 99% of the time you use the ZBD Payments API to send a payment it settles immediately, this is NOT a certainty. All payments are asynchronous so there's still chances they will not succeed.
The Lightning Network is a decentralized organism / network and therefore there are many factors and circumstances that are outside of ZBD's control. Whether it is another provider going offline, or improper management of Bitcoin liquidity on the network, payments in Lightning can fail and you as a developer must understand how to react to these failures.
### Receiving Bitcoin
To receive payments, you normally create [Charges](/payments/api/lightning-charges/create) (or [Static Charges](/payments/api/lightning-charges/create-static)). These are created and remain in a `pending` status until someone successfully scans and pays the Charge, at which point it becomes `completed`. If enough time is elapsed, the Charge can also `expire` making it impossible to be paid.
### Sending Bitcoin
On the other hand, to send Bitcoin you must use one of the many [Payment APIs](/payments#instant-bitcoin-payouts). Payments through the Lightning Network can always fail, due to a myriad of reasons (e.g. network connectivity, size of payment, destination or receipient, etc). Because of that, every payment starts in a `processing` state, and only when it's settled or failed does it receive the `completed` or `failed` status' respectively.
As you can understand now, **it is important that you subscribe to updates on all your Charges and Payments when using the ZBD Payments API**.
**ZBD Payments API retains a 97% payment success rate for all time payments in the Lightning Network.**
## Callback URL
In order to properly receive updates to any Payments, Charges, and Withdrawals created through the ZBD Payments API, you should always pass along the `callbackUrl` property.
This `callbackUrl` should point to an HTTP server that you control/manage and should expose a POST HTTP call on this endpoint. It could look like this:
```bash theme={null}
https://yourserver.com/api/v1/zbd/callback
```
**Callback URLs MUST be HTTPS**. HTTP calls are ignored. The server must be reachable to the public internet and not require any additional means of authentication.
We are in the process of adding `retry` functionality for these callback payloads, in case your server happens to be offline when a payment settles.
### Payloads
Below is a set of example payloads for the main APIs available:
#### Charges
```json theme={null}
{
"amount":"1000",
"callbackUrl":"",
"internalId":null,
"status":"expired",
"unit":"msats"
}
```
#### Payments
```json theme={null}
{
"amount":"10000",
"confirmedAt":"2024-02-06T11:51:26.985Z",
"description":"Test payment",
"fee":"0",
"id":"",
"internalId":null,
"invoice":"",
"preimage":"",
"processedAt":"2024-02-06T11:51:26.985Z",
"status":"completed",
"unit":"msats"
}
```
#### Withdrawalsย
```json theme={null}
{
"amount":"10000",
"callbackUrl":"",
"createdAt":"2024-02-06T12:02:08.166Z",
"description":"Test withdrawal",
"expiresAt":"2024-02-06T12:05:08.164Z",
"fee":"0",
"id":"",
"internalId":null,
"invoice":
{
"fastRequest":"",
"fastUri":"",
"uri":""
},
"status":"completed",
"unit":"msats"
}
```
#### Lightning Address
```json theme={null}
{
"amount":"1000",
"callbackUrl":"",
"internalId":null,
"invoice":"",
"preimage":"",
"unit":"msats",
"walletId":""
}
```
## Security / Integrity
It is important that developers are able to validate the integrity of all API calls performed to their `callbackUrl` endpoints.
To ensure that calls are coming from trusted ZBD Payments API servers, please use the [ZBD IP Address API Endpoint](/payments/api/utils/ip).
**By checking the originating IP address** of the request received to your callback servers, **with the approved IP address** returned from this API endpoint, you are able to verify the authenticity of the requests.
We have plans to expose features to further harden this security by providing data-encryption capabilities. The idea here is to allow developers to enter a secret key in their ZBD Project's Settings and then require this key to decode the encrypted data payload.
## Testing Locally
When developing with the ZBD Payments API, it is imperative that developers can test full end-to-end functionality of payment flows, including the async updates received through the `callbackUrl`s.
In order to receive updates locally, we recommend using a proxy server such as [ngrok](https://ngrok.com).
* [Follow the installation steps](https://ngrok.com/docs/getting-started/)
* Run command to start ngrok tunnel `ngrok http 8000`
* ngrok URL (e.g. `https://84c5dfxx39d74.ngrok.io`) is provided
* This URL is accessible to the world --> expose an API endpoint here and use it as the `callbackUrl` (e.g. `https://84c5dfxx39d74.ngrok.io/api/zbd-callback`).
You will now be able to receive async updates about your ZBD Payments API transactions directly to your local environment.
# Errors
Source: https://docs.zbdpay.com/payments/api/errors
Troubleshoot problems with API calls.
The ZBD API follows standard HTTP response codes for handling success and error requests.
If you are unfamiliar with HTTP requests, [please read more here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status).
## HTTP status codes
Below is an overview of HTTP status codes available in the standard, which ZBD API adheres by:
| HTTP Response Group | Status Code Range |
| ------------------------ | :---------------: |
| Informational Responses | `100-199` |
| **Successful Responses** | `200-299` |
| Redirects | `300-399` |
| **Client Errors** | `400-499` |
| **Server Errors** | `500-599` |
## Common response codes
These are the HTTP status codes most often returned by the ZBD API.
| Status Code | Details | Response Description |
| :---------: | :---------------: | ---------------------------------------------------------------------------------------------------------- |
| `200` | Success | Request was successful. |
| `400` | Bad Request | Either incorrect or missing parameters provided for the Request. |
| `401` | Unauthorized | Request is not authorized. Make sure correct API Key is provided. |
| `403` | Forbidden | Request is forbidden. Make sure your IP is located in a region/country that ZBD services are available in. |
| `404` | Not Found | The requested endpoint/resource was not found. |
| `422` | Missing Parameter | The requested endpoint expected a specific parameter to be provided. |
| `429` | Rate Limited | Too many requests in short time-frame. |
| `500` | Server Error | Something went wrong in the ZBD API servers. |
## Error messages
To help identify the issue with your API requests, the error JSON response returned will often include a **message** property that aims to be descriptive.
Example of an error message from the API:
```json theme={null}
{
"success": false,
"message": "Error creating Withdrawal Request: The \"amount\" property (in millisatoshis) is required.",
}
```
# ZBD Gamertags
Source: https://docs.zbdpay.com/payments/api/gamertags
How to send instant payments to ZBD Gamertags using ZBD Payments APIs.
Section coming soon.
# Create Charge
Source: https://docs.zbdpay.com/payments/api/gamertags/create-charge
POST https://api.zbdpay.com/v0/gamertag/charges
Generate a payment request for a ZBD User.
## Description
While you can use the [Pay to ZBD Gamertag](/payments/api/gamertags/send) endpoint to make a direct Bitcoin payment to a given ZBD user, if for whatever reason you'd want to create a general Lightning Network Charge / Payment Request QR code, you can use this endpoint.
The response payload will include a `invoiceRequest` which is the contents of the Charge / Payment Request QR code. Do note that Charges have defined expiration times. After a Charge expires it cannot be paid, simply create another Charge.
**Important:** ZBD Gamertags are mutable and can be changed by users at any time. If you're implementing user-specific limits (e.g., 1 withdrawal per day) or tracking balances, **do not use Gamertags as identifiers**. Instead, use the [Get ID by ZBD Gamertag](/payments/api/gamertags/retrieve-userid) endpoint to retrieve the immutable User ID for each Gamertag, then track limits and balances using that User ID. Otherwise, users can bypass your limits simply by changing their Gamertag.
## Configuration
### Header Parameters
ZBD Project API Key
Content Type
### Body
The amount for the Payment -> in millisatoshis
Destination ZBD Gamertag
Note or comment for this Payment (visible to recipient)
Time until Charge expiration -> in seconds
Open metadata string property
The endpoint ZBD will POST Charge updates to
```json Response theme={null}
{
"success": true,
"data": {
"unit": "msats",
"status": "CHARGE_PENDING",
"amount": "15000",
"createdAt": "2023-03-14T19:40:13.880Z",
"internalId": "test",
"callbackUrl": "https://my-website.ngrok.io/callback",
"description": "Requesting Charge for Gamertag",
"invoiceRequest": "lnbc10n1pjppnvapp5camc852ky0et9g46gyey9mnv5xgux6tnkkq5qc6cexrr65xw2j9sdps2fjhzat9wd6xjmn8yppksctjvajjqen0wgsywctdv4e8gct8cqzpgxqzjcsp5jgs84mxjwk8n9r7xmp7ulzycy7882f5m8zagk45s2qp23p7rprqq9qyyssq3ysvdsgg5qvx0nwtlz46hcrucs7m6nkvsffcze5nhes40hqrnnes3xjcq0a4wtpqvvrdqlyqy3sz5yhqh6944unan4d32py2azq38zgq4c5ysu",
"invoiceExpiresAt": "2023-03-14T19:50:13.859Z",
"invoiceDescriptionHash": null
}
}
```
# Get Gamertag By ZBD ID
Source: https://docs.zbdpay.com/payments/api/gamertags/retrieve-from-userid
GET https://api.zbdpay.com/v0/gamertag/user-id/{id}
Retrieve ZBD user ID from a Gamertag.
## Usage
This API endpoint is the reverse of the [Get User ID for ZBD Gamertag](/payments/api/gamertags/retrieve-userid) endpoint. Use this endpoint to get a given user's ZBD Gamertag.
If you've got a User ID and you wish to find out that user's ZBD Gamertag, perhaps to then [send that user payments](/payments/api/gamertags/send), you may use this endpoint.
## Configuration
### Path Parameters
User ID
### Header Parameters
ZBD Project API Key
```json Response theme={null}
{
"success": true,
"data": {
"gamertag": "foxp2"
},
"message": "Fetched gamertag from uuid"
}
```
# Get Payment
Source: https://docs.zbdpay.com/payments/api/gamertags/retrieve-payment
GET https://api.zbdpay.com/v0/gamertag/transaction/{id}
Retrieve all data about a Payment sent to ZBD User.
## Description
In order to get data on payments sent to ZBD Gamertags, you can use this endpoint. The data payload returned will inform you of the `status` of that transaction as well as any associated `fees`.
## Usage
The idea here is to use the `transactionId` property returned from the [Pay to ZBD Gamertag](/payments/api/gamertags/send) response, and use that ID to fetch the latest details of the given Payment.
## Configuration
### Path Parameters
Payment Transaction ID
### Header Parameters
ZBD Project API Key
```json Response theme={null}
{
"message": "Fetched charge.",
"success": true,
"data": {
"id": "903883f2-67d9-4707-a21b-ddff004fe041",
"receiverId": "ec9b38d5-b126-4307-9d1e-8aa0dfab5d7e",
"amount": "15000",
"fee": "1000",
"unit": "msats",
"processedAt": "2023-01-04T15:59:16.993Z",
"confirmedAt": "2023-01-04T15:59:16.989Z",
"comment": "Sending to ZBD Gamertag",
"status": "TRANSACTION_STATUS_COMPLETED"
}
}
```
# Get ID by ZBD Gamertag
Source: https://docs.zbdpay.com/payments/api/gamertags/retrieve-userid
GET https://api.zbdpay.com/v0/user-id/gamertag/{GAMERTAG}
Retrieve Gamertag from a ZBD user ID.
## Description
Make use of this endpoint to get a given User's ID when provided with a ZBD Gamertag.
## Usage
This is needed because ZBD Gamertags are akin to social media handles, they are swappable. In the scenario where a Developer is using a ZBD Gamertag as the identifier for transaction limits and/or thresholds (for example), a user could simply change their ZBD Gamertag and try to bypass those constraints. Using a truly unique and immutable ID like the `User ID` will provide Developers with the needed assurances.
## Configuration
### Path Parameters
ZBD Gamertag
### Header Parameters
ZBD Project API Key
```json 200 Response theme={null}
{
"success": true,
"data": {
"id": "0a59bf56-daa3-4888-8330-cbc02eba3a6e"
}
}
```
```json 400 Response theme={null}
{
"success": false,
"message": "No user found with this gamertag"
}
```
# Pay to ZBD Gamertag
Source: https://docs.zbdpay.com/payments/api/gamertags/send
POST https://api.zbdpay.com/v0/gamertag/send-payment
Send instant Bitcoin payments to ZBD Users.
Unlike other payout endpoints in the ZBD API, sending payments to ZBD Gamertags is a synchronous action -> this is why there is no `callbackUrl` property. The payment either succeeds or fails on the spot.
**Important:** ZBD Gamertags are mutable and can be changed by users at any time. If you're implementing user-specific limits (e.g., 1 withdrawal per day) or tracking balances, **do not use Gamertags as identifiers**. Instead, use the [Get ID by ZBD Gamertag](/payments/api/gamertags/retrieve-userid) endpoint to retrieve the immutable User ID for each Gamertag, then track limits and balances using that User ID. Otherwise, users can bypass your limits simply by changing their Gamertag.
## Configuration
### Header Parameters
ZBD Project API Key
Content Type
### Body
The amount for the Payment -> in millisatoshis
Destination ZBD Gamertag
Note or comment for this Payment (visible to recipient)
```json Response theme={null}
{
"success": true,
"data": {
"id": "49cccd62-b686-4057-9f44-f492bd54ac39",
"status": "settled",
"transactionId": "78fb6474-d791-47a1-bda1-a4b9023898c0",
"receiverId": "ec9b38d5-b126-4307-9d1e-8aa0dfab5d7e",
"amount": "1000",
"comment": "Sent to ZBD Gamertag",
"settledAt": "2023-07-19T21:42:54.993Z"
},
"message": "Payment done."
}
```
# Keysend
Source: https://docs.zbdpay.com/payments/api/keysend
How to send Keysend payments using ZBD Payments APIs.
Section coming soon.
# Send Keysend Payment
Source: https://docs.zbdpay.com/payments/api/keysend/send
POST https://api.zbdpay.com/v0/keysend-payment
Start sending Keysend payments on the Lightning Network.
## Description
Every Lightning Network node has a public key identifying it to the wider network. While most payments in the Lightning Network require Payment Requests ([Charges](/payments/glossary#charges)) QR codes to be created and then paid for, there is another type of payment known as `Spontaneous Payment` or `Keysend Payment` on a more technical note.
This endpoint exposes the ability to make payment directly to a Lightning Network node Public Key, without the need for a Payment Request / Charge.
## Usage
If your system relies on sending Bitcoin over the Lightning Network to specific node Public Keys, this API is what you're looking for.
Additional metadata and TLV record data can be attached to this endpoint as well. Both of these properties/values will be returned on the `callbackUrl` POST call from ZBD.
As with all asynchronous payments in the Lightning Network, in order to receive updates about whether a payment settled, you must provide the `callbackUrl` property. As you will note from the response example below, if the Keysend Payment doesn't settle (almost) immediately, you will receive a `Payment Processing` response payload. Once it settles, you'll receive the update on the `callbackUrl`.
## Configuration
### Header Parameters
ZBD Project API Key
Content Type
### Body
The amount for the Payment -> in millisatoshis
The Public Key for the destination Lightning node
The endpoint ZBD will POST Keysend Payment updates to
Open metadata object property
List of TLV records
type of the TLV record
value of the TLV record (hex encoded string)
```json Response theme={null}
{
"message": "Payment processing.",
"success": true,
"data": {
"keysendId": "0d54521c-f41c-4cec-8799-edf9d87cf6b5",
"paymentId": "276e6f54-65fb-4c30-9b86-b44659015cd6",
"transaction": {
"id": "a51bd8d3-1d65-4785-bfd2-3341dc696f84",
"walletId": "cce98a9b-c52a-427c-8cf5-d7ca39c5035e",
"type": "KEYSEND_PAYMENT",
"totalAmount": "3000",
"fee": "2000",
"amount": "1000",
"description": "Keysend Payment",
"status": "TRANSACTION_STATUS_PROCESSING",
"confirmedAt": null
}
}
}
```
# Lightning Address
Source: https://docs.zbdpay.com/payments/api/lightning-address
How to send instant payments to Lightning Addresses using ZBD Payments APIs.
## Description
A Lightning Address is an internet identifier (akin to an email address -- `andre@zbd.gg`) that anyone can use to send you Bitcoin Lightning Network payments.
Leverage this API to programmatically perform these types of payments.
A massively simpler way for anyone to send you Bitcoin instantly on the Lightning Network. Like an email address, but for your Bitcoin!
As creators of the [Lightning Address open source standard](https://github.com/andrerfneves/lightning-address), we believe this API to be the single most frictionless way of using Bitcoin today.
## APIs
Here are some Lightning Address APIs that you can use to programmatically help perform these types of payments.
Ensure destination is a valid Lightning Address.
Send a payment to a Lightning Address.
Create a charge QR code for a Lightning Address.
# Create Charge
Source: https://docs.zbdpay.com/payments/api/lightning-address/create-charge
POST https://api.zbdpay.com/v0/ln-address/fetch-charge
Generate a payment request for a Lightning Address.
## Description
If instead of sending Bitcoin payments directly to a given Lightning Address, you would rather create a Payment Request QR code for that Lightning Address, you can use this API.
## Usage
Depending on your system's configuration or your product's UX, you may need the ability to generate Charges for specific users. For ZBD users that's easy using the ZBD Gamertag endpoints. For any user that is using a different provider, you can use this endpoint.
The way to think of this is:
Given `user@domain.com` Lightning Address and an `amount` property, this endpoint generates a Charge.
If this new Charge is paid, the payment is received by `user@domain.com`.
## Configuration
### Header Parameters
ZBD Project API Key
Content Type
### Body
The Lightning Address of the intended recipient
The amount for the Charge -> in millisatoshis
Note or comment of this Charge
```json Response theme={null}
{
"success": true,
"data": {
"lnaddress": "andre@zbd.gg",
"amount": "15000",
"invoice": {
"uri": "lightning:lnbc100n1p3mtxc3pp5tz4srtee9c4gsvlcqa6azu60a2ak2fvrnk7jv52j6n0rzp0musnqhp5xt5ts9t74akf67j59ynkr5qz8x3hs4g09d3dttmjrwwdhrxgc03qcqzpgxqzfvsp5529t5y03yaakumqrm9cwr5t6nysnr5cv93pnvw089a25aa7wszas9qyyssqalswdlc92983203l5azl43nksehadfy2uahc9v6qwg67xs52z02hg6vh7alrm3yx46ywdrl3dxey75t9l2ekmuwc3tu4ug03dy729fspdld2lj",
"request": "lnbc100n1p3mtxc3pp5tz4srtee9c4gsvlcqa6azu60a2ak2fvrnk7jv52j6n0rzp0musnqhp5xt5ts9t74akf67j59ynkr5qz8x3hs4g09d3dttmjrwwdhrxgc03qcqzpgxqzfvsp5529t5y03yaakumqrm9cwr5t6nysnr5cv93pnvw089a25aa7wszas9qyyssqalswdlc92983203l5azl43nksehadfy2uahc9v6qwg67xs52z02hg6vh7alrm3yx46ywdrl3dxey75t9l2ekmuwc3tu4ug03dy729fspdld2lj"
}
}
}
```
# Pay to Lightning Address
Source: https://docs.zbdpay.com/payments/api/lightning-address/send
POST https://api.zbdpay.com/v0/ln-address/send-payment
Send instant Bitcoin payments to any Lightning Address.
## Configuration
### Header Parameters
ZBD Project API Key
Content Type
### Body
The Lightning Address of the intended recipient (e.g. [andre@zbd.gg](mailto:andre@zbd.gg))
The amount for the Payment -> in millisatoshis
Note or description of this Payment
Open metadata string property
The endpoint ZBD will POST Charge updates to
```json Response theme={null}
{
"success": true,
"data": {
"id": "db3e9e98-82c6-4ad5-acb8-900212d352aa",
"fee": "0",
"unit": "msats",
"amount": "10000",
"invoice": "lnbc100n1p3mtx6gpp5qpc7qvapr475jjazwkves7t7qdyq5njym52z5r6mg0a9d79w783shp5xt5ts9t74akf67j59ynkr5qz8x3hs4g09d3dttmjrwwdhrxgc03qcqzpgxqzfvsp59uasmyhqwfuek2dwyfla6dn7la5wxkwzmlvvhcuptxdxsxs8d24q9qyyssqslvgpll5cwtcmhr0ch607gg463xc3s2g7mfww2dr3l0f29czt7eq03rylyg5g09qz0w97qv87mmmcyj0ytfahhzc6y79je5vsf67tgcp8248ez",
"preimage": null,
"walletId": "2e686eed-82c6-4722-95bd-de1b23f27d44",
"transactionId": "2924bf31-96ba-4d26-80df-9e35c09a59cf",
"callbackUrl": "https://your-domain.com/zbd-callback",
"internalId": "uniqueIdFromYourSystem",
"comment": "Sending to a Lightning Address",
"processedAt": "2023-01-04T15:29:12.577Z",
"createdAt": "2023-01-04T15:29:12.514Z",
"status": "completed"
},
"message": "Payment done."
}
```
# Validate Lightning Address
Source: https://docs.zbdpay.com/payments/api/lightning-address/validate
GET https://api.zbdpay.com/v0/ln-address/validate/{address}
Verify the validity of a Lightning Address.
## Usage
Not all internet identifiers are Lightning Addresses. Make use of this API to validate whether a user's entered address is indeed a valid Lightning Address that can accept Bitcoin payments.
The idea behind this API endpoint is to verify whether an internet-indentifier (e.g. `user@domain.com`) is in fact a Lightning Address and NOT an email address. Once verified, use the [Send Lightning Address Payment](/payments/api/lightning-address/send) to perform the transaction.
## Configuration
### Path Parameters
Lightning Address to be verified
### Header Parameters
ZBD Project API Key
```json Response theme={null}
{
"success": true,
"data": {
"valid": true,
"metadata": {
"minSendable": 10000,
"maxSendable": 150000000,
"commentAllowed": 150,
"tag": "payRequest",
"metadata": "[[\"text/plain\",\"andre - Welcome to ZBD! ;)\"],[\"text/identifier\",\"andre@zbd.gg\"],[\"image/png;base64\",\"/9j/2wBDAAYEBQ......IUeDcpP9akPkjm3XPid8/OmYZB//9k=\"]]",
"callback": "https://api.zbdpay.com/v0/process-static-charges/d0f2c3da-e836-480a-bf8c-f5d5b167d6f8",
"payerData": {
"name": {
"mandatory": false
},
"identifier": {
"mandatory": false
}
},
"disposable": false
}
}
}
```
# Overview
Source: https://docs.zbdpay.com/payments/api/lightning-charges
How to create and manage Lightning Charges.
## Introduction
Static Charges are a type of Payment Request QR code in the Bitcoin Lightning Network that has some incredible characteristics and capabiltiies. Whereas [Charges](/payments/api/lightning-charges/create) create `fixed-amount` and `single-use` Payment Requests that expire, [Static Charges](/payments/api/lightning-charges/create-static) provide you a lot more flexibility & capabilities, including:
Static Charges have both `minAmount` and `maxAmount` properties, allowing for the payer to define the amount.
By default Static Charges have **no expiration time**, making them usable for any N payers and payments.
Slots allow you to define that only N payments are to be accepted for this specific Static Charge QR.
When a payment is completed, a `Success Message` can be displayed to the payer (e.g. receipt, link).
## What is a Static Charge?
A Static Charge is a static QR code that can be used an unlimited number of times, does not expire, accepts various amounts, accepts attached messages, and allows for full configurability, **all while maintaining the same QR code**.
A Static Charge QR code looks like the one show below:
## Common use cases
There are many potential scenarios where Static Charges are very handy. Let's take a look at couple of them.
### User deposits
Static Charges are excellent for handling user deposits. You can generate a Static Charge for each unique users in your platform and provide that QR code to them. Whenever payments come in through that specific Static QR code, you will know it belongs to such user. Your users can now receive money from any Bitcoin Lightning wallet.
Static Charges can be set up such that they can be used an unlimited number of times and they do not expire. It makes receiving payments extremely easy.
\*Just like every user has a \$venmotag for Venmo, every ZBD user has an Static Charge. The only difference between the two is that there is interoperability between different providers since they understand the same money protocol.
### In-person fundraising
One crafty thing you can do for donations is throw a Static Charge QR code on a large screen and anyone who has a wallet can now pay you directly just by scanning and confirming a payment.
The ZBD API makes this incredibly easy. Since there are no limits to the number of Static Charges created, you could create one for each campaign and properly measure donation goal reach. Cool, right?
This is great for nonprofits!
### Remote fundraising / donations
Streamers can take their QR image and easily add it to any streaming software (e.g. OBS) to enable direct payments to them from their audience. Never has this been possible before.
Please check out [ZBD Streamer](https://streamer.zebedee.io) to view this live in a ZBD product.
### User tips / Zaps / User-to-user payments
Static Charges are great for tips or commonly referred to as "zaps". It can also be used for user-to-user payments, since they are a static QR code. Though the best user experience for these use cases is a [Lightning Address](/payments/api/lightning-address/send).
### Deposits
Users can attach Static QR codes to their blog posts, social media posts, images, live streams, videos, & art.
# Create Charge
Source: https://docs.zbdpay.com/payments/api/lightning-charges/create
POST https://api.zbdpay.com/v0/charges
Start receiving instant Bitcoin payments through the ZBD API.
## Description
Creates a new [Charge](/payments/glossary#charge) in the Bitcoin Lightning Network, payable by any Lightning-capable wallet.
These payment requests are single-use, fixed-amount QR codes. If you're looking for multi-use and multi-amount payment requests you want [Static Charges](/payments/api/lightning-charges/create-static).
## Usage
This API is used for `receiving` funds into your ZBD Project. The primary idea is to create one of these Charges anytime someone wishes to perform a payment **TO** you.
On a successful `200 HTTP response` from the ZBD API, you should render the contents of `data.invoice.uri` in a QR code. This QR code can be scanned by any Bitcoin Lightning wallet.
## Configuration
### Header Parameters
ZBD Project API Key
Content Type
### Body
The amount for the Charge -> in millisatoshis
Note or comment for this Charge (visible to payer)
Time until Charge expiration -> in seconds
The endpoint ZBD will POST Charge updates to
Open metadata string property
```json Response theme={null}
{
"success": true,
"message": "Successfully created Charge.",
"data": {
"unit": "msats",
"amount": "10000",
"confirmedAt": null,
"status": "pending",
"description": "My Charge Description",
"createdAt": "2020-05-09T15:09:05.765Z",
"expiresAt": "2020-05-09T15:14:05.618Z",
"id": "1907b0fe-789b-4e25-b18a-0c4c0f5cced7",
"internalId": "11af01d092444a317cb33faa6b8304b8",
"callbackUrl": "https://your-website.com/callback",
"invoice": {
"request": "lnbc100n1p0td3u3pp5z2ed9yjfrz0rgu0fzuc5cdwfu8dtjlgrfztz5uga8hakkjy2yzgsdpzf4ujqsmgv9exwefqg3jhxcmjd9c8g6t0dccqzpgxqzfvsp5q8z5mkghmuzrnusxdwtmls7x8vuy63j25rt4z55gj3s7340dv72q9qy9qsqz02rlmlzcvew3vk90c6l0369ewk7tkr2tx0yrk3qa235v07w6d3qeksk99wm7y8f8ug7zqy6yjudu4cs2f4umpey43cw7msyj7uqj2qq8x03te",
"uri": "lightning:lnbc100n1p0td3u3pp5z2ed9yjfrz0rgu0fzuc5cdwfu8dtjlgrfztz5uga8hakkjy2yzgsdpzf4ujqsmgv9exwefqg3jhxcmjd9c8g6t0dccqzpgxqzfvsp5q8z5mkghmuzrnusxdwtmls7x8vuy63j25rt4z55gj3s7340dv72q9qy9qsqz02rlmlzcvew3vk90c6l0369ewk7tkr2tx0yrk3qa235v07w6d3qeksk99wm7y8f8ug7zqy6yjudu4cs2f4umpey43cw7msyj7uqj2qq8x03te"
}
}
}
```
# Create Static Charge
Source: https://docs.zbdpay.com/payments/api/lightning-charges/create-static
POST https://api.zbdpay.com/v0/static-charges
Start accepting payments on Lightning with Static QR codes.
## Description
Static Charges are static Payment Requests in the Bitcoin Lightning Network. Whereas [Charges](/payments/api/lightning-charges/create) create fixed-amount and single-use Payment Requests that expire, Static Charges provide you a lot more flexibility & capabilities, including:
* **Variable Amounts** -> Static Charges have both `minAmount` and `maxAmount` properties, allowing for variable amounts set by the payer.
* **Multi-use** -> By default Static Charges have no expiration time, making them usable for any N payers/payments.
* **Success Message** -> When a payer completes the payment flow for this Static Charge, a Success Message is displayed to them.
* **Slots** -> Static Charges can also have the notion of `allowedSlots`. This means only N payments will be accepted at this Static Charge before disabling them.
Static Charges also have the same core properties of Charges such as `internalId` and `callbackUrl`.
## Usage
Use this API endpoint to create advanced Payment Requests on the Bitcoin Lightning Network. Enjoy static QR codes that can accept any number of payments, for various amounts, with additional metadata in the payment flow.
You can create a `multi-use` Static Charge with a fixed amount too, simply by setting `minAmount` and `maxAmount` to the same amount.
To understand more use cases for Static Charges, [check our in-depth guide](/payments/api/lightning-charges).
## Configuration
### Header Parameters
ZBD Project API Key
Content Type
### Body
Minimum allowed amount for the Static Charge -> in millisatoshis
Maximum allowed amount for the Static Charge -> in millisatoshis
Note or comment for this Static Charge (visible to payer)
Message displayed to the payer AFTER payment settles. Maximum of 144 characters.
Number of payments this Static Charge can accept
Open metadata string property
The endpoint ZBD will POST Charge updates to
Used for Custom Lightning Addresses (see guide)
```json Response theme={null}
{
"message":"Successfully created Static Charge.",
"data":{
"id":"45c225b1-022b-4a37-98d6-5a5568f78d11",
"unit":"msats",
"slots":0,
"minAmount":"10000",
"maxAmount":"100000",
"createdAt":"2023-03-07T20:07:06.910Z",
"expiresAt":null,
"internalId":"myInternalId",
"description":"Static Charge API Ref",
"callbackUrl":"https://my-website/zbd-callback",
"allowedSlots":1000,
"successMessage":"Congratulations your payment was successful!",
"status":"active",
"invoice":{
"request":"lnurl1dp68gurn8ghj7ctsdyh85etzv4jx2efwd9hj7a3s9aex2ut4v4ehgttnw3shg6tr943ksctjvajhxte5x43nyv34vgcj6vpjxf3z6drpxvmj6wfcvsmz6dtpx56nvwrxxuuxgvf3uzvpfy",
"uri":"lightning:lnurl1dp68gurn8ghj7ctsdyh85etzv4jx2efwd9hj7a3s9aex2ut4v4ehgttnw3shg6tr943ksctjvajhxte5x43nyv34vgcj6vpjxf3z6drpxvmj6wfcvsmz6dtpx56nvwrxxuuxgvf3uzvpfy"
}
}
}
```
# Decode Charge
Source: https://docs.zbdpay.com/payments/api/lightning-charges/decode
POST https://api.zbdpay.com/v0/decode-invoice
Understand the inner properties of a Charge QR code.
## Usage
This API is used for further decyphering the details of a Charge. The idea is to use this API endpoint if you have to validate any of the specific properties of this Payment Request.
Any Lightning Network Charge can be used as a property here, even those not generated by the ZBD API.
## Configuration
### Header Parameters
ZBD Project API Key
Content Type
### Body
The Charge or Invoice QR code contents
```json Response theme={null}
{
"success": true,
"data": {
"amount": "212325000",
"lnRequest": "lnbc2123250n1pjpr2qmpp526234tljpx5756pa2fyj2zrdmn2tnz9rhj54km2s7dv0pap3vtkqhp5extcuyp2x0ydfwnfhrc5cwx8azxeen2g7hxr29464ezvn3k6w2fqcqzpgxqzjcsp5xzelh2w3twt4ysvva2ugu2klurmrl7nk8h46x2hcthf9cvee24jq9qyyssq6an9tjftjymjeklerjmw8cv4ccpsvd2vzuzxn5upt9s37hnw2r0z0n5cd8cqq8jq5ems00tugt5jnw5jn03tr84945nd6j4hsfsu7kqp9hptk2",
"lnExpiresAt": "2023-03-15T11:22:27.000Z",
"network": "bitcoin",
"description": "Paying for Master Sword #5546",
"descriptionHash": "c9978e102a33c8d4ba69b8f14c38c7e88d9ccd48f5cc3516baae44c9c6da7292",
"paymentHash": "56951aaff209a9ea683d524925086ddcd4b988a3bca95b6d50f358f0f43162ec",
"paymentSecret": "30b3fba9d15b9752418ceab88e2adfe0f63ffa763deba32af85dd25c33395564",
"payee": "036ff83834666d3ebfe61c2a7d1f8fc5d6b339a26559a61819e2b0d1b5f540fdfc",
"signature": "d76655c92b91372cdbf91cb6e3e195c60306354c170469d38159611f5e6e50de27ce9869f0001e40a67707bd7c42e929ba929be2b19ea5ad26dd4ab78261cf58"
}
}
```
# Retrieve Charge
Source: https://docs.zbdpay.com/payments/api/lightning-charges/retrieve
GET https://api.zbdpay.com/v0/charges/{id}
Retrieve all data about a single Charge.
## Usage
While the best approach is to rely on the `callbackUrl` property and expect async Charge updates sent from ZBD servers, this API can be used to check any information about any Charge, specifically properties such as `status` and `internalId` which are likely useful for your application.
For most use cases it is unlikely you will hit a rate limit, however it is recommended to avoid practices such as "long polling" especially if your server collectively "long polls" for multiple clients as this could cumulatively cause a rate limit to be reached.
## Configuration
### Path Parameters
Charge ID
### Header Parameters
ZBD Project API Key
```json Response theme={null}
{
"success": true,
"message": "Successfully retrieved Charge.",
"data": {
"unit": "msats",
"amount": "10000",
"confirmedAt": null,
"status": "pending",
"description": "My Charge Description",
"createdAt": "2020-05-09T15:09:05.765Z",
"expiresAt": "2020-05-09T15:14:05.618Z",
"id": "1907b0fe-789b-4e25-b18a-0c4c0f5cced7",
"internalId": "11af01d092444a317cb33faa6b8304b8",
"callbackUrl": "https://your-website.com/callback",
"invoice": {
"request": "lnbc100n1p0td3u3pp5z2ed9yjfrz0rgu0fzuc5cdwfu8dtjlgrfztz5uga8hakkjy2yzgsdpzf4ujqsmgv9exwefqg3jhxcmjd9c8g6t0dccqzpgxqzfvsp5q8z5mkghmuzrnusxdwtmls7x8vuy63j25rt4z55gj3s7340dv72q9qy9qsqz02rlmlzcvew3vk90c6l0369ewk7tkr2tx0yrk3qa235v07w6d3qeksk99wm7y8f8ug7zqy6yjudu4cs2f4umpey43cw7msyj7uqj2qq8x03te",
"uri": "lightning:lnbc100n1p0td3u3pp5z2ed9yjfrz0rgu0fzuc5cdwfu8dtjlgrfztz5uga8hakkjy2yzgsdpzf4ujqsmgv9exwefqg3jhxcmjd9c8g6t0dccqzpgxqzfvsp5q8z5mkghmuzrnusxdwtmls7x8vuy63j25rt4z55gj3s7340dv72q9qy9qsqz02rlmlzcvew3vk90c6l0369ewk7tkr2tx0yrk3qa235v07w6d3qeksk99wm7y8f8ug7zqy6yjudu4cs2f4umpey43cw7msyj7uqj2qq8x03te"
}
}
}
```
# Get Static Charge
Source: https://docs.zbdpay.com/payments/api/lightning-charges/retrieve-static
GET https://api.zbdpay.com/v0/static-charges/{id}
Retrieve all data about a single Static Charge.
You should rely on `callbackUrl` property and expect async Static Charge updates sent from ZBD servers. However, this API can be used to check any information about any specific Static Charge, specifically properties such as `slots`, `successMessage`, and `status`.
## Configuration
### Path Parameters
Static Charge ID
### Header Parameters
ZBD Project API Key
```json Response theme={null}
{
"message":"Successfully fetched Static Charge.",
"data":{
"id":"45c225b1-022b-4a37-98d6-5a5568f78d11",
"unit":"msats",
"slots":0,
"minAmount":"10000",
"maxAmount":"100000",
"createdAt":"2023-03-07T20:07:06.910Z",
"expiresAt":null,
"internalId":"myInternalId",
"description":"This is my static charge",
"callbackUrl":"https://my-website/zbd-callback",
"allowedSlots":1000,
"successMessage":"Congratulations your payment was successful!",
"status":"active",
"invoice":{
"request":"lnurl1dp68gurn8ghj7ctsdyh85etzv4jx2efwd9hj7a3s9aex2ut4v4ehgttnw3shg6tr943ksctjvajhxte5x43nyv34vgcj6vpjxf3z6drpxvmj6wfcvsmz6dtpx56nvwrxxuuxgvf3uzvpfy",
"uri":"lightning:lnurl1dp68gurn8ghj7ctsdyh85etzv4jx2efwd9hj7a3s9aex2ut4v4ehgttnw3shg6tr943ksctjvajhxte5x43nyv34vgcj6vpjxf3z6drpxvmj6wfcvsmz6dtpx56nvwrxxuuxgvf3uzvpfy"
}
}
}
```
# Update Static Charge
Source: https://docs.zbdpay.com/payments/api/lightning-charges/update-static
PATCH https://api.zbdpay.com/v0/static-charges/{id}
Change the configuration of a Static Charge QR code.
## Usage
Use this API endpoint with the specific Static Charge ID to update its properties. Usually you'd use this API for the purposes of `increasing slots` or `changing min/max amount` settings.
Updating a Static Charge **WILL NOT modify its QR code**. The point of a Static Charge is that it **does not change**, while its contents can remain dynamic.
## Configuration
### Path Parameters
Static Charge ID
### Header Parameters
ZBD Project API Key
Content Type
### Body
Minimum allowed amount for the Static Charge -> in millisatoshis
Maximum allowed amount for the Static Charge -> in millisatoshis
Note or comment for this Static Charge (visible to payer)
Message displayed to the payer AFTER payment settles
Number of payments this Static Charge can accept
Open metadata string property
The endpoint ZBD will POST Charge updates to
```json Response theme={null}
{
"message":"Successfully editted Static Charge.",
"data":{
"id":"45c225b1-022b-4a37-98d6-5a5568f78d11",
"unit":"msats",
"slots":0,
"minAmount":"10000",
"maxAmount":"100000",
"createdAt":"2023-03-07T20:07:06.910Z",
"expiresAt":null,
"internalId":"myInternalId",
"description":"This is my static charge",
"callbackUrl":"https://my-website/zbd-callback",
"allowedSlots":1000,
"successMessage":"Congratulations your payment was successful!",
"status":"active",
"invoice":{
"request":"lnurl1dp68gurn8ghj7ctsdyh85etzv4jx2efwd9hj7a3s9aex2ut4v4ehgttnw3shg6tr943ksctjvajhxte5x43nyv34vgcj6vpjxf3z6drpxvmj6wfcvsmz6dtpx56nvwrxxuuxgvf3uzvpfy",
"uri":"lightning:lnurl1dp68gurn8ghj7ctsdyh85etzv4jx2efwd9hj7a3s9aex2ut4v4ehgttnw3shg6tr943ksctjvajhxte5x43nyv34vgcj6vpjxf3z6drpxvmj6wfcvsmz6dtpx56nvwrxxuuxgvf3uzvpfy"
}
}
}
```
# Lightning Payments
Source: https://docs.zbdpay.com/payments/api/lightning-payments
How to send instant payments to Lightning Addresses using ZBD Payments APIs.
Section coming soon.
# Retrieve Payment
Source: https://docs.zbdpay.com/payments/api/lightning-payments/retrieve
GET https://api.zbdpay.com/v0/payments/{id}
Retrieve all data about a single Payment.
## Usage
Use this API endpoint to get all of the properties of a given Payment record, including its total fee, processing timestamp, and settlement status.
## Configuration
### Path Parameters
Payment ID
### Header Parameters
ZBD Project API Key
```json Response theme={null}
{
"success": true,
"message": "Fetched Payment.",
"data": {
"id": "caafd318-527c-466b-81f2-441d3a092aae",
"fee": "2000",
"unit": "msats",
"amount": "101000",
"invoice": "lnbc1010n1p3mt8akpp5uyxhllflux2fvl36wjxh80wtqzdh2vjmu8cdyzuap578az80v74qdqcd3hzucmpwd5zqurp09kk2mn5cqzpgxqzjhsp550s770zy4puj76wqnua0hmym883gr07dhuast5ygcm44grl6z7ns9qyyssqgf7sumdmxwzgkq2m0h9lcv530sqs0m4t4shlu98djrrckrtulcmr8rear70dyftdm67jvgncxgz4jmd6ksx87jvnj88e39un48ssk4gp8vs4u5",
"preimage": "8a14f6da89d4a8ffd09677f585b7c377de72744b7c3713d3c115fa71ca4fc290",
"internalId": "11af01d092444a317cb33faa6b8304b8",
"processedAt": "2023-01-04T15:48:29.805Z",
"confirmedAt": "2023-01-04T15:48:29.805Z",
"description": "Custom Payment Description",
"status": "completed"
}
}
```
# Send Payment
Source: https://docs.zbdpay.com/payments/api/lightning-payments/send
POST https://api.zbdpay.com/v0/payments
Start sending instant Bitcoin payments through the ZBD API.
## Description
This endpoint sends a payment on the Bitcoin Lightning Network. This is one of the main APIs which allow you to pay any [Charge](/payments/glossary#charge) in the network.
The words *Charge*, *Payment Request*, and *Invoice* can be seen as equivalent in the context of requesting funds in the Bitcoin Lightning Network.
## Usage
All payments in the Lightning Network are asynchronous. Though possible, most payments will not settle immediately on the API invocation. They do however tend to settle *immediately thereafter*, which means you **MUST** to provide the `callbackUrl` to receive updates about your app's payments.
If the Charge does not have a predefined amount (e.g. an `amountless invoice`), you can pass an `amount` property with a defined value to this API call.
## Configuration
### Header Parameters
ZBD Project API Key
Content Type
### Body
Lightning Network Payment Request / Charge
Note or comment for this Payment
Amount to be paid to this Charge/Invoice -> in millisatoshis *(only valid if Amountless Invoice)*
Open metadata string property
The endpoint ZBD will POST Payment updates to
```json Response theme={null}
{
"success": true,
"message": "Payment done.",
"data": {
"id": "caafd318-527c-466b-81f2-441d3a092aae",
"fee": "2000",
"unit": "msats",
"amount": "101000",
"invoice": "lnbc1010n1p3mt8akpp5uyxhllflux2fvl36wjxh80wtqzdh2vjmu8cdyzuap578az80v74qdqcd3hzucmpwd5zqurp09kk2mn5cqzpgxqzjhsp550s770zy4puj76wqnua0hmym883gr07dhuast5ygcm44grl6z7ns9qyyssqgf7sumdmxwzgkq2m0h9lcv530sqs0m4t4shlu98djrrckrtulcmr8rear70dyftdm67jvgncxgz4jmd6ksx87jvnj88e39un48ssk4gp8vs4u5",
"preimage": "8a14f6da89d4a8ffd09677f585b7c377de72744b7c3713d3c115fa71ca4fc290",
"internalId": "11af01d092444a317cb33faa6b8304b8",
"status": "completed"
"processedAt": "2023-01-04T15:48:29.805Z",
"confirmedAt": "2023-01-04T15:48:29.805Z",
"description": "Custom Payment Description",
}
}
```
# OpenAPI
Source: https://docs.zbdpay.com/payments/api/resources/openapi
Explore the OpenAPI specification for the ZBD Payments API.
The official OpenAPI specification for the ZBD Payments API.
# Utilities
Source: https://docs.zbdpay.com/payments/api/utils
Utility APIs for developers integrating with ZBD Payments.
Section coming soon.
# BTC USD Price Feed
Source: https://docs.zbdpay.com/payments/api/utils/btc-usd
GET https://api.zbdpay.com/v0/btcusd
Get the latest price for Bitcoin in US Dollars.
## Description
The exchange rate feed is refreshed every 5 seconds and is based upon a combination of industry-leading partner exchange providers's price feeds.
## Usage
Use this API to show your users the correct USD value when paying any Satoshis/BTC Charges or Payment Requests. This API allows you to stay synced with ZBD on the `price of Bitcoin`.
```json Response theme={null}
{
"success": true,
"data": {
"btcUsdPrice": "16825",
"btcUsdTimestamp": "1672847113"
},
"message": "Successfully retrieved BTC USD price ticker information."
}
```
# ZBD IP Address
Source: https://docs.zbdpay.com/payments/api/utils/ip
GET https://api.zbdpay.com/v0/prod-ips
Get the official IP addresses of ZBD servers.
## Description
The ZBD API relies on callback URLs for keeping you informed about updates that occur to any Charges, Payments, or Withdrawals you've created. In order to ensure that any incoming callback message is indeed from a trusted ZBD API infrastructure server, we provide this API endpoint for you to know which IP addresses `real` requests come from.
## Usage
The idea is to use this API endpoint to fetch the production IP addresses for ZBD servers, such that when you receive a callback about a payment having settled, you can check the originating IP address and confirm it indeed came from ZBD.
ZBD has a static set of IP addresses so, though possible, it is unlikely we will be changing these IPs often.
## Configuration
### Header Parameters
ZBD Project API Key
```json Response theme={null}
{
"success": true,
"data": {
"ips": [
"3.225.112.64"
]
}
}
```
# API Supported Regions
Source: https://docs.zbdpay.com/payments/api/utils/is-supported-region
GET https://api.zbdpay.com/v0/is-supported-region/{IP_ADDRESS}
Verify if a user is coming from a supported region.
## Description
The ZBD API is available in over 100 countries around the world, and we're always aiming to expand our reach.
If you wish to know whether the incoming user request is coming from a region where ZBD is supported or not, you can use this simple API endpoint and pass the target IP address as a parameter.
## Usage
The idea behind this endpoint is that in order to serve your users the best way possible, you should check where they are located and ensure that they can make use of ZBD services.
## Configuration
### Path Parameters
IP address to check
### Header Parameters
ZBD Project API Key
```json Response theme={null}
{
"success": true,
"data": {
"ipAddress": "66.109.221.0",
"isSupported": true,
"ipCountry": "US",
"ipRegion": "CO"
}
}
```
# Wallet
Source: https://docs.zbdpay.com/payments/api/wallet
How to manage your ZBD Wallet using ZBD Payments APIs.
Section coming soon.
# Initiate Internal Transfer
Source: https://docs.zbdpay.com/payments/api/wallet/internal-transfer
POST https://api.zbdpay.com/v0/internal-transfer
Performs a transfer of funds between two Projects.
## Usage
The API Key used is the Project Wallet that is `SPENDING` the funds, so the `apikey` used in this request must be the `SENDER` Project Wallet.
In the body payload, the API also expects a `receiverWalletId`. This Project Wallet ID has to match the `RECEIVING` Project Wallet ID, which can be found in the ZBD Developer Dashboard under Project Details.
This API is primarily to be used for any programmatic movement of funds between any two of your Projects.
## Configuration
### Header Parameters
ZBD Project API Key
Content Type
### Body
The amount to be transferred -> in millisatoshis
The Wallet ID of the recipient Project
```json Response theme={null}
{
"success": true,
"data": {
"id": "c8571a4c-3c34-47af-8ded-ebd476f519c1",
"senderWalletId": "b81fa874-ac60-4a08-84f2-0af79684c506",
"receiverWalletId": "b804ee02-ec0b-4fd4-b99f-1f2d3d0001a6",
"userId": "2470d896-9dc8-4d49-b18c-a4e83263e76d",
"sendTxId": "f532db20-2cfa-41b0-8c00-40cfbe124b54",
"receiveTxId": "4b4640e0-d8a2-4f1e-8973-d98196f79a88",
"status": "TRANSFER_STATUS_COMPLETED",
"amount": "1000",
"createdAt": "2023-02-16T02:28:32.754Z",
"updatedAt": "2023-02-16T02:28:32.777Z"
},
"message": "Internal Transfer done."
}
```
# Get Wallet
Source: https://docs.zbdpay.com/payments/api/wallet/retrieve-balance
GET https://api.zbdpay.com/v0/wallet
Retrieve all data about a ZBD Project's Wallet.
To retrieve the balance of the Wallet for a specific ZBD Project, just make sure to use that Project's API Key in the `apikey` header property in the API endpoint.
## Configuration
### Header Parameters
ZBD Project API Key
```json Response theme={null}
{
"message": "Successfully retrieved Wallet.",
"data": {
"unit": "msats",
"balance": "384000"
}
}
```
# Withdrawal Requests
Source: https://docs.zbdpay.com/payments/api/withdrawal-requests
How to create and manage Withdrawal Requests using ZBD Payments APIs.
Section coming soon.
# Create Withdrawal Request
Source: https://docs.zbdpay.com/payments/api/withdrawal-requests/create
POST https://api.zbdpay.com/v0/withdrawal-requests
Start creating Bitcoin voucher QR codes.
## Description
Withdrawal Requests can be thought of as exact opposites to Charges. Charges in the ZBD API are QR codes that represent Payment Requests in the Bitcoin Lightning Network. These QR codes expect that a payer will scan and perform a **payment** against it.
A Withdrawal Request is the same in that it also is represented as a QR code. But unlike Charges, Withdrawal Requests are QR codes that someone scans to **receive** Bitcoin. You can read more about the LNURL Withdrawal specification [here](https://github.com/lnurl/luds/blob/luds/03.md).
Think of it like this:
* **Charges** -> Lightning QR codes that **YOU SPEND**
* **Withdrawal Requests** -> Lightning QR codes that **YOU RECEIVE**
## Usage
The most common use of this API endpoint are 1) Fintechs adding the ability for Withdrawals / Payouts through the Lightning Network, and 2) Apps and Games sending Bitcoin microrewards through Withdrawal QR codes / redeem codes.
Every Withdrawal Request will be in a `pending` state until someone scans the QR code and claims the funds.
Withdrawal Requests created by ZBD API are `single-use` and will not work if attempted again.
## Configuration
### Header Parameters
ZBD Project API Key
Content Type
### Body
The amount for the Withdrawal Request -> in millisatoshis
Note or comment for this Withdrawal Request
Time until Withdrawal Request expiration -> in seconds
Open metadata string property
The endpoint ZBD will POST Charge updates to
```json 200 Response theme={null}
{
"success": true,
"data": {
"id": "e54e62d4-9cfa-495d-abb9-9c9f4bc4f2cf",
"unit": "msats",
"amount": "15000",
"createdAt": "2023-04-27T22:15:54.258Z",
"expiresAt": "2023-04-27T22:20:54.252Z",
"internalId": "1c3b1-f61j2",
"description": "Withdraw QR!",
"callbackUrl": "https://your-website.com/zbd-callback",
"status": "pending",
"fee": null,
"invoice": {
"request": "lnurl1dp68gurn8ghj7ctsdyh85etzv4jx2efwd9hj7a3s9acxz7tvdaskgtthd96xserjv9mkzmpdwfjhzat9wd6r7um9vdex2apax5exxwpjx9jx2wf4xajnxc3sx4jrwcenv56nwdm9vg6kxwtrx9sk2dtzvsunxce3vymkxef4vycxvwpexd3kxcmpxcmk2d3n8yenswqph63m6",
"fastRequest": "lnurl1dp68gurn8ghj7ctsdyh85etzv4jx2efwd9hj7a3s9acxz7tvdaskgtthd96xserjv9mkzmpdwfjhzat9wd6r7arpvu7hw6t5dpj8ycth2fjhzat9wd6zv6e3856nycecxgckgefex5mk2vmzxq6kgdmrxdjn2dehv43r2ceevvckzef4vfjrjvmrx9snwcm9x4snqe3c8yekxcmrvymrwefkxvunxwpcyekkjmjhd96xserjv9mkzcnvv57nzdfsxqczvmtp0ptkjargv3exzampvfkx20f3x5crqvpxv3jkvct4d36ygetnvdexjur5d9hku02hd96xserjv9mjq52jyynxxctvd33xzcmt8458garswvaz7tmpwp5ju7n9vfjkget99e5k7tmkxqhhqun0vdjhxuedwa5hg6rywfshwctv94ex2ut4v4ehg5thy6x",
"uri": "lightning:lnurl1dp68gurn8ghj7ctsdyh85etzv4jx2efwd9hj7a3s9acxz7tvdaskgtthd96xserjv9mkzmpdwfjhzat9wd6r7um9vdex2apax5exxwpjx9jx2wf4xajnxc3sx4jrwcenv56nwdm9vg6kxwtrx9sk2dtzvsunxce3vymkxef4vycxvwpexd3kxcmpxcmk2d3n8yenswqph63m6",
"fastUri": "lightning:lnurl1dp68gurn8ghj7ctsdyh85etzv4jx2efwd9hj7a3s9acxz7tvdaskgtthd96xserjv9mkzmpdwfjhzat9wd6r7arpvu7hw6t5dpj8ycth2fjhzat9wd6zv6e3856nycecxgckgefex5mk2vmzxq6kgdmrxdjn2dehv43r2ceevvckzef4vfjrjvmrx9snwcm9x4snqe3c8yekxcmrvymrwefkxvunxwpcyekkjmjhd96xserjv9mkzcnvv57nzdfsxqczvmtp0ptkjargv3exzampvfkx20f3x5crqvpxv3jkvct4d36ygetnvdexjur5d9hku02hd96xserjv9mjq52jyynxxctvd33xzcmt8458garswvaz7tmpwp5ju7n9vfjkget99e5k7tmkxqhhqun0vdjhxuedwa5hg6rywfshwctv94ex2ut4v4ehg5thy6x"
}
},
"message": "Successfully created Withdrawal Request."
}
```
```json 400 Response theme={null}
{
"success": false,
"message": "The minimum Withdrawal amount supported is 10 satoshis."
}
```
# Retrieve Withdrawal Request
Source: https://docs.zbdpay.com/payments/api/withdrawal-requests/retrieve
GET https://api.zbdpay.com/v0/withdrawal-requests/{id}
Retrieve all data about a single Withdrawal Request.
## Configuration
### Path Parameters
Withdrawal Request ID
### Header Parameters
ZBD Project API Key
```json Response theme={null}
{
"success": true,
"message": "Successfully retrieved Withdrawal Request.",
"data": {
"unit": "msats",
"amount": "12000",
"status": "pending",
"createdAt": "2020-05-09T15:15:30.222Z",
"expiresAt": "2020-05-09T15:20:30.212Z",
"description": "My Withdrawal Description",
"id": "243ee7f6-a91b-4644-aa5f-ecc9c9fdd351",
"internalId": "11af01d092444a317cb33faa6b8304b8",
"callbackUrl": "https://your-website.com/zbd-callback",
"invoice": {
"request": "lnurl1dp68gurn8ghj7er9wch85etzv4jx2efwd9hj7a3s9acxz7tvdaskgtthd96xserjv9mkzmpdwfjhzat9wd6r7um9vdex2apaxfsnqvmz8pjx2ephxucnzdphxa3xzd3kvfjk2vnyxejnzdf3x93n2e3evesnsetyxsenzvesvdjrxdtrv9jxxdnzxaskxvm9vscrzeg58h24k",
"fastRequest": "lnurl1dp68gurn8ghj7er9wch85etzv4jx2efwd9hj7a3s9acxz7tvdaskgtthd96xserjv9mkzmpdwfjhzat9wd6r7arpvu7hw6t5dpj8ycth2fjhzat9wd6zv6e385exzvpnvguxgetyxumnzvf5xumkycfkxe3x2efjvsmx2vf4xyckxdtx89nxzwr9vs6rxvfnxp3kgve4vdskgcekvgmkzcenv4jrqvt9yekkjmjhd96xserjv9mkzcnvv57nzv3sxqczvmtp0ptkjargv3exzampvfkx20f3xgcrqvpxv3jkvct4d36ygetnvdexjur5d9hku02d0ys9w6t5dpj8ycthv9kzq3r9wd3hy6tsw35k7m3xvdskcmrzv93kk0tgw368que69uhkgetk9eax2cn9v3jk2tnfduhhvvp0wpex7cm9wdej6amfw35xgunpwaskcttjv4ch2etnwsvtdjtq",
"uri": "lightning:lnurl1dp68gurn8ghj7er9wch85etzv4jx2efwd9hj7a3s9acxz7tvdaskgtthd96xserjv9mkzmpdwfjhzat9wd6r7um9vdex2apaxfsnqvmz8pjx2ephxucnzdphxa3xzd3kvfjk2vnyxejnzdf3x93n2e3evesnsetyxsenzvesvdjrxdtrv9jxxdnzxaskxvm9vscrzeg58h24k",
"fastUri": "lightning:lnurl1dp68gurn8ghj7er9wch85etzv4jx2efwd9hj7a3s9acxz7tvdaskgtthd96xserjv9mkzmpdwfjhzat9wd6r7arpvu7hw6t5dpj8ycth2fjhzat9wd6zv6e385exzvpnvguxgetyxumnzvf5xumkycfkxe3x2efjvsmx2vf4xyckxdtx89nxzwr9vs6rxvfnxp3kgve4vdskgcekvgmkzcenv4jrqvt9yekkjmjhd96xserjv9mkzcnvv57nzv3sxqczvmtp0ptkjargv3exzampvfkx20f3xgcrqvpxv3jkvct4d36ygetnvdexjur5d9hku02d0ys9w6t5dpj8ycthv9kzq3r9wd3hy6tsw35k7m3xvdskcmrzv93kk0tgw368que69uhkgetk9eax2cn9v3jk2tnfduhhvvp0wpex7cm9wdej6amfw35xgunpwaskcttjv4ch2etnwsvtdjtq"
}
}
}
```
# Global Support
Source: https://docs.zbdpay.com/payments/coverage
List of regions where ZBD Payments APIs, SDKs, and services are supported.
**This page lists region coverage for ZBD Payments products.** For ZBD Rewards product region coverage, please visit [Rewards Coverage](/earn/coverage).
### US States
The ZBD API is available in all US states, **except New York (NY)**.
***
### Countries
The ZBD API is **NOT YET** available in the following countries:
* Afghanistan
* Algeria
* Andorra
* Aruba
* Azerbaijan
* Bahamas
* Bangladesh
* Barbados
* Belarus
* Belize
* Benin
* Bolivia
* Bosnia-Herzegovina
* Botswana
* Burundi
* Cambodia
* Cayman Islands
* Central African Republic
* Chad
* China
* Comoros
* Congo (Brazzaville)
* Costa Rica
* Cuba
* Democratic Peopleโs Republic of Korea (DPRK)
* Democratic Republic of the Congo
* Dominican Republic
* Ecuador
* Equatorial Guinea
* Falkland Islands
* Faroe Islands
* Fiji
* French Polynesia
* Ghana
* Gibraltar
* Greenland
* Guam
* Guatemala
* Guinea
* Guinea-Bissau
* Haiti
* Honduras
* Iran
* Iraq
* Jamaica
* Kiribati
* Kyrgyzstan
* Lao Peopleโs Democratic Republic
* Lebanon
* Lesotho
* Liberia
* Libya
* Liechtenstein
* Macau
* Madagascar
* Maldives
* Mali
* Marshall Islands
* Martinique
* Mauritania
* Mauritius
* Monaco
* Mongolia
* Morocco
* Mozambique
* Myanmar (Burma)
* Nicaragua
* Niger
* Niue
* Norfolk Island
* Oman
* Pakistan
* Palau
* Palestine, State of
* Panama
* Pitcairn
* Russia
* Samoa
* San Marino
* Sao Tome & Prin.
* Saudi Arabia
* Senegal
* Seychelles
* Somalia
* South Sudan
* Sudan
* Syria
* Tanzania
* Timor-Leste
* Togo
* Trinidad and Tobago
* Turkmenistan
* Tuvalu
* Uganda
* Ukraine
* United States Virgin Islands
* Vanuatu
* Vatican City State
* Venezuela
* Vietnam
* Yemen
* Zimbabwe
# Glossary
Source: https://docs.zbdpay.com/payments/glossary
In this documentation portal you will encounter a handful of highly-specific technical terms that are used throughout the ZBD Platform and APIs. Below you will find a Glossary of the main terms used. This list is not exhaustive and is meant to help you understand the concepts and terminology used in the documentation.
## B[โ](#b "Direct link to heading")
### BOLTs[โ](#bolts "Direct link to heading")
The [Basis of Lightning Technology (BOLT)](https://github.com/lightning/bolts) are specifications that describe the rules and standards required for participants of the Lightning Network. The standards established by the BOLTs allow different Lightning implementation software to integrate with one another and form a network.
### BOLT11[โ](#bolt11 "Direct link to heading")
[BOLT11](https://bolt11.org) is the 11th specification document for the BOLTs (Basis of Lightning Technology), and it is the invoice protocol for Lightning Payments. It is a simple, extendable, QR-code-ready protocol for requesting payments over Lightning.
Learn more about BOLT11 on this [interactive visualizer](https://bolt11.org) as well as the core [RFC documentation](https://github.com/lightning/bolts/blob/master/11-payment-encoding.md).
## C[โ](#c "Direct link to heading")
### Charge[โ](#charge "Direct link to heading")
A Charge is the name given inside of the ZBD Platform to a Lightning Network invoice (BOLT11) that is sent to a peer to request a Bitcoin payment.
## I[โ](#i "Direct link to heading")
### Invoice[โ](#invoice "Direct link to heading")
Invoices, charges, payment requests -- these are all terms for the same thing. Invoices are used to request payments from a peer on the Bitcoin Lightning Network.
## L[โ](#l "Direct link to heading")
### Lightning Address[โ](#lightning-address "Direct link to heading")
A [Lightning Address](https://lightningaddress.com) is a unique internet identifier that identifies a user inside of a given provider. It resembles an email address format with `username` @ `provider.com`. The ZBD API allows a developer to send payments to any user of a company, wallet provider, or self-hosted user software that supports the open source protocol.
By leveraging Lightning Addresses a developer is able to support sending payments to users of any provider, with frictionless user experience.
**It's like an email address, but for your Bitcoin.**
Every ZBD user has a unique Lightning Address based on their [ZBD Gamertag](https://zbd.gg) resembling **[satoshi@zbd.gg](mailto:satoshi@zbd.gg)**.
### LNURL[โ](#lnurl "Direct link to heading")
LNURL is a set of HTTP open API standards that aim to facilitate the interactions between service providers (stores, websites, apps) and wallets (users). The idea is to provide for standardized ways of performing Bitcoin Lightning Network invoice-orchestration and payment-requesting.
LNURL technologis are leveraged inside of the ZBD Platform in places such as Static Charges and Withdrawal Requests. LNURL is also the base protocol for the Lightning Address user standard.
## M[โ](#m "Direct link to heading")
### Millisatoshi[โ](#millisatoshi "Direct link to heading")
The ZBD API is built on millisatoshis. 1,000 millisatoshis make up 1 satoshi. And 100 million satoshis make up 1 Bitcoin. This is akin to 100 cents being equal to 1 dollar.
While the API is built with millisatoshis in mind, the currently-allowed minimum amount for transaction sizes is 1 satoshi, or 1,000 millisatoshis. 1 satoshi is currently valued at \$0.0005 USD, and the system was built with scalability in mind, both in amount size, and Bitcoin valuation.
**100,000,000 satoshis = 1 Bitcoin**
**1,000 millisatoshis = 1 satoshi**
## O[โ](#o "Direct link to heading")
### On-chain[โ](#on-chain "Direct link to heading")
On-chain is a term used to describe any data that is registered on the Bitcoin blockchain, in contrast with off-chain data, which is not stored on the blockchain. On-chain data are always Bitcoin transactions, while off-chain data can be an unconfirmed Bitcoin transaction or any other type of data.
### Off-chain[โ](#off-chain "Direct link to heading")
Off-chain is a term used to describe any data that is not registered on the Bitcoin blockchain. Off-chain data can be Bitcoin transactions that were not sent to the blockchain, data on the Lightning Network, or data on other blockchains, such as a sidechain.
## S[โ](#s "Direct link to heading")
### Satoshi[โ](#satoshi "Direct link to heading")
Satoshis, or โsatsโ for short, are the atomic unit of Bitcoin, named after Bitcoinโs creator, Satoshi Nakamoto. A Bitcoin is equal to 100 million satoshis (1 BTC = 100,000,000 sats).
**There will only ever be 21 million bitcoin, and there will only ever be 2.1 quadrillion sats.**
This makes Bitcoin more divisible, enabling payments smaller than $0.01 at current prices. 1 satoshi is currently valued at ~$0.0005 USD.
## V[โ](#v "Direct link to heading")
### Voucher[โ](#voucher "Direct link to heading")
A ZBD Voucher is an 8-digit code that can be used in any product in the ZBD suite to redeem for Bitcoin on the Lightning Network. It is usually used as a tool for onboarding new users to Bitcoin.
Learn more about the capabilities of ZBD Vouchers on [this thread](https://twitter.com/andreneves/status/1517572121609879552).
## W[โ](#w "Direct link to heading")
### Withdrawal Request[โ](#withdrawal-request "Direct link to heading")
A Withdrawal Request is the direct opposite of a Payment Request (or Charge). The Withdrawal Request is used to request a withdrawal **from** a user's wallet, whereas a Payment Request is used to request a payment **to** a user's wallet.
While both user flows are nearly identical -- scan QR code and press submit -- behind the scenes one flow is sending Bitcoin over Lightning while the other is **request Bitcoin to be sent over Lightning**. If you ever want to **receive Bitcoin by scanning a QR code**, then that's what a Withdrawal Request effectively enables.
# Lightning Address
Source: https://docs.zbdpay.com/payments/lightning-address
The human-readable payment protocol that transformed Bitcoin UX forever
Lightning Address is the protocol that made Bitcoin payments as simple as sending an email. Created by Andre Neves (CTO of ZBD) in 2021, it has become the standard for user-friendly Bitcoin payments, processing millions of transactions across hundreds of apps and services globally.
**Fun Fact**: Lightning Address was invented at ZBD to solve a real problem โ our users were terrified of long Bitcoin Lightning invoices. Today, it's an open protocol used by the entire Lightning ecosystem.
[user@domain.com](mailto:user@domain.com) instead of lnbc1pvjluezpp5qqqsyq...
One address, unlimited payments. No more invoice generation.
Works across all supporting wallets and services globally.
## The UX Revolution
### Before Lightning Address (2009-2021)
```bash theme={null}
# Bitcoin address (hard to remember, changes each time)
bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh
# Lightning invoice (expires, single use, even longer)
lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w
```
**Problems:**
* ๐คฏ Impossible to memorize
* โฑ๏ธ Invoices HAVE to expire at a certain time
* ๐ New address / invoice needed for each payment
* ๐ฑ Required QR codes or copy-paste
### After Lightning Address (2021-Present)
```bash theme={null}
# Lightning Address (simple, permanent, reusable)
andre@zbd.gg
```
**Solved:**
* โ
Easy as email โ memorizable and shareable
* โ
Works 24/7 โ receive payments while you sleep
* โ
One address forever โ put it in your bio
* โ
No QR codes needed โ just type it
## How Lightning Address Works
Sender enters `gamer@zbd.gg` in any Lightning wallet
Wallet looks up `https://zbd.gg/.well-known/lnurlp/gamer`
ZBD creates a fresh Lightning invoice for this payment
Standard Lightning payment flows to recipient
```mermaid theme={null}
sequenceDiagram
participant User
participant Wallet
participant ZBD Server
participant Recipient
User->>Wallet: Pay andre@zbd.gg
Wallet->>ZBD Server: GET /.well-known/lnurlp/andre
ZBD Server->>Wallet: Payment details + invoice
Wallet->>ZBD Server: Lightning payment
ZBD Server->>Recipient: Payment received!
```
## Real-World Impact
Since its creation in 2021, Lightning Address has transformed Bitcoin payments and ZBD is here to support it.
Maintaining the protocol specification, contributing improvements back to community, and advocating for widespread adoption.
### Success Stories
**๐ฎ Gaming Revolution**
* Players share addresses as gamertags
* Streamers receive tips without interruption
* Tournament payouts to `player@zbd.gg`
* Cross-game portable identity
**๐ธ Creator Economy**
* Artists put addresses in social bios
* Direct fan-to-creator payments
* No platform fees or restrictions
* Instant global monetization
**๐ Global Remittances**
* Families share memorable addresses
* No bank account required
* Instant settlement, minimal fees
* Works on basic smartphones
## Why Lightning Address Matters
### 1. Mainstream-Ready UX
**Email Model**: Everyone understands email. Lightning Address uses the same mental model โ user\@domain โ making Bitcoin accessible to billions.
No technical knowledge required. No long strings. No QR codes. Just human-readable addresses.
**Set It and Forget It**: Unlike invoices that expire or addresses that change, your Lightning Address is permanent. Put it on business cards, social profiles, or billboards.
Receive payments 24/7/365 without generating new invoices.
**Universal Standard**: Send from any supporting wallet to any other supporting service. Like email, it just works across providers.
ZBD to Cash App โ all seamless.
### 2. Enhanced Privacy
* No on-chain footprint for address lookups
* Rotating invoices prevent payment correlation
* Optional sender/receiver metadata
* No public address / invoice reuse
### 3. Developer Friendly
```javascript Node.js theme={null}
// Send to Lightning Address
const payment = await zbd.sendLightningAddress({
lnAddress: 'andre@zbd.gg',
amount: 1000, // sats
comment: 'Thanks for creating this!'
});
// Validate Lightning Address
const isValid = await zbd.validateLightningAddress({
lnAddress: 'user@domain.com'
});
```
```python Python theme={null}
# Create Lightning Address for user
address = zbd.create_lightning_address(
username="newuser",
user_id="usr_123"
)
# Returns: newuser@zbd.gg
# Receive notification on payment
@webhook_handler
def payment_received(data):
print(f"Received {data.amount} sats")
```
## Advanced Features
### Custom Domains
Run Lightning Address on your own domain:
```nginx theme={null}
# Nginx config for yourgame.com
location /.well-known/lnurlp/ {
proxy_pass https://api.zbdpay.com/lnurlp/;
proxy_set_header Host api.zbdpay.com;
}
```
Now users can have `player@yourgame.com` addresses!
## The Future of Lightning Address
ZBD continues to evolve the protocol:
* **๐ Multi-Currency Support**: Send USD and receive BTC seamlessly
* **๐ Cross-Currency Payments**: Automatic currency conversion on payment
* **๐ Bitcoin Onchain**: Support for traditional Bitcoin addresses (fallback)
* **๐ BOLT12 Integration**: Enhanced Lightning Network features
**Want to onramp users directly to Lightning Addresses?**
[ZBD Ramp](/payments/ramp) is the first and only fiat-to-crypto widget that supports Lightning Address delivery. Let your users buy Bitcoin and receive it instantly at their Lightning Address!
## Start Using Lightning Address
[Contact our team](https://zbd.one/sales) to get your API keys
Use our SDKs to create and manage Lightning Addresses
Help users understand their new superpower
Complete Lightning Address API docs
Quick integration with our SDKs
Ramp directly to Lightning Addresses
***
Lightning Address is an open protocol. While ZBD created and maintains it, it's free for anyone to implement. We're proud to have contributed this UX breakthrough to the Bitcoin ecosystem and continue supporting its growth.
# Lightning Network
Source: https://docs.zbdpay.com/payments/lightning-network
Instant global payments that unlock new business models impossible with traditional systems
The Lightning Network transforms Bitcoin from digital gold into a global payment rail capable of millions of transactions per second. It's not just faster Bitcoin โ it's an entirely new paradigm for moving money.
Payments settle in milliseconds, not minutes or days
Send 1 cent for \$0.0001 in fees. Finally, micropayments that work.
Millions of TPS capacity. Built for the scale of the internet.
## What Makes Lightning Different
### Traditional Payments vs Lightning
**Credit Cards**: 2-3 business days to settle\
**Bank Transfers**: 1-5 business days\
**Bitcoin**: 10-60 minutes\
**Lightning**: \< 1 second โก
Lightning payments are literally faster than loading a webpage.
**Credit Cards**: `2.9% + $0.30`\
**International Wire**: `$25-50`\
**Bitcoin**: `$1-20` (varies)\
**Lightning**: `< $0.01` (typical)
Send 1 cent profitably.
## New Business Models Now Possible
Lightning enables entirely new ways to monetize and transfer value that were impossible before:
### ๐ฐ True Micropayments
Finally, payments smaller than a dollar that actually make sense:
* \$0.01 to read an article
* \$0.001 per API call
* \$0.05 to skip ads
* Tips as small as \$0.0001
* Buy items for pennies
* Micro-wagers on matches
* Pay-to-play arcade style
* Fractional tournament fees
### ๐ Instant Global Settlement
Send money anywhere, instantly, 24/7/365:
```javascript theme={null}
// Monday 3am in Tokyo โ Sunday 2pm in New York
// Settlement: 847 milliseconds
// Fee: $0.003
// No banks involved
await zbd.send({
amount: 1000000, // sats (~$500)
receiver: 'business@tokyo.jp',
comment: 'Invoice #12345'
});
```
**Unlocked Use Cases:**
* Global gig economy payments
* Cross-border gaming economies
* International remittances
* 24/7 market settlement
### โก Machine-to-Machine Payments
APIs and IoT devices that pay each other automatically:
```javascript theme={null}
// Car pays for parking by the second
while (parked) {
await payParkingMeter({
amount: 0.001, // $0.001 per second
meterId: 'meter_sf_123'
});
await sleep(1000);
}
```
**Future Applications:**
* Self-paying autonomous vehicles
* IoT device micropayments
* AI agents with wallets
* Automated supply chains
## How Lightning Actually Works
Think of Lightning like having bar tabs across the internet. Instead of settling every drink immediately, you keep a running tab and settle up later.
### The Bar Tab Analogy
You and the bartender each put \$100 in a lockbox (the channel)
Each drink updates the balance. You owe more, bartender owes less.
Your friend can pay through your tab if they know the bartender
Close the tab and take your remaining balance
### Technical Architecture
```mermaid theme={null}
graph TB
A[Your Wallet] ---|Payment Channel| B[ZBD Node]
B ---|Routing| C[Lightning Network]
C ---|Multiple Hops| D[Recipient]
style A fill:#f9f,stroke:#333,stroke-width:2px
style D fill:#9f9,stroke:#333,stroke-width:2px
```
**Key Concepts:**
Two-party Bitcoin smart contracts that enable unlimited off-chain transactions. Only opening and closing hit the blockchain.
Payments find the best path through multiple channels automatically. Like internet packet routing, but for money.
HTLCs (Hash Time-Locked Contracts) ensure payments either complete fully or fail completely. No money gets stuck.
Channels need Bitcoin on both sides to route payments. ZBD manages this complexity for you.
## Common Myths Debunked
**"Lightning is too complex for users"**
Reality: Users just see an email-like address ([user@zbd.gg](mailto:user@zbd.gg)). The complexity is abstracted away, just like email hides SMTP.
**"Lightning is centralized"**
Reality: 15,000+ nodes globally. More decentralized than traditional DNS or BGP internet routing.
**"Lightning is still experimental"**
Reality: \$400M+ total network capacity, processing millions of payments daily across major apps and games.
## Building on Lightning
### Quick Start Examples
```javascript Node.js theme={null}
// Receive streaming payments
const invoice = await zbd.createCharge({
amount: 1000, // sats
description: 'Power-up purchase'
});
// Send instant payment
await zbd.sendPayment({
receiver: 'gamer@zbd.gg',
amount: 500,
comment: 'GG! Nice win'
});
```
```python Python theme={null}
# Generate Lightning invoice
invoice = zbd.create_charge(
amount=1000,
description="API credits"
)
# Pay Lightning Address
payment = zbd.send_payment(
receiver="api@zbd.gg",
amount=100
)
```
### Best Practices
Begin with simple send/receive before complex flows
Use satoshis (1 BTC = 100M sats) for precision
## The Future is Streaming Money
Lightning isn't just an improvement to payments โ it's a fundamental shift in how money moves. Just as the internet packetized information, Lightning packetizes value.
### What's Next
* **AI Agents**: LLMs with Lightning wallets paying for resources
* **Streaming Contracts**: Smart contracts that execute based on payment flows
* **Global Gaming Economies**: Truly borderless in-game currencies
* **Machine Economy**: Billions of devices transacting autonomously
Get your API keys and build the next generation of payment experiences. The internet of money is here.
## Resources
Human-readable payment addresses
Complete Lightning API docs
Step-by-step implementation
***
Lightning Network is open source and permissionless. While you can technically run your own node, ZBD handles all of the complexity of channel management, liquidity, and routing so you can focus on building great experiences.
# MCP Server
Source: https://docs.zbdpay.com/payments/mcp
Extend the capabilities of your AI Agents with ZBD Payments as an MCP Server
## What is MCP?
MCP is a protocol for integrating tools with AI Agents. It can greatly enhance the capabilities of your AI Agents by providing them with real-time data and context.
ZBD has MCP support built into the [ZBD Payments TypeScript SDK](/payments/sdk/typescript).
## Using ZBD Payments as an MCP server
You need a MCP-capable Agent environment to use ZBD Payments as an MCP server. A few of them are Claude and Cursor.
### Claude
Add the following server definition to your claude\_desktop\_config.json file:
```json theme={null}
{
"mcpServers": {
"zbd": {
"command": "npx",
"args": ["-y", "@zbdpay/payments-sdk-mcp", "--client=claude"],
"env": {
"ZBD_PAYMENTS_API_KEY": "YOUR_ZBD_API_KEY"
}
}
}
}
```
## Filtering tools
You can run the package on the command line to discover and filter the set of tools that are exposed by the ZBD MCP Server. This can be helpful for large APIs where including all endpoints at once is too much for your AI's
context window.
You can filter by multiple aspects:
* `--tool` includes a specific tool by name
* `--resource` includes all tools under a specific resource, and can have wildcards, e.g. `my.resource*`
* `--operation` includes just read (get/list) or just write operations
See more information with `--help`.
All of these command-line options can be repeated, combined together, and have corresponding exclusion versions (e.g. `--no-tool`).
Use `--list` to see the list of available tools, or see below.
### Specifying the MCP Client
Different clients have varying abilities to handle arbitrary tools and schemas.
You can specify the client you are using with the `--client` argument, and the MCP server will automatically
serve tools and schemas that are more compatible with that client.
* `--client=`: Set all capabilities based on a known MCP client
* Valid values: `openai-agents`, `claude`, `claude-code`, `cursor`
* Example: `--client=cursor`
Additionally, if you have a client not on the above list, or the client has gotten better
over time, you can manually enable or disable certain capabilities:
* `--capability=`: Specify individual client capabilities
* Available capabilities:
* `top-level-unions`: Enable support for top-level unions in tool schemas
* `valid-json`: Enable JSON string parsing for arguments
* `refs`: Enable support for \$ref pointers in schemas
* `unions`: Enable support for union types (anyOf) in schemas
* `formats`: Enable support for format validations in schemas (e.g. date-time, email)
* `tool-name-length=N`: Set maximum tool name length to N characters
* Example: `--capability=top-level-unions --capability=tool-name-length=40`
* Example: `--capability=top-level-unions,tool-name-length=40`
## Importing the tools and server individually
```js theme={null}
// Import the server, generated endpoints, or the init function
import { server, endpoints, init } from "@zbdpay/payments-sdk-mcp/server";
// import a specific tool
import createChargeGamertags from "@zbdpay/payments-sdk-mcp/tools/gamertags/create-charge-gamertags";
// initialize the server and all endpoints
init({ server, endpoints });
// manually start server
const transport = new StdioServerTransport();
await server.connect(transport);
// or initialize your own server with specific tools
const myServer = new McpServer(...);
// define your own endpoint
const myCustomEndpoint = {
tool: {
name: 'my_custom_tool',
description: 'My custom tool',
inputSchema: zodToJsonSchema(z.object({ a_property: z.string() })),
},
handler: async (client: client, args: any) => {
return { myResponse: 'Hello world!' };
})
};
// initialize the server with your custom endpoints
init({ server: myServer, endpoints: [createChargeGamertags, myCustomEndpoint] });
```
## Available Tools
The following tools are available in this MCP server.
### Resource `gamertags`:
* `create_charge_gamertags` (`write`): Generate a payment request for a ZBD User.
* `retrieve_by_gamertag_gamertags` (`read`): Retrieve Gamertag from a ZBD user ID.
* `retrieve_by_zbd_id_gamertags` (`read`): Retrieve ZBD user ID from a Gamertag.
* `retrieve_payment_gamertags` (`read`): Retrieve all data about a Payment sent to ZBD User.
* `send_payment_gamertags` (`write`): Send instant Bitcoin payments to ZBD Users.
### Resource `lightning_charges`:
* `create_lightning_charges` (`write`): Start receiving instant Bitcoin payments through the ZBD API.
* `retrieve_lightning_charges` (`read`): Retrieve all data about a single Charge.
### Resource `internal_transfer`:
* `initiate_internal_transfer` (`write`): Performs a transfer of funds between two Projects.
### Resource `lightning_address`:
* `create_charge_lightning_address` (`write`): Generate a payment request for a Lightning Address.
* `send_payment_lightning_address` (`write`): Send instant Bitcoin payments to any Lightning Address.
* `validate_lightning_address` (`read`): Verify the validity of a Lightning Address.
### Resource `lightning_static_charges`:
* `create_lightning_static_charges` (`write`): Start accepting payments on Lightning with Static QR codes.
* `retrieve_lightning_static_charges` (`read`): Retrieve all data about a single Static Charge.
* `update_lightning_static_charges` (`write`): Change the configuration of a Static Charge QR code.
### Resource `vouchers`:
* `create_vouchers` (`write`): Create Voucher
* `retrieve_vouchers` (`read`): Get Voucher
* `redeem_vouchers` (`write`): Redeem Voucher
* `revoke_vouchers` (`write`): Revoke Voucher
### Resource `withdrawal_requests`:
* `create_withdrawal_requests` (`write`): Start creating Bitcoin voucher QR codes.
* `retrieve_withdrawal_requests` (`read`): Retrieve all data about a single Withdrawal Request.
### Resource `lightning_payments`:
* `retrieve_lightning_payments` (`read`): Retrieve all data about a single Payment.
* `send_lightning_payments` (`write`): Start sending instant Bitcoin payments through the ZBD API.
### Resource `wallet`:
* `retrieve_balance_wallet` (`read`): Retrieve all data about a ZBD Project's Wallet.
### Resource `utils`:
* `check_ip_support_utils` (`read`): Verify if a user is coming from a supported region.
* `decode_lightning_charge_utils` (`write`): Understand the inner properties of a Charge QR code.
* `list_prod_ips_utils` (`read`): Get the official IP addresses of ZBD servers.
* `retrieve_btc_usd_utils` (`read`): Get the latest price for Bitcoin in US Dollars.
### Resource `oauth2`:
* `create_authorization_url_oauth2` (`read`): Create an authorization URL for ZBD Login.
* `refresh_token_oauth2` (`write`): Generate a new accessToken for a ZBD Login user.
* `retrieve_user_data_oauth2` (`read`): Fetch user-related information about a logged-in ZBD User.
* `retrieve_wallet_data_oauth2` (`read`): Fetch wallet-related information about a logged-in ZBD User.
### Resource `keysend_payments`:
* `send_keysend_payments` (`write`): Start sending Keysend payments on the Lightning Network.
# Request Charge for ZBD Gamertag
Source: https://docs.zbdpay.com/payments/payins/gamertags
Learn how to generate payment requests specifically for ZBD users.
## ZBD Gamertag Charge API
While you can use the [Send Bitcoin to ZBD Gamertag](/payments/api/gamertags/send) endpoint to make a direct payment to a ZBD user, if you want to create a regular Lightning Network payment request / Charge QR code for that given user, you can do so using the [Create Charge for ZBD Gamertag](/payments/api/gamertags/create-charge) endpoint.
In order to request Charges from a ZBD Gamertag you just need to make a simple API call with a payload like the example below.
```bash theme={null}
curl --location --request POST 'https://api.zbdpay.com/v0/gamertag/charges' \
--header 'Content-Type: application/json' \
--header 'apikey: API_KEY_HERE' \
--data-raw '{
"gamertag": "andre",
"amount": "15000",
"internalId": "12347ahk1n",
"description": "ZBD so fast!",
"callbackUrl": "https://your-app.com/zbd-callback"
}'
```
In order to receive updates about the status of this request, make sure to include a `callbackUrl` in the `data` portion of the request.
The response payload will include a **`invoiceRequest`** which is the contents of the payment request QR code.
```json theme={null}
{
"success": true,
"data": {
"status": "CHARGE_PENDING",
"amount": "15000",
"internalId": "12347ahk1n",
"description": "ZBD so fast!",
"invoiceRequest": "lnbc100n1pskq45xpp5q5rtpfzydndqk4fu2y4h64fdc284y6av0fn8gz4dmkq89xuxd28sdpvw3jhxarfdenjqctxw3jhygrjv4kk7anfdenjqmnpd4jscqzpgxqzjcsp59g6vgrss790zkwjqc3adndzt5tcq2ltaxynfstggjl6rre3kk6js9qyyssqfyan62pzsjsm62l6zq57jv9pmlpmycvqu649ccfjn7mar77awgykz2gft3fcgl9x5m4mz78nyng920uf6zwft5qnt2qjtqgpl7c72dgq4z7mgv",
"invoiceExpiresAt": "2021-10-08T14:42:38.759Z",
"unit": "msats",
"createdAt": "2022-02-08T14:32:38.793Z"
},
"message": "Successfully created charge request for gamertag"
}
```
Charges / Payment Requests have defined expiration times. After a Charge expires it cannot be paid, but you can simply create another one.
Once a payment is made and the Charge settles, the ZBD User will receive a Push Notification alerting them they've been sent some Bitcoin.
## Multiplayer Commerce through ZBD Gamertag Charges
For App or Game Developers that wish to provide a more robust commerce experience for their users and gamers, requesting an invoice from ZBD Gamertags can be one of the ways.
**Think of yourself, the Developer, as the Orchestrator of payments.**
By leveraging Lightning Network invoices tied to specific ZBD Gamertag, you can ensure that the payment is made to the correct user, and that the payment has truly settled.
### 1. Two-Step Payment Orchestration
Let's say **Alice** wants to buy a sword from **Bob** inside of the game. Well that's easy, and there's two ways:
### 2. ZBD Gamertag Charges
You can use the above endpoint to create a Charge from **Bob**'s ZBD Gamertag, and display it for **Alice** to pay. Pass a `callbackUrl` on the ZBD API request and when the payment settles, you will be alerted of it and can then transfer the sword asset inside the game.
**Yes, it's really that simple!**
### 3. Project Wallet as Escrow
Alternatively you could rely on the Project's own Wallet as a form of escrow. You can create a Charge using the `/charges` endpoint passing a `callbackUrl` to receive settlement updates, and have **Alice** pay it.
Alice will effectively be paying the Project Wallet in this scenario.
Once payment is settled, you can use the [Send Bitcoin to ZBD Gamertag](/payments/api/gamertags/send) endpoint and send the same amount to **Bob**. When this second payment settles you can then transfer the sword asset inside the game.
While this technically involves two transactions, it provides a bit more control to the Developer that wishes to manage the transfer of assets/in-game items a bit more closely and with more checks and balances in place. Both approaches are recommended.
# Receiving Lightning Payments
Source: https://docs.zbdpay.com/payments/payins/lightning-charges
Learn how to receive Lightning payments using the ZBD API.
## Charge API
In order to receive Bitcoin through the Lightning Network using the ZBD API, you must first create a **Charge**.
A Charge accepts the following attributes:
| Property | Description |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `amount` | The Charge amount (in millisatoshis --> 1 satoshi = 1,000 millisatoshi). |
| `description` | The Charge note or comment. |
| `internalId` | An optional free-use attribute. Usually used by setting the Player/User ID of your Game/Application in order to link specific Charges to specific Players. |
| `callbackUrl` | The URL ZBD API will make a POST HTTP request to with information about the Charges's status updates. See [Callbacks](/payments/api/callbacks) for more. |
| `expiresIn` | The desired expiration time for this Charge (in seconds). |
To create a Charge, use the [`/charges`](/payments/api/lightning-charges/create) API (or leverage one of our SDKs) by passing the correct attributes:
```js NodeJS theme={null}
// Using ZBD's NodeJS SDK
import { zbd } from '@zbdpay/payments-sdk';
// Instantiate ZBD client
const ZBD = new zbd(API_KEY_HERE);
// Set the payload
const payload = {
expiresIn: 300,
amount: '50000',
internalId: '118304b8',
description: 'My Charge Description',
callbackUrl: 'https://your-app.com/zbd-callback',
};
// Create Charge
try {
const response = await ZBD.createCharge(payload);
} catch(error) {
console.log({ error });
}
```
```bash cURL theme={null}
curl --location --request POST 'https://api.zbdpay.com/v0/charges' \
--header 'Content-Type: application/json' \
--header 'apikey: API_KEY_HERE' \
--data-raw '{
"expiresIn": 300,
"amount": "50000",
"internalId": "118304b8",
"description": "My Charge Description",
"callbackUrl": "https://your-app.com/zbd-callback"
}'
```
If the submitted request suceeds, you can expect a JSON API response that resembles the following:
```json theme={null}
{
"message": "Successfully created Charge.",
"data": {
"id": "c121356b-9671-4fbd-a751-987fb4b3d0b3",
"description": "My Charge Description",
"createdAt": "2022-12-23T03:58:17.993Z",
"callbackUrl": "https://your-app.com/zbd-callback",
"internalId": "118304b8",
"amount": "50000",
"status": "pending",
"invoice": {
"expiresAt": "2019-12-23T04:08:18.038Z",
"request": "lnbc20n1p0qqw66pp5lhjn2sshvh03h2lsxyzg5eyfwt0760207q3hake25fs7l3psegtqdpgg3jhxcmjd9c8g6t0dcsx7e3qw35x2gzrdpshyem9cqzpgxqzjcfppjramjf8sxnvy3k8dq84kv56dy5gq9mlqs9qy9qsqsp5jvf69w282jdxkyt824dn0crxdentx8nvncfdt0ulqp75lvkjpagqqwpgcttpmfzaxc3akfn85jlu8cmtv5l2hu8q3yqttru8vlryg3vnewssy8w00yyfsghzqj03j45kj3uhhjek6q6e8djfu5u4gna6wjcqdsdhwe"
}
}
}
```
## Receiving Funds
Now that you have created a Charge, you can allow anyone to pay this Payment Request in the Bitcoin Lightning Network. Depending on your use-case you may choose to display the `invoice.request` property as a QR Code that others can scan with their mobile apps, or you can put it under a button click.
**Any Bitcoin Lightning-capable wallets can read, decode, and effect payments against these Charges.**
## Charge Updates
All Charges will either **complete** or **expire**. If no payment is effected until the Charge's expiration time, the Charge (and underlying Bitcoin Lightning Network Invoice) will expire and will therefore no longer be payable. To know the status of your specific Charge, check the `status` attribute returned from the `/charges/create` or `/charges/get` API calls.
```json theme={null}
{
...
"status": "pending" // pending | completed | expired | error
...
}
```
To subscribe to updates on any given Charge object, the recommended approach is to provide a `callbackUrl` property when creating that Charge. Whenever there are updates to the Charge (a payment was made, or Charge expires) the ZBD API will make a POST request to the URL provided in `callbackUrl`, passing the Charge updates as data in the request.
# Send Bitcoin to ZBD Gamertag
Source: https://docs.zbdpay.com/payments/payouts/gamertags
Learn how to instantly send Bitcoin to a ZBD user.
## Introduction
The ZBD API allows for developers to create Charges, perform Payments, and process Withdrawal Requests for any user, wallet, or service that understands / speaks the Lightning Network protocol. These capabilities are possible because of the underlying `Lightning Charges / Payment Requests / Invoices`, the single-use payable QR codes the platform can create and handle.
While this Payment Request provides a truly interoperable experience for ZBD-powered apps, games, and services to interact with any type of Bitcoin Lightning wallet a user may use, it can at times provide for a much poorer user experience (UX). This is the case because it involves a switch of context, going in-and-out of the app / game and wallet applications to perform a simple payment or withdrawal. This is especially true for mobile devices (which comprises over 2/3 of the entire gaming market for example).
## Enter the ZBD Gamertag
**Every ZBD user has a ZBD Gamertag.** A ZBD Gamertag is a user's unique identifier that allows a developer to directly interact with the given user. This means you can directly send payments to users / gamers.
### Send Bitcoin to ZBD Gamertags
In order to send payments directly to a ZBD user you just need to make a simple API call with a payload similar to the example below.
```js NodeJS theme={null}
// Using ZBD's NodeJS SDK
import { zbd } from '@zbdpay/payments-sdk';
// Instantiate ZBD client
const ZBD = new zbd(API_KEY_HERE);
// Set the payload
const payload = {
amount: '50000',
gamertag: 'andre',
description: 'ZBD so fast!',
};
// Send Payment
try {
const response = await ZBD.sendGamertagPayment(payload);
} catch(error) {
console.log({ error });
}
```
```bash cURL theme={null}
curl --location --request POST 'https://api.zbdpay.com/v0/gamertag/send-payment' \
--header 'Content-Type: application/json' \
--header 'apikey: API_KEY_HERE' \
--data-raw '{
"gamertag": "andre",
"amount": "50000",
"description": "ZBD so fast!",
}'
```
If the ZBD Gamertag exists and is active, the ZBD API will perform the transaction and will return a 200 status code and a JSON response with the following:
```json theme={null}
{
"success": true,
"data": {
"status": "settled",
"settledAt": "2021-10-08T14:21:55.378Z",
"amount": "5000",
"receiverId": "3dde8e48-7c13-4db7-a0e5-caf2a48c002a",
"comment": "So fast!"
},
"message": "Payment done."
}
```
At this stage the user will receive a Push Notification alerting them they've been sent some Bitcoin.
## In-Game Bitcoin Rewards through ZBD Gamertag
One of the many benefits of the ZBD API is that it is flexible enough to allow for developers to create simple in-game / in-app reward systems for their users. This is shown to increase user retention and improve general KPIs around ROAS and CPI. This is done by asking the user to enter their ZBD Gamertag in the game, and whenever the user completes a level, or triggers an event (depends entirely on the game or app and the developer's needs), some Bitcoin is immediately streamed to the user's ZBD App.
# Send Bitcoin to Lightning Address
Source: https://docs.zbdpay.com/payments/payouts/lightning-address
Learn how to instantly send Bitcoin to any Lightning Address.
## Lightning Address API
To send Bitcoin to a given Lightning Address, all you need to do is call the [Send Lightning Address Payment API](/payments/api/lightning-address/send) with a POST call providing the following properties:
| Property | Description |
| ----------- | --------------------------------------------------------- |
| `lnAddress` | Recipient Lightning Address to send payment to. |
| `amount` | Total amount of satoshis to send (in millisatoshis). |
| `comment` | comment to be sent with the payment (max 150 characters). |
Example: If you wish to reward 20 satoshis to a user for their completion of a challenge, and you have the user's Lightning Address, you can use the information below to send it in 1 API call.
## Sending Transaction
```js NodeJS theme={null}
// Using ZBD's NodeJS SDK
import { zbd } from '@zbdpay/payments-sdk';
// Instantiate ZBD client
const ZBD = new zbd(API_KEY_HERE);
// Set the payload
const payload = {
amount: "20000",
lnAddress: "andre@zbd.gg",
comment: "Sending to a Lightning Address"
};
// Send Lightning Address payment
try {
const response = await ZBD.sendLightningAddressPayment(payload);
} catch(error) {
console.log({ error });
}
```
```bash cURL theme={null}
curl --location --request POST 'https://api.zbdpay.com/v0/ln-address/send-payment' \
--header 'Content-Type: application/json' \
--header 'apikey: API_KEY_HERE' \
--data-raw '{
"amount": "20000",
"lnAddress": "andre@zbd.gg",
"comment": "Sending to a Lightning Address"
}'
```
## API Response
Response payloads will usually look similar to this:
```json theme={null}
{
"success": true,
"data": {
"id": "795de178-573f-47dd-9535-a710cd9daf56",
"fee": "2000",
"unit": "msats",
"amount": "20000",
"preimage": "e2fb59fe93a2c63082be50f58f4f3e92841d436339fab93fdd67b1280650cf47",
"status": "completed",
"invoice": "lnbc200n1p3277jtpp5594ju6twysvqkqtus4l0pnawuum4w8l9x3ktz234pp93nfsdc5fshp574njlu3jdr3y2k3g4g9xcp323kgwh7n4v7n4yraakvwh7xmeyugqcqzpgxqyz5vqsp55ju2g2j06u8ua59kj80wqrpgsnkjclzdv28d0msgah90fcx9t9ds9qyyssqphhva4akne5hr0f6t3gmmdpllu00zxgvu70s0kp5g8wmg5vv26qygsearsyrcm4xjecttazfy3ul9lmcvas9ha24n9jtpc3ca9g2mqsppxzvdl",
"walletId": "73c1db10-d1f7-499f-bbbe-eea2f73bb27c",
"transactionId": "9ab28811-97d3-40d0-b603-50cfa00bba2d",
"createdAt": "2022-06-19T19:34:35.955Z",
"processedAt": "2022-06-19T19:34:36.936Z"
},
"message": "Payment done."
}
```
Wow that's easy. Welcome to instantaneous payments!
Fees are added post-send given that the routing fees for the Lightning Network are variable.(e.g if you send a 10,000 satoshi transaction to a Lightning Address and the fee ends up being 10 satoshis, the total paid by the API wallet is 10,010 satoshis).
# Pay Lightning Network Charges
Source: https://docs.zbdpay.com/payments/payouts/lightning-charges
Learn how to pay Lightning Network Charges using ZBD.
## Payment API
In order to make a payment against a Lightning Network Payment Request (a.k.a Lightning Invoices, or Charges), you simply have to use the [Send Payment API](/payments/api/lightning-payments/send) endpoint. A Payment accepts the following attributes:
| Property | Status | Description |
| ------------- | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invoice` | *String* | The Lightning payment request, or invoice, that is meant to be paid. |
| `description` | *String* | The Payment description (only used for internal record-keeping). |
| `internalId` | *String* | An optional free-use attribute. Usually used by setting the Player/User ID of your Game/Application in order to link specific Payments to specific users. |
| `callbackUrl` | *String* | The URL ZBD services will make a POST HTTP request to with information about the Payment's status updates. See [Callbacks](/payments/api/callbacks) for more. |
To perform the Payment, use the API (or leverage one of our SDKs) by passing the correct attributes:
```js NodeJS theme={null}
// Using ZBD's NodeJS SDK
import { zbd } from '@zbdpay/payments-sdk';
// Instantiate the client
const ZBD = new zbd('API_KEY_HERE');
// Set the payload
const payload = {
description: 'My Payment Description',
callbackUrl: 'https://your-app.com/zbd-callback',
internalId: '11af01d09a6b8304b8',
invoice: 'lnbc450n1p0yeytspp55yrs0j42wnkw0qutr8e0tgsf2yplxs9986t5gqmfpn7mfd0ckc8sdzq2pshjmt9de6zqen0wgsrgdfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2sp5pyccqt6apxelz62d2ndrt0ssahndpcua4wklea80glaczx80t3wqrzjqfn4cln8jwe4dh4dmscddrmd6sdw6hzkn702l6ghwvr8lhad0ez5vzt8vyqqf2sqqqqqqqlgqqqqqqgq9q9qy9qsqqeft09gryr80aaghm7rmh7eeqfl7hxlcynp99730yk7qh534d9nyfwp0nc628rp8hpgp23fxzj5l2aet4y6sc4t79uj3wyjxffejmvgqrmxkvr',
};
// Make Payment
try {
const response = await ZBD.sendPayment(payload);
} catch(error) {
console.log({ error });
}
```
```bash cURL theme={null}
curl --location --request POST 'https://api.zbdpay.com/v0/payments' \
--header 'Content-Type: application/json' \
--header 'apikey: API_KEY' \
--data-raw '{
"invoice": "lnbc450n1p0yeytspp55yrs0j42wnkw0qutr8e0tgsf2yplxs9986t5gqmfpn7mfd0ckc8sdzq2pshjmt9de6zqen0wgsrgdfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2sp5pyccqt6apxelz62d2ndrt0ssahndpcua4wklea80glaczx80t3wqrzjqfn4cln8jwe4dh4dmscddrmd6sdw6hzkn702l6ghwvr8lhad0ez5vzt8vyqqf2sqqqqqqqlgqqqqqqgq9q9qy9qsqqeft09gryr80aaghm7rmh7eeqfl7hxlcynp99730yk7qh534d9nyfwp0nc628rp8hpgp23fxzj5l2aet4y6sc4t79uj3wyjxffejmvgqrmxkvr",
"description": "My Payment Description",
"callbackUrl": "https://myapp.com/zbd-callback",
"internalId": "11af01d0cb33faa6b8304b8"
}'
```
If the submitted request suceeds, you can expect a JSON API response that resembles the following:
```json theme={null}
{
"message": "Successfully made Payment.",
"data": {
"id": "31d60de1-efe0-4e55-9942-910934efe7a3",
"fee": "1000",
"unit": "msats",
"amount": "10000",
"status": "completed",
"description": "My Payment Description",
"processedAt": "2020-01-01T19:34:42.794Z",
"invoice": "lnbc450n1p0yeytspp55yrs0j42wnkw0qutr8e0tgsf2yplxs9986t5gqmfpn7mfd0ckc8sdzq2pshjmt9de6zqen0wgsrgdfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2sp5pyccqt6apxelz62d2ndrt0ssahndpcua4wklea80glaczx80t3wqrzjqfn4cln8jwe4dh4dmscddrmd6sdw6hzkn702l6ghwvr8lhad0ez5vzt8vyqqf2sqqqqqqqlgqqqqqqgq9q9qy9qsqqeft09gryr80aaghm7rmh7eeqfl7hxlcynp99730yk7qh534d9nyfwp0nc628rp8hpgp23fxzj5l2aet4y6sc4t79uj3wyjxffejmvgqrmxkvr"
}
}
```
## Payment Updates
There are cases where Lightning Payments can `get stuck` in transit (e.g. a network node on the path to the payment's destination suddenly goes offline). ZBD aims to provide cutting edge Lightning Network infrastructure with high availability and liquidity. That said, due to the possibility of running into stuck payment cases, the recommended method of getting Payment updates is through the `callbackUrl` attribute. By providing a `callbackUrl` when making the Payment, the [ZBD API](/payments/api) is able to make a POST request to that URL and pass along Payment updates as they happen.
# Using Withdrawal QR Codes
Source: https://docs.zbdpay.com/payments/payouts/withdrawal-requests
Understanding the Lightning Charge Workflow Problem
## Introduction
The Lightning Network is a push-based system: in order to receive payments, the payee must first create a Charge / Payment Request, which the payer can pay. This is a great workflow when users are making payments for goods and services. Usually this workflow is as follows:
User performs action that requires a Payment.
A Lightning Charge is created for the specified amount and displayed as QR.
User uses a Lightning Wallet (usually mobile app) to scan QR and decode payment.
User reviews and confirms request details and accepts the Payment.
You receive the Payment and unlock content/action for the user.
This is a great user experience between User and a Project (e.g. game or application). However, this user workflow is limited to **User paying Project**.
To perform the reverse (Project paying User), as with all Lightning payments, the Project must first ask for a Lightning Network Charge/Invoice from the User, for a specific amount. This provides for subpar UX because the User is now an active participant when receiving funds.
## Withdrawal Request API
Withdrawal Requests are the exact opposite of a Charge. Whereas a Charge is a payment from a User to a Project, a Withdrawal Request can be understood as a payment from a Project to a User. In other words, a Charge is a QR code that accepts a Bitcoin payment while **a Withdrawal Request is a QR code that allows a User to claim Bitcoin**.
ZBD's Withdrawal Request capability is based on the [LNURL open-source specification](https://github.com/fiatjaf/lnurl-rfc), which provides a standard for easier communication between Wallets (users) and Projects, through the use of HTTP requests. A developer building upon ZBD's Withdrawal Requests functionality does not need to understand the underlying methodology and workflow of LNURL - the [ZBD API](/payments/api) abstracts away any complexities. Let's see how to do it.
If you've been able to create a ZBD Charge, then you will be able to create a ZBD Withdrawal Request easily.
### Creating a Withdrawal Request
When creating a Withdrawal Request code to allow Lightning Wallets to withdraw funds from the Project (game or application) you must provide the following attributes to the [`/withdrawal-requests`](/payments/api/withdrawal-requests/create) endpoint:
| Property | Status | Description |
| ------------- | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `amount` | *String* | The Withdrawal Request amount (in millisatoshis). |
| `description` | *String* | The Withdrawal Request description. |
| `internalId` | *String* | An optional free-use attribute. Usually used by setting the Player/User ID of your Game/Application in order to link specific Withdrawal Requests to specific Players. |
| `callbackUrl` | *String* | The URL ZBD services will make a POST HTTP request to with information about the Withdrawal Requests's status updates. See [Callbacks](/payments/api/callbacks) for more. |
| `expiresIn` | *Number* | The desired expiration time for this Withdrawal Request (in seconds). |
To create the Withdrawal Request, use the ZBD API (or leverage one of our SDKs) by passing the correct attributes:
```js NodeJS theme={null}
// Using ZBD's NodeJS SDK
import { zbd } from '@zbdpay/payments-sdk';
// Instantiate the client
const ZBD = new zbd('API_KEY_HERE');
// Set the payload
const payload = {
expiresIn: 300,
amount: '50000',
internalId: '11af01d0924aa6b8304b8',
description: 'My Custom Withdrawal Request',
callbackUrl: 'https://yourapp.com/zbd-callback',
};
// Create Withdrawal Request
try {
const response = await ZBD.createWithdrawalRequest(payload);
} catch(error) {
console.log({ error });
}
```
```bash cURL theme={null}
curl --location --request POST 'https://api.zbdpay.com/v0/withdrawal-requests' \
--header 'Content-Type: application/json' \
--header 'apikey: API_KEY' \
--data-raw '{
"expiresIn": 300,
"amount": "50000",
"internalId": "11af01d0926b8304b8",
"description": "My Custom Withdrawal Description",
"callbackUrl": "https://example.com/zbd-callback"
}'
```
If the submitted request suceeds, you can expect a JSON API response that resembles the following:
```json theme={null}
{
"message": "Successfully created Withdrawal Request.",
"data": {
"id": "c121356b-9671-4fbd-a751-987fb4b3d0b3",
"unit": "msats",
"amount": "50000",
"status": "pending",
"description": "Description of the Withdrawal Request",
"createdAt": "2019-12-23T03:58:17.993Z",
"callbackUrl": "https://example.com/zbd-callback",
"internalId": "11af01d092444a317cb33faa6b8304b8",
"invoice": {
"expiresAt": "2019-12-23T04:08:18.038Z",
"request": "lnurl1dp68gurn8ghj7cn9w3sj6ctsdyh85etzv4jx2efwd9hj7a3s9acxz7tvdaskgtthd96xserjv9mkzmpdwfjhzat9wd6r7um9vdex2apav3skxdfev93rwvm9x43nwcf5vvekgdmpxq6rgvrrxumnxdp3vserqe35v4jr2efs8y6k2epkxf3rxc35vg6nwdrpx9jrqv3kxfjxxwqxmuhcd"
}
}
}
```
### Displaying as QR Code
In order to display the Withdrawal Request as a QR code, simple use the `data.invoice.request` attribute returned from the API response and pass it to your QR code generator of choice. The QR code should look something like this:
## Request Updates
Note that the Withdrawal Request is created in a `pending` state as it still hasn't *been claimed*. All Withdrawal Requests will either **complete** or **expire**. If no User is able to Withdraw the funds on time before the Withdrawal Request's expiration time, the Request will expire and will therefore no longer be valid. To know the status of your specific Withdrawal Request, check the `status` attribute returned from the `CreateWithdrawalRequest` or `GetWithdrawalRequestDetails` API calls.
```json theme={null}
{
...
"status": "expired" // pending | completed | expired | error
...
}
```
To subscribe to updates on a specific Withdrawal Request, the recommended approach is to provide a `callbackUrl` property when creating that Withdrawal Request. Whenever there are updates to the Withdrawal Request (a payment was made, or it has expired) the [ZBD API](/payments/api) will make a POST request to the URL provided in `callbackUrl`, passing the Withdrawal Request updates as data in the request.
# ZBD Ramp
Source: https://docs.zbdpay.com/payments/ramp
Enable Bitcoin purchases directly in your app with our embeddable ramp widget
Enable your users to buy Bitcoin directly within your app or game. ZBD Ramp is the first widget to support Lightning Address delivery, providing instant Bitcoin access for gaming and interactive entertainment.
First-in-market Lightning Address support for instant Bitcoin delivery
Built for high-throughput gaming transactions and in-app purchases
Integrated KYC/AML with gaming-friendly risk assessment
## How It Works
Your backend creates a secure session with our API
Users verify identity and purchase Bitcoin through the widget
Funds are delivered via Lightning Address or onchain to your users
## Perfect for Gaming Use Cases
### ๐ฎ In-Game Wallet Top-ups
Let players add funds to their game wallets for purchasing skins, items, or premium content. With Lightning-fast delivery, they're back in the game in seconds.
### ๐ Tournament Entry Fees
Enable players to fund tournament entries directly. Our high-throughput system handles peak tournament registration periods without breaking a sweat.
### ๐ฐ Play-to-Earn Onboarding
Onboard new players into your crypto-powered game economy. They can purchase their first Bitcoin and start earning immediately.
## Integration at a Glance
```typescript theme={null}
// 1. Initialize session (backend)
import { initRampSession, QuoteCurrencyEnum, BaseCurrencyEnum } from '@zbdpay/ramp-ts';
const response = await initRampSession({
apikey: process.env.ZBD_API_KEY,
email: "player@game.com",
webhook_url: "https://yourgame.com/webhooks/zbd",
quote_currency: QuoteCurrencyEnum.USD,
base_currency: BaseCurrencyEnum.BTC,
destination: "spiderman@zbd.gg",
reference_id: "player_123"
});
const sessionToken = response.data.session_token;
// 2. Embed widget (frontend)
import { ZBDRamp } from '@zbdpay/ramp-react';
console.log('Purchase completed:', data)}
/>
// 3. Handle webhooks
app.post('/webhooks/zbd', (req, res) => {
if (req.body.event === 'purchase.completed') {
// Credit user's game wallet
}
});
```
That's it. One backend endpoint, one iframe, one webhook handler.
## Key Features
### Available Now
* **Pay by Bank** - ACH transfers via Plaid for US users
* Low fees and high success rates
Coming soon: Credit/Debit cards, Apple Pay, Google Pay, and Stablecoin payments
### Bitcoin Delivery
* **Lightning Address** - Instant delivery (industry first!)
* **Onchain Bitcoin** - Standard Bitcoin addresses
### Built-in Protection
* Advanced AI-native fraud detection
* Automated KYC/AML processes
* Gaming-optimized risk scoring
* Bot/VPN/Emulator detection
## Why ZBD Ramp?
### For Developers
* Simple 3-step integration
* No crypto complexity
* Comprehensive webhook events
* White-label customization
### For Your Users
* Seamless in-app experience
* Fast KYC process
* Competitive rates
* Multiple payment options
## Ready to Get Started?
Schedule a 15-minute demo to see ZBD Ramp in action and get your API keys.
Get up and running in minutes with our integration guide
Explore the API endpoints and webhook events
***
ZBD Ramp requires a business account and KYB verification. Individual developers can explore our Rewards products while preparing for business verification.
# AI-Generated Themes
Source: https://docs.zbdpay.com/payments/ramp/ai-themes
Use AI to instantly generate custom themes that match your brand
ZBD Ramp features an innovative AI-powered theme generator that can create a custom theme from your existing brand materials in seconds.
## How It Works
Our AI theme generator analyzes your uploaded images and automatically maps your brand colors and design patterns to create a perfectly matched widget theme.
Upload a screenshot, logo, or any image that represents your brand's visual identity
Select whether to start from a light or dark theme foundation
Optionally provide guidance like "focus on blue and green colors" or "make it vibrant"
Click generate and watch AI create your custom theme in seconds
Edit any element using the Live Preview editor to perfect your theme
## Support
Need help with AI theme generation?
* Access the generator in your [ZBD Developer Dashboard](https://dashboard.zebedee.io)
* Contact your account manager for guidance
* Review generated theme examples in the dashboard
# Email Receipts
Source: https://docs.zbdpay.com/payments/ramp/emails
Email notifications and receipts for ZBD Ramp transactions
Email receipt functionality is coming soon. This feature will provide ability for you to customize email notifications and transaction receipts to users.
# Frequently Asked Questions
Source: https://docs.zbdpay.com/payments/ramp/faq
Onboard into Bitcoin with ZBD Ramp.
ZBD Ramp currently supports:
* Bitcoin (BTC)
Users can withdraw their funds to:
* Bitcoin on-chain addresses
* Lightning Network addresses
Currently, ZBD Ramp supports:
* Pay by Bank with ACH
Coming soon:
* Google Pay
* Apple Pay
* Credit Cards
* Debit Cards
The ZBD Ramp widget is designed as an embeddable web view, making it compatible with virtually any development environment. The ZBD team is actively working on wrapper SDKs and libraries for popular development environments to make integration even easier.
For detailed pricing information and fee structures, please speak with your ZBD customer success manager or [connect with our sales team](https://zbd.one/sales).
Customer support is handled internally by the ZBD team. The ZBD Ramp widget includes a built-in chatbot that can handle a wide range of user inquiries, create support tickets directly, and even provide guides and detailed walkthroughs to help assist users.
ZBD Ramp implements multiple layers of security:
* Secure API endpoints
* Encrypted data transmission and webhook payloads
* Compliance with financial regulations (KYC, AML, etc.)
* Real-time AI-based transaction monitoring and fraud detection
To integrate ZBD Ramp, you need:
1. A ZBD developer account
2. API credentials
3. A secure backend environment
4. Compliance with our integration guidelines
Transaction limits vary based on your specific needs. Contact your ZBD Customer Success Manager for specific limit details.
# Platform Details
Source: https://docs.zbdpay.com/payments/ramp/platform
Configure your platform branding and identity in ZBD Ramp
Configure your platform's branding elements to ensure a seamless, cohesive experience for your users throughout the ZBD Ramp widget.
## Platform Configuration
Your platform details appear throughout the widget experience, providing users with confidence and brand continuity. All platform details can be configured through the ZBD Developer Dashboard.
### Available Settings
Your platform or application name that appears in the widget header and throughout the user journey
Your platform logo displayed prominently in the widget. Recommended: SVG or PNG with transparent background, minimum 200x200px
Optional wordmark/text logo for extended branding opportunities. Used in specific widget layouts where horizontal space allows
Brief description of your platform that helps users understand the context of their transaction. Maximum 160 characters
Your platform's website URL for additional credibility and user reference
## Best Practices
### Logo Guidelines
* **Format**: Use SVG for best quality across all screen sizes
* **Size**: Minimum 200x200px, maximum 1000x1000px
* **Background**: Transparent background recommended
* **Color**: Ensure logo works on your chosen theme backgrounds
### Platform Name
* Keep it concise and recognizable
* Use the name your users know you by
* Avoid special characters that might not render properly
* Consider international users if applicable
### Description Tips
* Be clear and concise (160 characters max)
* Focus on what your platform does
* Build trust with professional language
* Avoid marketing jargon
## Configuration Steps
Log into your [ZBD Developer Dashboard](https://dashboard.zbdpay.com)
Select your project and go to Ramp โ Platform Details
Upload your logo and wordmark using the provided upload fields
Fill in your platform name, description, and website URL
Use the Live Preview to see how your platform details appear in the widget
Save your configuration to apply changes immediately
# Integration Guide
Source: https://docs.zbdpay.com/payments/ramp/quickstart
Complete guide to integrating ZBD Ramp into your application
Get ZBD Ramp integrated into your application in minutes using our official SDKs. This guide covers session initialization, widget rendering, and webhook handling.
## Choose Your SDK
Select the SDK that best fits your application:
Core SDK for Node.js, browsers, and JavaScript frameworks
React components and hooks for web applications
Native components for iOS and Android apps
Flutter plugin for cross-platform mobile apps
## Integration Flow
Add the appropriate ZBD Ramp SDK to your project
Create a session token from your backend using the SDK
Display the Ramp widget using SDK components
Process webhooks and widget callbacks
## Quick Start
### 1. Install the SDK
```bash theme={null}
npm install @zbdpay/ramp-ts
```
```bash theme={null}
npm install @zbdpay/ramp-react
```
```bash theme={null}
npm install @zbdpay/ramp-react-native react-native-webview
```
### 2. Initialize Session (Backend)
Create a session token from your backend to keep your API key secure:
```typescript theme={null}
import { initRampSession, QuoteCurrencyEnum, BaseCurrencyEnum } from '@zbdpay/ramp-ts';
const response = await initRampSession({
apikey: process.env.ZBD_API_KEY,
email: 'user@example.com',
destination: 'lightning-address@zbd.gg',
quote_currency: QuoteCurrencyEnum.USD,
base_currency: BaseCurrencyEnum.BTC,
webhook_url: 'https://yourapp.com/webhooks/zbd',
reference_id: 'user_123_purchase_456',
metadata: {
user_id: '123',
session_id: 'abc'
}
});
const sessionToken = response.data.session_token;
```
```javascript theme={null}
const { initRampSession, QuoteCurrencyEnum, BaseCurrencyEnum } = require('@zbdpay/ramp-ts');
const response = await initRampSession({
apikey: process.env.ZBD_API_KEY,
email: 'user@example.com',
destination: 'lightning-address@zbd.gg',
quote_currency: QuoteCurrencyEnum.USD,
base_currency: BaseCurrencyEnum.BTC,
webhook_url: 'https://yourapp.com/webhooks/zbd'
});
const sessionToken = response.data.session_token;
```
```bash theme={null}
curl -X POST https://api.zbdpay.com/api/v1/ramp-widget \
-H "Content-Type: application/json" \
-H "apikey: YOUR_API_KEY" \
-d '{
"email": "user@example.com",
"webhook_url": "https://yourapp.com/webhooks/zbd",
"quote_currency": "USD",
"base_currency": "BTC",
"destination": "lightning-address@zbd.gg"
}'
```
### 3. Render Widget (Frontend)
Display the Ramp widget in your application:
```tsx theme={null}
import { ZBDRamp } from '@zbdpay/ramp-react';
function PaymentPage({ sessionToken }) {
return (
{
console.log('Payment successful:', data);
// Handle successful payment
}}
onError={(error) => {
console.error('Payment error:', error);
// Handle error
}}
onClose={() => {
console.log('Widget closed');
// Handle close
}}
style={{ width: '100%', height: '600px' }}
/>
);
}
```
```javascript theme={null}
import { createRamp } from '@zbdpay/ramp-ts';
const ramp = createRamp({
sessionToken: 'your-session-token',
container: '#ramp-container',
onSuccess: (data) => {
console.log('Payment successful:', data);
},
onError: (error) => {
console.error('Payment error:', error);
},
onClose: () => {
console.log('Widget closed');
}
});
ramp.mount();
```
```tsx theme={null}
import { ZBDRamp } from '@zbdpay/ramp-react-native';
import { View, StyleSheet } from 'react-native';
function PaymentScreen({ sessionToken }) {
return (
{
console.log('Payment successful:', data);
}}
onError={(error) => {
console.error('Payment error:', error);
}}
style={styles.webview}
/>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
webview: { flex: 1 }
});
```
### 4. Handle Webhooks
Process webhook events on your backend:
```javascript theme={null}
app.post('/webhooks/zbd', (req, res) => {
const event = req.body;
switch(event.type) {
case 'ramp.purchase.completed':
// Payment successful - credit user account
const { amount, currency, reference_id } = event.data;
creditUserAccount(reference_id, amount);
break;
case 'ramp.purchase.failed':
// Payment failed - notify user
notifyUserOfFailure(event.data.reference_id);
break;
case 'ramp.kyc.completed':
// KYC completed - update user status
updateUserKYCStatus(event.data.user_id);
break;
}
res.status(200).send('OK');
});
```
## Complete Example
Here's a full implementation using React and Node.js:
### Backend (Node.js + Express)
```javascript theme={null}
import express from 'express';
import { initRampSession, QuoteCurrencyEnum, BaseCurrencyEnum } from '@zbdpay/ramp-ts';
const app = express();
app.use(express.json());
// Create session endpoint
app.post('/api/ramp/session', async (req, res) => {
try {
const response = await initRampSession({
apikey: process.env.ZBD_API_KEY,
email: req.body.email,
destination: req.body.destination,
quote_currency: QuoteCurrencyEnum.USD,
base_currency: BaseCurrencyEnum.BTC,
webhook_url: `${process.env.BASE_URL}/webhooks/zbd`,
reference_id: req.user.id,
metadata: {
user_id: req.user.id,
session_id: req.session.id
}
});
res.json({ sessionToken: response.data.session_token });
} catch (error) {
res.status(500).json({ error: 'Failed to create session' });
}
});
// Webhook handler
app.post('/webhooks/zbd', async (req, res) => {
const event = req.body;
if (event.type === 'ramp.purchase.completed') {
// Credit user's account
await creditUserBalance(
event.data.reference_id,
event.data.amount,
event.data.currency
);
}
res.status(200).send('OK');
});
```
### Frontend (React)
```tsx theme={null}
import React, { useState, useEffect } from 'react';
import { ZBDRamp } from '@zbdpay/ramp-react';
function BuyBitcoin() {
const [sessionToken, setSessionToken] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch session token from backend
fetch('/api/ramp/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: user.email,
destination: user.lightningAddress
})
})
.then(res => res.json())
.then(data => {
setSessionToken(data.sessionToken);
setLoading(false);
});
}, []);
if (loading) return Loading...;
return (
Buy Bitcoin
{
alert('Payment successful!');
window.location.href = '/wallet';
}}
onError={(error) => {
alert(`Payment failed: ${error.message}`);
}}
style={{ width: '100%', height: '600px' }}
/>
);
}
```
## Session Parameters
When initializing a session, you can configure various parameters:
| Parameter | Type | Required | Description |
| ---------------- | ------ | -------- | -------------------------------- |
| `apikey` | string | Yes | Your ZBD API key |
| `email` | string | Yes | User's email address |
| `webhook_url` | string | Yes | URL for webhook notifications |
| `destination` | string | No | Lightning or Bitcoin address |
| `quote_currency` | enum | No | Source currency (USD, EUR, etc.) |
| `base_currency` | enum | No | Target currency (BTC) |
| `reference_id` | string | No | Your internal reference ID |
| `metadata` | object | No | Additional data to attach |
Sessions expire after 15 minutes. Create a new session if the user needs more time.
## Security Best Practices
* Never expose API keys in frontend code
* Always create sessions from your backend
* Use environment variables
* Rotate keys regularly
* Use HTTPS endpoints only
* Implement idempotency
* Verify webhook signatures
* Log all events for audit
## Demo Applications
To help you get started quickly, we've created example applications showing ZBD Ramp integration across different platforms. These demos showcase best practices and real-world implementation patterns.
**Features:**
* Clean TypeScript implementation
* Session management
* Webhook handling
* Error handling examples
Perfect for web applications and Node.js backends. See the [TypeScript SDK documentation](/payments/ramp/sdks/typescript) for more details.
**Features:**
* Native mobile integration
* WebView implementation
* iOS and Android support
* Navigation patterns
Ideal for cross-platform mobile apps. See the [React Native SDK documentation](/payments/ramp/sdks/react-native) for implementation details.
**Features:**
* Flutter widget integration
* Platform-specific handling
* Material Design patterns
* State management
Flutter widget integration for iOS and Android. See the [Flutter SDK documentation](/payments/ramp/sdks/flutter) for implementation details.
These example applications demonstrate production-ready patterns for integrating ZBD Ramp. Use them as a starting point for your own implementation.
***
## Next Steps
See a complete walkthrough of the user experience
Learn how to customize the widget to match your brand
Explore all available webhook events and payloads
View the complete API documentation
Schedule a technical support session with our team
# Flutter SDK
Source: https://docs.zbdpay.com/payments/ramp/sdks/flutter
Flutter plugin for ZBD Ramp - Enable Bitcoin purchases in your Flutter apps
The `zbd_ramp` package provides a native Flutter widget for integrating ZBD Ramp into your iOS, Android, and Web applications.
## Features
Built specifically for Flutter with native WebView integration
Works seamlessly on iOS, Android, and Web
Full Dart type safety with comprehensive type definitions
PostMessage communication for error handling, logging, and step tracking
## Installation
Add the package to your `pubspec.yaml`:
```yaml theme={null}
dependencies:
zbd_ramp: ^1.0.0
http: ^1.1.0
```
Then run:
```bash theme={null}
flutter pub get
```
## Quick Start
### 1. Create Session Token
First, initialize a ramp session from your backend or directly in your Flutter app:
```dart theme={null}
import 'package:zbd_ramp/zbd_ramp.dart';
final response = await initRampSession(InitRampSessionConfig(
apikey: 'your-zbd-api-key',
email: 'user@example.com',
destination: 'lightning-address@zbd.gg',
quoteCurrency: QuoteCurrency.USD,
baseCurrency: BaseCurrency.BTC,
webhookUrl: 'https://your-webhook-url.com',
referenceId: 'order-123',
metadata: {'userId': '456', 'plan': 'premium'},
));
if (response.success) {
final sessionToken = response.data.sessionToken;
// Use sessionToken with ZBDRampWidget
} else {
print('Failed to create session: ${response.error}');
}
```
```dart theme={null}
import 'package:zbd_ramp/zbd_ramp.dart';
final response = await initRampSession(InitRampSessionConfig(
apikey: 'your-zbd-api-key',
accessToken: 'user-access-token',
destination: 'lightning-address@zbd.gg',
quoteCurrency: QuoteCurrency.USD,
baseCurrency: BaseCurrency.BTC,
webhookUrl: 'https://your-webhook-url.com',
));
if (response.success) {
final sessionToken = response.data.sessionToken;
// Use sessionToken with ZBDRampWidget
}
```
### 2. Display the Ramp Widget
Add the widget to your Flutter app:
```dart theme={null}
import 'package:flutter/material.dart';
import 'package:zbd_ramp/zbd_ramp.dart';
class PaymentScreen extends StatelessWidget {
final String sessionToken;
const PaymentScreen({Key? key, required this.sessionToken}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Buy Bitcoin')),
body: ZBDRampWidget(
config: RampConfig(sessionToken: sessionToken),
callbacks: RampCallbacks(
onSuccess: (data) {
print('Payment successful: $data');
// Handle successful payment
},
onError: (error) {
print('Payment error: ${error.message}');
// Handle error
},
onStepChange: (step) {
print('Current step: $step');
// Track user progress
},
),
height: 600,
),
);
}
}
```
## API Reference
### initRampSession()
Creates a new session token for the ZBD Ramp widget.
#### Parameters
```dart theme={null}
class InitRampSessionConfig {
final String apikey; // Required: Your ZBD API key
final String? email; // Email authentication
final String? accessToken; // Access token authentication
final String destination; // Required: Lightning address or username
final QuoteCurrency quoteCurrency; // Required: Quote currency (USD)
final BaseCurrency baseCurrency; // Required: Base currency (BTC)
final String? webhookUrl; // Optional: Webhook URL
final String? referenceId; // Optional: Your reference ID
final Map? metadata; // Optional: Additional metadata
}
```
Either `email` OR `accessToken` must be provided for authentication.
#### Returns
```dart theme={null}
class InitRampSessionResponse {
final InitRampSessionData data;
final String? error;
final bool success;
final String message;
}
class InitRampSessionData {
final String sessionToken; // Session token for widget
final String expiresAt; // Token expiration time
final String widgetUrl; // Direct widget URL
}
```
### refreshAccessToken()
Refreshes an expired access token using a refresh token.
**Token Lifecycle:**
* Access tokens expire after 30 days
* Refresh tokens expire after 90 days
* Both tokens are received via webhook after user completes OTP login with email
#### Parameters
```dart theme={null}
class RefreshAccessTokenConfig {
final String apikey; // Required: Your ZBD API key
final String accessTokenId; // Required: ID of access token to refresh
final String refreshToken; // Required: Refresh token
}
```
#### Returns
```dart theme={null}
class RefreshAccessTokenResponse {
final RefreshAccessTokenData data;
final String? error;
final bool success;
final String message;
}
class RefreshAccessTokenData {
final String accessTokenId;
final String accessToken; // New access token
final String refreshToken; // New refresh token
final String accessTokenExpiresAt;
final String refreshTokenExpiresAt;
}
```
#### Example
```dart theme={null}
try {
final response = await refreshAccessToken(RefreshAccessTokenConfig(
apikey: 'your-zbd-api-key',
accessTokenId: '7b585ffa-9473-43ca-ba1d-56e9e7e2263b',
refreshToken: 'user-refresh-token',
));
if (response.success) {
final newAccessToken = response.data.accessToken;
final newRefreshToken = response.data.refreshToken;
// Store the new tokens securely
}
} catch (error) {
print('Token refresh error: $error');
}
```
### ZBDRampWidget
Main Flutter widget that renders the ZBD Ramp interface.
#### Constructor
```dart theme={null}
ZBDRampWidget({
Key? key,
required RampConfig config,
required RampCallbacks callbacks,
double? width,
double? height,
})
```
#### RampConfig
Configuration for the widget:
```dart theme={null}
class RampConfig {
final String sessionToken; // Required: Session token
final String? secret; // Optional: Widget secret
}
```
#### RampCallbacks
Event handlers for widget lifecycle:
```dart theme={null}
class RampCallbacks {
final OnSuccessCallback? onSuccess; // Payment successful
final OnErrorCallback? onError; // Error occurred
final OnStepChangeCallback? onStepChange; // User navigated to new step
final OnLogCallback? onLog; // Debug/info logging
final OnReadyCallback? onReady; // Widget fully loaded
final OnCloseCallback? onClose; // User closed widget
}
```
## Complete Example
Here's a full implementation with all callbacks:
```dart theme={null}
import 'package:flutter/material.dart';
import 'package:zbd_ramp/zbd_ramp.dart';
class BuyBitcoinScreen extends StatefulWidget {
@override
_BuyBitcoinScreenState createState() => _BuyBitcoinScreenState();
}
class _BuyBitcoinScreenState extends State {
String? sessionToken;
bool isLoading = true;
String? error;
@override
void initState() {
super.initState();
_createSession();
}
Future _createSession() async {
try {
final response = await initRampSession(InitRampSessionConfig(
apikey: 'your-zbd-api-key',
email: 'user@example.com',
destination: 'lightning@zbd.gg',
quoteCurrency: QuoteCurrency.USD,
baseCurrency: BaseCurrency.BTC,
webhookUrl: 'https://your-webhook.com',
referenceId: 'user_123',
));
if (response.success) {
setState(() {
sessionToken = response.data.sessionToken;
isLoading = false;
});
} else {
setState(() {
error = response.error;
isLoading = false;
});
}
} catch (e) {
setState(() {
error = e.toString();
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Buy Bitcoin')),
body: isLoading
? Center(child: CircularProgressIndicator())
: error != null
? Center(child: Text('Error: $error'))
: ZBDRampWidget(
config: RampConfig(sessionToken: sessionToken!),
callbacks: RampCallbacks(
onSuccess: (data) {
print('โ
Payment successful: $data');
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Payment successful!')),
);
},
onError: (error) {
print('โ Error: ${error.message}');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: ${error.message}'),
backgroundColor: Colors.red,
),
);
},
onStepChange: (step) {
print('๐ Step changed: $step');
},
onReady: () {
print('โ
Widget ready');
},
onClose: () {
print('๐ Widget closed');
Navigator.of(context).pop();
},
onLog: (log) {
print('๐ Log: $log');
},
),
height: 600,
),
);
}
}
```
## Error Handling
Handle errors gracefully in your application:
```dart theme={null}
void _handleError(RampError error) {
// Error structure: { code: string, message: string, details?: any }
print('Error Code: ${error.code}');
print('Error Message: ${error.message}');
if (error.details != null) {
print('Error Details: ${error.details}');
}
// Show user-friendly message
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Payment Error'),
content: Text(error.message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
),
);
}
```
## Platform Setup
### iOS Configuration
Add the following to your `ios/Runner/Info.plist`:
```xml theme={null}
NSAppTransportSecurity
NSAllowsArbitraryLoads
NSCameraUsageDescription
Camera access is required for KYC verification
```
### Android Configuration
Add permissions to your `android/app/src/main/AndroidManifest.xml`:
```xml theme={null}
```
## Try the Example App
The quickest way to see the SDK in action:
```bash theme={null}
git clone https://github.com/zbdpay/ramp-flutter.git
cd ramp-flutter/example
```
```bash theme={null}
flutter pub get
```
```bash theme={null}
flutter run
```
* Enter your ZBD API Key
* Fill in email and Lightning destination
* Tap "Create Session & Load Ramp"
## Resources
View the official Flutter package
View source code and example app
Complete API documentation
Handle webhook notifications
## Other SDKs
Core TypeScript/JavaScript package for web applications.
[View TypeScript SDK โ](/payments/ramp/sdks/typescript)
React components for web applications.
[View React SDK โ](/payments/ramp/sdks/react)
Native components for iOS and Android apps.
[View React Native SDK โ](/payments/ramp/sdks/react-native)
***
## Support
Need help? Create an issue on [GitHub](https://github.com/zbdpay/ramp-flutter/issues) or reach out to our [support team](https://zbd.one/sales).
# React SDK
Source: https://docs.zbdpay.com/payments/ramp/sdks/react
React wrapper for ZBD Ramp
The `@zbdpay/ramp-react` package is a React wrapper for the ZBD Ramp widget that enables Buying Bitcoin with USD directly to the Lightning Network.
## Features
* **React Optimized**: Built specifically for React with hooks and components
* **TypeScript Support**: Full type safety with comprehensive TypeScript definitions
* **Ref API**: Access to underlying ramp instance methods via React refs
* **Hook Support**: `useZBDRamp` hook for programmatic usage
* **Session Token Based**: Uses secure session tokens for authentication
## Installation
```bash theme={null}
npm install @zbdpay/ramp-react
```
```bash theme={null}
yarn add @zbdpay/ramp-react
```
```bash theme={null}
pnpm add @zbdpay/ramp-react
```
## Quick Start
### 1. Create Session Token
First, create a session token using the ZBD API:
```tsx theme={null}
import { initRampSession, QuoteCurrencyEnum, BaseCurrencyEnum } from '@zbdpay/ramp-react';
const response = await initRampSession({
apikey: 'your-zbd-api-key',
email: 'user@example.com',
destination: 'lightning-address@zbd.gg',
quote_currency: QuoteCurrencyEnum.USD,
base_currency: BaseCurrencyEnum.BTC,
webhook_url: 'https://your-webhook-url.com',
});
const sessionToken = response.data.session_token;
```
### 2. Use ZBDRamp Component
```tsx theme={null}
import { ZBDRamp } from '@zbdpay/ramp-react';
function App() {
const handleSuccess = (data) => {
console.log('Payment successful:', data);
};
const handleError = (error) => {
console.error('Payment error:', error);
};
return (
);
}
```
## API Reference
### initRampSession
Creates a new session token for the ZBD Ramp widget.
Configuration object for creating a session
#### Configuration Parameters
Your ZBD API key
User's email address
Lightning address or Bitcoin address
Quote currency (e.g., USD)
Base currency (e.g., BTC)
Webhook URL for notifications
Your internal reference ID
Additional metadata to attach to the session
### ZBDRamp Component
React component that renders the ZBD Ramp widget.
#### Props
Session token from ZBD API
Widget width
Widget height (minimum 600px recommended)
CSS class for container
Inline styles for container
#### Callback Props
Called when payment is successful
Called when an error occurs
Called when user navigates to a different step
Debug/info logging callback
Called when widget is fully loaded
Called when user closes the widget
#### Ref API
```typescript theme={null}
interface ZBDRampRef {
mount: (container?: HTMLElement | string) => void;
unmount: () => void;
destroy: () => void;
}
```
### useZBDRamp Hook
Hook for creating and managing ZBD Ramp instances programmatically.
```tsx theme={null}
const { createInstance, destroyInstance, instance } = useZBDRamp(options);
```
## Usage Examples
### Basic Component
```tsx theme={null}
import React from 'react';
import { ZBDRamp } from '@zbdpay/ramp-react';
function PaymentWidget({ sessionToken }) {
const handleSuccess = (data: any) => {
console.log('Payment successful:', data);
// Handle successful payment
};
const handleError = (error: any) => {
console.error('Payment error:', error);
// Handle error
};
return (
Make a Payment
);
}
```
### With Ref for Control
```tsx theme={null}
import React, { useRef } from 'react';
import { ZBDRamp } from '@zbdpay/ramp-react';
import type { ZBDRampRef } from '@zbdpay/ramp-react';
function ControlledPayment({ sessionToken }) {
const rampRef = useRef(null);
const closeWidget = () => {
rampRef.current?.unmount();
};
const destroyWidget = () => {
rampRef.current?.destroy();
};
return (
console.log('Success:', data)}
onError={(error) => console.error('Error:', error)}
/>
);
}
```
### Using the Hook
```tsx theme={null}
import React, { useState } from 'react';
import { useZBDRamp } from '@zbdpay/ramp-react';
function HookExample({ sessionToken }) {
const [isOpen, setIsOpen] = useState(false);
const { createInstance, destroyInstance } = useZBDRamp({
sessionToken,
container: '#ramp-container',
onSuccess: (data) => {
console.log('Payment successful:', data);
setIsOpen(false);
},
onClose: () => {
setIsOpen(false);
},
});
const openRamp = () => {
setIsOpen(true);
createInstance();
};
const closeRamp = () => {
setIsOpen(false);
destroyInstance();
};
return (
{isOpen && (
)}
);
}
```
### Error Handling
```tsx theme={null}
import React, { useState } from 'react';
import { ZBDRamp } from '@zbdpay/ramp-react';
import type { RampError } from '@zbdpay/ramp-react';
function PaymentWithErrorHandling({ sessionToken }) {
const [error, setError] = useState(null);
const handleError = (error: RampError) => {
switch (error.code) {
case 'INVALID_CONFIG':
setError('Configuration error. Please check your settings.');
break;
case 'NETWORK_ERROR':
setError('Network error. Please check your connection.');
break;
case 'PAYMENT_FAILED':
setError('Payment failed. Please try again.');
break;
default:
setError('An unexpected error occurred.');
}
};
const clearError = () => setError(null);
return (
{error && (
Error: {error}
)}
setError(null)}
onError={handleError}
/>
);
}
```
### Session Token Creation
```tsx theme={null}
import React, { useState } from 'react';
import { ZBDRamp, initRampSession, QuoteCurrencyEnum, BaseCurrencyEnum } from '@zbdpay/ramp-react';
function SessionTokenExample() {
const [sessionToken, setSessionToken] = useState('');
const [isLoading, setIsLoading] = useState(false);
const createSession = async () => {
setIsLoading(true);
try {
const response = await initRampSession({
apikey: 'your-zbd-api-key',
email: 'user@example.com',
destination: 'lightning-address@zbd.gg',
quote_currency: QuoteCurrencyEnum.USD,
base_currency: BaseCurrencyEnum.BTC,
webhook_url: 'https://your-webhook.com',
});
if (response.success) {
setSessionToken(response.data.session_token);
} else {
console.error('Failed to create session:', response.error);
}
} catch (error) {
console.error('Error creating session:', error);
} finally {
setIsLoading(false);
}
};
return (
{!sessionToken ? (
) : (
console.log('Success:', data)}
/>
)}
);
}
```
### Modal Integration
```tsx theme={null}
import React, { useState } from 'react';
import { ZBDRamp } from '@zbdpay/ramp-react';
function ModalExample({ sessionToken }) {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<>
{isModalOpen && (
setIsModalOpen(false)}>
e.stopPropagation()}>
{
console.log('Payment successful:', data);
setIsModalOpen(false);
}}
onClose={() => setIsModalOpen(false)}
style={{ width: '100%', height: '600px' }}
/>
)}
>
);
}
```
## TypeScript Support
The package includes comprehensive TypeScript definitions:
```typescript theme={null}
import type {
ZBDRampProps,
ZBDRampRef,
RampConfig,
RampCallbacks,
RampOptions,
RampError,
RampLog,
RampInstance,
PostMessageData,
InitRampSessionConfig,
InitRampSessionData,
InitRampSessionResponse,
QuoteCurrencyEnum,
BaseCurrencyEnum,
} from '@zbdpay/ramp-react';
```
### Type Examples
```typescript theme={null}
// Define typed configuration
const config: InitRampSessionConfig = {
apikey: process.env.REACT_APP_ZBD_API_KEY!,
email: 'user@example.com',
destination: 'lightning-address@zbd.gg',
quote_currency: QuoteCurrencyEnum.USD,
base_currency: BaseCurrencyEnum.BTC,
};
// Handle typed responses
const handleSuccess = (data: any): void => {
console.log('Payment completed:', data);
};
const handleError = (error: RampError): void => {
console.error(`Error ${error.code}: ${error.message}`);
};
// Create typed ref
const rampRef = useRef(null);
```
## Complete Example App
The repository includes a complete example application:
```bash theme={null}
# Clone and run example
git clone https://github.com/zbdpay/ramp-react.git
cd ramp-react/example
npm install
npm run dev
```
The example includes:
* Session token creation form
* Complete ZBD Ramp integration
* Error handling and callbacks
* TypeScript implementation
## Related Packages
Core TypeScript/JavaScript SDK
React Native components
Flutter plugin
## Resources
* [GitHub Repository](https://github.com/zbdpay/ramp-react)
* [NPM Package](https://www.npmjs.com/package/@zbdpay/ramp-react)
* [API Documentation](/payments/ramp/session)
* [Webhook Events](/payments/ramp/webhooks)
## Support
For support and questions:
* GitHub Issues: [Create an issue](https://github.com/zbdpay/ramp-react/issues)
# React Native SDK
Source: https://docs.zbdpay.com/payments/ramp/sdks/react-native
React Native wrapper for ZBD Ramp
The `@zbdpay/ramp-react-native` package is a React Native wrapper for the ZBD Ramp widget that enables Buying Bitcoin with USD directly to the Lightning Network.
## Features
* **React Native Optimized**: Built specifically for React Native with WebView
* **TypeScript Support**: Full type safety with comprehensive TypeScript definitions
* **Cross-Platform**: Works on iOS and Android
* **Ref API**: Access to WebView methods and ramp instance
* **Hook Support**: `useZBDRamp` hook for programmatic usage
## Installation
```bash theme={null}
npm install @zbdpay/ramp-react-native react-native-webview
```
```bash theme={null}
yarn add @zbdpay/ramp-react-native react-native-webview
```
```bash theme={null}
pnpm add @zbdpay/ramp-react-native react-native-webview
```
### iOS Setup
#### 1. Install CocoaPods dependencies
```bash theme={null}
cd ios && pod install
```
#### 2. Add Privacy Permissions
Add the following entries to your `ios/{YourAppName}/Info.plist`:
```xml theme={null}
NSCameraUsageDescription
ZBD Ramp needs camera access for document verification and identity verification processes.
NSMicrophoneUsageDescription
ZBD Ramp needs microphone access for liveness detection during identity verification.
NSPhotoLibraryUsageDescription
ZBD Ramp needs photo library access to upload documents for verification purposes.
NSPhotoLibraryAddUsageDescription
ZBD Ramp may save verification photos to your photo library.
```
These permissions are required for the ZBD Ramp widget to access device features like camera (document verification) and microphone (liveness detection) within the WebView context.
### Android Setup
Add the following permissions to your `android/app/src/main/AndroidManifest.xml`:
```xml theme={null}
```
These permissions prevent `permissions_unavailable` errors when the ZBD Ramp widget tries to access device features.
## Quick Start
### 1. Create Session Token
First, create a session token using the ZBD API:
```tsx theme={null}
import { initRampSession, QuoteCurrencyEnum, BaseCurrencyEnum } from '@zbdpay/ramp-react-native';
const response = await initRampSession({
apikey: 'your-zbd-api-key',
email: 'user@example.com',
destination: 'lightning-address@zbd.gg',
quote_currency: QuoteCurrencyEnum.USD,
base_currency: BaseCurrencyEnum.BTC,
webhook_url: 'https://your-webhook-url.com',
});
const sessionToken = response.data.session_token;
```
### 2. Use ZBDRamp Component
```tsx theme={null}
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { ZBDRamp } from '@zbdpay/ramp-react-native';
function App() {
return (
console.log('Success:', data)}
onError={(error) => console.error('Error:', error)}
onStepChange={(step) => console.log('Step:', step)}
style={styles.webview}
/>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
webview: {
flex: 1,
},
});
```
## API Reference
### initRampSession
Creates a new session token for the ZBD Ramp widget.
Configuration object for creating a session
#### Configuration Parameters
Your ZBD API key
User's email address
Lightning address or Bitcoin address
Quote currency (e.g., USD)
Base currency (e.g., BTC)
Webhook URL for notifications
Your internal reference ID
Additional metadata to attach to the session
### ZBDRamp Component
React Native component that renders the ZBD Ramp widget using WebView.
#### Props
Session token from ZBD API
WebView style
Additional WebView props
#### Callback Props
Called when payment is successful
Called when an error occurs
Called when user navigates to a different step
Debug/info logging callback
Called when widget is fully loaded
Called when user closes the widget
#### Ref API
```typescript theme={null}
interface ZBDRampRef {
mount: (container?: HTMLElement | string) => void;
unmount: () => void;
destroy: () => void;
}
```
### useZBDRamp Hook
Hook for managing ZBD Ramp instances programmatically.
```tsx theme={null}
const { rampRef, sendMessage, updateConfig, reload } = useZBDRamp(options);
```
## Usage Examples
### Basic Component
```tsx theme={null}
import React from 'react';
import { View, StyleSheet, Alert } from 'react-native';
import { ZBDRamp } from '@zbdpay/ramp-react-native';
function PaymentScreen() {
const handleSuccess = (data: any) => {
Alert.alert('Success', 'Payment completed successfully!');
console.log('Payment data:', data);
};
const handleError = (error: any) => {
Alert.alert('Error', error.message);
console.error('Payment error:', error);
};
return (
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
webview: {
flex: 1,
backgroundColor: 'transparent',
},
});
```
### With Ref for Control
```tsx theme={null}
import React, { useRef } from 'react';
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
import { ZBDRamp } from '@zbdpay/ramp-react-native';
import type { ZBDRampRef } from '@zbdpay/ramp-react-native';
function ControlledPayment() {
const rampRef = useRef(null);
const unmountWidget = () => {
rampRef.current?.unmount();
};
const destroyWidget = () => {
rampRef.current?.destroy();
};
return (
Unmount
Destroy
console.log('Success:', data)}
onError={(error) => console.error('Error:', error)}
style={styles.webview}
/>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
controls: {
flexDirection: 'row',
justifyContent: 'space-around',
padding: 10,
backgroundColor: '#fff',
},
button: {
backgroundColor: '#ff6b35',
paddingHorizontal: 15,
paddingVertical: 8,
borderRadius: 4,
},
buttonText: {
color: 'white',
fontSize: 12,
fontWeight: 'bold',
},
webview: {
flex: 1,
},
});
```
### Using the Hook
```tsx theme={null}
import React, { useState } from 'react';
import { View, TouchableOpacity, Text, StyleSheet, Modal } from 'react-native';
import { ZBDRamp, useZBDRamp } from '@zbdpay/ramp-react-native';
function HookExample() {
const [isVisible, setIsVisible] = useState(false);
const { rampRef, sendMessage, updateConfig, reload } = useZBDRamp({
sessionToken: 'your-session-token',
onSuccess: (data) => {
console.log('Payment successful:', data);
setIsVisible(false);
},
onClose: () => {
setIsVisible(false);
},
});
const openPayment = () => {
setIsVisible(true);
};
const closePayment = () => {
setIsVisible(false);
};
return (
Open Payment
Close
{
console.log('Success:', data);
setIsVisible(false);
}}
style={styles.webview}
/>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
openButton: {
backgroundColor: '#ff6b35',
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 8,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
modalContainer: {
flex: 1,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'flex-end',
padding: 15,
backgroundColor: '#f8f8f8',
},
closeButton: {
fontSize: 16,
color: '#007AFF',
},
webview: {
flex: 1,
},
});
```
### Custom WebView Configuration
```tsx theme={null}
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { ZBDRamp } from '@zbdpay/ramp-react-native';
function CustomWebViewRamp() {
return (
console.log('Success:', data)}
onError={(error) => console.error('Error:', error)}
/>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
webview: {
flex: 1,
backgroundColor: '#ffffff',
},
});
```
### Error Handling
```tsx theme={null}
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Alert } from 'react-native';
import { ZBDRamp } from '@zbdpay/ramp-react-native';
import type { RampError } from '@zbdpay/ramp-react-native';
function PaymentWithErrorHandling() {
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const handleError = (error: RampError) => {
setIsLoading(false);
let errorMessage = 'An unexpected error occurred.';
switch (error.code) {
case 'INVALID_CONFIG':
errorMessage = 'Configuration error. Please check your settings.';
break;
case 'NETWORK_ERROR':
errorMessage = 'Network error. Please check your connection.';
break;
case 'PAYMENT_FAILED':
errorMessage = 'Payment failed. Please try again.';
break;
}
setError(errorMessage);
Alert.alert('Payment Error', errorMessage);
};
const handleReady = () => {
setIsLoading(false);
setError(null);
};
const retry = () => {
setError(null);
setIsLoading(true);
};
return (
{isLoading && (
Loading payment widget...
)}
{error && (
{error}
Retry
)}
{!error && (
{
setError(null);
Alert.alert('Success', 'Payment completed!');
}}
style={styles.webview}
/>
)}
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
errorText: {
color: 'red',
textAlign: 'center',
marginBottom: 20,
fontSize: 16,
},
retryButton: {
backgroundColor: '#ff6b35',
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 8,
},
retryButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
webview: {
flex: 1,
},
});
```
## TypeScript Support
The package includes comprehensive TypeScript definitions:
```typescript theme={null}
import type {
ZBDRampProps,
ZBDRampRef,
RampConfig,
RampCallbacks,
RampOptions,
RampError,
RampLog,
PostMessageData,
InitRampSessionConfig,
InitRampSessionData,
InitRampSessionResponse,
QuoteCurrencyEnum,
BaseCurrencyEnum,
} from '@zbdpay/ramp-react-native';
```
### Type Examples
```typescript theme={null}
// Define typed configuration
const config: InitRampSessionConfig = {
apikey: process.env.ZBD_API_KEY!,
email: 'user@example.com',
destination: 'lightning-address@zbd.gg',
quote_currency: QuoteCurrencyEnum.USD,
base_currency: BaseCurrencyEnum.BTC,
};
// Handle typed responses
const handleSuccess = (data: any): void => {
console.log('Payment completed:', data);
};
const handleError = (error: RampError): void => {
console.error(`Error ${error.code}: ${error.message}`);
};
// Create typed instance
const ramp: ZBDRampRef = useRef(null);
```
## Development Setup
### Running the Example App
The repository includes a complete React Native example app:
```bash theme={null}
# Clone the repository
git clone https://github.com/zbdpay/ramp-react-native.git
cd ramp-react-native
# Install dependencies
npm install
# Navigate to example app
cd example
npm install
# For iOS
cd ios && pod install && cd ..
npm run ios
# For Android
npm run android
```
The example app includes:
* Session token creation
* Debug logging
* Full integration with all event handlers
* Error handling examples
## Related Packages
Core TypeScript/JavaScript SDK
React components and hooks
Flutter plugin
## Resources
* [GitHub Repository](https://github.com/zbdpay/ramp-react-native)
* [NPM Package](https://www.npmjs.com/package/@zbdpay/ramp-react-native)
* [API Documentation](/payments/ramp/session)
* [Webhook Events](/payments/ramp/webhooks)
## Support
For support and questions:
* GitHub Issues: [Create an issue](https://github.com/zbdpay/ramp-react-native/issues)
# TypeScript SDK
Source: https://docs.zbdpay.com/payments/ramp/sdks/typescript
Core TypeScript/JavaScript SDK for ZBD Ramp integration
The `@zbdpay/ramp-ts` package is the core TypeScript/JavaScript iframe wrapper for the ZBD Ramp widget that enables Bitcoin purchase interface for web applications.
## Features
* **TypeScript First**: Full type safety with comprehensive TypeScript definitions
* **PostMessage Communication**: Real-time error handling, logging, and step tracking
* **Lightweight**: No dependencies, tree-shakeable
* **Framework Agnostic**: Works with any JavaScript framework or vanilla JS
## Installation
```bash theme={null}
npm install @zbdpay/ramp-ts
```
```bash theme={null}
yarn add @zbdpay/ramp-ts
```
```bash theme={null}
pnpm add @zbdpay/ramp-ts
```
```html theme={null}
```
## Quick Start
### 1. Create Session Token
First, create a session token using the ZBD API:
```typescript theme={null}
import { initRampSession, QuoteCurrencyEnum, BaseCurrencyEnum } from '@zbdpay/ramp-ts';
const response = await initRampSession({
apikey: 'your-zbd-api-key',
email: 'user@example.com',
destination: 'lightning-address@zbd.gg',
quote_currency: QuoteCurrencyEnum.USD,
base_currency: BaseCurrencyEnum.BTC,
webhook_url: 'https://your-webhook-url.com',
});
const sessionToken = response.data.session_token;
```
### 2. Create and Mount Widget
```typescript theme={null}
import { createRamp } from '@zbdpay/ramp-ts';
const ramp = createRamp({
sessionToken,
container: '#ramp-container',
onSuccess: (data) => console.log('Success:', data),
onError: (error) => console.error('Error:', error),
onStepChange: (step) => console.log('Step:', step),
});
ramp.mount();
```
## API Reference
### initRampSession
Creates a new session token for the ZBD Ramp widget.
Configuration object for creating a session
#### Configuration Parameters
Your ZBD API key
User's email address
Lightning address or Bitcoin address
Quote currency (e.g., USD)
Base currency (e.g., BTC)
Webhook URL for notifications
Your internal reference ID
Additional metadata to attach to the session
#### Response
```typescript theme={null}
interface InitRampSessionResponse {
data: {
session_token: string; // Session token for widget
expires_at: string; // Token expiration time
widget_url: string; // Direct widget URL
};
error: string | null; // Error message if failed
success: boolean; // Success status
message: string; // Response message
}
```
#### Example
```typescript Async/Await theme={null}
import { initRampSession, QuoteCurrencyEnum, BaseCurrencyEnum } from '@zbdpay/ramp-ts';
try {
const response = await initRampSession({
apikey: 'your-zbd-api-key',
email: 'user@example.com',
destination: 'lightning-address@zbd.gg',
quote_currency: QuoteCurrencyEnum.USD,
base_currency: BaseCurrencyEnum.BTC,
webhook_url: 'https://your-webhook.com',
reference_id: 'order-123',
metadata: {
userId: '456',
plan: 'premium'
},
});
if (response.success) {
const sessionToken = response.data.session_token;
console.log('Session created:', sessionToken);
} else {
console.error('Failed to create session:', response.error);
}
} catch (error) {
console.error('Session creation error:', error);
}
```
```typescript Promise theme={null}
import { initRampSession, QuoteCurrencyEnum, BaseCurrencyEnum } from '@zbdpay/ramp-ts';
initRampSession({
apikey: 'your-zbd-api-key',
email: 'user@example.com',
destination: 'lightning-address@zbd.gg',
quote_currency: QuoteCurrencyEnum.USD,
base_currency: BaseCurrencyEnum.BTC,
})
.then(response => {
if (response.success) {
console.log('Session token:', response.data.session_token);
}
})
.catch(error => {
console.error('Error:', error);
});
```
### createRamp
Creates a new ZBD Ramp widget instance.
Configuration options for the widget
#### Options
Session token from initRampSession
Container element or CSS selector
Widget width
Widget height (minimum 600px recommended)
#### Callback Options
Called when payment is successful
Called when an error occurs
Called when user navigates to a different step
Debug/info logging callback
Called when widget is fully loaded
Called when user closes the widget
#### Returns
```typescript theme={null}
interface RampInstance {
mount: (container?: HTMLElement | string) => void;
unmount: () => void;
destroy: () => void;
}
```
## Usage Examples
### Basic Implementation
```typescript theme={null}
import { createRamp } from '@zbdpay/ramp-ts';
const ramp = createRamp({
sessionToken: 'your-session-token',
container: document.getElementById('ramp-container'),
});
ramp.mount();
```
### With Event Handlers
```typescript theme={null}
const ramp = createRamp({
sessionToken: 'your-session-token',
onSuccess: (data) => {
console.log('Payment successful:', data);
// Update UI, redirect, etc.
window.location.href = '/success';
},
onError: (error) => {
console.error('Payment error:', error);
// Show error message to user
alert(`Error: ${error.message}`);
},
onStepChange: (step) => {
console.log('Current step:', step);
// Track user progress
analytics.track('Ramp Step', { step });
},
onReady: () => {
console.log('Widget ready');
// Hide loading spinner
},
onClose: () => {
console.log('Widget closed');
// Handle user closing widget
}
});
```
### Custom Dimensions
```typescript theme={null}
const ramp = createRamp({
sessionToken: 'your-session-token',
width: '400px',
height: '700px',
});
// Default iframe styles applied:
// - border: none
// - border-radius: 8px
// - allow: payment
// - allowtransparency: true
// - min-height: 600px
```
### Programmatic Control
```typescript theme={null}
const ramp = createRamp({
sessionToken: 'your-session-token',
});
// Mount to specific container
ramp.mount('#my-container');
// Unmount iframe (keeps instance alive)
ramp.unmount();
// Remount to different container
ramp.mount('#another-container');
// Clean up completely
ramp.destroy();
```
### Error Handling
```typescript theme={null}
const ramp = createRamp({
sessionToken: 'your-session-token',
onError: (error) => {
// Error structure
console.error('Error Code:', error.code);
console.error('Error Message:', error.message);
if (error.details) {
console.error('Error Details:', error.details);
}
// Handle specific error codes
switch(error.code) {
case 'SESSION_EXPIRED':
// Create new session
createNewSession();
break;
case 'KYC_REQUIRED':
// Inform user about KYC
showKYCMessage();
break;
default:
// Generic error handling
showErrorMessage(error.message);
}
},
});
```
## TypeScript Support
The package includes comprehensive TypeScript definitions:
```typescript theme={null}
import type {
RampConfig,
RampCallbacks,
RampOptions,
RampError,
RampLog,
RampInstance,
PostMessageData,
InitRampSessionConfig,
InitRampSessionData,
InitRampSessionResponse,
QuoteCurrencyEnum,
BaseCurrencyEnum,
} from '@zbdpay/ramp-ts';
```
### Type Examples
```typescript theme={null}
// Define typed configuration
const config: InitRampSessionConfig = {
apikey: process.env.ZBD_API_KEY!,
email: 'user@example.com',
destination: 'lightning-address@zbd.gg',
quote_currency: QuoteCurrencyEnum.USD,
base_currency: BaseCurrencyEnum.BTC,
};
// Handle typed responses
const handleSuccess = (data: any): void => {
console.log('Payment completed:', data);
};
const handleError = (error: RampError): void => {
console.error(`Error ${error.code}: ${error.message}`);
};
// Create typed instance
const ramp: RampInstance = createRamp({
sessionToken: 'token',
onSuccess: handleSuccess,
onError: handleError,
});
```
## Simple Iframe Alternative
If you don't need JavaScript callbacks, you can use a simple iframe:
```html theme={null}
```
## Framework Integrations
This is the core package. For framework-specific integrations, see:
React components and hooks
React Native components
Flutter plugin
## Try It Out
### Interactive Example
To try the interactive example locally:
1. Clone the repository:
```bash theme={null}
git clone https://github.com/zbdpay/ramp-ts.git
cd ramp-ts
```
2. Start a local server:
```bash theme={null}
npx http-server . -p 8000 -o /example/js.html
```
3. Fill the form with your API key and user details
4. Click "Create Session & Load Ramp" to see it in action
### CodeSandbox Example
Try it online: [CodeSandbox Demo](https://codesandbox.io/s/zbd-ramp-example)
## Resources
* [GitHub Repository](https://github.com/zbdpay/ramp-ts)
* [NPM Package](https://www.npmjs.com/package/@zbdpay/ramp-ts)
* [API Documentation](/payments/ramp/session)
* [Webhook Events](/payments/ramp/webhooks)
## Support
For support and questions:
* GitHub Issues: [Create an issue](https://github.com/zbdpay/ramp-ts/issues)
# Create Ramp Session
Source: https://docs.zbdpay.com/payments/ramp/session
POST https://api.zbdpay.com/api/v1/ramp-widget
Initialize a new ramp widget session for a user
## API Overview
### Base URL
All API requests should be made to:
```
https://api.zbdpay.com
```
### Authentication
All ZBD endpoints are protected by an API Key. To make requests against these endpoints you must pass a header property called `apikey` with your [ZBD Project API Key](/get-started/api-keys).
```json theme={null}
apikey: "your-api-key-here"
```
Get your API keys by [scheduling a call](https://zbd.one/sales) with our sales team.
### SSL / HTTPS Access
**ZBD only provides a secure interface over HTTPS with SSL certificate support**. Any requests that attempt to reach the ZBD API in an insecure fashion (plain-text over HTTP requests) will be rejected.
## Description
This endpoint creates a session token and widget URL that can be used to embed the ZBD Ramp widget in your game or application.
## Usage
The ramp widget allows your users to purchase Bitcoin using their preferred fiat currency. The session creation process involves:
Create a session with user details and configuration options
Receive a widget URL and session token to embed in your application
## Configuration
### Header Parameters
ZBD Project API Key
Content Type
### Body Parameters
Email address of the user launching the Ramp
URL to receive webhook notifications
Existing user session token (for returning users)
Currency to convert from (e.g., USD)
Currency to convert to (e.g., BTC)
Destination address for the funds (Lightning Address or onchain address)
Your internal reference ID for this transaction
Additional metadata for the transaction
```bash cURL theme={null}
curl -X POST https://api.zbdpay.com/api/v1/ramp-widget \
-H "Content-Type: application/json" \
-H "apikey: YOUR_API_KEY" \
-d '{
"email": "user@example.com",
"webhook_url": "https://yourapp.com/webhooks/zbd"
}'
```
```javascript Node.js theme={null}
const response = await fetch('https://api.zbdpay.com/api/v1/ramp-widget', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': process.env.ZBD_API_KEY
},
body: JSON.stringify({
email: 'user@example.com',
webhook_url: 'https://yourapp.com/webhooks/zbd'
})
});
const data = await response.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"session_token": "eyJraWQiOiJzLWE1OWNkMjc4...",
"widget_url": "https://ramp.zbdpay.com/?session=eyJraWQiOiJzLWE1OWNkMjc4...",
"session_id": "ses_9n3f7h2u4b",
"expires_at": "2025-06-09T12:00:00Z"
}
}
```
### Error Responses
```json theme={null}
{
"success": false,
"error": {
"code": "INVALID_EMAIL",
"message": "Please provide a valid email address"
}
}
```
Common error codes:
* `INVALID_EMAIL` - Email format is invalid
* `INVALID_WEBHOOK_URL` - Webhook URL is not accessible
* `UNAUTHORIZED` - Invalid API key
* `RATE_LIMITED` - Too many requests
## Webhook Events
Webhooks are sent as POST requests to your specified `webhook_url`.
### Event Structure
All webhook events follow this structure:
```json theme={null}
{
"id": "evt_2n4f8gu3nf",
"type": "ramp.purchase.completed",
"created_at": "2025-06-09T10:30:00Z",
"livemode": true,
"data": {
// Event-specific data
}
}
```
# Themes Customization
Source: https://docs.zbdpay.com/payments/ramp/themes
Customize every visual element of ZBD Ramp to match your brand
ZBD Ramp provides complete control over the widget's appearance through an advanced theming system. Customize every visual element to create a seamless brand experience.
## Theme Options
### Pre-built Themes
Start with our professionally designed themes:
Clean, bright interface perfect for daytime use and professional applications
Modern dark interface that reduces eye strain and saves battery on OLED screens
## Live Preview Editor
The ZBD Developer Dashboard features a powerful Live Preview editor that makes customization intuitive and immediate.
### How It Works
Navigate to Ramp โ Theme in your ZBD Developer Dashboard
Click directly on any element in the preview to select it
Adjust the element's properties in the side panel
Watch your changes apply in real-time
Save your custom theme when satisfied
### Customizable Properties
For every element in the widget, you can customize:
* **Background Color**: Primary background color
* **Text Color**: Primary text color
* **Border Color**: Element border color
* **Border Radius**: Corner rounding
* **Hover Background**: Background color on hover
* **Hover Text**: Text color on hover
* **Active Background**: Background when clicked/active
* **Active Text**: Text color when active
* **Disabled State**: Appearance when disabled
* **Shadow**: Box shadow effects
* **Padding & Margins**: Spacing adjustments
## Custom Theme JSON
Every theme configuration is exportable as JSON, allowing you to:
* Version control your theme
* Share themes across projects
* Backup theme configurations
* Programmatically apply themes
### Example Theme JSON
```json theme={null}
{
"name": "My Custom Theme",
"base": "light",
"variables": {
"--primary": "210 100% 50%",
"--primary-foreground": "0 0% 100%",
"--secondary": "210 40% 96%",
"--secondary-foreground": "222 47% 11%",
"--accent": "210 40% 90%",
"--accent-foreground": "222 47% 11%",
"--destructive": "0 84% 60%",
"--destructive-foreground": "0 0% 100%",
"--border": "214 32% 91%",
"--input": "214 32% 91%",
"--ring": "210 100% 50%",
"--radius": "0.5rem",
"--widget-background": "#FFFFFF",
"--widget-card-background": "#FAFAFA",
"--amount-input-display-text": "#000000",
"--amount-input-display-error": "#ef4444",
"--amount-input-quick-amount-bg": "#F5F5F5",
"--amount-input-quick-amount-text": "#000000",
"--amount-input-quick-amount-border": "#E0E0E0",
"--amount-input-quick-amount-hover-bg": "#EEEEEE",
"--amount-input-quick-amount-active-bg": "#E0E0E0",
"--amount-input-numpad-bg": "transparent",
"--amount-input-numpad-text": "#000000",
"--amount-input-numpad-hover-bg": "#F5F5F5",
"--amount-input-numpad-active-bg": "#EEEEEE",
"--button-primary-bg": "210 100% 50%",
"--button-primary-text": "0 0% 100%",
"--button-primary-hover-bg": "210 100% 45%",
"--button-primary-active-bg": "210 100% 40%",
"--button-secondary-bg": "210 40% 96%",
"--button-secondary-text": "222 47% 11%",
"--button-secondary-hover-bg": "210 40% 92%",
"--button-secondary-active-bg": "210 40% 88%"
}
}
```
### Importing/Exporting Themes
#### Export Your Theme
1. Open the Theme editor in your dashboard
2. Click "Export Theme" button
3. Copy the JSON or download as a file
#### Import a Theme
1. Open the Theme editor
2. Click "Import Theme"
3. Paste your JSON or upload a file
4. Preview and apply
## Version Control Integration
### Best Practices
1. **Store in Repository**: Keep your theme JSON in your project repository
2. **Use Environment Configs**: Different themes for dev/staging/production
3. **Document Changes**: Comment significant theme updates
4. **Test Across Devices**: Ensure theme works on all screen sizes
## Testing Your Theme
Always test your custom theme across different devices and browsers to ensure a consistent experience.
## Support
Need help with theme customization?
* Use the Live Preview in your [ZBD Developer Dashboard](https://dashboard.zbdpay.com)
* Contact your ZBD account manager for advanced customization
* Review our example themes in the dashboard
# User Flow Walkthrough
Source: https://docs.zbdpay.com/payments/ramp/user-flow
Visual walkthrough of the complete ZBD Ramp user experience
This guide walks through the complete user journey in the ZBD Ramp widget, from initial screen to completed purchase. Use this reference to understand what your users will experience when buying Bitcoin through your app.
## 1. Initial Screen
When users first launch the ramp widget, they're greeted with a clean, intuitive interface to begin their purchase journey.
The initial screen presents the core functionality and guides users to begin the purchase process.
***
## 2. Email Verification
To ensure account security and compliance, users must verify their email address before proceeding.
Users receive a verification code via email and enter it to confirm their identity and proceed to KYC.
***
## 3. KYC Flow
The Know Your Customer (KYC) process is streamlined into clear steps. This is required for regulatory compliance and fraud prevention.
### Step 1: Getting Started
Users are introduced to the KYC process and what information they'll need to provide.
### Step 2: Country Selection
Users select their country of residence. This determines available payment methods and compliance requirements.
### Step 3: Personal Information
Users provide basic personal information including full name, date of birth, and other required details.
### Step 4: Address
Users enter their residential address for identity verification purposes.
### Step 5: Document Selection
Users choose which type of government-issued ID they'll upload (passport, driver's license, etc.).
### Step 6: Document Upload
Users can upload documents directly or receive a secure link to complete the upload on their phone.
Users can scan a QR code or receive a link to upload from their mobile device.
A secure, one-time link is generated for mobile document uploads.
Confirmation that the mobile device is connected and ready for upload.
***
## 4. KYC Processing
After submitting KYC information, users enter the verification stage.
Users see a processing screen while their information is being verified. This typically takes just a few moments.
Upon successful verification, users are cleared to proceed with their purchase.
If verification fails, users receive clear guidance on next steps or alternative options.
***
## 5. Payment Method Selection
Once KYC is approved, users choose how they want to pay for their Bitcoin.
Users can connect their bank account via Plaid for ACH payments. Additional payment methods are coming soon.
If a user hasn't linked a bank account yet, they'll see an option to connect via Plaid.
***
## 6. Plaid Bank Connection
To enable bank payments, users securely connect their bank account through Plaid.
### Plaid Flow Steps
Users are introduced to the secure Plaid connection process.
Users search for and select their bank from thousands of supported institutions.
Users log into their bank account securely through Plaid's interface.
Confirmation that the bank account is successfully connected and ready for payments.
***
## 7. Payment Confirmation
After payment method setup, users confirm their purchase details.
Users see a success screen confirming their Bitcoin purchase and delivery details.
If the payment fails, users receive clear error messages and options to retry.
***
## 8. Transaction Management
Users can view their transaction history and details at any time.
### Transaction History
A comprehensive list of all past and pending transactions, with status indicators for easy tracking.
### Transaction Details
Detailed view of individual transactions including amounts, timestamps, fees, and destination addresses.
***
## Integration Tips
Design your app to seamlessly transition users into the ramp widget. Consider the context and timing of when you present the purchase option.
Implement webhook handlers to process purchase events and update your app state in real-time as users progress through the flow.
***
## Next Steps
Learn how to integrate the ZBD Ramp widget into your application
Customize the widget's theme to match your brand
Handle webhook events to track user progress and purchases
Explore the complete API documentation
# Webhook Events
Source: https://docs.zbdpay.com/payments/ramp/webhooks
Complete reference for ZBD Ramp webhook events
## Overview
Webhooks allow you to receive real-time notifications about events that occur during the ZBD Ramp lifecycle. From session initialization to payment completion, webhooks provide crucial updates about your users' transactions.
ZBD Ramp sends webhook events to your specified endpoint as HTTP POST requests. Each event contains detailed information about what occurred, allowing you to update your systems accordingly.
### Webhook URL Setup
Configure your webhook URL when creating a session:
```typescript @zbdpay/ramp-ts theme={null}
import { initRampSession, QuoteCurrencyEnum, BaseCurrencyEnum } from '@zbdpay/ramp-ts';
const response = await initRampSession({
apikey: process.env.ZBD_API_KEY,
email: 'user@example.com',
destination: 'lightning-address@zbd.gg',
quote_currency: QuoteCurrencyEnum.USD,
base_currency: BaseCurrencyEnum.BTC,
webhook_url: 'https://app.xyz/webhooks/zbd', // <---- YOUR WEBHOOK URL HERE
reference_id: 'order-123',
metadata: {
userId: 'user-456',
plan: 'premium'
}
});
const sessionToken = response.data.session_token;
```
```typescript Fetch theme={null}
const response = await fetch('https://api.zbdpay.com/api/v1/ramp-widget', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': process.env.ZBD_API_KEY
},
body: JSON.stringify({
email: 'user@example.com',
destination: 'lightning-address@zbd.gg',
quote_currency: 'USD',
base_currency: 'BTC',
webhook_url: 'https://app.xyz/webhooks/zbd', // <---- YOUR WEBHOOK URL HERE
reference_id: 'order-123',
metadata: {
userId: 'user-456',
plan: 'premium'
}
})
});
const data = await response.json();
const sessionToken = data.data.session_token;
```
## Event Categories
Webhook events are organized into six main categories:
Widget lifecycle events
User verification events
Identity verification events
Bank connection events
Transaction events
Crypto delivery events
## Event Structure
All webhook events follow a consistent structure:
```json theme={null}
{
"event": "RAMP_WIDGET.V1.EVENT_NAME",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789",
// Event-specific data
"metadata": {
// Your custom metadata
}
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T10:30:00Z"
}
```
## Complete Event Reference
### Session Events
**Event:** `RAMP_WIDGET.V1.SESSION.INITIATED`
Triggered when a Ramp session is successfully initiated.
```json theme={null}
{
"event": "RAMP_WIDGET.V1.SESSION.INITIATED",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789"
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T10:30:00Z"
}
```
**Event:** `RAMP_WIDGET.V1.SESSION.CLOSED`
Fired when a Ramp session is closed, either by unmount, user action, or completion.
```json theme={null}
{
"event": "RAMP_WIDGET.V1.SESSION.CLOSED",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789"
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T11:15:00Z"
}
```
### Authentication Events
**Event:** `RAMP_WIDGET.V1.CODE_VERIFICATION.SUCCEEDED`
Sent when a user successfully verifies their 6-digit code.
```json theme={null}
{
"event": "RAMP_WIDGET.V1.CODE_VERIFICATION.SUCCEEDED",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789"
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T10:32:00Z"
}
```
**Event:** `RAMP_WIDGET.V1.CODE_VERIFICATION.FAILED`
Triggered when a 6-digit code verification attempt fails.
```json theme={null}
{
"event": "RAMP_WIDGET.V1.CODE_VERIFICATION.FAILED",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789"
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T10:33:00Z"
}
```
**Event:** `RAMP_WIDGET.V1.USER_ACCESS_TOKEN.CREATED`
Fired when user access and refresh tokens are generated.
```json theme={null}
{
"event": "RAMP_WIDGET.V1.USER_ACCESS_TOKEN.CREATED",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789",
"access_token_id": "tok_abc123",
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"access_token_expires_at": "2025-01-18T11:45:00Z",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token_expires_at": "2025-02-18T10:45:00Z",
"metadata": {
"customer_id": "cust_123",
"source": "widget"
}
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T10:45:00Z"
}
```
### KYC Events
**Event:** `RAMP_WIDGET.V1.KYC.INITIATED`
Fired when KYC (Know Your Customer) verification process is initiated.
```json theme={null}
{
"event": "RAMP_WIDGET.V1.KYC.INITIATED",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789",
"metadata": {
"customer_id": "cust_123",
"source": "widget"
}
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T10:35:00Z"
}
```
**Event:** `RAMP_WIDGET.V1.KYC.PROCESSING`
Indicates that KYC verification is being processed.
```json theme={null}
{
"event": "RAMP_WIDGET.V1.KYC.PROCESSING",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789",
"metadata": {
"customer_id": "cust_123",
"source": "widget"
}
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T10:36:00Z"
}
```
**Event:** `RAMP_WIDGET.V1.KYC.COMPLETED`
Sent when KYC verification is successfully completed.
```json theme={null}
{
"event": "RAMP_WIDGET.V1.KYC.COMPLETED",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789",
"metadata": {
"customer_id": "cust_123",
"source": "widget"
}
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T10:40:00Z"
}
```
**Event:** `RAMP_WIDGET.V1.KYC.REJECTED`
Triggered when KYC verification is rejected or fails.
```json theme={null}
{
"event": "RAMP_WIDGET.V1.KYC.REJECTED",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789",
"metadata": {
"customer_id": "cust_123",
"source": "widget"
}
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T10:41:00Z"
}
```
### Bank Connection Events
**Event:** `RAMP_WIDGET.V1.BANK_CONNECTION.INITIATED`
Triggered when bank connection flow is initiated.
```json theme={null}
{
"event": "RAMP_WIDGET.V1.BANK_CONNECTION.INITIATED",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789",
"metadata": {
"customer_id": "cust_123",
"source": "widget"
}
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T10:50:00Z"
}
```
**Event:** `RAMP_WIDGET.V1.BANK_CONNECTION.COMPLETED`
Sent when bank connection is successfully established.
```json theme={null}
{
"event": "RAMP_WIDGET.V1.BANK_CONNECTION.COMPLETED",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789",
"metadata": {
"customer_id": "cust_123",
"source": "widget"
}
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T10:55:00Z"
}
```
**Event:** `RAMP_WIDGET.V1.BANK_CONNECTION.FAILED`
Fired when bank connection attempt fails.
```json theme={null}
{
"event": "RAMP_WIDGET.V1.BANK_CONNECTION.FAILED",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789",
"metadata": {
"customer_id": "cust_123",
"source": "widget"
}
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T10:56:00Z"
}
```
### Payment Events
**Event:** `RAMP_WIDGET.V1.PAYMENT.INITIATED`
Triggered when a payment transaction is initiated.
```json theme={null}
{
"event": "RAMP_WIDGET.V1.PAYMENT.INITIATED",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789",
"base_amount": "100.00",
"base_currency": "USD",
"quote_amount": "0.0025",
"quote_currency": "BTC",
"metadata": {
"customer_id": "cust_123",
"source": "widget"
}
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T11:00:00Z"
}
```
**Event:** `RAMP_WIDGET.V1.PAYMENT.SETTLED`
Sent when a payment transaction is successfully settled.
```json theme={null}
{
"event": "RAMP_WIDGET.V1.PAYMENT.SETTLED",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789",
"base_amount": "100.00",
"base_currency": "USD",
"quote_amount": "0.0025",
"quote_currency": "BTC",
"metadata": {
"customer_id": "cust_123",
"source": "widget"
}
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T11:05:00Z"
}
```
**Event:** `RAMP_WIDGET.V1.PAYMENT.FAILED`
Fired when a payment transaction fails.
```json theme={null}
{
"event": "RAMP_WIDGET.V1.PAYMENT.FAILED",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789",
"base_amount": "100.00",
"base_currency": "USD",
"quote_amount": "0.0025",
"quote_currency": "BTC",
"metadata": {
"customer_id": "cust_123",
"source": "widget"
}
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T11:06:00Z"
}
```
### Withdrawal Events
**Event:** `RAMP_WIDGET.V1.WITHDRAWAL.SUCCEEDED`
Triggered when a withdrawal is successfully completed.
```json theme={null}
{
"event": "RAMP_WIDGET.V1.WITHDRAWAL.SUCCEEDED",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789",
"base_amount": "100.00",
"base_currency": "USD",
"quote_amount": "0.0025",
"quote_currency": "BTC",
"destination": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"payment_hash": "c0ff35a42445041592aa5ff982606971ae46b3f9df0a100cb15f05f61718f223",
"metadata": {
"customer_id": "cust_123",
"source": "widget"
}
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T11:10:00Z"
}
```
**Event:** `RAMP_WIDGET.V1.WITHDRAWAL.FAILED`
Sent when a withdrawal attempt fails.
```json theme={null}
{
"event": "RAMP_WIDGET.V1.WITHDRAWAL.FAILED",
"data": {
"email": "user@example.com",
"session_id": "sess_abc123xyz789",
"base_amount": "100.00",
"base_currency": "USD",
"quote_amount": "0.0025",
"quote_currency": "BTC",
"destination": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"metadata": {
"customer_id": "cust_123",
"source": "widget"
}
},
"url": "https://partner.com/webhooks",
"created_at": "2025-01-18T11:11:00Z"
}
```
## Webhook Implementation
### Basic Handler Examples
Here are webhook handler implementations in popular frameworks:
```typescript Next.js theme={null}
// app/api/webhooks/zbd/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const event = await request.json();
// Log the event
console.log(`Received webhook: ${event.event}`);
// Handle different event types
switch(event.event) {
case 'RAMP_WIDGET.V1.PAYMENT.SETTLED':
// Update user balance, send confirmation, etc.
await handlePaymentSettled(event.data);
break;
case 'RAMP_WIDGET.V1.KYC.COMPLETED':
// Update user verification status
await handleKYCCompleted(event.data);
break;
case 'RAMP_WIDGET.V1.WITHDRAWAL.SUCCEEDED':
// Log successful withdrawal
await handleWithdrawalSuccess(event.data);
break;
default:
console.log(`Unhandled event type: ${event.event}`);
}
// Always respond with 200 OK
return NextResponse.json({ received: true }, { status: 200 });
}
async function handlePaymentSettled(data: any) {
// Your payment processing logic
console.log('Payment settled:', data);
}
async function handleKYCCompleted(data: any) {
// Your KYC completion logic
console.log('KYC completed:', data);
}
async function handleWithdrawalSuccess(data: any) {
// Your withdrawal success logic
console.log('Withdrawal succeeded:', data);
}
```
```go Go theme={null}
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
)
type WebhookEvent struct {
Event string `json:"event"`
Data map[string]interface{} `json:"data"`
URL string `json:"url"`
CreatedAt string `json:"created_at"`
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading body", http.StatusBadRequest)
return
}
var event WebhookEvent
err = json.Unmarshal(body, &event)
if err != nil {
http.Error(w, "Error parsing JSON", http.StatusBadRequest)
return
}
// Log the event
log.Printf("Received webhook: %s", event.Event)
// Handle different event types
switch event.Event {
case "RAMP_WIDGET.V1.PAYMENT.SETTLED":
handlePaymentSettled(event.Data)
case "RAMP_WIDGET.V1.KYC.COMPLETED":
handleKYCCompleted(event.Data)
case "RAMP_WIDGET.V1.WITHDRAWAL.SUCCEEDED":
handleWithdrawalSuccess(event.Data)
default:
log.Printf("Unhandled event type: %s", event.Event)
}
// Always respond with 200 OK
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "OK")
}
func handlePaymentSettled(data map[string]interface{}) {
// Your payment processing logic
log.Printf("Payment settled: %v", data)
}
func handleKYCCompleted(data map[string]interface{}) {
// Your KYC completion logic
log.Printf("KYC completed: %v", data)
}
func handleWithdrawalSuccess(data map[string]interface{}) {
// Your withdrawal success logic
log.Printf("Withdrawal succeeded: %v", data)
}
func main() {
http.HandleFunc("/webhooks/zbd", webhookHandler)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
```
```rust Rust theme={null}
use actix_web::{web, App, HttpResponse, HttpServer, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Deserialize, Serialize)]
struct WebhookEvent {
event: String,
data: Value,
url: String,
created_at: String,
}
async fn webhook_handler(event: web::Json) -> Result {
// Log the event
println!("Received webhook: {}", event.event);
// Handle different event types
match event.event.as_str() {
"RAMP_WIDGET.V1.PAYMENT.SETTLED" => {
handle_payment_settled(&event.data).await;
}
"RAMP_WIDGET.V1.KYC.COMPLETED" => {
handle_kyc_completed(&event.data).await;
}
"RAMP_WIDGET.V1.WITHDRAWAL.SUCCEEDED" => {
handle_withdrawal_success(&event.data).await;
}
_ => {
println!("Unhandled event type: {}", event.event);
}
}
// Always respond with 200 OK
Ok(HttpResponse::Ok().body("OK"))
}
async fn handle_payment_settled(data: &Value) {
// Your payment processing logic
println!("Payment settled: {:?}", data);
}
async fn handle_kyc_completed(data: &Value) {
// Your KYC completion logic
println!("KYC completed: {:?}", data);
}
async fn handle_withdrawal_success(data: &Value) {
// Your withdrawal success logic
println!("Withdrawal succeeded: {:?}", data);
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("Server starting on http://127.0.0.1:8080");
HttpServer::new(|| {
App::new()
.route("/webhooks/zbd", web::post().to(webhook_handler))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
```
```javascript Node.js/Express theme={null}
app.post('/webhooks/zbd', (req, res) => {
const event = req.body;
// Log the event
console.log(`Received webhook: ${event.event}`);
// Handle different event types
switch(event.event) {
case 'RAMP_WIDGET.V1.PAYMENT.SETTLED':
// Update user balance, send confirmation, etc.
handlePaymentSettled(event.data);
break;
case 'RAMP_WIDGET.V1.KYC.COMPLETED':
// Update user verification status
handleKYCCompleted(event.data);
break;
case 'RAMP_WIDGET.V1.WITHDRAWAL.SUCCEEDED':
// Log successful withdrawal
handleWithdrawalSuccess(event.data);
break;
default:
console.log(`Unhandled event type: ${event.event}`);
}
// Always respond with 200 OK
res.status(200).send('OK');
});
function handlePaymentSettled(data) {
// Your payment processing logic
console.log('Payment settled:', data);
}
function handleKYCCompleted(data) {
// Your KYC completion logic
console.log('KYC completed:', data);
}
function handleWithdrawalSuccess(data) {
// Your withdrawal success logic
console.log('Withdrawal succeeded:', data);
}
```
```python Python/Flask theme={null}
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/zbd', methods=['POST'])
def handle_webhook():
event = request.json
# Log the event
print(f"Received webhook: {event['event']}")
# Handle different event types
if event['event'] == 'RAMP_WIDGET.V1.PAYMENT.SETTLED':
handle_payment_settled(event['data'])
elif event['event'] == 'RAMP_WIDGET.V1.KYC.COMPLETED':
handle_kyc_completed(event['data'])
elif event['event'] == 'RAMP_WIDGET.V1.WITHDRAWAL.SUCCEEDED':
handle_withdrawal_success(event['data'])
else:
print(f"Unhandled event type: {event['event']}")
# Always respond with 200 OK
return 'OK', 200
def handle_payment_settled(data):
# Your payment processing logic
print(f"Payment settled: {data}")
def handle_kyc_completed(data):
# Your KYC completion logic
print(f"KYC completed: {data}")
def handle_withdrawal_success(data):
# Your withdrawal success logic
print(f"Withdrawal succeeded: {data}")
if __name__ == '__main__':
app.run(port=8080)
```
## Security Best Practices
**Important:** Always validate webhook authenticity before processing events.
### Webhook Verification
1. **Use HTTPS Only** - Always use HTTPS endpoints for webhooks
2. **Verify Signatures** - Validate webhook signatures when provided
3. **IP Allowlisting** - Restrict webhook access to ZBD IP addresses
4. **Idempotency** - Handle duplicate events gracefully
5. **Timeout Handling** - Respond quickly (within 5 seconds)
### Error Handling
* Always respond with `200 OK` status, even if processing fails
* Log all webhook events for debugging
* Implement retry logic for critical operations
* Use message queues for asynchronous processing
## Testing Webhooks
### Local Development
Use tools like [ngrok](https://ngrok.com) to expose your local webhook endpoint:
```bash theme={null}
ngrok http 3000
```
Then use the ngrok URL as your webhook endpoint during development.
### Webhook Testing Checklist
Essential events to handle:
* โ
PAYMENT.SETTLED
* โ
WITHDRAWAL.SUCCEEDED
* โ
PAYMENT.FAILED
* โ
WITHDRAWAL.FAILED
Improve user experience:
* โ
KYC.COMPLETED
* โ
KYC.REJECTED
* โ
BANK\_CONNECTION.COMPLETED
* โ
SESSION.CLOSED
For advanced integrations:
* โช SESSION.INITIATED
* โช CODE\_VERIFICATION.SUCCEEDED
* โช KYC.INITIATED
* โช PAYMENT.INITIATED
## Troubleshooting
### Common Issues
* Verify your webhook URL is publicly accessible
* Check for firewall or security rules blocking requests
* Ensure your endpoint responds with 200 status
* Confirm webhook URL was correctly set during session creation
* Implement idempotency using the session\_id
* Store processed event IDs to prevent reprocessing
* Use database transactions for critical operations
* Process webhooks asynchronously using queues
* Respond immediately with 200 OK
* Move heavy processing to background jobs
## Next Steps
Learn how to create widget sessions
Complete integration walkthrough
# ZBD Payments SDK
Source: https://docs.zbdpay.com/payments/sdk
Use ZBD Payments in your preferred development environment.
## Official SDKs
The following SDKs are maintained by the ZBD Payments core team.
Easiest way to interact with the ZBD Payments APIs. Quickstart guides and source code for Next.js, Node.js, and Express applications.
## Community SDKs
Supported by the ZBD open source developer community.
Rust SDK for ZBD Payments APIs
Go SDK for ZBD Payments APIs
C# SDK for ZBD Payments APIs
# C# SDK
Source: https://docs.zbdpay.com/payments/sdk/csharp
Learn how to send and receive instant Bitcoin payments with C# and ZBD.
## Prerequisites
To complete this guide, you will need the following:
* [ZBD Project with a Live API key](/get-started/api-keys)
* [Visual Studio](https://visualstudio.microsoft.com/downloads/)
## 1. Create project
Create a new project or open an existing project in Visual Studio.
## 2. Install the ZBD NuGet package
NuGet package name: `dev.zbd.csharp`
```bash Package Manager theme={null}
NuGet\Install-Package dev.zbd.csharp
```
```bash .NET CLI theme={null}
dotnet add package dev.zbd.csharp
```
```xml Package Reference theme={null}
```
## 3. Send and receive Bitcoin
The below code is an example of how you can create a charge.
```csharp theme={null}
using ZebedeeAPI;
var handler = new ZebedeeHandler("");
var charge =
await handler.CreateCharge(
expiresIn: "10000",
amount: "100000",
description: "A Charge Created with ZBD!"
);
Console.WriteLine(charge.data.invoice.request);
```
You're looking for the `data.invoice.request` property in the JSON response. It starts with `lnbc1` and is the payment request anyone in the Bitcoin Lightning Network can use to pay you.
```
lnbc1u1pjdlax9pp5t7jhkd7h2wntd4f2v7xp22dknmjxp0q8nm7hfcny4p7a5mr7x3rsdp9f4hkueteypshggrfde6x2unwv46zqumsv4jkgcqzzsxqzjcsp5dsayu6m6632p28rnkeeqsr7d54amrkv6wh46yrv42gdgca8xl8gs9qyyssqgj2zrkax733rzulfkzc5mqsr8fpwrva82stpa7e0frw32722trv37jlq8mvlqfp8y75lr6mz63zd7qnxar8hhsehuy22pvfq6wjxwqqqa60lx3
```
Charges and payment requests are usually shown to users as QR codes that can be scanned by mobile apps (e.g. [ZBD](https://zbd.one/download)). Read [Callbacks](/payments/api/callbacks) to understand how to receive updates about your payment asynchronously.
# Go SDK
Source: https://docs.zbdpay.com/payments/sdk/go
Learn how to use the Go SDK to send Bitcoin to any Email.
This guide is coming soon. Until then you can check the [Go SDK repository](https://github.com/zebedeeio/go-sdk).
# Rust SDK
Source: https://docs.zbdpay.com/payments/sdk/rust
Learn how to send and receive instant Bitcoin payments with Rust and ZBD.
## Prerequisites
To complete this guide, you will need the following:
* [ZBD Project with a Live API key](/get-started/api-keys)
* [Rust](https://www.rust-lang.org/tools/install) and [cargo](https://www.rust-lang.org/tools/install) installed
## 1. Create a Rust project
```bash theme={null}
cargo new
cd
```
## 2. Install dependencies
Add both the `zebedee-rust` and `tokio` crates to your project:
```bash theme={null}
cargo add zebedee-rust tokio
```
## 3. Update your Cargo.toml
Update your `Cargo.toml` file to use full features in tokio.
```yaml theme={null}
[package]
name = "zbd-rust"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { version = "1.29.1", features = ['full']}
zebedee-rust = "0.4.4"
```
## 4. Create a Charge
Open your project's `main.rs` file and add the following code:
```rust theme={null}
use std::env;
use zebedee_rust::{charges::*, ZebedeeClient};
#[tokio::main]
async fn main(){
let apikey: String = env::var("ZBD_API_KEY").unwrap();
let zbd_client = ZebedeeClient::new().apikey(apikey).build();
let charge = Charge{
amount: String::from("5000"),
..Default::default()
};
let charges_res = zbd_client.create_charge(&charge).await.unwrap();
println!("{:?}", charge_res);
}
```
Make sure to create an environment variable called **ZBD\_API\_KEY** in your Rust project. This is the API key you get from your ZBD Project.
Run the following command to create your charge:
```bash theme={null}
cargo run
```
You can now create charges using the ZBD and Rust!
Charges and payment requests are usually shown to users as QR codes that can be scanned by mobile apps (e.g. [ZBD](https://zbd.one/download)). Read [Callbacks](/payments/api/callbacks) to understand how to receive updates about your payment asynchronously.
## 5. Try it yourself
You can now begin receiving instant Bitcoin payments with Rust + ZBD!
See the full source code.
# TypeScript SDK
Source: https://docs.zbdpay.com/payments/sdk/typescript
ZBD is your one-stop shop API for all things instant payments in TypeScript.
## Installation
```bash theme={null}
pnpm install @zbdpay/payments-sdk
```
## Quickstart
Use the following guides to get started with the TypeScript SDK for ZBD Payments:
} href="/payments/sdk/typescript/next">
Application codebase that allows you to quickly get started with ZBD and Next.js -- ZBD API Playground and a Starter Kit for ZBD-powered Next.js apps.
} href="/payments/sdk/typescript/vercel/send" />
} href="/payments/sdk/typescript/supabase/send" />
# Express
Source: https://docs.zbdpay.com/payments/sdk/typescript/express
Learn how to send and receive instant Bitcoin payments with Express and ZBD.
## Prerequisites
To complete this guide, you will need the following:
* [ZBD Project with a Live API key](/get-started/api-keys)
## 1. Create a Node.js project
Start with a brand new empty directory called `express-example` and run the following command to initiate a Node.js project:
```bash theme={null}
npm init -y
```
## 2. Install Express.js
Inside your project directory, run the following command to install Express.js:
```bash yarn theme={null}
yarn add express
```
```bash npm theme={null}
npm install express
```
```bash pnpm theme={null}
pnpm add express
```
## 3. Install @zbdpay/payments-sdk Node.js SDK
Inside your Express.js
```bash yarn theme={null}
yarn add @zbdpay/payments-sdk
```
```bash npm theme={null}
npm install @zbdpay/payments-sdk
```
```bash pnpm theme={null}
pnpm add @zbdpay/payments-sdk
```
## 4. Send and receive Bitcoin
Create a new file called `index.js` and add the following code:
```js theme={null}
import express from "express";
import ZbdPayments from '@zbdpay/payments-sdk';
const ZBD_API_KEY = 'b7YW3s2JzZKGcXjIf5Dqof8wjKT2RuWr8'; // dummy key
const client = new ZbdPayments({
apikey: ZBD_API_KEY,
});
// Create Express app
const app = express();
// Creating a Bitcoin Lightning payment request
app.get("/request", async (req, res) => {
try {
const data = await client.lightningCharges.create({
amount: '100000',
callbackUrl: 'https://your-app.com/zbd-callback',
description: 'Express + ZBD!',
});
res.status(200).json({ data });
} catch (error) {
res.status(500).json({ error });
}
});
// Send a payment to a Bitcoin Lightning Address
app.get("/send", async (req, res) => {
try {
const data = await client.lightningAddress.sendPayment({
lnAddress: "andreneves@zbd.gg", // Who is the recipient?
amount: "100000", // 100 satoshis (100,000 msats)
comment: "Express + ZBD!",
});
res.status(200).json({ data });
} catch (error) {
res.status(500).json({ error });
}
});
app.listen(3000, () => {
console.log("Express server w/ ZBD listening on https://example.com");
});
```
Run the following command to start your Express.js server:
```bash yarn theme={null}
yarn start
```
```bash npm theme={null}
npm start
```
Opening your browser to the following URL: `https://example.com/request` should return a JSON response with a payment request.
You can also test this using curl command:
```bash theme={null}
curl https://example.com/request
```
You're looking for the `data.invoice.request` property in the JSON response. It starts with `lnbc1` and is the payment request anyone in the Bitcoin Lightning Network can use to pay you.
```
lnbc1u1pjdlax9pp5t7jhkd7h2wntd4f2v7xp22dknmjxp0q8nm7hfcny4p7a5mr7x3rsdp9f4hkueteypshggrfde6x2unwv46zqumsv4jkgcqzzsxqzjcsp5dsayu6m6632p28rnkeeqsr7d54amrkv6wh46yrv42gdgca8xl8gs9qyyssqgj2zrkax733rzulfkzc5mqsr8fpwrva82stpa7e0frw32722trv37jlq8mvlqfp8y75lr6mz63zd7qnxar8hhsehuy22pvfq6wjxwqqqa60lx3
```
Charges and payment requests are usually shown to users as QR codes that can be scanned by mobile apps (e.g. [ZBD](https://zbd.one/download)). Read [Callbacks](/payments/api/callbacks) to understand how to receive updates about your payment asynchronously.
Opening your browser to the following URL: `https://example.com/send` should return a JSON response with the payment sent message.
You can also test this using curl command:
```bash theme={null}
curl https://example.com/send
```
You're looking for the `status` of `completed` to know that the payment settled successfully.
Payments in the Lightning Network are asynchronous so you may see a response stating the payment is `processing`. This is expected -- use the `callbackUrl` property to receive updates about your payments.
## 3. Try it yourself
You can now send and receive instant Bitcoin payments using Express.js + ZBD!
See the full source code.
# Next.js
Source: https://docs.zbdpay.com/payments/sdk/typescript/next
Learn how to use the ZBD with Next.js and our Playground Starter Kit.
## Playground Starter Kit
The ZBD + Next.js Playground Starter Kit is an application codebase that allows you to quickly get started with ZBD and Next.js. It includes two main sections: a ZBD API Playground and a Starter Kit for ZBD-powered Next.js applications.
Open the ZBD + Next.js Playground
View the source code.
} href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fzebedeeio%2Fnextjs-zebedee-starter&env=ZBD_API_KEY&envDescription=Your%20ZBD%20project%20API%20key&demo-title=ZBD%20%2B%20Next.js%20Starter&demo-description=Starter%20kit%20for%20using%20ZBD%20API%20with%20Next.js%20fullstack%20applications&demo-url=https%3A%2F%2Fnextjs-zebedee-starter.vercel.app%2F&demo-image=https%3A%2F%2Fi.imgur.com%2FNf8wRgv.png">
1-click deploy your own instance.
## Next.js
To use ZBD with Next.js and complete this guide, you will need the following:
* [ZBD Project with a Live API key](/get-started/api-keys)
### 1. Create a Next.js project
Start with a brand new Next.js project. To begin run the following command and walk through the initializing steps:
```bash theme={null}
npx create-next-app@latest
```
### 2. Install @zbdpay/payments-sdk Node.js SDK
Install ZBD:
```bash yarn theme={null}
yarn add @zbdpay/payments-sdk
```
```bash npm theme={null}
npm install @zbdpay/payments-sdk
```
```bash pnpm theme={null}
pnpm add @zbdpay/payments-sdk
```
### 3. Create Send and Receive API routes
Create a new file `/app/api/receive/route.ts` and add the following code to create an API route that will receive instant Bitcoin payments:
```js theme={null}
import { NextResponse } from 'next/server';
import ZbdPayments from '@zbdpay/payments-sdk';
const ZBD_API_KEY = 'b7YW3s2JzZKGcXjIf5Dqof8wjKT2RuWr8'; // dummy key
const client = new ZbdPayments({
apikey: ZBD_API_KEY,
});
export async function POST() {
try {
const data = await client.lightningCharges.create({
amount: '100000',
callbackUrl: 'https://your-app.com/zbd-callback',
description: 'Next.js + ZBD!',
});
return NextResponse.json(data);
} catch (error) {
return NextResponse.json({ error });
}
}
```
Then create a new file `/app/api/send/route.ts` and add the following code to create an API route that will send Bitcoin Lightning Address payments:
```js theme={null}
import { NextResponse } from 'next/server';
import ZbdPayments from '@zbdpay/payments-sdk';
const ZBD_API_KEY = 'b7YW3s2JzZKGcXjIf5Dqof8wjKT2RuWr8';
const client = new ZbdPayments({
apikey: ZBD_API_KEY,
});
export async function POST() {
try {
const data = await client.lightningAddress.sendPayment({
lnAddress: "andreneves@zbd.gg", // Who is the recipient?
amount: "100000", // 100 satoshis (100,000 msats)
comment: "Next.js + ZBD!",
});
return NextResponse.json(data);
} catch (error) {
return NextResponse.json({ error });
}
}
```
### 4. Run your Next.js server
Run the following command to start your Next.js server:
```bash yarn theme={null}
yarn dev
```
```bash npm theme={null}
npm run dev
```
Using an API tool like [Postman](https://postman.com), you should be able to make a POST call to this endpoint and receive a JSON response with a payment request:
```bash theme={null}
https://example.com/api/request
```
You're looking for the `data.invoice.request` property in the JSON response. It starts with `lnbc1` and is the payment request anyone in the Bitcoin Lightning Network can use to pay you.
```
lnbc1u1pjdlax9pp5t7jhkd7h2wntd4f2v7xp22dknmjxp0q8nm7hfcny4p7a5mr7x3rsdp9f4hkueteypshggrfde6x2unwv46zqumsv4jkgcqzzsxqzjcsp5dsayu6m6632p28rnkeeqsr7d54amrkv6wh46yrv42gdgca8xl8gs9qyyssqgj2zrkax733rzulfkzc5mqsr8fpwrva82stpa7e0frw32722trv37jlq8mvlqfp8y75lr6mz63zd7qnxar8hhsehuy22pvfq6wjxwqqqa60lx3
```
Charges and payment requests are usually shown to users as QR codes that can be scanned by mobile apps (e.g. [ZBD](https://zbd.one/download)). Read [Callbacks](/payments/api/callbacks) to understand how to receive updates about your payment asynchronously.
Using an API tool like [Postman](https://postman.com), you should be able to make a POST call to this endpoint and receive a JSON response with the payment success message:
```bash theme={null}
https://example.com/api/send
```
You're looking for the `status` of `completed` to know that the payment settled successfully.
Payments in the Lightning Network are asynchronous so you may see a response stating the payment is `processing`. This is expected -- use the `callbackUrl` property to receive updates about your payments.
### 5. Try it yourself
You can now send and receive instant Bitcoin payments using Next.js + ZBD!
See the full source code.
# Node.js
Source: https://docs.zbdpay.com/payments/sdk/typescript/node
Learn how to send and receive instant Bitcoin payments with Node.js and ZBD.
## Prerequisites
To complete this guide, you will need the following:
* [ZBD Project with a Live API key](/get-started/api-keys)
## 1. Create a Node.js project
Start with a brand new empty directory called `node-example` and run the following command to initiate a Node.js project:
```bash theme={null}
npm init -y
```
## 2. Install @zbdpay/payments-sdk Node.js SDK
Install ZBD:
```bash yarn theme={null}
yarn add @zbdpay/payments-sdk
```
```bash npm theme={null}
npm install @zbdpay/payments-sdk
```
```bash pnpm theme={null}
pnpm add @zbdpay/payments-sdk
```
## 3. Send and receive Bitcoin
Create a new file called `index.js` and add the following code:
```js theme={null}
import ZbdPayments from '@zbdpay/payments-sdk';
const ZBD_API_KEY = 'b7YW3s2JzZKGcXjIf5Dqof8wjKT2RuWr8'; // dummy key
const client = new ZbdPayments({
apikey: ZBD_API_KEY,
});
(async function () {
try {
const data = await client.lightningCharges.create({
amount: '100000',
callbackUrl: 'https://your-app.com/zbd-callback',
description: 'Node.js + ZBD!',
});
console.log(data);
} catch (error) {
console.error(error);
}
})();
```
Run the following command to start your Node.js server:
```bash yarn theme={null}
yarn start
```
```bash npm theme={null}
npm start
```
You may need to add a `start` script to your `package.json` file such as `node index.js`.
You're looking for the `data.invoice.request` property in the JSON response. It starts with `lnbc1` and is the payment request anyone in the Bitcoin Lightning Network can use to pay you.
```
lnbc1u1pjdlax9pp5t7jhkd7h2wntd4f2v7xp22dknmjxp0q8nm7hfcny4p7a5mr7x3rsdp9f4hkueteypshggrfde6x2unwv46zqumsv4jkgcqzzsxqzjcsp5dsayu6m6632p28rnkeeqsr7d54amrkv6wh46yrv42gdgca8xl8gs9qyyssqgj2zrkax733rzulfkzc5mqsr8fpwrva82stpa7e0frw32722trv37jlq8mvlqfp8y75lr6mz63zd7qnxar8hhsehuy22pvfq6wjxwqqqa60lx3
```
Charges and payment requests are usually shown to users as QR codes that can be scanned by mobile apps (e.g. [ZBD](https://zbd.one/download)). Read [Callbacks](/payments/api/callbacks) to understand how to receive updates about your payment asynchronously.
## 3. Try it yourself
You can now send and receive instant Bitcoin payments using Express.js + ZBD!
See the full source code.
# Receiving instant Bitcoin payments with Supabase
Source: https://docs.zbdpay.com/payments/sdk/typescript/supabase/receive
Learn how to receive Bitcoin using Supabase Edge Functions.
## Prerequisites
To complete this guide, you will need the following:
* [ZBD Project with a Live API key](/get-started/api-keys)
* [Supabase account](https://supabase.com/) + [Supabase CLI installed](https://supabase.com/docs/guides/cli#installation)
## 1. Create a Supabase Function
To create a Supabase Edge Function you must run the following command locally:
```bash theme={null}
supabase functions new zbd-receive
```
This will create a `/supabase/functions/zbd-receive` folder structure in your project.
## 2. Write the Edge Function
Use the code below as a starting point for your handler function. You will need to replace the `ZBD_API_KEY` with your own ZBD Project's API key.
```typescript theme={null}
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
const ZBD_API_KEY = "b7Ya3s2JZKZcXXX2Dqf8wjKTZZZRuWr8";
const zbdReceive = async (_request: Request): Promise => {
const res = await fetch("https://api.zbdpay.com/v0/charges", {
method: "POST",
headers: {
"Content-Type": "application/json",
"apikey": `${ZBD_API_KEY}`,
},
body: JSON.stringify({
amount: '100000', // 100 satoshis (100,000 msats) -- ~$0.03
description: 'Money at internet speed', // What is this payment request for?
}),
});
const data = await res.json();
return new Response(JSON.stringify(data), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
};
serve(zbdReceive);
```
## 3. Receive payment locally
To run this Supabase Edge Function locally you can spin up your Supabase instance using the following command. *You may already have this running at this stage.*
```bash theme={null}
supabase functions start
```
Then you can run the following command to start your serverless function:
```bash theme={null}
supabase functions serve zbd-receive --no-verify-jwt
```
Note that we add the `--no-verify-jwt` flag to the command to disable JWT verification. This is because we are not passing a JWT token to the function when we call it. **This is not recommended for production use.**
Supabase CLI will output a HTTP URL endpoint that you can use to test your function by issuing a POST request to that resource.
## 4. Deploying to Supabase Edge
After testing it locally, you can deploy your function to Supabase Edge using the following command. *You will need to enter your Supabase project ID to deploy.*
```bash theme={null}
supabase functions deploy zbd-receive
```
Once you deploy you will receive a URL that you can use to view that function in production on Supabase's Web Dashboard:
Opening your browser (or another HTTP client) to the URL provided by Supabase: `https://xxxxxxxxxxxxx.supabase.co/functions/v1/zbd-receive` should return a JSON response with the payment request details.
You can also test this using curl command:
```bash theme={null}
curl https://xxxxxxxxxxxxx.supabase.co/functions/v1/zbd-receive
```
You're looking for the `data.invoice.request` property in the JSON response. It starts with `lnbc1` and is the payment request anyone in the Bitcoin Lightning Network can use to pay you.
```
lnbc1u1pjdlax9pp5t7jhkd7h2wntd4f2v7xp22dknmjxp0q8nm7hfcny4p7a5mr7x3rsdp9f4hkueteypshggrfde6x2unwv46zqumsv4jkgcqzzsxqzjcsp5dsayu6m6632p28rnkeeqsr7d54amrkv6wh46yrv42gdgca8xl8gs9qyyssqgj2zrkax733rzulfkzc5mqsr8fpwrva82stpa7e0frw32722trv37jlq8mvlqfp8y75lr6mz63zd7qnxar8hhsehuy22pvfq6wjxwqqqa60lx3
```
Charges and payment requests are usually shown to users as QR codes that can be scanned by mobile apps (e.g. [ZBD](https://zbd.one/download)). Read [Callbacks](/payments/api/callbacks) to understand how to receive updates about your payment asynchronously.
## 5. Try it yourself
You can now begin receiving instant Bitcoin payments on the edge with Supabase + ZBD!
See the full source code.
# Sending instant Bitcoin payments with Supabase
Source: https://docs.zbdpay.com/payments/sdk/typescript/supabase/send
Learn how to send Bitcoin using Supabase Edge Functions.
## Prerequisites
To complete this guide, you will need the following:
* [ZBD Project with a Live API key](/get-started/api-keys)
* [Supabase account](https://supabase.com/) + [Supabase CLI installed](https://supabase.com/docs/guides/cli#installation)
## 1. Create a Supabase Function
To create a Supabase Edge Function you must run the following command locally:
```bash theme={null}
supabase functions new zbd-send
```
This will create a `/supabase/functions/zbd-send` folder structure in your project.
## 2. Write the Edge Function
Use the code below as a starting point for your handler function. You will need to replace the `ZBD_API_KEY` with your own ZBD Project's API key.
```typescript theme={null}
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
const ZBD_API_KEY = "b7Ya3s2JZKZcXXX2Dqf8wjKTZZZRuWr8";
const zbdSend = async (_request: Request): Promise => {
const res = await fetch("https://api.zbdpay.com/v0/ln-address/send-payment", {
method: "POST",
headers: {
"Content-Type": "application/json",
"apikey": `${ZBD_API_KEY}`,
},
body: JSON.stringify({
lnAddress: 'andre@zbd.gg', // Who is the recipient of the payment?
amount: '100000', // 100 satoshis (100,000 msats) -- ~$0.03
comment: 'Money at internet speed', // What is this payment for?
}),
});
const data = await res.json();
return new Response(JSON.stringify(data), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
};
serve(zbdSend);
```
## 3. Send payment locally
In order to successfully send payments through the API, you must have an active balance in the ZBD Project you are using. [Learn more about depositing funds into a ZBD Project wallet](/get-started/add-funds).
If you do not have funds in the ZBD Project you are using, you will receive a 4xx error from the API stating insufficient funds.
To run this Supabase Edge Function locally you can spin up your Supabase instance using the following command. *You may already have this running at this stage.*
```bash theme={null}
supabase functions start
```
Then you can run the following command to start your serverless function:
```bash theme={null}
supabase functions serve zbd-send --no-verify-jwt
```
Note that we add the `--no-verify-jwt` flag to the command to disable JWT verification. This is because we are not passing a JWT token to the function when we call it. **This is not recommended for production use.**
Supabase CLI will output a HTTP URL endpoint that you can use to test your function by issuing a POST request to that resource.
## 4. Deploying to Supabase Edge
After testing it locally, you can deploy your function to Supabase Edge using the following command. *You will need to enter your Supabase project ID to deploy.*
```bash theme={null}
supabase functions deploy zbd-send
```
Once you deploy you will receive a URL that you can use to view that function in production on Supabase's Web Dashboard:
Opening your browser (or another HTTP client) to the URL provided by Supabase: `https://xxxxxxxxxxxxx.supabase.co/functions/v1/zbd-send` should return a JSON response with the payment sent message.
You can also test this using curl command:
```bash theme={null}
curl https://xxxxxxxxxxxxx.supabase.co/functions/v1/zbd-send
```
You're looking for the `status` of `completed` to know that the payment settled successfully.
Payments in the Lightning Network are asynchronous so you may see a response stating the payment is `processing`. This is expected -- use the `callbackUrl` property to receive updates about your payments.
## 5. Try it yourself
You can now begin sending instant Bitcoin payments on the edge with Supabase + ZBD!
See the full source code.
# Receiving instant Bitcoin payments on Vercel
Source: https://docs.zbdpay.com/payments/sdk/typescript/vercel/receive
Learn how to receive Bitcoin using Vercel Edge Functions.
## Prerequisites
To complete this guide, you will need the following:
* [ZBD Project with a Live API key](/get-started/api-keys)
* [Vercel account](https://vercel.com/signup) + [Vercel CLI installed](https://vercel.com/docs/cli#installing-vercel-cli)
## 1. Create a Next.js Project
Follow the prompts on the create-next-app CLI to create a new project, and then change into the directory of your project.
```bash theme={null}
yarn create next-app
```
```bash theme={null}
cd
```
## 2. Write an Edge Function
For this guide we will be making use of the [App Router](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) inside of Next.js.
Create a new file in the `app/api/request/route.ts` that creates a Bitcoin Lightning Charge (payment request) with the following code:
```javascript app/api/request/route.ts theme={null}
import { NextResponse } from 'next/server';
export const runtime = 'edge';
export const dynamic = 'force-dynamic';
const ZBD_BASE_URL = 'https://api.zbdpay.com';
const ZBD_API_KEY = 'b7YW3s2JzZKGcXjIf5Dqof8wjKT2RuWr8';
export async function GET() {
const res = await fetch(`${ZBD_BASE_URL}/v0/charges`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': `${ZBD_API_KEY}`,
},
body: JSON.stringify({
amount: '100000', // 100 satoshis (100,000 msats) -- ~$0.03
description: 'Money at internet speed', // What is this payment request for?
}),
});
if (res.ok) {
const data = await res.json();
return NextResponse.json(data);
}
}
```
## 3. Create payment request locally
Run function locally:
```bash theme={null}
npx next dev
```
Opening your browser to the following URL: `https://example.com/api/request` should return a JSON response with a payment request.
You can also test this using curl command:
```bash theme={null}
curl https://example.com/api/request
```
You're looking for the `data.invoice.request` property in the JSON response. It starts with `lnbc1` and is the payment request anyone in the Bitcoin Lightning Network can use to pay you.
```
lnbc1u1pjdlax9pp5t7jhkd7h2wntd4f2v7xp22dknmjxp0q8nm7hfcny4p7a5mr7x3rsdp9f4hkueteypshggrfde6x2unwv46zqumsv4jkgcqzzsxqzjcsp5dsayu6m6632p28rnkeeqsr7d54amrkv6wh46yrv42gdgca8xl8gs9qyyssqgj2zrkax733rzulfkzc5mqsr8fpwrva82stpa7e0frw32722trv37jlq8mvlqfp8y75lr6mz63zd7qnxar8hhsehuy22pvfq6wjxwqqqa60lx3
```
Charges and payment requests are usually shown to users as QR codes that can be scanned by mobile apps (e.g. [ZBD](https://zbd.one/download)). Read [Callbacks](/payments/api/callbacks) to understand how to receive updates about your payment asynchronously.
## 4. Create payment request in production
Deploy your project to Vercel:
```bash theme={null}
vercel
```
Opening your browser to the following URL: `https://project-name.vercel.app/api/request` should now return this in production.
You can also test this using curl command:
```bash theme={null}
curl https://project-name.vercel.app/api/request
```
## 5. Try it yourself
You can now begin receiving instant Bitcoin payments on the edge with Vercel + ZBD!
See the full source code.
# Sending instant Bitcoin payments on Vercel
Source: https://docs.zbdpay.com/payments/sdk/typescript/vercel/send
Learn how to send Bitcoin using Vercel Edge Functions.
## Prerequisites
To complete this guide, you will need the following:
* [ZBD Project with a Live API key](/get-started/api-keys)
* [Vercel account](https://vercel.com/signup) + [Vercel CLI installed](https://vercel.com/docs/cli#installing-vercel-cli)
## 1. Create a Next.js Project
Follow the prompts on the create-next-app CLI to create a new project, and then change into the directory of your project.
```bash theme={null}
yarn create next-app
```
```bash theme={null}
cd
```
## 2. Write an Edge Function
For this guide we will be making use of the [App Router](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) inside of Next.js.
Create a new file in the `app/api/send/route.ts` that makes a payment to a [Lightning Address](/payments/glossary#lightning-address) (e.g. [andre@zbd.gg](mailto:andre@zbd.gg)) with the following code:
```javascript app/api/send/route.ts theme={null}
import { NextResponse } from 'next/server';
export const runtime = 'edge';
export const dynamic = 'force-dynamic';
const ZBD_BASE_URL = 'https://api.zbdpay.com';
const ZBD_API_KEY = 'b7YW3s2JzZKGcXjIf5Dqof8wjKT2RuWr8';
export async function GET() {
const res = await fetch(`${ZBD_BASE_URL}/v0/ln-address/send-payment`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': `${ZBD_API_KEY}`,
},
body: JSON.stringify({
lnAddress: 'andre@zbd.gg', // Who is the recipient of the payment?
amount: '100000', // 100 satoshis (100,000 msats) -- ~$0.03
comment: 'Money at internet speed', // What is this payment for?
}),
});
if (res.ok) {
const data = await res.json();
return NextResponse.json(data);
}
}
```
## 3. Send payment locally
In order to successfully send payments through the API, you must have an active balance in the ZBD Project you are using. [Learn more about depositing funds into a ZBD Project wallet](/get-started/add-funds).
If you do not have funds in the ZBD Project you are using, you will receive a 4xx error from the API.
Run function locally:
```bash theme={null}
npx next dev
```
Opening your browser to the following URL: `https://example.com/api/send` should return a JSON response with the payment sent message.
You can also test this using curl command:
```bash theme={null}
curl https://example.com/api/send
```
You're looking for the `status` of `completed` to know that the payment settled successfully.
Payments in the Lightning Network are asynchronous so you may see a response stating the payment is `processing`. This is expected -- use the `callbackUrl` property to receive updates about your payments.
## 4. Send payment in production
Deploy your project to Vercel:
```bash theme={null}
vercel
```
Opening your browser to the following URL: `https://project-name.vercel.app/api/send` should now return this in production.
You can also test this using curl command:
```bash theme={null}
curl https://project-name.vercel.app/api/send
```
## 5. Try it yourself
You can now begin sending instant Bitcoin payments on the edge with Vercel + ZBD!
See the full source code.
# ZBD Widget
Source: https://docs.zbdpay.com/widget
Embed a hosted cashout experience so your users can withdraw earned balances to bank accounts, Cash App, and more.
## Overview
The ZBD Widget is an embeddable iframe that runs the cashout experience for your users โ OTP verification, identity verification, bank account linking, and bank payouts. Your backend creates widget users, funds their point balances, creates sessions, and handles its own end-user terms and disclosures before the widget opens.
## Prerequisites
Before integrating:
1. Make sure you already have a [ZBD Developer Dashboard account](/get-started/create-account) and a [project](/get-started/create-project).
2. Copy the project API key from the project's [API section](/get-started/api-keys).
3. Open the **Widget** tab in the Developer Dashboard to configure sandbox balance and webhook settings.
4. If the Widget tab is not available yet, contact ZBD support to enable it for your project.
After that, fund the sandbox account before you try to cash out.
## Quick Start
To get sandbox running quickly, start with the [sandbox guide](/widget/sandbox):
1. Create a sandbox user.
2. Fund the sandbox user.
3. Create a widget session.
4. Embed the returned `widget_url` in your frontend.
5. Listen for widget events and webhook deliveries.
## Terms and disclosures
Your application must present and record acceptance of its applicable Terms of
Service, Privacy Policy, and required disclosures before it creates a widget
session. The widget does not render those documents or collect that acceptance
on the publisher's behalf, but it validates that the required terms have been
accepted before the user can access its services.
## Country availability
The widget uses the country resolved from the user's IP before identity
verification. After identity verification, it uses the user's verified
residential country to select the available bank rail.
### USD bank cashout
USD bank cashout (ACH) is available only in the United States.
### EUR bank cashout
EUR bank cashout (SCT) is available in Austria, Belgium, Denmark, Estonia,
Finland, France, Germany, Italy, Latvia, Lithuania, the Netherlands, Poland,
Portugal, Spain, and Sweden.
SCT is not available in other countries, including Bulgaria, Croatia, Cyprus,
Czechia, Greece, Hungary, Ireland, Luxembourg, Malta, Romania, Slovakia,
Slovenia, and Switzerland.
### Restricted locations
The widget is not supported for users in Cuba, Iran, North Korea, or Syria.
## Integration Flow
```
1. Create User POST /api/v1/widget/users (your server โ ZBD API)
2. Fund User POST /api/v1/widget/users/fund (your server โ ZBD API)
3. Reverse a Fund POST /api/v1/widget/users/deplete (your server โ ZBD API)
4. Get Balance GET /api/v1/widget/users/{userId}/balance (your server โ ZBD API)
5. Create Session POST /api/v1/widget/users/session (your server โ ZBD API)
6. Embed Widget (your frontend)
7. Listen for Events window.addEventListener("message", ...) (your frontend)
```
Use `POST /api/v1/widget/users/deplete` to reverse an available balance that
your server previously funded, such as when a reward is canceled.
## Events
The ZBD Widget emits browser events to your frontend and server webhooks to your backend.
Handle iframe callbacks in your frontend.
Process signed backend webhook deliveries.
## Authentication
Widget endpoints use two auth patterns:
| Context | Auth | Header |
| ----------------------- | ----------------------- | --------------------------------------- |
| **Your server โ ZBD** | Publisher API key | `apikey: YOUR_API_KEY` |
| **Widget iframe โ ZBD** | Session JWT (automatic) | `Authorization: Bearer {session_token}` |
Never expose your publisher API key in the browser. Steps 1โ3 must happen on your server.
## Embedding the Widget
After creating a session, load the returned `widget_url` in your client. Your backend should create the session; your game or web client only receives the `widget_url`.
```html theme={null}
```
Use a native WebView package for your Unity target and load the `widget_url` returned by your backend.
```csharp theme={null}
using UnityEngine;
public class ZbdWidgetLauncher : MonoBehaviour
{
[SerializeField] private GameObject webViewContainer;
public void OpenWidget(string widgetUrl)
{
// Replace this with your Unity WebView package API.
var webView = webViewContainer.GetComponent();
webView.LoadUrl(widgetUrl);
webView.SetVisible(true);
}
}
public interface IWebView
{
void LoadUrl(string url);
void SetVisible(bool visible);
}
```
Enable Unreal's **Web Browser** plugin, add a Web Browser widget to your UMG screen, and load the `widget_url` returned by your backend.
```cpp theme={null}
#include "Components/WebBrowser.h"
void UCashoutScreen::OpenZbdWidget(const FString& WidgetUrl)
{
if (ZbdWidgetBrowser)
{
ZbdWidgetBrowser->LoadURL(WidgetUrl);
}
}
```
**Allow document downloads.** The widget lets users download documents (disclosure PDFs, cashout receipts/statements). Browsers only permit a framed page to start a download if the iframe explicitly allows it. If you apply a `sandbox` attribute โ as in the example above โ it **must** include **`allow-downloads`** (alongside `allow-scripts allow-same-origin allow-forms allow-popups`). Without `allow-downloads`, the browser blocks the in-frame download and the widget falls back to opening the document in a **new browser tab**. If you do not set a `sandbox` attribute at all, downloads work by default.
## Embed Parameters
Pass these as URL query parameters on the widget URL:
| Parameter | Required | Description |
| --------------- | -------- | --------------------------------------------------------------- |
| `session_token` | Yes | JWT from Create Session |
| `flow` | No | `cashout` (default), `kyc`, `add-method` |
| `theme` | No | `zbd-default`, `zbd-light` |
| `embed` | No | `true` for chrome-less mode (no header/footer) |
| `component` | No | `balance`, `history`, `method-picker` for standalone components |
# Browser Events
Source: https://docs.zbdpay.com/widget/browser-events
Reference for ZBD Widget iframe events sent to your frontend with postMessage.
## Overview
The ZBD Widget communicates with your page from the iframe using browser `postMessage` events. Use these events for frontend state such as widget readiness, close actions, cashout submission, identity verification, and linked payout methods.
Server-side cashout status updates are delivered separately as signed webhooks. See [Server Webhooks](/widget/webhooks).
## Listening for Events
Listen for events on the parent window:
```javascript theme={null}
window.addEventListener("message", (event) => {
const { type, payload } = event.data ?? {};
if (!type?.startsWith("ZBD_")) return;
switch (type) {
case "ZBD_WIDGET_READY":
console.log("Widget loaded", payload?.user);
break;
case "ZBD_CASHOUT_SUCCESS":
console.log("Cashout submitted", payload);
// Close the widget iframe, refresh balances
break;
case "ZBD_WIDGET_CLOSE":
// User closed the widget โ remove the iframe
break;
}
});
```
## Event Types
| Event | Payload | Description |
| ---------------------- | ------------------------------------------------------- | -------------------------------- |
| `ZBD_WIDGET_READY` | `{ user? }` | Widget loaded and authenticated |
| `ZBD_WIDGET_CLOSE` | โ | User closed the widget |
| `ZBD_CASHOUT_SUCCESS` | `{ cashout_id, amount, currency_code, usd_equivalent }` | Cashout submitted successfully |
| `ZBD_KYC_COMPLETE` | `{ tier }` | Identity verification approved |
| `ZBD_METHOD_ADDED` | `{ payout_method_id, type, label }` | Bank account linked via Plaid |
| `ZBD_AUTH_TOKEN_READY` | `{ auth_token, user? }` | OTP verified, auth JWT available |
| `ZBD_WIDGET_ERROR` | `{ code, message }` | An error occurred |
# Create Session
Source: https://docs.zbdpay.com/widget/create-session
POST https://api.zbdpay.com/api/v1/widget/users/session
Mint a widget session and get the embeddable URL.
## Description
Creates a widget session for a user. Returns a `session_token` (JWT) and a `widget_url` that you embed as an iframe. The session is valid for the duration specified in `expires_at`.
The widget handles OTP email verification, identity verification (Onfido), bank linking (Plaid), and cashout initiation internally.
Before creating a session, your application must already have presented and
recorded the user's acceptance of its applicable Terms of Service, Privacy
Policy, and required disclosures. The widget does not display or collect those
acceptances for the publisher.
The session can report outstanding disclosures as a read-only status. It does
not render them or collect acceptance. Cashout-specific disclosures are enforced
when a cashout is submitted. See [Disclosure Agreements](/widget/disclosures).
## Configuration
### Header Parameters
Your ZBD project API key (production scope).
Content Type
### Body Parameters
User's email address. Required when `access_token` is not supplied.
Optional HTTPS server URL to receive widget webhook events.
Optional publisher reference for the session.
Optional key-value metadata to attach to the session.
Existing access token. When supplied, `email` is not required and OTP is skipped.
```bash cURL theme={null}
curl -X POST https://api.zbdpay.com/api/v1/widget/users/session \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "player@example.com",
"webhook_url": "https://your-server.com/zbd/webhooks",
"reference_id": "player-42",
"metadata": { "source": "your-game" }
}'
```
```json 201 theme={null}
{
"success": true,
"data": {
"session_token": "eyJhbGciOiJSUzI1NiIs...",
"expires_at": "2026-06-01T00:00:00.000Z",
"widget_url": "https://widget.zbd.gg?session_token=eyJhbGci..."
}
}
```
# Create User
Source: https://docs.zbdpay.com/widget/create-user
POST https://api.zbdpay.com/api/v1/widget/users
Create or fetch a publisher-linked widget user.
## Description
Provisions a user in ZBD for your project. Idempotent โ if a user with the same `reference_id` or `email` already exists for your project, the existing user is returned.
Call this from your server before creating a widget session.
Some cashout methods require acceptance of the latest disclosure before they can proceed. When `outstanding_disclosures` is not empty, your application must present those documents and record acceptance before creating a widget session. See [Disclosure Agreements](/widget/disclosures).
## Configuration
### Header Parameters
Your ZBD project API key (production scope).
Content Type
### Body Parameters
Your stable internal user identifier. Must be unique per project.
User's email address. Used for OTP verification in the widget.
```bash cURL theme={null}
curl -X POST https://api.zbdpay.com/api/v1/widget/users \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reference_id": "player-42",
"email": "player@example.com"
}'
```
## Response
The response `data` includes the new (or existing) user plus an `outstanding_disclosures` array โ the disclosure versions this user still needs to accept. This is returned on the **first user request**, so you have the user's outstanding disclosures immediately, with no extra call.
Disclosure versions the user has not yet accepted. Empty when the user is current on all disclosures. Each entry includes `id`, `type_id`, `name`, `description`, `version`, `content_uri`, `created_at`, and `due_date`.
`outstanding_disclosures` lists only what is still outstanding. To list a user's full acceptance state (including what they have **already accepted**), call [Get Disclosure Status](/widget/get-disclosure-status) at any time. See [Disclosure Agreements](/widget/disclosures) for the full model.
```json 201 theme={null}
{
"success": true,
"data": {
"id": "4ac4fd8a-cc2c-4d03-af09-a76f4e89d652",
"reference_id": "player-42",
"email": "player@example.com",
"outstanding_disclosures": [
{
"id": 12,
"type_id": 2,
"name": "Terms of Service",
"description": "ZBD Terms of Service Document.",
"version": "1.4.0",
"content_uri": "https://...",
"created_at": "2026-06-24T20:11:45Z",
"due_date": "2025-08-11T00:00:00"
}
]
}
}
```
# Deplete User
Source: https://docs.zbdpay.com/widget/deplete-user
POST https://api.zbdpay.com/api/v1/widget/users/deplete
Debit USDZ points from a widget user's point balance.
## Description
Reverse an available fund by debiting publisher points from a widget user's
point balance and returning them to the project's configured sink/source
account. This creates a new debit; it does not modify the original fund
transfer. Amount is in **currency minor units**. For USDZ, `100` equals `$1.00`
worth of points.
This endpoint is idempotent on `idempotency_key`. The user must already have enough point balance for the debit.
## Configuration
### Header Parameters
Your ZBD project API key (production scope).
Content Type
### Body Parameters
The ZBD user ID returned by Create User.
Amount in currency minor units. For USDZ: `100` = $1.00, `1000` = $10.00.
Point currency code. Use `USDZ` unless your project has a different configured point currency.
Unique key to prevent duplicate transfers. Use a UUID.
```bash cURL theme={null}
curl -X POST https://api.zbdpay.com/api/v1/widget/users/deplete \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "4ac4fd8a-cc2c-4d03-af09-a76f4e89d652",
"amount": 500,
"currency": "USDZ",
"idempotency_key": "551d1b1b-c387-4e16-9a7a-c15be30d0d62"
}'
```
```json 200 theme={null}
{
"success": true,
"message": "Point transfer completed successfully.",
"data": {
"transfer_id": "f4ba94c1-1234-5678-abcd-ef0123456789",
"status": "COMPLETED"
},
"error": null
}
```
```json 400 theme={null}
{
"success": false,
"message": "Insufficient point balance.",
"data": null,
"error": {
"code": "TX3017",
"message": "Insufficient point balance."
}
}
```
# Disclosure Agreements
Source: https://docs.zbdpay.com/widget/disclosures
Understand how widget user disclosure acceptance works before cashout.
## Overview
Some cashout flows require the user to accept the latest legal disclosure before
continuing. ZBD tracks acceptance by disclosure type and version, so a user may
need to accept a new version even if they accepted an older version before.
Your application is responsible for presenting the documents and collecting the
user's acceptance before it creates a widget session. The widget does not render
Terms of Service, Privacy Policy, or other disclosure documents, or record
acceptance on the publisher's behalf. It validates that the required terms have
been accepted before the user can access its services.
## End-to-end Flow
```
1. Your server creates or resolves the widget user.
2. Your server reads `outstanding_disclosures` from the response or calls Get Disclosure Status.
3. Your application presents each outstanding document to the user outside the widget.
4. Your server records acceptance with Submit Disclosure Acceptance.
5. Your server creates a widget session.
6. Your frontend opens the returned `widget_url`.
7. The user can continue to bank linking, identity verification, and cashout.
```
Do not open a widget session until the publisher-side disclosure flow is complete. The disclosure APIs require your server-side API key and must not be called from a browser, game client, or WebView.
## Outstanding Disclosures on User Creation
You do not need a separate call to discover what a user still owes. The [Create User](/widget/create-user) response (`POST /api/v1/widget/users`) already includes an **`outstanding_disclosures`** array on that **first user request** โ the disclosure versions this user has not yet accepted. Surface or record them right away; if the array is empty, the user is current.
`outstanding_disclosures` only lists what is **still outstanding**. When you need the complete picture โ including which disclosures a user has **already accepted** and when โ use the dedicated endpoints:
* [Get Disclosure Status](/widget/get-disclosure-status) โ list every current disclosure type with the user's acceptance state (`tos_current`, `accepted_at`), so you can show accepted vs. outstanding.
* [Submit Disclosure Acceptance](/widget/submit-disclosure-acceptance) โ record acceptance of the latest version from your server.
The same `outstanding_disclosures` shape also appears in the widget session status during a session (see [Session Status](#session-status)). Create User is the initial snapshot at provisioning time; Get Disclosure Status is the source of truth for the full, up-to-date acceptance state.
## Disclosure Types
The widget disclosure endpoints can check and record acceptance for all current disclosure types.
| Disclosure type | Type ID | Used for |
| ------------------------- | ------: | ----------------------------- |
| Terms of Service | `2` | General ZBD terms |
| Privacy Policy | `3` | General privacy disclosure |
| Electronic Funds Transfer | `6` | ACH bank payout authorization |
ACH cashout submission requires the current Electronic Funds Transfer disclosure to be accepted. If the user has not accepted the latest Electronic Funds Transfer disclosure, the cashout request is rejected until acceptance is recorded.
## Session Status
During a widget session, ZBD can return outstanding disclosure information as a
read-only status. The field is named `outstanding_disclosures`. This does not
cause the widget to display the documents or collect acceptance.
Terms of Service and Privacy Policy are expected to be accepted before session
creation. Cashout-specific checks, such as Electronic Funds Transfer acceptance
for ACH, are enforced when the user submits the cashout.
Example session status shape:
```json theme={null}
{
"success": true,
"data": {
"id": "4ac4fd8a-cc2c-4d03-af09-a76f4e89d652",
"email": "player@example.com",
"is_id_verified": true,
"kyc_tier": 2,
"kyc_status": "approved",
"capabilities": [],
"outstanding_disclosures": [
{
"id": 12,
"type_id": 2,
"name": "Terms of Service",
"description": "ZBD Terms of Service Document.",
"version": "1.4.0",
"content_uri": "https://...",
"created_at": "2026-06-24T20:11:45Z",
"due_date": "2025-08-11T00:00:00"
}
]
},
"message": "Session account status retrieved successfully.",
"error": null
}
```
If `outstanding_disclosures` is empty, the user is current on all session disclosures returned by this endpoint. If it is not empty, return the user to the publisher-side acceptance flow rather than expecting the widget to collect it.
## Recording acceptance before the widget
Use the widget disclosure APIs from your server before opening the widget. The
API key determines the project context, so these endpoints only require the
widget user ID in the path.
Disclosure endpoints require your server-side API key. Do not call these endpoints directly from a browser, mobile client, game client, or WebView.
Use [Get Disclosure Status](/widget/get-disclosure-status) to check current disclosure acceptance and [Submit Disclosure Acceptance](/widget/submit-disclosure-acceptance) to record acceptance for the latest version of one or more disclosure types.
## Existing Users
Disclosure acceptance is versioned. If a user already exists and needs to accept a current disclosure, submit the acceptance with the disclosure endpoint above.
Do not rely on an idempotent user-create call to update disclosure acceptance for an existing user. User creation can return an existing user without recording new disclosure acceptance.
## Sandbox
Sandbox uses the same disclosure status and acceptance model as production. When testing disclosure handling, use your sandbox API key and sandbox API base URL:
```text theme={null}
https://sandbox-api.zbdpay.com
```
Some sandbox bypass settings can skip cashout checks for faster testing. If you specifically need to verify cashout disclosure blocking behavior, make sure your sandbox setup is not bypassing the cashout disclosure gate.
# Fund User
Source: https://docs.zbdpay.com/widget/fund-user
POST https://api.zbdpay.com/api/v1/widget/users/fund
Credit USDZ points to a widget user's point balance.
## Description
Credit publisher points to a widget user's point balance. Amount is in **currency minor units**. For USDZ, `100` equals `$1.00` worth of points.
This endpoint is idempotent on `idempotency_key`.
## Reverse a fund
To take back points that you previously funded, call
[`POST /api/v1/widget/users/deplete`](/widget/deplete-user). This creates a new
debit from the user's currently available point balance back to your configured
sink/source account; it does not undo or mutate the original fund transfer.
Use a new `idempotency_key` for the deplete request. A deplete can only succeed
while the user has enough available points. It cannot recover points that have
already been spent or cashed out.
## Configuration
### Header Parameters
Your ZBD project API key (production scope).
Content Type
### Body Parameters
The ZBD user ID returned by Create User.
Amount in currency minor units. For USDZ: `100` = $1.00, `1000` = $10.00.
Point currency code. Use `USDZ` unless your project has a different configured point currency.
Unique key to prevent duplicate transfers. Use a UUID.
```bash cURL theme={null}
curl -X POST https://api.zbdpay.com/api/v1/widget/users/fund \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "4ac4fd8a-cc2c-4d03-af09-a76f4e89d652",
"amount": 1000,
"currency": "USDZ",
"idempotency_key": "90f9371e-c48a-4bc9-9075-ef7a4c6816ec"
}'
```
```json 200 theme={null}
{
"success": true,
"message": "Point transfer completed successfully.",
"data": {
"transfer_id": "f4ba94c1-1234-5678-abcd-ef0123456789",
"status": "COMPLETED"
},
"error": null
}
```
# Get User Balance
Source: https://docs.zbdpay.com/widget/get-balance
GET https://api.zbdpay.com/api/v1/widget/users/{user_id}/balance
Retrieve a widget user's available and total reward balance.
## Description
Returns the user's widget reward balance for your project. Use this from your server when you need to show a user's cashout-ready balance outside the widget.
Amounts are returned in **currency minor units**. For USD, `100` equals `$1.00`.
## Configuration
### Header Parameters
Your ZBD project API key (production scope).
### Path Parameters
The ZBD user ID returned by Create User.
```bash cURL theme={null}
curl https://api.zbdpay.com/api/v1/widget/users/4ac4fd8a-cc2c-4d03-af09-a76f4e89d652/balance \
-H "apikey: YOUR_API_KEY"
```
```json 200 theme={null}
{
"userId": "4ac4fd8a-cc2c-4d03-af09-a76f4e89d652",
"balances": [
{
"currency": "USD",
"availableBalance": 5000,
"totalBalance": 5000
}
]
}
```
# Get Disclosure Status
Source: https://docs.zbdpay.com/widget/get-disclosure-status
GET https://api.zbdpay.com/api/v1/widget/users/{userId}/disclosures
Check a widget user's current disclosure acceptance status.
## Description
Returns the user's acceptance status for each current disclosure type.
Use this from your server to determine which current disclosures the user must accept. Present any outstanding documents and collect the user's acceptance in your application before creating a widget session, then record it with [Submit Disclosure Acceptance](/widget/submit-disclosure-acceptance). Do not call this endpoint from a browser, mobile client, game client, or WebView.
The API key determines the publisher/project context. `userId` is the ZBD user ID returned when the widget user is created or resolved.
## Configuration
### Header Parameters
Your ZBD project API key.
### Path Parameters
The ZBD user ID returned when the widget user is created or resolved.
```bash cURL theme={null}
curl https://api.zbdpay.com/api/v1/widget/users/{userId}/disclosures \
-H "apikey: YOUR_API_KEY"
```
```json 200 theme={null}
{
"success": true,
"message": "Publisher user ToS status retrieved successfully.",
"data": [
{
"type_id": 2,
"name": "Terms of Service",
"version": "1.4.0",
"tos_current": true,
"accepted_at": "2026-01-15T12:00:00Z"
},
{
"type_id": 6,
"name": "Electronic Funds Transfer",
"version": "1.0.0",
"tos_current": false,
"accepted_at": null
}
],
"error": null
}
```
`tos_current` means the user has accepted the current version of that disclosure type. A value of `false` means there is no acceptance on record, or the user's latest acceptance is for an older version.
## Errors
| Status | Reason |
| ------ | ------------------------------------------------------------------- |
| `401` | Missing or invalid API key |
| `403` | The API key is not allowed to access widget disclosure APIs |
| `404` | `userId` does not belong to the project associated with the API key |
# Get User Transactions
Source: https://docs.zbdpay.com/widget/get-transactions
GET https://api.zbdpay.com/api/v1/widget/users/{user_id}/transactions
Retrieve a paginated transaction history for a widget user.
## Description
Returns a widget user's transaction history for your project. Use this from your server when you need to show reward funding and cashout activity outside the widget.
Amounts are returned in **currency minor units**. For USD, `100` equals `$1.00`.
## Configuration
### Header Parameters
Your ZBD project API key (production scope).
### Path Parameters
The ZBD user ID returned by Create User.
### Query Parameters
Number of transactions to skip. Defaults to `0`.
Maximum number of transactions to return. Defaults to `20`; maximum is `100`.
Optional transaction type filter.
Optional ISO 8601 start date filter.
Optional ISO 8601 end date filter.
```bash cURL theme={null}
curl "https://api.zbdpay.com/api/v1/widget/users/4ac4fd8a-cc2c-4d03-af09-a76f4e89d652/transactions?skip=0&limit=20" \
-H "apikey: YOUR_API_KEY"
```
```json 200 theme={null}
{
"userId": "4ac4fd8a-cc2c-4d03-af09-a76f4e89d652",
"transactions": [
{
"id": "f4ba94c1-1234-5678-abcd-ef0123456789",
"type": "reward",
"status": "completed",
"description": "Level 37 reward",
"amount": 1000,
"currency": "USD",
"fee": 0,
"counterparty": "Game Rewards",
"createdAt": "2026-05-28T22:21:03.067Z"
}
],
"total": 1,
"skip": 0,
"limit": 20
}
```
# Get User
Source: https://docs.zbdpay.com/widget/get-user
GET https://api.zbdpay.com/api/v1/widget/users/{user_id}
Retrieve a widget user's details and verification status.
## Description
Returns the user's details including their current identity verification status. Use this to check if a user has completed identity verification before showing cashout options.
## Configuration
### Header Parameters
Your ZBD project API key (production scope).
### Path Parameters
The ZBD user ID returned by Create User.
```bash cURL theme={null}
curl https://api.zbdpay.com/api/v1/widget/users/4ac4fd8a-cc2c-4d03-af09-a76f4e89d652 \
-H "apikey: YOUR_API_KEY"
```
```json 200 theme={null}
{
"success": true,
"data": {
"id": "4ac4fd8a-cc2c-4d03-af09-a76f4e89d652",
"reference_id": "player-42",
"email": "player@example.com",
"is_id_verified": true
}
}
```
# Sandbox
Source: https://docs.zbdpay.com/widget/sandbox
Test the full ZBD Widget flow without real money, real identity verification, or real bank transfers.
## Overview
Every ZBD project includes a **sandbox environment** for testing the ZBD Widget integration end-to-end. Sandbox mode simulates the entire flow โ funding users, initiating cashouts, and receiving webhooks โ without moving real money through ACH or requiring real identity verification.
**Sandbox API keys** are separate from production keys. Make sure you're using the right key for the right environment.
**Sandbox API base URL:** `https://sandbox-api.zbdpay.com`
## What Sandbox Does
| Feature | Sandbox | Production |
| ------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| **User creation** | Creates sandbox-scoped widget users | Creates production widget users |
| **Balance funding** | Credits test reward balance; no live pool is debited | Transfers publisher points from your live pool |
| **Identity verification** | Controlled through sandbox identity verification status helpers; no live Onfido review | Required for bank cashouts |
| **Bank linking (Plaid)** | Full Plaid sandbox flow | Real bank connections |
| **ACH payout** | Simulated when sandbox bypass is enabled; no real transfer | Real ACH via banking partner |
| **Disclosures** | Same status and acceptance model; bypass settings may skip cashout disclosure checks | Required disclosures can block cashout until accepted |
| **Webhooks** | Fire normally to your `webhook_url` | Same |
| **Widget UI** | Identical to production | Same |
## Getting Started
### 1. Get Your Sandbox API Key
In the [ZBD Developer Dashboard](https://dashboard.zebedee.io), navigate to your project's **API** tab and copy the **Sandbox** key.
Never use sandbox keys in production or production keys in sandbox. The endpoints will reject mismatched keys.
### 2. Create a Test User
```bash theme={null}
curl -X POST https://sandbox-api.zbdpay.com/api/v1/cashout/sandbox/users \
-H "apikey: YOUR_SANDBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reference_id": "test-player-1",
"email": "test@yourcompany.com"
}'
```
Sandbox user creation uses a sandbox-only endpoint. Use the returned `data.id` as `zbd_user_id` when funding the user and as the user ID when setting identity verification status. Use `POST /api/v1/widget/users` only for production widget users.
### 3. Fund the Test User
```bash theme={null}
curl -X POST https://sandbox-api.zbdpay.com/api/v1/widget/users/fund \
-H "apikey: YOUR_SANDBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "USER_ID_FROM_STEP_2",
"amount": 5000,
"currency": "USDZ",
"idempotency_key": "UNIQUE_UUID"
}'
```
In sandbox, funding transfers don't debit a real pool. The balance is credited for testing purposes.
### 4. Create a Widget Session
```bash theme={null}
curl -X POST https://sandbox-api.zbdpay.com/api/v1/widget/users/session \
-H "apikey: YOUR_SANDBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "test@yourcompany.com",
"webhook_url": "https://your-server.com/zbd/webhooks"
}'
```
### 5. Open the Widget
Embed the returned `widget_url` in an iframe. In sandbox, the URL should point at `https://widget.sandbox.zbd.gg`. The widget behaves identically to production โ same UI, same flows โ but cashouts complete without real bank transfers.
When you reach bank linking in sandbox, you can use the Plaid sandbox institution **Platypus First Bank** with the credentials `user_good` / `pass_good`. If Plaid asks for a phone number step, you can bypass it in the sandbox flow.
When you reach identity verification in sandbox, use a **US address** so the approval path can complete successfully.
## Sandbox Controls
Use these sandbox-only controls on `https://sandbox-api.zbdpay.com` with `apikey: YOUR_SANDBOX_API_KEY` to put a test user or monetary account into a known state before opening the widget.
For user rewards, use `POST /api/v1/widget/users/fund` and `POST /api/v1/widget/users/deplete`. The monetary-account helpers below are for state setup when you already have the monetary account ID.
| Control | Endpoint | Request body |
| ------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Create sandbox user | `POST /api/v1/cashout/sandbox/users` | `{ "reference_id": "test-player-1", "email": "test@yourcompany.com" }` |
| Set user identity verification status | `POST /api/v1/principal-accounts/{zbd_user_id}/kyc-status` | `{ "status": "approved", "country_code": "US" }` |
| Add sandbox balance | `POST /api/v1/monetary-accounts/{monetary_account_id}/fund` | `{ "amount": 5000 }` |
| Remove sandbox balance | `POST /api/v1/monetary-accounts/{monetary_account_id}/deplete` | `{ "amount": 5000 }` |
The identity verification `status` must be `approved`, `rejected`, or `processing`. Include `country_code` when setting `approved`.
Balance helper amounts use currency minor units. For USD, `5000` means `$50.00`.
Disclosure acceptance can be tested in sandbox with the same flow as production. If you need to verify that cashout is blocked by a missing disclosure, make sure your sandbox setup is not using a bypass mode that skips the disclosure gate.
ACH return-code simulation and explicit payout failure-mode controls are still being wired for sandbox. Until those controls are available, use the webhook examples to prepare your handler for `cashout.failed` and `cashout.returned` events.
## Sandbox Webhooks
Sandbox webhooks fire to your `webhook_url` with the same payload structure as production:
```json theme={null}
{
"event_id": "evt_sandbox_123",
"event_type": "cashout.initiated",
"user_reference_id": "test-player-1",
"amount_cents": 5000,
"status": "initiated",
"occurred_at": "2026-06-01T12:00:00Z"
}
```
In sandbox, the `cashout.completed` webhook can fire shortly after `cashout.initiated` since there's no real ACH settlement delay. See [Server Webhooks](/widget/webhooks) for the full event reference.
## Moving to Production
When you're ready to go live:
Verify all flows work: funding, cashout, webhooks, error handling
Set your production webhook endpoint in the dashboard
Replace your sandbox API key with the production key
Contact ZBD to fund your production CRB pool
Deploy and monitor your first real cashouts
Production cashouts move real money. Make sure your webhook handler correctly processes `cashout.completed`, `cashout.failed`, and `cashout.returned` events before going live.
# Submit Disclosure Acceptance
Source: https://docs.zbdpay.com/widget/submit-disclosure-acceptance
POST https://api.zbdpay.com/api/v1/widget/users/{userId}/disclosures
Record disclosure acceptance for a widget user.
## Description
Records the user's acceptance of the latest version of each disclosure type in `acceptedDisclosureTypeIds`.
Use this from your server after your application has presented the applicable
documents and collected the user's acceptance. Do not call this endpoint from a
browser, mobile client, game client, or WebView.
## Configuration
### Header Parameters
Your ZBD project API key.
Content Type
### Path Parameters
The ZBD user ID returned when the widget user is created or resolved.
### Body Parameters
Disclosure type IDs accepted by the user. ZBD records acceptance for the latest version of each type.
```bash cURL theme={null}
curl -X POST https://api.zbdpay.com/api/v1/widget/users/{userId}/disclosures \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"acceptedDisclosureTypeIds": [6]
}'
```
```json 201 theme={null}
{
"success": true,
"message": "Publisher user disclosure agreements recorded successfully.",
"data": {
"acceptedDisclosureTypeIds": [6]
},
"error": null
}
```
## Disclosure Type IDs
| Disclosure type | Type ID |
| ------------------------- | ------: |
| Terms of Service | `2` |
| Privacy Policy | `3` |
| Electronic Funds Transfer | `6` |
## Existing Users
If a user already exists and needs to accept a current disclosure, call this endpoint. Do not rely on an idempotent user-create call to update disclosure acceptance for an existing user.
## Errors
| Status | Reason |
| ------ | ------------------------------------------------------------------------------------- |
| `400` | `acceptedDisclosureTypeIds` is empty, missing, or contains an unknown disclosure type |
| `401` | Missing or invalid API key |
| `403` | The API key is not allowed to access widget disclosure APIs |
| `404` | `userId` does not belong to the project associated with the API key |
# Update User
Source: https://docs.zbdpay.com/widget/update-user
PATCH https://api.zbdpay.com/api/v1/widget/users/{user_id}
Update a widget user's details.
## Description
Updates a widget user's details. This is a partial update. Only the fields you send are changed, and today `email` is the only updatable field.
Use this when a user changes the email tied to their account. The new email becomes the recipient for OTP verification codes in the widget, so any code requested after the update is sent to the new address.
## Configuration
### Header Parameters
Your ZBD project API key.
Content Type
### Path Parameters
The ZBD user ID returned by Create User.
### Body Parameters
The user's new email address. Used for OTP verification in the widget.
```bash cURL theme={null}
curl -X PATCH https://api.zbdpay.com/api/v1/widget/users/4ac4fd8a-cc2c-4d03-af09-a76f4e89d652 \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "new-email@example.com"
}'
```
```json 200 theme={null}
{
"success": true,
"message": "Cashout user updated successfully.",
"data": {
"id": "4ac4fd8a-cc2c-4d03-af09-a76f4e89d652",
"reference_id": "player-42",
"email": "new-email@example.com"
},
"error": null
}
```
# Server Webhooks
Source: https://docs.zbdpay.com/widget/webhooks
Reference for ZBD Widget publisher webhook events, delivery, signing, retries, and payloads.
## Overview
ZBD sends webhook events to your configured HTTPS endpoint when a widget cashout or reversal changes status. Your endpoint validates the signature, acknowledges receipt, and processes the event asynchronously.
Webhook configuration is completed with ZBD during publisher onboarding. ZBD provides the shared signing secret used to sign each webhook request.
The MVP does not support subscribing to individual event types. ZBD sends all widget webhook event types to the configured endpoint, and your system should filter by the `event_type` field.
## Webhook Delivery Request
ZBD sends each event as an HTTP POST request:
```http theme={null}
POST
X-ZBD-Signature:
X-ZBD-Event-Id:
X-ZBD-Event-Type:
Content-Type: application/json
```
Your endpoint should return HTTP `200` within 10 seconds to acknowledge receipt. Any non-200 response, timeout, 5xx response, or connection failure triggers retry.
Validate the webhook signature against the raw request body before processing the event.
## Webhook Event Types
| Event type | Description |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `cashout.initiated` | The user initiated a cashout against the publisher-funded balance, and ZBD submitted the ACH to the partner bank. |
| `cashout.completed` | The ACH settled successfully and funds landed in the user's external bank account. |
| `cashout.failed` | The ACH was rejected before settlement, such as a closed account, invalid routing, or NSF response. |
| `cashout.returned` | The ACH was reversed after settlement within the return window. |
| `reversal.status_changed` | A reversal request previously submitted by the publisher changed status. |
## Cashout Webhook Events
Cashout lifecycle webhooks identify the event, reference the publisher's user identifier, include the current status, include the amount, and include the time the event occurred at the bank or inside ZBD.
### Cashout Initiated
```json theme={null}
{
"event_id": "evt_a1b2c3",
"event_type": "cashout.initiated",
"user_reference_id": "player-42",
"amount_cents": 500,
"status": "initiated",
"occurred_at": "2026-07-15T17:45:00Z"
}
```
### Cashout Completed
```json theme={null}
{
"event_id": "evt_d4e5f6",
"event_type": "cashout.completed",
"user_reference_id": "player-42",
"amount_cents": 500,
"status": "completed",
"occurred_at": "2026-07-17T14:20:00Z"
}
```
### Cashout Failed
```json theme={null}
{
"event_id": "evt_g7h8i9",
"event_type": "cashout.failed",
"user_reference_id": "player-42",
"amount_cents": 500,
"status": "failed",
"reason_code": "R02",
"reason_description": "account_closed",
"occurred_at": "2026-07-16T09:12:00Z"
}
```
### Cashout Returned
```json theme={null}
{
"event_id": "evt_j1k2l3",
"event_type": "cashout.returned",
"user_reference_id": "player-42",
"amount_cents": 500,
"status": "returned",
"reason_code": "R10",
"reason_description": "unauthorized_by_customer",
"occurred_at": "2026-07-25T11:30:00Z"
}
```
### Cashout Payload Fields
| Field | Type | Description |
| -------------------- | ------- | ----------------------------------------------------------------------------------------- |
| `event_id` | string | Unique event identifier. Use this value for deduplication. |
| `event_type` | string | One of `cashout.initiated`, `cashout.completed`, `cashout.failed`, or `cashout.returned`. |
| `user_reference_id` | string | Your stable user identifier from Create User. |
| `amount_cents` | integer | Cashout amount in USD cents. |
| `status` | string | Current cashout status. |
| `reason_code` | string | Bank reason code. Present on failed and returned events. |
| `reason_description` | string | Human-readable bank reason. Present on failed and returned events. |
| `occurred_at` | string | ISO 8601 timestamp for when the event occurred. |
## Reversal Webhook Events
Reversal status webhooks identify the reversal action, the new status, the reversal method, the amount, and the timestamp.
### Reversal Completed
```json theme={null}
{
"event_id": "evt_m4n5o6",
"event_type": "reversal.status_changed",
"reversal_id": "R-002",
"amount_cents": 500,
"status": "completed",
"method": "bank_reversal",
"occurred_at": "2026-07-20T15:45:00Z"
}
```
### Reversal Failed
```json theme={null}
{
"event_id": "evt_p7q8r9",
"event_type": "reversal.status_changed",
"reversal_id": "R-002",
"amount_cents": 500,
"status": "failed",
"method": "bank_reversal",
"occurred_at": "2026-07-20T15:45:00Z"
}
```
### Reversal Payload Fields
| Field | Type | Description |
| -------------- | ------- | ------------------------------------------------------------------ |
| `event_id` | string | Unique event identifier. Use this value for deduplication. |
| `event_type` | string | Always `reversal.status_changed`. |
| `reversal_id` | string | Unique reversal identifier. |
| `amount_cents` | integer | Reversal amount in USD cents. |
| `status` | string | New reversal status, such as `completed` or `failed`. |
| `method` | string | Reversal method, such as `bank_reversal` for bank-level reversals. |
| `occurred_at` | string | ISO 8601 timestamp for when the status changed. |
## Webhook Event Sources
`cashout.initiated` events are generated when the user submits a cashout through the ZBD Widget and ZBD submits the ACH to the partner bank.
`cashout.completed`, `cashout.failed`, and `cashout.returned` events are generated from partner bank webhook callbacks. ZBD translates the bank-level status update into the publisher-facing event.
`reversal.status_changed` events are generated when a reversal transitions between states. Instant reversals emit on completion. Queued bank-level reversals emit when ZBD records the final outcome from the partner bank.
## Retries
ZBD attempts initial delivery within seconds of event generation. If your endpoint does not return HTTP `200`, ZBD retries with exponential backoff: roughly 1 minute, 5 minutes, 30 minutes, 2 hours, then 12 hours between attempts.
After approximately five failed delivery attempts over roughly 15 hours, the event moves to a dead-letter queue and ZBD operations is alerted to investigate.
## Idempotency
Webhook delivery is at least once. The same event may be delivered more than once if a retry happens. Store and deduplicate by `event_id`.
## Auditability
ZBD logs each webhook delivery attempt, including the attempt timestamp, response or timeout reason, and final outcome.