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.

Base URL (production): https://api-portal-svc-3wdlqy54rq-el.a.run.app
Base 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.

StepAction
1Go to the portal and create an account.
2Verify your email (OTP), then log in to the dashboard.
3Open API Keys in the dashboard → generate a key. Copy the API Key and API Secret — the secret is shown once.
4Set your Partner API name in the dashboard (used to tag your topup records, e.g. tournaop-ff).
5Store the credentials in your .env.
.env
magenta_gaming_apis="klm_9929da3fbe6dcf31ace91de79d3e3c7a"
magenta_gaming_secret="c8cab24d4f6d33394d16c56bbaa17c3e4f40c711be78f5058f488e2bc96bc32f"

# optional overrides
MAGENTA_BASE_URL="https://api-portal-svc-3wdlqy54rq-el.a.run.app"
Keep the secret server-side only. It's used to sign every request body — never ship it to a client/browser.

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:

HeaderValue
X-API-KeyYour API key, e.g. klm_9929da3f...
X-SignatureHMAC_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:

1Check balance is sufficient
2Atomically debit USD balance
3Create an api_portal_topup_records row, status processing
4Call the delivery service with confirm: true
5On 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.

Executes can take 30–120 seconds for Free Fire deliveries — set your HTTP client timeout to 3+ minutes.

4. Package catalog (Free Fire)

packageKeyPackageUSD
25dms25 Diamond$0.16
50dms50 Diamond$0.29
115dms115 Diamond$0.63
240dms240 Diamond$1.25
610dms610 Diamond$3.18
1240dms1240 Diamond$6.27
2530dms2530 Diamond$12.75
weeklyPassWeekly Membership$1.27
monthlyPassMonthly 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.

config/magentaConfig.js
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 },
  },
};
lib/magentaClient.js
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:

usage/topup.js
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).

The key used for this test run has since been rotated by the partner — it's shown here only as evidence of the request shape, not as a usable credential.

Test 1 — List packages 200 OK

GET /api/v1/topup/packages?matchType=FF
Request
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, "{}")>"
Response
{
  "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

POST /api/v1/topup/preview
Request body
{"uid":"2808917404","matchType":"FF","packageKey":"25dms"}
Response
{
  "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

POST /api/v1/topup/execute
Request body
{"uid":"2808917404","matchType":"FF","packageKey":"25dms","confirm":true,"partnerRef":"test-verify-001"}
Response
{
  "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.

scripts/callTopupApi.js
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

CaseResponse
Missing auth headers401
API key only, no signature401
Invalid signature401
Key created before HMAC support401 — rotate the key in the dashboard
Insufficient balance402/400 — top up your USD balance first
Delivery failure on executeUSD auto-refunded, record marked refunded
Dashboard-only (JWT, not HMAC): GET /api/auth/topups?limit=200&page=1 — your own topup history for the UI, not for partner integrations.