Introduction
The G2Bulk API lets you resell game top-ups, voucher codes and digital goods straight from your own code. Every call is plain JSON over HTTPS, so there's no SDK to install. Orders are idempotent, and whatever you buy comes back instantly or after a short poll.
Every request goes to the versioned base URL below. You fund your wallet and get an API key through the Telegram bot @G2BULKBOT, so there's no separate dashboard to sign up for.
How a purchase flows
- Browse
/categoryand/productsto find what to sell. - Call the purchase or order endpoint with your API key.
- Get codes back instantly, or poll the delivery URL until ready.
- Reconcile against
/transactions, which records every balance change.
Authentication
Protected endpoints expect your secret key in an X-API-Key header. Public catalog endpoints (categories, products, games, catalogue) need no key at all.
Keys are tied to your wallet balance — treat them like a password and never ship one in client-side code.
X-API-Key: your_api_key_here
curl -X GET https://api.g2bulk.com/v1/getMe \
-H "X-API-Key: your_api_key_here"
Current user
Returns the profile and live wallet balance for the key holder. Call it on boot to confirm credentials and check available funds before charging.
{
"success": true,
"user_id": 123456789,
"username": "johndoe",
"first_name": "John Doe",
"balance": 8.74
}
Categories
List every product category, or pass an :id to get the products inside one. Public — no key required. Each category carries a live product_count.
{
"success": true,
"categories": [
{
"id": 1,
"title": "PUBG Mobile UC Vouchers",
"description": "",
"image_url": "https://example.com/pubg.png",
"product_count": 11
},
{
"id": 2,
"title": "Razer Gold Accounts",
"description": "",
"image_url": null,
"product_count": 5
}
]
}
Products
Every product with its current unit_price, printed face_value (the denomination on the voucher — null when not set) and live stock. Fetch the single-product route before a purchase so you charge against the freshest price.
{
"success": true,
"products": [
{
"id": 1,
"title": "60 UC Voucher",
"description": "",
"category_id": 1,
"category_title": "PUBG Mobile UC Vouchers",
"unit_price": 0.84,
"face_value": 1,
"image_url": null,
"stock": 1006
}
]
}
Buy a product
Charges your wallet and reserves stock. Send the quantity in the body. The response is either COMPLETED with codes attached, or PENDING with a poll_url to follow.
| Body | Type | Description |
|---|---|---|
| quantity req | integer | Number of units to buy. |
X-Idempotency-Key to dedupe retries — the same key within a 30-minute window returns the original response instead of charging twice. The key must be a 36-character UUID (e.g. 550e8400-e29b-41d4-a716-446655440000); keys of any other length are rejected with an error. Omit the header entirely to skip idempotency.When the status is PENDING, follow poll_url until it returns 200 with the codes — typically within 5–10 seconds. A pre-order mode for non-instant products is in the works and will reuse the same poll_url pattern.
curl -X POST https://api.g2bulk.com/v1/products/1/purchase \
-H "X-API-Key: your_api_key_here" \
-H "X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-H "Content-Type: application/json" \
-d '{"quantity": 5}'
{
"success": true,
"order_id": 123,
"transaction_id": 456,
"product_id": 1,
"product_title": "60 UC Voucher",
"status": "COMPLETED",
"delivery_items": ["KEY1", "KEY2", "KEY3"]
}
{
"success": true,
"order_id": 124,
"transaction_id": 457,
"product_id": 2,
"product_title": "PSN $20 Gift Card",
"status": "PENDING",
"delivery_items": null,
"poll_url": "/v1/orders/124/delivery"
}
Orders
Paginated order history, newest first. Pass page, limit (max 100) and an optional search query, or fetch a single order by id.
curl -X GET "https://api.g2bulk.com/v1/orders?page=1&limit=50" \
-H "X-API-Key: your_api_key_here"
{
"success": true,
"orders": [
{
"id": 1,
"user_id": 123456789,
"product_id": 1,
"product_title": "60 UC Voucher",
"quantity": 1,
"total_price": "0.840",
"status": "COMPLETED",
"description": "Purchase from G2Bulk Bot",
"created_at": "2025-10-19T05:30:00Z"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 120,
"total_pages": 3
}
}
Delivery polling
For PENDING orders, poll this every 2–5 seconds. The HTTP status tells you where things stand without parsing the body:
| Code | Meaning |
|---|---|
| 200 | Codes are ready in delivery_items. |
| 202 | Still PROCESSING — try again shortly. |
| 410 | Terminal failure — auto-refunded. |
| 404 | Order not found, or not yours. |
200 or 410. Delivery items stay available for 30 days after created_at — save them on your side before then.{
"success": true,
"order_id": 124,
"product_id": 2,
"product_title": "PSN $20 Gift Card",
"quantity": 1,
"status": "COMPLETED",
"delivery_items": ["XXXX-XXXX-XXXX"]
}
{
"success": true,
"order_id": 124,
"status": "PROCESSING",
"message": "Order is still being fulfilled. Try again shortly."
}
{
"success": false,
"order_id": 124,
"status": "REFUNDED",
"refunded": true,
"message": "Order failed and was refunded automatically."
}
Transactions
The ledger behind your wallet — every charge and top-up with balance_before / balance_after, so reconciliation is exact. Paginated like orders, with an optional search filter.
| Type | Meaning |
|---|---|
| add_balance | Balance added — manual addition or refund. |
| charge_balance | Balance deducted — purchase or top-up. |
{
"success": true,
"data": [
{
"id": 45,
"user_id": 123456789,
"transaction_type": "charge_balance",
"amount": "1.126",
"balance_before": "10.000",
"balance_after": "8.874",
"status": "success",
"description": "Purchase PUBG Mobile 60 UC",
"created_at": "2025-10-19T05:30:00Z"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 200,
"total_pages": 4
}
}
Games & fields
Direct top-up writes credits straight to a player account — no voucher code. Start with /games for the supported list, then ask /games/fields which inputs a title needs (user id, server, charname).
Some games also require a server. /games/servers returns them, or a 403 meaning no server needed — that's not an error, just check /games/fields for the real requirements.
| Body | Type | Description |
|---|---|---|
| game req | string | Game code identifier, e.g. mlbb. |
{
"success": true,
"games": [
{
"id": 1,
"code": "pubg_mobile",
"name": "PUBG Mobile",
"image_url": "/images/pubg_mobile.png"
},
{
"id": 2,
"code": "free_fire",
"name": "Free Fire",
"image_url": "/images/free_fire.png"
}
]
}
{
"code": "200",
"info": {
"fields": ["userid", "serverid"],
"notes": "Not available for Indonesia users"
}
}
{
"code": "200",
"servers": {
"SouthEast Asia": "SouthEast Asia",
"America": "America",
"Europe": "Europe"
}
}
Validate player
Confirm a player id resolves to a real account before you charge — it returns the in-game name so the buyer can sanity-check the recipient.
| Body | Type | Description |
|---|---|---|
| game req | string | Game code identifier. |
| user_id req | string | Player's user ID. |
| server_id cond | string | Server identifier, if the game uses one. |
| charname cond | string | Game-specific account/character identifier. |
charname means different things per game (character, server or account id). Read the notes field from /games/fields to know which.{
"game": "mlbb",
"user_id": "123456789",
"server_id": "2001",
"charname": "charname"
}
{
"valid": "valid",
"name": "John Doe",
"openid": "41581795132966184"
}
Catalogue & ETA
The catalogue lists every denomination with its live amount. The ETA endpoint estimates how long a top-up takes so you can set buyer expectations.
ETA labels
| Label | Range |
|---|---|
| instant | < 30 seconds |
| less_than_1_minute | 30s – 1 min |
| less_than_2_minutes | 1 – 2 min |
| less_than_5_minutes | 2 – 5 min |
| less_than_10_minutes | 5 – 10 min |
| less_than_30_minutes | 10 – 30 min |
| more_than_30_minutes | over 30 min |
| no_data | not enough data yet |
{
"success": true,
"game": {
"code": "pubgm",
"name": "PUBG Mobile",
"image_url": "/images/pubgm.png"
},
"catalogues": [
{ "id": 1, "name": "60 UC", "amount": 0.88 },
{ "id": 2, "name": "120 UC", "amount": 1.75 }
]
}
{
"success": true,
"estimated_time": {
"label": "less_than_5_minutes",
"display": "Less than 5 minutes",
"median_seconds": 187
}
}
Place a top-up order
The player id is validated and balance checked before the order is created. It comes back PENDING; track it with the status endpoint or a webhook.
| Body | Type | Description |
|---|---|---|
| catalogue_name req | string | Denomination, e.g. "60 UC". |
| player_id req | string | Player's in-game ID. |
| server_id cond | string | Server identifier if applicable. |
| charname cond | string | Game-specific character identifier. |
| remark opt | string | Your own order note. |
| callback_url opt | string | Webhook for status updates. |
X-Idempotency-Key to dedupe retries — the same key within a 30-minute window returns the original response instead of charging twice. The key must be a 36-character UUID (e.g. 550e8400-e29b-41d4-a716-446655440000); keys of any other length are rejected with an error. Omit the header entirely to skip idempotency.Status values
PENDING queued PROCESSING in flight COMPLETED delivered FAILED refunded
{
"catalogue_name": "60 UC",
"player_id": "12345678",
"server_id": "2001",
"charname": "charname",
"remark": "Optional note",
"callback_url": "https://your-domain.com/webhook/order-status"
}
{
"success": true,
"message": "Order created successfully. We are processing your order.",
"order": {
"order_id": 42,
"game": "PUBG Mobile",
"catalogue": "60 UC",
"player_id": "12345678",
"player_name": "PlayerName",
"price": 0.88,
"status": "PENDING",
"callback_url": "https://your-domain.com/webhook/order-status"
}
}
Webhooks
Pass a callback_url on an order and we'll POST to it the moment the order reaches a terminal state — no polling required.
| Property | Detail |
|---|---|
| Method | POST with JSON body |
| Timeout | 10 seconds |
| Retry | Single retry on failure |
| Fires on | COMPLETED or FAILED |
{
"order_id": 42,
"game_code": "pubgm",
"game_name": "PUBG Mobile",
"player_id": "12345678",
"player_name": "PlayerName",
"server_id": "2001",
"denom_id": "60 UC",
"price": 0.88,
"status": "COMPLETED",
"message": "Order completed successfully",
"remark": "your order remark",
"timestamp": "2024-01-15T10:30:00Z"
}
{
"success": true,
"orders": [
{
"order_id": 42,
"game_code": "pubgm",
"game_name": "PUBG Mobile",
"player_id": "12345678",
"player_name": "PlayerOne",
"denom_id": "60",
"price": 0.84,
"status": "completed",
"is_refunded": false,
"created_at": "2025-10-19T05:30:00Z",
"completed_at": "2025-10-19T05:31:00Z"
}
],
"pagination": { "page": 1, "limit": 50, "total": 85, "total_pages": 2 }
}
Status & limits
Standard HTTP semantics throughout — the body always carries a success boolean and a human message on errors.
| Limit | Value |
|---|---|
| Rate limit | 1000 requests / 10s per key |
| Idempotency window | 30 minutes |
| Pending poll cadence | every 2–5 seconds |
| Typical completion | 5–10 seconds |
| Delivery window | 30 days after created_at |
| Page size | max 100 items |
| Auth failures | permanent IP ban |
HTTP status codes
| Code | Meaning |
|---|---|
| 200 | OK — request successful. |
| 202 | Accepted — still pending/processing, poll again. |
| 400 | Bad request — invalid format or parameters. |
| 401 | Unauthorized — auth failed or key invalid. |
| 404 | Not found — resource does not exist. |
| 410 | Gone — failed, refunded or cancelled (terminal). |
| 429 | Too many requests — rate limit exceeded. |
| 500 | Internal server error. |
{
"success": false,
"message": "Order failed and was refunded automatically."
}
{
"detail": {
"code": "403",
"message": "Game does not requires any servers"
}
}
SMM panel API
If you already run an SMM panel, G2Bulk speaks the same protocol you're used to. There's one endpoint, every action is a POST, and you pick what to do with an action field in the body.
The base URL is https://api.g2bulk.com/api/v2. Send the body as JSON or form-urlencoded, whichever your panel already uses.
Authentication
Your key goes in the request body as key, not in a header. Grab one from the Telegram bot @G2BULKBOT.
X-API-Key header.Topping up a game
For top-ups, put the player details in link and keep quantity at 1. Depending on the game, link is the player ID on its own, Player ID|Server ID, or Player ID|Server ID|Charname.
curl https://api.g2bulk.com/api/v2 \
-d key=your_api_key_here \
-d action=balance
Get services
Lists every service you can order, each with its live rate in USD. Pass category to narrow it to one game.
| Body | Type | Description |
|---|---|---|
| key req | string | Your API key. |
| action req | string | Must be services. |
| category opt | string | Filter by game name. |
curl https://api.g2bulk.com/api/v2 \
-d key=your_api_key_here \
-d action=services
[
{
"service": 1,
"name": "PUBG Mobile 60 UC",
"type": "Package",
"category": "PUBG Mobile",
"rate": "0.88",
"min": "1",
"max": "1",
"refill": false,
"cancel": false
}
]
Add order
Places a top-up. The order comes back with an order id you poll for status. Balance is checked first, so a failure returns an error instead.
| Body | Type | Description |
|---|---|---|
| key req | string | Your API key. |
| action req | string | Must be add. |
| service req | integer | Service ID from services. |
| link req | string | Player ID, or Player ID|Server ID|Charname. |
| quantity req | integer | Always 1 for game top-ups. |
curl https://api.g2bulk.com/api/v2 \
-d key=your_api_key_here \
-d action=add \
-d service=1 \
-d "link=12345678|2001" \
-d quantity=1
{ "order": 12345 }
{ "error": "Insufficient balance" }
Order status
Processing happens in the background, so poll this to follow an order. Send one order, or a comma-separated orders list (up to 100) to check a batch in one call.
| Body | Type | Description |
|---|---|---|
| key req | string | Your API key. |
| action req | string | Must be status. |
| order cond | integer | A single order ID. |
| orders cond | string | Comma-separated IDs, up to 100. |
{
"charge": "0.88",
"start_count": "0",
"status": "Completed",
"remains": "0",
"currency": "USD"
}
{
"12345": {
"charge": "0.88",
"start_count": "0",
"status": "Completed",
"remains": "0",
"currency": "USD"
},
"12346": {
"charge": "1.75",
"start_count": "0",
"status": "Pending",
"remains": "0",
"currency": "USD"
}
}
Check balance
Returns your current wallet balance and currency.
| Body | Type | Description |
|---|---|---|
| key req | string | Your API key. |
| action req | string | Must be balance. |
curl https://api.g2bulk.com/api/v2 \
-d key=your_api_key_here \
-d action=balance
{
"balance": "150.5000",
"currency": "USD"
}
Status & errors
Order status is one of these values:
| Status | Meaning |
|---|---|
| Pending | Waiting to be processed. |
| In progress | Being processed now. |
| Completed | Delivered successfully. |
| Partial | Partially completed. |
| Canceled | Canceled or failed. |
error field — check for error before reading the rest of the response.{ "error": "Insufficient balance" }
"Invalid API key"
"Invalid action"
"Service ID is required"
"Link (Player ID) is required"
"Service not found or inactive"
"Game is currently unavailable"
"Insufficient balance"
"Order not found"