INVO Network API Overview

The INVO Network API gives game and platform developers a unified ledger and payments infrastructure for virtual currency, real-money top-ups, item sales, cross-game transfers, and player-to-player sends. Server-to-server, REST, JSON, single-header auth.

Two Environments

INVO runs Production and Sandbox as fully isolated databases with independent SDK keys. Develop against Sandbox, cut over to Production when ready.

🚀 Production

• Base URL: https://invo.network
• Routes: /api/...
• Real-money transactions
• Console: console.invo.network

🧪 Sandbox

• Base URL: https://sandbox.invo.network
• Routes: /sandbox/api/...
• Test cards only, no real money
• Console: console.invo.network/test

What differs between Sandbox and Production

  • • The same code and the same fees. Sandbox runs the release Production runs, and the fees in its responses and webhooks are the Production fees.
  • • Test approval shortcuts. Sandbox has test-only tools to settle approvals without a real device or passkey (see Sandbox Testing). They do not exist in Production.
  • • Platform status without company verification. In Sandbox you can claim platform status without completing company verification; in Production it requires it.
  • • Smaller capacity. Sandbox is sized for integration testing, not load tests. Production limits are on Rate Limits.

Real-Money Top-ups

  • • Players buy your branded currency with credit/debit cards
  • • Hosted checkout: partners are out of scope for full PCI
  • • Strong customer authentication (3DS) where required
  • • Players pay the listed price; you pay the card fee (3.5% + $0.15 on Open, 5.0% + $0.15 on Merchant of Record), with fraud screening included on every card payment and a fraud chargeback guarantee on Merchant of Record and on Open card payments made before October 21, 2026 (Tiers)
  • • Refunds of unspent coins; chargebacks with automatic balance debits

Self-Transfers

  • • A player moves their own currency between titles they use
  • • The sender approves with a passkey or QR device approval
  • • Claim flow on the receiving title
  • • Network fee split applied automatically

Player-to-Player Sends

  • • Send currency to another player by phone number
  • • Same game or cross-game: an in-game recipient collects with their player token; INVO emails the claim link when the receiver's email is already on file for that phone; otherwise the receiver collects in the game
  • • The sender approves with a passkey or QR device approval
  • • Velocity limits + fraud detection on every step

Player Reads

  • • Single-player balance lookup by email
  • • Batch balance lookup (up to 100K emails per call)
  • • Streaming reconciliation endpoint for nightly diffs
  • • Cross-game join key (identity_id, from player registration; changes with the email or phone)

Security & Reliability

  • • Single-header auth (X-Game-Secret-Key)
  • • Per-IP and per-key rate limiting with progressive penalties
  • • Idempotency-keyed write endpoints (replay-safe)
  • • Circuit breakers on every external dependency

Core API Endpoints

Player Balance

Read player balances. Single, batch, or streaming.

GET /api/player-balances/player/by-email/{email}
POST /api/player-balances/batch
GET /api/player-balances/stream-all

Currency Purchase

Real-money top-ups, hosted checkout, payment confirmation.

POST /api/checkout/sessions
POST /api/currency-purchases/quote
POST /api/currency-purchases/purchase-currency
POST /api/currency-purchases/confirm-payment
GET /api/currency-purchases/order-details
GET /api/currency-purchases/player-purchase-history

Item Purchase

Spend a player's branded currency on items inside your title.

POST /api/item-purchases/purchase-item
GET /api/item-purchases/player-purchase-history
GET /api/item-purchases/order-details

Self-Transfer (cross-game)

A player moves their own balance from one title to another.

POST /api/transfers/available-destinations
POST /api/transfers/initiate-transfer
POST /api/sdk/transfers/{id}/approve
POST /api/transfers/claim-transfer
GET /api/transfers/{id}/status
GET /api/transfers/transfer-history

Player-to-Player Send

Phone-based send to another player, same or different title.

POST /api/currency-sends/initiate-send
POST /api/sdk/send/{id}/approve
POST /api/currency-sends/claim-currency
GET /api/currency-sends/{id}/status
GET /api/currency-sends/send-history

Withdrawals (developer revenue)

