Dealer Open API
Place batch orders at your dealer prices, query orders & balance, and receive callbacks from your own system.
Generate & manage your API key and configure webhooks in the dealer console.
Open dealer console →Authentication
Base URL: https://merchant.foreverfriendsgo.com
Every request must include these 4 headers:
| Parameter | Type | Description |
|---|---|---|
| X-Dealer-Keyrequired | string | Your API Key |
| X-Dealer-Timestamprequired | string | Unix seconds, within ±5 min of our clock |
| X-Dealer-Noncerequired | string | Unique random string per request (anti-replay, ≤64 chars) |
| X-Dealer-Signaturerequired | string | HMAC-SHA256(canonical, api_secret) hex |
canonical = segments joined by newline, then HMAC-SHA256 with api_secret:
METHOD
path (e.g. /api/dealer/create-order)
sorted querystring (k=v&k=v, empty if none)
timestamp
nonce
sha256(body)// Node.js signing example (any language works the same way)
const crypto = require('crypto');
const API_KEY = 'dk_live_xxx', API_SECRET = 'sk_xxx';
function signedHeaders(method, path, query, bodyStr) {
const ts = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(12).toString('hex');
const qs = Object.keys(query).sort().map(k => k + '=' + query[k]).join('&');
const bodyHash = crypto.createHash('sha256').update(bodyStr || '', 'utf8').digest('hex');
const canonical = [method.toUpperCase(), path, qs, ts, nonce, bodyHash].join('\n');
const sig = crypto.createHmac('sha256', API_SECRET).update(canonical, 'utf8').digest('hex');
return { 'Content-Type': 'application/json', 'X-Dealer-Key': API_KEY,
'X-Dealer-Timestamp': ts, 'X-Dealer-Nonce': nonce, 'X-Dealer-Signature': sig };
}Catalog
⏰ Items (especially bundles) rotate weekly — do not hardcode item_name; call GET /api/dealer/products before ordering. Your current prices:
Log in and call GET /api/dealer/products to fetch this week's orderable items and your live prices.
Create order
/api/dealer/create-order| Parameter | Type | Description |
|---|---|---|
| out_order_norequired | string | Your idempotency key: re-sending the same id returns the existing order with no double charge; also the handle for querying. Internal only — never shown to customers. |
| login_methodrequired | string | Login method: ptc / google / facebook |
| game_accountrequired | string | Game account |
| game_passwordrequired | string | Game password (encrypted in transit & at rest) |
| platform_order_nooptional | string | Your customer-facing order number (optional). Can be shown on the tracking page the customer sees, and is searchable. Same optional field as the manual order form. |
| trackingoptional | object | If provided, a tracking link is created with the order and returned as tracking_url. The three booleans default to true. |
| tracking.show_platform_order_nooptional | boolean | Whether the tracking page shows your platform order number — only applies when platform_order_no is set on this order, otherwise ignored (the internal DOD number is never shown for dealer orders). |
| tracking.allow_pwd_changeoptional | boolean | Whether the customer may change the password on the tracking page |
| tracking.show_screenshotsoptional | boolean | Whether the tracking page shows fulfillment screenshots |
| itemsrequired | array | Items array, 1-20; multiple on one account = parent order. |
| items[].item_namerequired | string | Item name (per /products, rotates weekly) |
| items[].typerequired | string | coin / ticket / item |
| items[].quantityrequired | number | Quantity 1-999 |
| items[].platform_order_nooptional | string | Per-item override of the order-level platform_order_no |
POST https://merchant.foreverfriendsgo.com/api/dealer/create-order
{
"out_order_no": "A-20260628-001",
"login_method": "ptc",
"game_account": "player@example.com",
"game_password": "secret",
"platform_order_no": "TB1234567890",
"tracking": { "show_platform_order_no": true, "allow_pwd_change": true, "show_screenshots": true },
"items": [
{ "item_name": "Pokémon Storage", "type": "item", "quantity": 1 }
]
}{
"success": true,
"out_order_no": "A-20260628-001",
"parent_order_no": null,
"total": 9.99,
"balance": 490.01,
"tracking_url": "https://order.mypokemongo.com/xxxxxxxx",
"orders": [
{ "order_no": "DOD...", "item_name": "Pokémon Storage", "quantity": 1, "price": 9.99 }
]
}Prices are authoritatively computed server-side at your dealer rate; if any item fails validation the whole order is rejected and nothing is charged. A tracking link is created (and tracking_url returned) only when you pass tracking; otherwise it is null.
Query order
/api/dealer/order| Parameter | Type | Description |
|---|---|---|
| out_order_norequired | string | The out_order_no you used (query param) |
{
"success": true,
"out_order_no": "A-20260628-001",
"parent_order_no": null,
"orders": [
{ "order_no": "DOD...", "item_name": "Pokémon Storage", "quantity": 1, "price": 9.99,
"status": "completed", "platform_order_no": "TB1234567890",
"created_at": "...", "completed_at": "..." }
]
}Products
/api/dealer/productsReturns this week's orderable items + your prices (same as Catalog above). GET has no body; use sha256("") for the body segment.
Balance
/api/dealer/balanceReturns your current prepaid balance (CNY).
GET https://merchant.foreverfriendsgo.com/api/dealer/balance
{ "success": true, "balance": 490.01 }Webhook
Once callback_url is set, we push on final/need_modify states. Verify: expected = HMAC-SHA256(X-FFG-Timestamp + "." + raw body, api_secret), compare with X-FFG-Signature. Return 2xx within 10s; failures retry with backoff 30s→2m→10m→1h→6h, up to 6 times.
POST <callback_url>
X-FFG-Event: order.completed
X-FFG-Timestamp: 1700000000
X-FFG-Signature: <hex>
X-FFG-Delivery: 123
{
"event": "order.completed",
"delivery_id": 123,
"out_order_no": "A-20260628-001",
"order_no": "DOD...",
"item_name": "Pokémon Storage",
"quantity": 1,
"status": "completed",
"occurred_at": "2026-..."
}Order status
| paid | Charged, queued for fulfillment |
| queued / verifying / processing | Fulfillment in progress |
| completed | Completed (final) |
| need_modify | Needs fix (wrong account/password) |
| cancelled | Cancelled (final) |
Error codes
| HTTP | code | Description |
|---|---|---|
| 401 | UNAUTHORIZED / INVALID_SIGNATURE / TIMESTAMP_EXPIRED / REPLAY | Auth failed: missing headers / bad signature / timestamp beyond ±5 min / nonce replay |
| 402 | INSUFFICIENT_BALANCE | Insufficient balance |
| 422 | PRICE_UNVERIFIABLE / PRICE_OUT_OF_RANGE | Item invalid this week or amount out of range |
| 400 | INVALID_PARAM / INVALID_ITEM / BAD_JSON | Invalid parameters |
| 429 | RATE_LIMITED | Over 120 req/min |
Errors are always { "success": false, "error": { "code", "message" } }











