Currency Purchase Integration
Real-money top-ups for your branded in-game currency. Players check out on an INVO-hosted page, payments process server-side, and the player's balance is credited automatically. No payment UI to build, no card data on your servers.
Note on minor accounts: currency top-ups are not gated by Invo's guardian-approval flow. The guardian's approval lives at the parent's payment-method level (their card / bank), not at the Invo API level — Invo trusts that whatever card was used to fund the purchase is authorized by the account holder. See Guardian Approval for the full scope.
Hosted Checkout
Your backend mints a short-lived signed session URL. You open that URL in an iframe or WebView. Player completes the purchase. INVO credits the player's balance and (optionally) notifies your backend via webhook.
What you get
- • Card data never crosses your servers (SAQ-A scope)
- • Strong Customer Authentication (3DS) where required
- • Saved card support for returning players
- • Fraud detection + velocity limits
- • Mobile-optimised checkout UI
- • Server-side credit applied on payment success
How it works
- 1. Server:
POST /api/checkout/sessionswith player email + USD amount - 2. Get back a signed
checkout_url(JWT, 15 min TTL, single-use) - 3. Open that URL in iframe/WebView
- 4. Player pays
- 5. Listen for the
INVO_CHECKOUT_COMPLETEpostMessage and/or the server-to-server webhook
Step-by-step
1. Create a checkout session (server-side)
Call this from your game backend. The SDK key never leaves your server.
POST https://invo.network/api/checkout/sessions
Headers:
X-Game-Secret-Key: ivsdk_<your_sdk_key>
Content-Type: application/json
Body:
{
"player_email": "player@example.com",
"usd_amount": "10.00",
"success_url": "https://yourgame.com/store/success",
"cancel_url": "https://yourgame.com/store/cancel",
"metadata": { "your_user_id": "u_42", "coin_pack": "starter" }
}
Response 201:
{
"session_id": "<jti>",
"checkout_url": "https://console.invo.network/checkout/?session=<jwt>",
"expires_at": "2024-12-09T12:49:56Z",
"expires_in_seconds": 900
}checkout_url contains a 15-minute signed token. Single-use — reusing the same URL after a successful payment returns 409 SESSION_ALREADY_CONSUMED.
2. Open the URL in an iframe or WebView
The hosted page handles card collection, 3DS challenges, saved-card selection, and the actual charge.
// Web (WebGL build / browser)
const iframe = document.createElement('iframe');
iframe.src = checkoutUrl;
iframe.style.cssText = 'width:480px; height:720px; border:0;';
document.body.appendChild(iframe);
// Mobile (Unity, Unreal, native)
// Open checkout_url in your platform's WebView component.3. Listen for completion
Two signals — use whichever fits your stack. The server-to-server webhook is the authoritative one.
Browser postMessage (UX hint)
window.addEventListener('message', (event) => {
if (event.data?.type === 'INVO_CHECKOUT_COMPLETE') {
const { status, currency_received, new_balance } = event.data.data;
if (status === 'success') {
updatePlayerBalance(new_balance);
}
}
});postMessage is not signed. Treat it as a UX hint to refresh the player's balance optimistically. For ledger-of-record accuracy, rely on the webhook below or re-read the player's balance from the API.
Server-to-server webhook (authoritative)
POST <your_webhook_url>
Headers:
X-Invo-Signature: t=<unix_timestamp>,v1=<hex_hmac_sha256>
X-Invo-Event-Id: <uuid>
Body:
{
"event_id": "evt_01HX...",
"event_type": "purchase.completed",
"created_at": "2024-12-09T12:34:56Z",
"tenant_id": "<your game_id>",
"data": {
"transaction_id": "txn_...",
"order_id": "ord_...",
"player_email": "player@example.com",
"identity_id": "f3a1b8c0d4e5...",
"usd_amount": "10.00",
"currency_amount": "100",
"currency_name": "Gold Coins",
"metadata": { "your_user_id": "u_42", "coin_pack": "starter" }
}
}Verify the HMAC-SHA256 signature using your tenant's webhook signing secret. Dedupe on X-Invo-Event-Id. Reject timestamps older than 5 minutes.
Direct API (advanced)
If you operate your own PCI-compliant card collection (rare — most integrations should use hosted checkout), you can post a tokenised payment method directly:
POST https://invo.network/api/currency-purchases/purchase-currency
Headers:
X-Game-Secret-Key: ivsdk_<your_sdk_key>
Content-Type: application/json
Body:
{
"player_email": "player@example.com",
"usd_amount": "10.00",
"payment_method_id": "<tokenised_card_id_from_your_card_collector>",
"purchase_reference": "<uuid_v4>", // REQUIRED — see "Idempotency" below
"metadata": { "your_user_id": "u_42" }
}
Possible responses:
200, status:"success" — purchase complete; body has transaction_id + new_balance
200, status:"requires_action" — 3-D Secure needed; see "Handling 3-D Secure" below
200, status:"success", duplicate:true — replay of a prior completed purchase_reference
409, status:"pending", duplicate:true — purchase already in flight for this purchase_reference
400, error_code:"MISSING_PURCHASE_REFERENCE" — field omitted
400, error_code:"INVALID_PURCHASE_REFERENCE" — field >255 chars
4xx — validation, rate limit, or card-issuer decline (see error envelope)Branch on the status field — never treat a non-success response as a decline. requires_action is a normal step for many cards (mandatory for European cards under SCA), not a failure; prompting the player for a different card here is the wrong move.
Idempotency — purchase_reference is REQUIRED
Every call to /purchase-currency must include a stable, unique purchase_reference (a UUID v4 is fine). This is the single most important field for avoiding duplicate charges on a flaky network.
- • Generate it once per logical purchase attempt — at the moment the player taps "Buy", not per HTTP call.
- • Resend the same value on every retry. If the network dropped mid-response, INVO returns the prior result (HTTP 200,
duplicate: true) instead of charging the card a second time. - • Generate a new one for a genuinely new purchase (e.g. the player cancels, then taps Buy again).
- • Max length 255 chars; uniqueness is per game.
client_request_id. The API still accepts that name as an alias. New integrations should send purchase_reference; existing integrations sending client_request_id continue to work without change.Handling 3-D Secure (status: requires_action)
When a card needs Strong Customer Authentication, /purchase-currency returns HTTP 200 with status: "requires_action". No money has been charged and nothing has failed — the cardholder just has to complete a bank challenge.
Step 1. The response carries what you need to run the challenge:
{
"status": "requires_action",
"client_secret": "pi_xxx_secret_xxx",
"payment_intent_id": "pi_xxx",
"order_id": "ord_xxx",
"new_balance": null
}Step 2. Run the 3-D Secure challenge on the client using the JavaScript SDK your card collector provided, passing the client_secret — this presents the bank's authentication step to the player:
// Web — pseudocode. INVO's auth challenge uses the automatic-confirmation
// flow, so call your card-collector SDK's "confirm" variant (the one that
// confirms AND runs the challenge in a single call), NOT a manual-server-
// confirm-only "handle action" variant. If your SDK exposes both, the
// confirm variant is the correct call here.
const { error, paymentIntent } = await processor.confirmCardPayment(client_secret);
if (error) {
// Player abandoned 3DS, bank declined, etc. No money moved.
// Show the user a retry / different-card prompt.
} else if (paymentIntent && paymentIntent.status === 'succeeded') {
// 3DS passed and the auth challenge completed. Now proceed to Step 3
// so INVO credits the player's branded currency.
}
// Mobile: use the iOS / Android SDK from your card collector. Same rule —
// pick the confirm-payment variant, not a manual-confirm-only handler.
// This step talks directly to your card processor and does not use your INVO key.Step 3. Once the SDK reports success, finalise the purchase server-side — this is where the player's branded currency is credited:
POST https://invo.network/api/currency-purchases/confirm-payment
Headers:
X-Game-Secret-Key: ivsdk_<your_sdk_key>
Content-Type: application/json
Body:
{ "payment_intent_id": "pi_xxx", "order_id": "ord_xxx" }
Response 200:
{
"status": "success",
"transaction_id": "txn_...",
"order_id": "ord_xxx",
"new_balance": "100",
"already_processed": false
}confirm-payment is idempotent — a retry, or a late call after INVO has already credited the purchase, returns already_processed: true instead of crediting twice.
Spending limits
INVO enforces hourly, daily, and monthly USD caps per (player_email, game). A purchase that would exceed any cap returns HTTP 429 with error: "spending_limit_exceeded" and a human-readable message naming the cap that was hit.
Only successful purchases consume the cap. Declined cards, abandoned 3-D Secure challenges, and circuit-breaker rejections leave the cap untouched, so a player whose card just failed can immediately try again with a different card.
Defaults are configurable per environment; ask your INVO contact if you need higher limits for a specific tenant.
What hosted checkout handles for you
Fast Checkout
Typical checkout completes in under 30 seconds. Players get their currency the moment payment confirms.
Compliance Out of the Box
Your scope is SAQ-A — no card data crosses your servers. We handle PCI, 3DS, and dispute messaging end-to-end.
Global Reach
Major credit and debit cards plus regional payment methods supported worldwide.
Player Experience
Saved Cards for Returning Players
Players can save payment methods for one-tap re-purchase. The selector appears automatically when a returning player checks out.
Saved-card flow
- 1Player clicks "Buy Currency" → card selector modal appears
- 2Saved cards listed with brand, last 4 digits, and expiration
- 3Player picks a saved card or selects "Use New Card"
- 4Review-purchase modal confirms the amount and card
- 5Player confirms → instant purchase, no card re-entry
Zero extra integration: saved cards work automatically with hosted checkout.
Review & Confirmation
Before charging a saved card we show a friendly review dialog. This satisfies FTC guidance on surprise charges and reduces accidental purchases.
Review dialog
- Friendly UI — checkmark icon, clear "Review Your Purchase" header.
- Purchase summary — amount, currency credited, card last 4.
- Explicit "Complete Purchase" button — no implicit consent.
- Easy cancel — clear secondary action.
First-Time Purchase
When a player has no saved cards, they go straight to the secure card-entry page. From there they can:
- Enter card details on the INVO-hosted page (your servers never see them)
- Opt to save the card for next time (default-on, easy to opt out)
- Complete 3DS challenges if their bank requires SCA
- See instant confirmation and have currency credited automatically
Outbound webhook events
INVO emits server-to-server webhooks for every meaningful event on a purchase. All deliveries carry an X-Invo-Signature HMAC header and an X-Invo-Event-Id for dedupe. Retries follow a back-off schedule (30s, 2m, 10m, 1h, 6h, 24h).
| Event type | When it fires | Balance effect |
|---|---|---|
purchase.completed | Card charged + branded currency credited. | +credit |
purchase.failed | Card declined, 3DS abandoned, PaymentIntent canceled, or async payment failed. | none |
purchase.refunded | Full or partial refund issued through INVO's admin tooling. Partial refunds proportionally debit the player's branded currency; the order stays in completed status until the charge is fully refunded. | −debit (full only) |
purchase.disputed | Chargeback opened by the issuer. Fires again on close with dispute_status = won / lost. | −debit on lost |
purchase.fraud_warning | Early-fraud-warning signal from the card network — the issuing bank flagged the charge as likely fraudulent. Chargeback probable. | none (advisory) |
purchase.refunded — full refund
{
"event_id": "evt_01HX...",
"event_type": "purchase.refunded",
"tenant_id": "<your game_id>",
"data": {
"order_id": "ORD_...",
"transaction_id": "txn_...",
"payment_intent_id": "pi_...",
"charge_id": "ch_...",
"usd_refunded": "10.00",
"usd_original": "10.00",
"fully_refunded": true,
"currency_debited": "100",
"new_balance": "0",
"currency_name": "Gold Coins",
"order_status": "refunded"
}
}purchase.disputed — opened
{
"event_id": "evt_01HX...",
"event_type": "purchase.disputed",
"tenant_id": "<your game_id>",
"data": {
"order_id": "ORD_...",
"transaction_id": "txn_...",
"payment_intent_id": "pi_...",
"charge_id": "ch_...",
"dispute_id": "dp_...",
"reason": "fraudulent",
"usd_amount": "10.00"
}
}purchase.disputed — closed (lost — clawback applied)
{
"event_type": "purchase.disputed",
"data": {
"order_id": "ORD_...",
"dispute_id": "dp_...",
"dispute_status": "lost",
"currency_debited": "100",
"new_balance": "0"
}
}purchase.fraud_warning — Early Fraud Warning
{
"event_id": "evt_01HX...",
"event_type": "purchase.fraud_warning",
"tenant_id": "<your game_id>",
"data": {
"order_id": "ORD_...",
"transaction_id": "txn_...",
"payment_intent_id": "pi_...",
"charge_id": "ch_...",
"fraud_type": "made_with_stolen_card",
"actionable": true,
"usd_amount": "10.00",
"recommendation": "consider_refund" // or "monitor"
}
}purchase.failed — payment canceled / declined
{
"event_type": "purchase.failed",
"data": {
"payment_intent_id": "pi_...",
"order_id": "ORD_...",
"player_email": "player@example.com",
"usd_amount": "10.00",
"failure_code": "card_declined", // or "payment_canceled"
"failure_message": "Your card was declined."
}
}Order status lifecycle
Every purchase passes through a deterministic set of states. The /order-details endpoint returns the current state and timestamps; INVO retains a full append-only audit trail of every transition (server-side, available to support and finance on request).
pending_payment— order created, waiting on the payment processor.requires_action— 3-D Secure challenge pending on the cardholder.completed— charge confirmed, branded currency credited.failed— card declined or async payment failed.cancelled— PaymentIntent was canceled (player abandoned 3DS, PI timed out, or admin canceled).refunded— full refund processed; currency debited.disputed_lost— chargeback resolved against the merchant; currency clawed back.
Webhook delivery + the order-details endpoint are the two authoritative read paths. Do not infer state from response status codes alone.
Platform Support
Hosted checkout works in iframe/WebView on every major game platform:
Unity
WebGL & Mobile
Unreal Engine
All Platforms
iOS
Native & Hybrid
Android
Native & Hybrid
Why hosted checkout
Less work, faster ship
Skip the months it takes to build a payment UI, integrate a card processor, pass a PCI audit, and run dispute operations.
- • No payment UI to build
- • No PCI audit (SAQ-A scope)
- • No fraud-management infrastructure
- • No SCA / 3DS implementation
Enterprise features built in
The features production payment systems need ship by default — not as paid add-ons.
- • Saved cards / one-tap re-buy
- • 3D Secure / Strong Customer Authentication
- • Fraud detection + velocity limits
- • Dispute lifecycle + automatic balance reconciliation
Ready to integrate?
The full integration guide has platform-specific code for Unity, Unreal, iOS, and Android.