Move developer revenue out of Invo to your bank. Each payout carries the payout fee (free on the payout schedule; 1%, capped at $500, on demand).

GET /api/withdrawals/available-balance
POST /api/withdrawals/request
GET /api/withdrawals/history

Partner and Creator Payouts

Platform partners: pay out your owed balance, or have INVO verify and pay the creators you register. See creator payouts.

POST /api/partner-payouts/request
GET /api/partner-payouts
POST /api/partner-payouts/{payout_id}/cancel
POST /api/partner-payees
GET /api/partner-payees
POST /api/partner-payees/{payee_id}/resend-invite

Player History

One player's money activity in your title: combined, or one kind at a time. See below.

GET /api/sdk/players/activity
GET /api/platform-commerce/purchase-history
GET /api/currency-purchases/player-purchase-history
GET /api/item-purchases/player-purchase-history
GET /api/currency-sends/send-history
GET /api/transfers/transfer-history

Player history reads

Server-to-server, authenticated with X-Game-Secret-Key, read-only. Each returns only what your title is a party to. In the SDKs: getPlayerActivity / iteratePlayerActivity and platformCommerce.getPurchaseHistory / iteratePurchaseHistory in the web SDK from 3.22.0; get_player_activity / iterate_player_activity and platform_commerce.get_purchase_history / iterate_purchase_history in the Python SDK from 3.20.0.

EndpointWhat it lists
GET /api/sdk/players/activityEverything below in one newest-first list: currency purchases, item purchases, Platform Commerce purchases, sends and transfers, both directions.
GET /api/platform-commerce/purchase-historyOne player's Platform Commerce purchases (platform titles).
GET /api/currency-purchases/player-purchase-historyCurrency purchases (Currency Purchase).
GET /api/item-purchases/player-purchase-historyItem purchases (Item Purchase).
GET /api/currency-sends/send-historyPlayer-to-player sends, with direction incoming / outgoing.
GET /api/transfers/transfer-historyTransfers, with direction incoming / outgoing.

GET /api/sdk/players/activity

Query parameters:

  • • player_email (required): the player in your title.
  • • types (optional): a comma list of currency_purchase, item_purchase, platform_commerce, send, transfer. Default: all five. An unknown type is 400 INVALID_INPUT.
  • • limit (optional): 1 to 100, default 50. Values outside the range are clamped.
  • • cursor (optional): the previous page's next_cursor.
Request and response
GET /api/sdk/players/activity?player_email=bo@example.com&types=send,transfer&limit=2
X-Game-Secret-Key: <your game secret>

200 OK   (Cache-Control: no-store)
{
  "player_email": "bo@example.com",
  "game_id": "123456789012",
  "types": ["send", "transfer"],
  "activity": [
    {
      "id": "TXN_1760000000_AB12CD",
      "type": "transfer",
      "direction": "in",
      "amount": "90.00",
      "amount_unit": "currency",
      "currency": { "currency_id": 7, "currency_name": "Gems" },
      "status": "verified",
      "status_detail": "pending_claim",
      "expiry_reason": null,
      "counterparty_game": { "game_id": "123456789012", "game_name": "Example Game" },
      "created_at": "2026-10-09T14:02:11+00:00",
      "completed_at": null,
      "order_id": "ORD_...",
      "gross_amount": "100.00",
      "fee_amount": "10.00"
    }
  ],
  "limit": 2,
  "has_more": true,
  "next_cursor": "<opaque>"
}
Entry fieldMeaning
idThe transaction id.
typeOne of the five types above.
directionin (credits this player), out (debits this player) or self (a send or transfer where this player is both sender and recipient in your title). A currency purchase is always in; item and Platform Commerce purchases are out. These are not the values the send and transfer history reads use (incoming / outgoing); map them if you merge the two.
amount, amount_unitA decimal string. amount_unit is currency (your game currency), or usd for a Platform Commerce card sale. On an inbound send or transfer, amount is what the player receives.
currencycurrency_id and currency_name.
status, status_detailThe transaction's status and detail, the same values the per-type reads return.
expiry_reasonapproval_expired, pin_expired or claim_expired when the send or transfer expired; otherwise null. See Webhooks.
counterparty_gameFor a send or transfer with another title: that title's game_id and game_name. Otherwise null. The other side is named by title only, never by another player's email, phone or name.
created_at, completed_atISO 8601; completed_at is null until it completes.
order_idThe order id, or null.
fee_amount, net_amount, gross_amountSends and transfers only. The sender pays the whole fee out of the amount sent; the recipient pays nothing. On the sender's entry (out or self), amount is the gross sent, with fee_amount and net_amount (what the recipient gets). On the recipient's entry (in), amount is what arrived, with gross_amount (what the sender sent) and fee_amount. The fee is taken when the recipient collects, so while a send is waiting these show the fee that will apply. They are null when the send ended without being collected (nothing was taken). On the recipient's entry for a send or transfer from another game, gross_amount and fee_amount are in the recipient's currency, so gross_amount minus fee_amount equals amount exactly. Purchases do not carry these fields.

