Partner Topup API HMAC-signed diamond topup integration for magenta.Gaming partners
This page documents how a partner integrates with the /api/v1/topup API to check package
pricing, preview a player's topup, and execute a paid diamond delivery against their prepaid USD balance.
https://api-portal-svc-3wdlqy54rq-el.a.run.appBase URL (local dev):
http://localhost:9100
1. Setup
Create a partner account and generate an API key/secret pair from the dashboard, then store them in your server's environment.
| Step | Action |
|---|---|
| 1 | Go to the portal and create an account. |
| 2 | Verify your email (OTP), then log in to the dashboard. |
| 3 | Open API Keys in the dashboard → generate a key. Copy the API Key and API Secret — the secret is shown once. |
| 4 | Set your Partner API name in the dashboard (used to tag your topup records, e.g. tournaop-ff). |
| 5 | Store the credentials in your .env. |
magenta_gaming_apis="klm_9929da3fbe6dcf31ace91de79d3e3c7a"
magenta_gaming_secret="c8cab24d4f6d33394d16c56bbaa17c3e4f40c711be78f5058f488e2bc96bc32f"
# optional overrides
MAGENTA_BASE_URL="https://api-portal-svc-3wdlqy54rq-el.a.run.app"
2. Request signing (HMAC-SHA256)
Every call (except account setup in the browser) is authenticated with two headers, computed against the exact raw JSON body you send:
| Header | Value |
|---|---|
X-API-Key | Your API key, e.g. klm_9929da3f... |
X-Signature | HMAC_SHA256(apiSecret, rawJsonBody), hex-encoded |
For GET requests, sign the string {} (empty JSON object) since there is no body.
# curl example — sign and send a preview request
BASE="https://api-portal-svc-3wdlqy54rq-el.a.run.app"
BODY='{"uid":"2808917404","matchType":"FF","packageKey":"25dms"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')
curl -X POST "$BASE/api/v1/topup/preview" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-H "X-Signature: $SIG" \
-d "$BODY"
3. Endpoints
GET /api/v1/topup/packages
HMAC Returns package keys and USD prices for a game. Query: ?matchType=FF
POST /api/v1/topup/preview
HMAC Validates the player UID, checks your balance, and returns a unipin preview — no money moves.
// body
{ "uid": "2808917404", "matchType": "FF", "packageKey": "25dms" }
POST /api/v1/topup/execute
HMAC Deducts USD from your balance and delivers the diamonds. Flow:
| 1 | Check balance is sufficient |
| 2 | Atomically debit USD balance |
| 3 | Create an api_portal_topup_records row, status processing |
| 4 | Call the delivery service with confirm: true |
| 5 | On success → completed. On failure → USD refunded, status refunded |
GET /api/v1/topup/:id
HMAC Fetch the status of a previously created topup record by its portal ID.
4. Package catalog (Free Fire)
| packageKey | Package | USD |
|---|---|---|
25dms | 25 Diamond | $0.16 |
50dms | 50 Diamond | $0.29 |
115dms | 115 Diamond | $0.63 |
240dms | 240 Diamond | $1.25 |
610dms | 610 Diamond | $3.18 |
1240dms | 1240 Diamond | $6.27 |
2530dms | 2530 Diamond | $12.75 |
weeklyPass | Weekly Membership | $1.27 |
monthlyPass | Monthly Membership | $6.36 |
Fetch this live via GET /api/v1/topup/packages?matchType=FF rather than hardcoding — prices can change.
5. File snapshots
A minimal integration typically has three files: config (secrets + catalog), a signed API client, and your usage code. Here's what each would look like.
require('dotenv').config();
module.exports = {
baseUrl: process.env.MAGENTA_BASE_URL || 'https://api-portal-svc-3wdlqy54rq-el.a.run.app',
apiKey: process.env.magenta_gaming_apis,
apiSecret: process.env.magenta_gaming_secret,
// mirrors config/partnerPricing.js — refresh from /packages periodically
catalog: {
'25dms': { label: '25 Diamond', usd: 0.16 },
'50dms': { label: '50 Diamond', usd: 0.29 },
'115dms': { label: '115 Diamond', usd: 0.63 },
'240dms': { label: '240 Diamond', usd: 1.25 },
'610dms': { label: '610 Diamond', usd: 3.18 },
'1240dms': { label: '1240 Diamond', usd: 6.27 },
'2530dms': { label: '2530 Diamond', usd: 12.75 },
'weeklyPass': { label: 'Weekly Membership', usd: 1.27 },
'monthlyPass':{ label: 'Monthly Membership', usd: 6.36 },
},
};
const crypto = require('crypto');
const { baseUrl, apiKey, apiSecret } = require('../config/magentaConfig');
function sign(body) {
const raw = JSON.stringify(body || {});
return crypto.createHmac('sha256', apiSecret).update(raw).digest('hex');
}
async function request(method, path, body) {
const payload = body || {};
const res = await fetch(`${baseUrl}${path}`, {
method,
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
'X-Signature': sign(payload),
},
body: method === 'GET' ? undefined : JSON.stringify(payload),
});
const data = await res.json();
if (!res.ok) throw new Error(data.message || `Magenta API error (${res.status})`);
return data;
}
module.exports = {
getPackages: (matchType = 'FF') => request('GET', `/api/v1/topup/packages?matchType=${matchType}`),
initiatePreview: (packageKey, uid, matchType = 'FF') =>
request('POST', '/api/v1/topup/preview', { uid, matchType, packageKey }),
executeTopup: (packageKey, uid, matchType = 'FF') =>
request('POST', '/api/v1/topup/execute', { uid, matchType, packageKey }),
getTopupStatus: (id) => request('GET', `/api/v1/topup/${id}`),
};
6. Integration helper (your usage pattern)
Wrap the client so callers just pass a user + package key, and the balance check happens up front:
const { catalog } = require('../config/magentaConfig');
const { initiatePreview, executeTopup } = require('../lib/magentaClient');
/**
* @param {{ _id: string, balance: number }} user
* @param {string} packageKey e.g. '25dms'
*/
const topup = (user, packageKey) => {
const pkg = catalog[packageKey];
if (!pkg) throw new Error(`Unknown packageKey: ${packageKey}`);
if (!user || user.balance < pkg.usd) {
return Promise.reject(new Error('Insufficient balance'));
}
const initiate = async () => {
const preview = await initiatePreview(packageKey, user._id);
if (!preview.valid) throw new Error(preview.message || 'Preview failed');
const result = await executeTopup(packageKey, user._id);
return result; // { status: 'completed' | 'refunded', recordId, ... }
};
return initiate();
};
module.exports = { topup };
// --- usage ---
// const { topup } = require('./usage/topup');
// const record = await topup(currentUser, '25dms');
7. Live test log
Verified against production (api-portal-svc-3wdlqy54rq-el.a.run.app) on 2026-07-07 using a
real API key/secret pair. Requests below are the exact curl commands sent; responses are the
exact bodies returned (formatted for readability, values unchanged).
Test 1 — List packages 200 OK
curl -X GET "https://api-portal-svc-3wdlqy54rq-el.a.run.app/api/v1/topup/packages?matchType=FF" \
-H "Content-Type: application/json" \
-H "X-API-Key: klm_568d13f791ca0dc357913aaf5317aac8" \
-H "X-Signature: <hmac_sha256(secret, "{}")>"
{
"success": true,
"matchType": "FF",
"packages": [
{ "packageKey": "25dms", "label": "25 Diamond", "priceUsd": 0.16 },
{ "packageKey": "50dms", "label": "50 Diamond", "priceUsd": 0.29 },
{ "packageKey": "115dms", "label": "115 Diamond", "priceUsd": 0.63 },
{ "packageKey": "240dms", "label": "240 Diamond", "priceUsd": 1.25 },
{ "packageKey": "610dms", "label": "610 Diamond", "priceUsd": 3.18 },
{ "packageKey": "1240dms", "label": "1240 Diamond", "priceUsd": 6.27 },
{ "packageKey": "2530dms", "label": "2530 Diamond", "priceUsd": 12.75 },
{ "packageKey": "weeklyPass", "label": "Weekly Membership", "priceUsd": 1.27 },
{ "packageKey": "monthlyPass", "label": "Monthly Membership", "priceUsd": 6.36 }
]
}
Matches the documented catalog exactly — auth + signing confirmed working.
Test 2 — Preview topup 200 OK
{"uid":"2808917404","matchType":"FF","packageKey":"25dms"}
{
"success": true,
"requiresConfirmation": true,
"canProceed": false,
"message": "Insufficient balance.",
"preview": {
"uid": "2808917404",
"zoneId": null,
"matchType": "FF",
"packageKey": "25dms",
"diamondOption": "25 Diamond",
"amountUsd": 0.16,
"availableBalanceUsd": 0,
"player": null,
"unipin": {
"success": false,
"message": "Route not found"
}
}
}
Request was authenticated correctly (HTTP 200, not 401). It resolved to
canProceed: false because the test account had $0 balance — this is the API
behaving as documented, not an error. Note unipin.message: "Route not found", which looks like
a downstream routing issue on the delivery-service side worth checking on your end.
Test 3 — Execute topup 400 Bad Request
{"uid":"2808917404","matchType":"FF","packageKey":"25dms","confirm":true,"partnerRef":"test-verify-001"}
{
"success": false,
"message": "Insufficient balance",
"requiredUsd": 0.16,
"availableUsd": 0
}
Correctly rejected before any balance debit or delivery attempt — no charge occurred.
This is the expected guard for a $0 balance account. A successful 200 execute (with
recordId / billId) can only be verified once the account is funded via
+ Add Credit in the dashboard.
8. Node.js — full call example
End-to-end script that lists packages, previews, and (if allowed) executes a topup, logging each response. Mirrors the exact requests used in the live test above.
require('dotenv').config();
const crypto = require('crypto');
const BASE_URL = process.env.MAGENTA_BASE_URL || 'https://api-portal-svc-3wdlqy54rq-el.a.run.app';
const API_KEY = process.env.magenta_gaming_apis;
const API_SECRET = process.env.magenta_gaming_secret;
function sign(bodyString) {
return crypto.createHmac('sha256', API_SECRET).update(bodyString).digest('hex');
}
async function call(method, path, bodyObj) {
const bodyString = JSON.stringify(bodyObj || {});
const res = await fetch(BASE_URL + path, {
method,
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY,
'X-Signature': sign(bodyString),
},
body: method === 'GET' ? undefined : bodyString,
});
const data = await res.json();
console.log(`${method} ${path} -> ${res.status}`);
console.log(JSON.stringify(data, null, 2));
return { status: res.status, data };
}
(async () => {
// 1. List packages
await call('GET', '/api/v1/topup/packages?matchType=FF');
// 2. Preview
const uid = '2808917404';
const packageKey = '25dms';
const { data: preview } = await call('POST', '/api/v1/topup/preview', {
uid, matchType: 'FF', packageKey,
});
// 3. Execute — only if preview says we can proceed
if (preview.canProceed) {
await call('POST', '/api/v1/topup/execute', {
uid, matchType: 'FF', packageKey, confirm: true, partnerRef: `order-${Date.now()}`,
});
} else {
console.log('Skipped execute:', preview.message);
}
})();
$ node scripts/callTopupApi.js
GET /api/v1/topup/packages?matchType=FF -> 200
{ "success": true, "matchType": "FF", "packages": [ ... 9 packages ... ] }
POST /api/v1/topup/preview -> 200
{
"success": true,
"canProceed": false,
"message": "Insufficient balance.",
"preview": { "amountUsd": 0.16, "availableBalanceUsd": 0, ... }
}
Skipped execute: Insufficient balance.
9. Errors & notes
| Case | Response |
|---|---|
| Missing auth headers | 401 |
| API key only, no signature | 401 |
| Invalid signature | 401 |
| Key created before HMAC support | 401 — rotate the key in the dashboard |
| Insufficient balance | 402/400 — top up your USD balance first |
| Delivery failure on execute | USD auto-refunded, record marked refunded |
GET /api/auth/topups?limit=200&page=1 — your own topup history for the UI, not for partner integrations.