API v1.0
Updated 2026-07-10

FunTokenz Merchant API

A prepaid voucher system for merchants who want to accept deposits and issue withdrawals in USD or PGK without card infrastructure. Authenticated with HMAC-SHA256.

Deposit flow

Redeem a customer-supplied voucher code and credit their wallet.

Withdrawal flow

Debit a customer balance and issue a fresh FunTokenz voucher.

Webhooks

Receive signed events for redemptions, voids and reversals.

Base URLs

Production
https://funtokenz.com/api/public/merchant/v1
Sandbox
https://project--a8d167f0-2759-414d-9002-033c558df52b-dev.lovable.app/api/public/merchant/v1

All amounts are integer minor units (cents for USD, toea for PGK). Supported currencies: USD, PGK.

0. Provisioning

Credentials are minted in the FunTokenz admin console. Each merchant receives:

  • merchant_id (slug, e.g. puntpal)
  • api_key (public identifier)
  • api_secret — HMAC signing key, shown once
  • webhook_secret — shown once

Both secrets can be rotated from the admin console; rotation invalidates the previous secret immediately. Use separate merchant records for sandbox and production.

1. Authentication

Every request is authenticated with the merchant API key plus an HMAC-SHA256 signature over the raw request body.

Required headers

code
Content-Type:       application/json
X-FTZ-Merchant-Id:  puntpal
X-FTZ-Api-Key:      pk_live_xxxxxxxxxxxx
X-FTZ-Timestamp:    1751500000            # Unix seconds, within ±300s of server time
X-FTZ-Nonce:        550e8400-e29b-41d4-a716-446655440000
X-FTZ-Signature:    sha256=<hex-hmac>

Signature construction

code
string_to_sign = X-FTZ-Timestamp + "." + X-FTZ-Nonce + "." + HTTP_METHOD + "." + REQUEST_PATH + "." + RAW_BODY
signature      = hex( HMAC_SHA256( api_secret, string_to_sign ) )
  • HTTP_METHOD is uppercase (POST).
  • REQUEST_PATH starts with /api/public/merchant/v1/... and includes any query string.
  • RAW_BODY is the exact request body bytes (empty string for GET).
  • The server compares signatures with a constant-time equality check.

Node.js example

typescript
import { createHmac, randomUUID } from "node:crypto";

const body  = JSON.stringify(payload);
const ts    = Math.floor(Date.now() / 1000).toString();
const nonce = randomUUID();
const stringToSign = `${ts}.${nonce}.POST./api/public/merchant/v1/deposit/redeem.${body}`;
const signature = createHmac("sha256", process.env.FTZ_API_SECRET!)
  .update(stringToSign).digest("hex");

await fetch("https://funtokenz.com/api/public/merchant/v1/deposit/redeem", {
  method: "POST",
  headers: {
    "Content-Type":      "application/json",
    "X-FTZ-Merchant-Id": "puntpal",
    "X-FTZ-Api-Key":     process.env.FTZ_API_KEY!,
    "X-FTZ-Timestamp":   ts,
    "X-FTZ-Nonce":       nonce,
    "X-FTZ-Signature":   `sha256=${signature}`,
  },
  body,
});

Every response includes an X-FTZ-Request-Id header — log it against your transaction row for support.

2. Idempotency

State-changing endpoints require an idempotency_key UUID in the body.

  • Retrying the same key returns the original response with "idempotent_replay": true.
  • Reusing a key with a different payload returns 409 idempotency_conflict.
  • merchant_txn_id is unique per merchant per endpoint — reusing it also returns 409.

3.1 POST /deposit/verify (optional)

Look up a voucher without redeeming it — useful for previewing the amount before the customer confirms.

Request

json
{ "code": "FTZ-ABCD-EFGH-JKLM" }

Response 200

json
{
  "code":      "FTZ-ABCD-EFGH-JKLM",
  "status":    "active",
  "amount":    5000,
  "currency":  "USD",
  "issued_at": "2026-07-10T09:12:33.000Z"
}

Status values

StatusMeaning
activeRedeemable
voidedAlready spent or manually voided
expiredPast expires_at
unknownCode was never issued

3.2 POST /deposit/redeem