An inbound row is not a credit until its status says so. An inbound send or transfer appears as soon as it names this player as the recipient, in any status: awaiting collection, completed, expired or refunded. Read status and status_detail before showing an in row as received.

Pagination. Newest first. While has_more is true, pass next_cursor as cursor. A cursor is opaque and valid only for the same player and the same types; a mismatched or malformed one is 400 INVALID_CURSOR.

Errors (the code is in the body's code field): 400 INVALID_INPUT (missing player_email, an unknown type, a non-integer limit), 400 INVALID_CURSOR, 401 INVALID_GAME_SECRET, 404 PLAYER_NOT_FOUND, 500 PLAYER_ACTIVITY_FAILED (retry).

GET /api/platform-commerce/purchase-history

For platform titles. Query: player_email (required), limit (1 to 100, default 50), cursor. Newest first. Each entry carries exactly the fields of GET /api/platform-commerce/purchase-status/<order_id>, so the two never disagree.

200 OK
{
  "player_email": "bo@example.com",
  "game_id": "123456789012",
  "purchases": [
    {
      "order_id": "ORD_...",
      "status": "completed",
      "game_currency_amount": "50.00",
      "usd_amount": "5.00",
      "payment_method": "card",          // or "network_currency" for the balance leg
      "created_at": "2026-10-09T14:02:11+00:00",
      "invo_fee_usd": "0.33",            // everything INVO kept on the sale, tax excluded
      "partner_share_usd": "4.67"        // your earnings from the sale
    }
  ],
  "limit": 50,
  "has_more": false,
  "next_cursor": null
}

invo_fee_usd is everything INVO kept on the sale, tax excluded: the card fee plus any international card fee on a card sale, the platform fee on a balance sale. partner_share_usd is your earnings from the sale. Both are null until the sale has settled (additive).

On a Merchant of Record card sale an entry also carries receipt_number and receipt_url once the receipt is issued. Errors (the code is in error_code): 400 INVALID_INPUT, 400 INVALID_CURSOR, 404 PLAYER_NOT_FOUND, plus the same authentication and platform errors as purchase-status. See Platform Commerce.

Withdrawals before identity verification

You can submit a withdrawal request before your identity verification has been approved. It is accepted and held rather than refused. The request is created, the amount is set aside against your available balance, and it counts toward your pending-request limit exactly like any other pending request. Nobody at INVO can approve it while the hold is on. Once your verification is approved it moves into the normal review queue on its own, with no action from you.

This replaced a production 403. Submitting without approved verification used to be refused outright. If your integration has a branch that handles that refusal, or a pre-check that hides the submit button, it now receives a 201instead. Treat it as accepted and waiting, not as success and not as failure: show the developer that the request is on hold and what clears it. Every other check on the request is unchanged and still runs, so an insufficient balance, a request over the pending limit or an unverified bank account is refused as before.

What a held request does and does not do

  • • It reserves the money. The amount is deducted from what you can request next, the same as any pending request.
  • • It counts toward the pending limit. A held request occupies one of your pending slots.
  • • It cannot be approved. No administrator can approve it, from any door, until your verification passes.
  • • It can still be rejected. An administrator can reject it at any time, which releases the reserved amount back to your available balance.
  • • It never expires. It waits as long as it needs to, including while you fix and resubmit a verification that was rejected or revoked.

The response fields, all additive

Every key POST /api/withdrawals/request returned before is unchanged, including status, which is still "pending_review". There is no new status to learn. Three keys were added:

KeyTypeMeaning
kyc_holdboolean or nulltrue means accepted and held until verification is approved. null means the verification state could not be read on this request; treat it as unknown rather than as held or clear. The approval gate enforces verification regardless of what this said.
kyc_hold_reasonstring or nullWhy it is held: KYC_REQUIRED, KYC_IN_REVIEW, KYC_REJECTED, KYC_REVOKED or KYC_EXPIRED. null when the request is not held.
next_stepsstring or nullA sentence you can show the developer saying what clears the hold. null when the request is not held.
POST /api/withdrawals/request

Response 201:
{
  "success": true,
  "withdrawal_id": "WDL_20260917120000000000_ab12cd",
  "status": "pending_review",
  "requested_amount": "100",
  "net_payout_amount": "99.00",
  "processing_fee": "1.00",
  "offboarding_fee_pct": "1",
  "offboarding_fee_cap_usd": "500.00",
  "initiated_by": "on_demand",
  "fee_reason": "on_demand",
  "total_pending_withdrawals": 1,
  "kyc_hold": true,
  "kyc_hold_reason": "KYC_IN_REVIEW",
  "next_steps": "Your withdrawal request is on hold until your identity verification is approved. ..."
}

The same fields on the reads

  • • GET /api/withdrawals/history carries kyc_hold, kyc_hold_reason and next_steps at the top level, describing the developer now, plus kyc_hold on each row so you can badge the individual requests that are waiting.
  • • GET /api/dev/withdrawals/summary carries the same three at the top level and kyc_hold on each pending row. A held request still counts in the pending total and still reduces what is requestable, because the money really is reserved.
  • • GET /api/dev/overview/attention gains an item under the withdrawals_kyc component with the code WITHDRAWAL_AWAITING_KYC, carrying count (how many requests are held), amount_usd (their combined requested amount) and reason. Its severity is info while a verification submission is actually in review, because there is nothing for the developer to do, and blocker otherwise, because there is. It is absent once verification is approved and absent when nothing is held.

All of this applies to production. Sandbox does not require identity verification to withdraw, so nothing is held there and kyc_hold is never true.

Withdrawable balance: reserve, dispute holds and payouts in flight

available_balance on GET /api/withdrawals/available-balance, and available_balance_usd and requestable_usd on GET /api/dev/withdrawals/summary, are what you can withdraw now. These are subtracted before you see them, and the reads carry fields that explain them (additive):

  • • Partner payouts INVO has approved and is sending, on every tier: in_flight_payouts_usd. Almost always "0.00". While a payout is in flight the balance can drop by a little more than the payout, and the difference returns when it completes.
  • • The Merchant of Record chargeback reserve: reserve_held_usd, reserve_next_release_at, reserve_release_schedule, reserve_terms and balance_before_reserve_usd. On Open there is no reserve. See the reserve.
  • • Amounts held for open disputes, on every tier: dispute_holds_usd (the total held, already deducted from available_balance) and disputes_on_hold (how many disputes are holding money), on GET /api/withdrawals/available-balance. A hold is released when the dispute is won, or when its loss is booked. See dispute holds.

If the reserve cannot be read these fields are null; show nothing then, never $0.00. A withdrawal request refused for insufficient balance while something is held carries the same fields, and its message gains a sentence naming the held amount. Read the fields, not the message.

Account reserves and account holds

On either tier, INVO can apply a reserve to an account, or hold its payouts, under account standing. Both reads carry these fields (additive):

  • • account_reserve_held_usd: the total held by account reserves now, already deducted from the available balance. "0.00" with no account reserve, and null if it cannot be read (show nothing then).
  • • account_reserves: the active account reserves, each with kind (rolling_pct, a percentage of your earnings over a number of days; fixed, an amount; or full, the whole balance), pct, amount_usd, days, starts_at, ends_at (null while it has no end date), source, held_usd (what it holds now) and status. [] with no account reserve.
  • • account_hold: null while the account can be paid out, otherwise {code, message} saying why not. code is ACCOUNT_SUSPENDED, PAYMENTS_PAUSED or ACCOUNT_UNDER_REVIEW (payouts are held while INVO reviews the account); message is safe to show the account owner.

While account_hold is set, can_withdraw on GET /api/withdrawals/available-balance is false, requestable_usd on GET /api/dev/withdrawals/summary is "0.00", and each pending row there carries account_hold: true. A new POST /api/withdrawals/request is refused with 403 and the same error_code. A pending request is held, not canceled.

Authentication

Every server-to-server API call carries a single header. No login flow, no token refresh, no cookies.

One-header authentication
// Production keys start ivsdk_live_, test-mode keys ivsdk_test_.
// Each works only on its own server.
const SDK_KEY = process.env.INVO_SDK_KEY;

// Production
const r = await fetch(
  'https://invo.network/api/player-balances/player/by-email/player@example.com',
  { headers: { 'X-Game-Secret-Key': SDK_KEY } }
);

// Sandbox
const r2 = await fetch(
  'https://sandbox.invo.network/sandbox/api/player-balances/player/by-email/player@example.com',
  { headers: { 'X-Game-Secret-Key': SDK_KEY } }
);

console.log(await r.json());

See Authentication for full details, including key rotation and error codes.

Idempotency on writes

Every write endpoint accepts a client_request_id in the body. Generate a UUID per logical action; replay the same id on retry. A duplicate never applies twice. On item purchases, transfers and sends it answers 409 Conflict with a message only, so keep your own record of the ids the first success returned (or read the order back); a partner payout request returns 200 with the payout already created.

Replay-safe writes
// Client generates a fresh UUID per checkout button click
const clientRequestId = crypto.randomUUID();

const r = await fetch('https://invo.network/api/item-purchases/purchase-item', {
  method: 'POST',
  headers: {
    'X-Game-Secret-Key': SDK_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    client_request_id: clientRequestId,
    player_email: 'player@example.com',
    player_name: 'PlayerOne',
    item_id: 'sword_001',
    item_name: 'Iron Sword',
    item_quantity: 1,
    unit_price: '100.00',
    total_price: '100.00'
  })
});