Atomically voids the voucher and records a redemption against the merchant. Call from the merchant backend only, never the customer's browser.

Request

json
{
  "idempotency_key":    "b6f3b0b2-8f5e-4a2d-9a0e-1c1c9a5b3e11",
  "code":               "FTZ-ABCD-EFGH-JKLM",
  "customer_reference": "puntpal_user_9812",
  "merchant_txn_id":    "deposit_2026071011223301"
}

Fields

FieldTypeRequiredNotes
idempotency_keyuuidyesSee §2
codestringyesFull voucher code, case-insensitive
customer_referencestring ≤ 128yesMerchant-side user id
merchant_txn_idstring ≤ 64yesUnique per merchant deposit id

Response 200

json
{
  "redemption_id":     "6f0c8b3e-2ac1-4b7d-a5c8-2f1e3d4b5c67",
  "code":              "FTZ-ABCD-EFGH-JKLM",
  "amount":            5000,
  "currency":          "USD",
  "redeemed_at":       "2026-07-10T11:22:33.412Z",
  "idempotent_replay": false
}

Common errors

HTTPerror.codeMeaning
404voucher_not_foundCode does not exist
409voucher_already_redeemedCode was already spent
409voucher_voidedManually voided by FunTokenz staff
410voucher_expiredPast its expiry date
409idempotency_conflictKey or merchant_txn_id reused

4.1 POST /withdrawal/issue

Debit the customer's balance and mint a fresh voucher code returned to the customer. If customer_email matches a FunTokenz account, the voucher is bound to that account; otherwise it is bearer.

Request

json
{
  "idempotency_key":    "d1a2c9f4-1e23-4a10-9b7f-2c0f4e6a01f2",
  "amount":             5000,
  "currency":           "USD",
  "customer_reference": "puntpal_user_9812",
  "customer_email":     "player@example.com",
  "merchant_txn_id":    "withdraw_2026071011400712"
}

Fields

FieldTypeRequiredNotes
idempotency_keyuuidyesSee §2
amountintegeryesMinor units. USD: 100–500 000. PGK: 500–1 500 000
currencyUSD | PGKyesMust be enabled for the merchant
customer_emailstringnoBound to matching FunTokenz account if found
customer_referencestring ≤ 128yesMerchant user id
merchant_txn_idstring ≤ 64yesUnique per merchant withdrawal id

Response 200

json
{
  "issuance_id":       "2c1d4f60-b7a8-4e11-9d3a-4a1b6e2f8a09",
  "code":              "FTZ-QWER-TYUI-OPAS",
  "amount":            5000,
  "currency":          "USD",
  "expires_at":        "2027-07-10T11:40:07.000Z",
  "issued_at":         "2026-07-10T11:40:07.412Z",
  "idempotent_replay": false
}

Merchant must

  1. Confirm the customer's balance covers amount + fees before calling.
  2. Debit the customer in the same DB transaction that stores issuance_id / code.
  3. Present code to the customer once. Never log it in plaintext outside the customer's account view.

Common errors

HTTPerror.codeMeaning
400invalid_amountBelow/above allowed range
400currency_not_enabledMerchant not enabled for that currency
402merchant_credit_exhaustedDaily issuance limit reached
409idempotency_conflictKey or merchant_txn_id reused

5. Webhooks (optional)

Configure a webhook URL when provisioning the merchant. FunTokenz posts JSON events signed with the merchant's webhook_secret.

  • POST with JSON body.
  • Headers include X-FTZ-Webhook-Signature: sha256=<hex-hmac> and X-FTZ-Event-Id.
  • Endpoint must return 2xx within 5 s. Non-2xx responses are retried with exponential backoff up to 24 h.
  • Handle events idempotently on id.

Event types (planned)

EventWhen
voucher.redeemedA voucher issued by this merchant was redeemed anywhere
voucher.voidedFunTokenz staff manually voided a voucher affecting this merchant
redemption.reversedA previously accepted deposit was reversed — debit the customer

Example payload