// On 5xx or network failure: retry the SAME request body with the same
// client_request_id. If the first attempt went through, the retry answers
// 409 { "message": "Duplicate client_request_id. ..." } and nothing is charged twice.

Rate Limits & Error Contract

Rate limits

  • • Limits are per endpoint, counted per game, per player or per calling IP: see Rate Limits for the full table
  • • A 429 carries Retry-After and retry_after: the seconds until the limit that fired resets

Error envelope

{
  "error": "Human-readable message",
  "error_code": "MACHINE_READABLE_CODE",
  "error_id": "ERR_<yyyymmdd>_<hex>",
  "timestamp": "2024-12-09T12:34:56Z"
}

Capture error_id when reporting issues. We map it to the matching server log entry instantly.

Single-Header Auth

X-Game-Secret-Key on every call. Server-side only, never bundled into clients.

Real-Time Processing

Balance updates apply on commit. Webhooks deliver state changes to your backend in seconds.

Built for Scale

Pagination, batch reads, streaming reconciliation. Designed for portfolios with millions of players.

Quick Integration Path

1

Register your developer account + title

Sandbox first (console.invo.network/test). Capture your SDK key (ivsdk_test_…; production keys start ivsdk_live_…).

2

Configure your branded currency

Name, symbol, transfer policy.

3

Wire up balance reads + item purchase

Smallest end-to-end loop: top up via hosted checkout → read balance → spend on an item.

4

Add transfers + sends if your design needs them

Optional. Same auth, same response envelopes.

5

Promote to Production

You do not rebuild the title by hand. Each sandbox title is copied into production for you, once, automatically, with new keys of its own, in Testing (press Copy to production in the sandbox console only to ask again after a refused or failed copy). Then swap base URL + SDK key and set the title to Live when you are ready. See Sandbox & Testing for what does and does not cross.