json
{
  "id":         "evt_01J8ZA1MK0X6C4C7Q4Q0F1RTPB",
  "type":       "voucher.redeemed",
  "created_at": "2026-07-10T12:00:00.000Z",
  "data": {
    "issuance_id": "2c1d4f60-b7a8-4e11-9d3a-4a1b6e2f8a09",
    "code":        "FTZ-QWER-TYUI-OPAS",
    "amount":      5000,
    "currency":    "USD",
    "redeemed_at": "2026-07-10T11:59:58.000Z"
  }
}

Signature verification

typescript
import { createHmac, timingSafeEqual } from "node:crypto";

const raw = await request.text();
const sig = request.headers.get("x-ftz-webhook-signature")!.slice(7);
const expected = createHmac("sha256", process.env.FTZ_WEBHOOK_SECRET!)
  .update(raw).digest("hex");
const ok = sig.length === expected.length &&
  timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));

6. Merchant credit / float

/withdrawal/issue is capped by a daily issuance limit per currency (defaults: 50 000 USD, 150 000 PGK). Contact FunTokenz admin to raise. Deposit endpoints have no cap beyond global rate limits.

7. Standard error shape

All 4xx / 5xx responses use this shape:

json
{
  "error": {
    "code":       "voucher_already_redeemed",
    "message":    "This voucher has already been redeemed.",
    "request_id": "req_5a3f0c8b3e2ac14b7da5c82f1e3d4b5c"
  }
}

HTTP status reference

HTTPMeaning
400Malformed request / validation failure
401Missing or invalid API key or signature
403Merchant disabled
404Resource not found
409State conflict (already spent, idempotency mismatch)
410Resource expired
429Rate limited
5xxFunTokenz-side failure — retry with the same idempotency_key

8. Currencies & amounts

CurrencyMin per voucherMax per voucherMinor unit
USD100 ($1.00)500 000 ($5 000.00)cents
PGK500 (K5.00)1 500 000 (K15 000.00)toea

No FX conversion is performed. A PGK voucher must be issued and redeemed as PGK.

9. Security requirements

  • Store api_secret and webhook_secret in a secrets manager. Never commit them.
  • Enforce HTTPS on the webhook endpoint; reject any request whose HMAC does not verify.
  • Reject webhook requests whose created_at is older than 5 minutes.
  • Do not expose FunTokenz API endpoints or credentials to browser code — all calls originate from the merchant backend.
  • Log X-FTZ-Request-Id and X-FTZ-Event-Id alongside every transaction for reconciliation.

10. Testing

Use a separate sandbox merchant record against the sandbox base URL. Issue a small voucher via /withdrawal/issue, then verify + redeem it via /deposit/verify and /deposit/redeem using its own code.

Curl smoke test for /deposit/verify

bash
BODY='{"code":"FTZ-XXXX-XXXX-XXXX"}'
TS=$(date +%s)
NONCE=$(uuidgen)
PATH_="/api/public/merchant/v1/deposit/verify"
SIG=$(printf "%s.%s.POST.%s.%s" "$TS" "$NONCE" "$PATH_" "$BODY" \
  | openssl dgst -sha256 -hmac "$FTZ_API_SECRET" | awk '{print $2}')

curl -s https://funtokenz.com$PATH_ \
  -H "Content-Type: application/json" \
  -H "X-FTZ-Merchant-Id: puntpal" \
  -H "X-FTZ-Api-Key: $FTZ_API_KEY" \
  -H "X-FTZ-Timestamp: $TS" \
  -H "X-FTZ-Nonce: $NONCE" \
  -H "X-FTZ-Signature: sha256=$SIG" \
  -d "$BODY"

11. Changelog

  • v1.0 (2026-07-10) — Initial release: HMAC auth, deposit verify + redeem, withdrawal issue, webhook delivery.

Appendix A — Onboarding checklist

  1. FunTokenz provisions sandbox merchant → shares credentials via a secure channel.
  2. Merchant integrates deposit (redeem) flow; verifies expected error codes.
  3. Merchant integrates withdrawal (issue) flow; verifies balance debit + code display.
  4. Merchant registers webhook URL; FunTokenz confirms signature verification.
  5. Reconciliation smoke test: N deposits + N withdrawals; both sides match on merchant_txn_id.
  6. Production merchant provisioned; go-live scheduled.

Questions? Contact the FunTokenz integrations team with your merchant_id and a recent X-FTZ-Request-Id.