FAQ / Transfers and Sends
Transfers and Sends: Questions and Answers
Cross-game self-transfers, player-to-player sends, destinations, collecting, and the error reference. 159 questions, grouped by the documentation page each one is about. Where a response or webhook carries a figure, read it there rather than recomputing it.
Cross-Game Transfers
Read the Cross-Game Transfers page
What are the stages of a cross-game transfer (initiate, approve, claim code, phone-secured claim, completion)?
(1) Your server calls POST /api/transfers/initiate-transfer with the game secret, which reserves the amount and opens the approval window. (2) The player approves with the INVO-hosted page (passkey) or a QR device grant, and your side calls POST /api/sdk/transfers/{id}/approve with the player token; this moves the transaction to pending_claim. (3) That approve call issues the claim_code and returns it to the caller. (4) The player collects in the destination title, normally with POST /api/sdk/transfers/{id}/confirm-receipt, or with the claim code at POST /api/transfers/claim-transfer as a fallback; both only credit the phone number the transfer was addressed to. (5) On collect the destination is credited, fees are distributed, and transfer.sent / transfer.received fire.
Why is target_player_phone required, and what happens on a phone mismatch at claim?
Transfers are routed by phone: the value can only be collected by the number given in target_player_phone, so it is a required field on initiate. On claim-transfer, a target_player_phone that does not match the addressed number returns 403 ("This claim code can only be redeemed by the phone number it was sent to"). On confirm-receipt, a player whose phone on file in the destination title does not match returns 403 not_intended_receiver.
Can currency move between a game and a platform tenant, and when are GAME_TO_PLATFORM_BLOCKED and PLATFORM_TO_GAME_BLOCKED returned?
Yes. Currency moves between games and platform tenants (video, creator and social platforms) in both directions, through transfers and sends, the same as game to game, and the destination lists include both. 409 GAME_TO_PLATFORM_BLOCKED and 409 PLATFORM_TO_GAME_BLOCKED are returned only if INVO restricts movement between games and platforms for the network. Nothing is reserved when either fires.
Can currency bought through a platform store move to titles outside that store, and can other currency move into those store titles?
Currency bought through a platform store is subject to that store's rules: it can only move between titles on that store, and a move that would take it outside answers 409 STEAM_VALUE_NON_TRANSFERABLE. Currency from a title outside the store can never enter a title distributed through it: that answers 409 NON_STEAM_VALUE_INTO_STEAM_BLOCKED, and those store titles are left out of the destination lists of sources outside the store. Handle both codes.
Does where currency was bought affect what a player can do with it?
Currency bought through a platform store is subject to that store's rules. Currency from outside the store can never enter a store title. Read the refusal codes on each call rather than assuming a balance can be used anywhere.
How is the 10% fee split, and what does a 100.00 transfer net?
The total fee is 10% of the amount: 3.5% to the sending title, 3.5% to the receiving title, and 3% to INVO. A 100.00 transfer has total_fee 10.00 (source_game_fee 3.50, target_game_fee 3.50, invo_fee 3.00), and net_amount is 90.00. INVO's share is the remainder after rounding, so on some amounts it can differ by a cent: read the figures from fees_preview rather than computing them.
What does the approve response return?
A 200 from POST /api/sdk/transfers/{id}/approve returns status: "approved", next: "pending_claim", transaction_id, claim_code, claim_code_expires_at and claim_delivery. claim_delivery is "email" when the code was also emailed to the sender's own address on file, and "none" when nothing was sent and the code in the response is the only copy. When the destination title collects in-app through confirm-receipt, it is usually "none". "sms" is a legacy value. The source title can read the code again later from GET /api/transfers/{id}/status.
How long is a claim code valid, and what happens to an expired transfer?
Read the deadline from claim_code_expires_at or the pending item's expires_at; do not hard-code it. If nobody collects in time, the transaction becomes expired_claim and the full reserved amount (the gross, since fees are only taken at collect) goes back to the sender. transfer.claim_expired fires to both titles and transfer.refunded with reason: "claim_expired" fires to the source title.
Is the default daily transfer amount 1,000,000 units per title, and can it be changed?
The default is 1,000,000 currency units, but the cap is per player per day across the whole platform, not set per title. The platform administers it as a single system setting (DAILY_TRANSFER_AMOUNT_LIMIT), so a title cannot change it through the API. Sends have their own separate daily cap, also 1,000,000 by default. Going over the cap returns 429 with error: "velocity_limit_exceeded".
What does GET /api/transfers/security-status return?
It is a diagnostic read, authenticated with your game secret and called with a player_email query parameter (missing returns 400). It returns player_email, game_id, ip_address, a security_status object that says per restriction type whether the player is blocked, with message and time_remaining, and a velocity_status object with hourly_transfers, daily_transfer_amount, hourly_limit and daily_limit. If the status store is unavailable it answers 503. Use it for support and debugging. It is not a gate to check before every transfer.
What happens when the claimed currency ID differs from the source currency?
On claim-transfer the player is credited in the target_currency_id you name, which must belong to your title (otherwise 400 "Target currency not found or does not belong to this game"). If the two currencies have different per-game rates, the net amount is converted at each currency's own rate and rounded down to 0.01; at equal rates the player gets the net unchanged. The transfer.received webhook and the response report what was actually credited.
Sends & Transfers, Stage by Stage
Read the Sends & Transfers, Stage by Stage page
Which call actually moves money, and why doesn't polling a device grant or status endpoint move anything?
Only four calls move money: POST /api/sdk/send/{id}/approve and POST /api/sdk/transfers/{id}/approve on the sending side, and POST /api/sdk/send/{id}/confirm-receipt and POST /api/sdk/transfers/{id}/confirm-receipt on the receiving side. A device grant that polls to approved is only proof of who the player is, and a status read only reports state. If your integration stops at the poll, the transaction stays in pending_pin_verification and is refunded when the approval window lapses.
Which bearer token goes on the approve call, and can the game secret be used instead?
Send Authorization: Bearer <player token>, using the sender's token minted by the sending title through POST /api/sdk/player-token. The game secret cannot approve anything: /api/sdk/player-token is the only /api/sdk/* endpoint that accepts it, and sending it to approve or confirm-receipt is rejected as unauthenticated.
How do the approve paths differ for sends and transfers, and which flow values must the device grant match?
A peer send uses the singular path /api/sdk/send/{id}/approve, and a transfer uses the plural path /api/sdk/transfers/{id}/approve. The grant's flow must match the call that will settle it: send or transfer for the sender's approve, and send_receipt or transfer_receipt for the recipient's confirm-receipt. A grant begun for the wrong flow is refused at settle time with 400 DEVICE_APPROVAL_NOT_APPROVED.
What are transaction_status and verification_state between initiate and approval?
transaction_status is pending_pin_verification and verification_state is awaiting. Once the sender's approve succeeds they become pending_claim and approved.
How does initiate affect the sender's available_balance?
Initiate moves the amount out of the sender's available balance into a reserved balance. Top-level new_balance in the 201 is the available balance after that reservation. The reservation is finalized at collect, or released in full if the transaction expires.
What happens to the sender's funds when the approval window lapses, and which reason and expiry_reason appear?
INVO's expiry sweep releases the whole reservation back to the sender's available balance and sets the status to expired_pin_verification. transfer.refunded fires to the sending title only, with reason: "pin_expired" and expiry_reason: "approval_expired" (or "pin_expired" if a legacy PIN message was involved). No transfer.claim_expired is sent, because nothing was ever collectable. The status endpoints also return expiry_reason once a transaction has expired.
Is refunded_amount on transfer.refunded gross or net?
Gross. It is the full amount the sender committed, because fees are only taken at collect and an expiry returns the entire reservation.
Where do I read claim_code_expires_at for a send versus a transfer?
For a send, read claim_info.claim_code_expires_at from GET /api/currency-sends/{id}/status. For a transfer, read it from the approve response or from the top-level claim_code_expires_at on GET /api/transfers/{id}/status, but only the source title sees it there, after approval. On the receiving side, use expires_at on the pending item or claim_code_expires_at from inbound-pending or the transfer.claim_pending webhook.
What happens when the claim window passes, and which titles receive transfer.claim_expired and transfer.refunded?
The sweep marks the transaction expired_claim and returns the full reservation to the sender. transfer.claim_expired (with expiry_reason: "claim_expired") goes to both titles, direction: "outbound" to the sender's and "inbound" to the recipient's. transfer.refunded with reason: "claim_expired" goes to the sending title. When sender and recipient share one title, that title receives both copies.
What does 403 WEBAUTHN_NOT_ENABLED_FOR_TENANT mean for my title, and what should I build instead?
It means the in-client passkey ceremony, which runs on your own domain, is not available for your title. That is the normal answer for most titles, and new partner domains are no longer accepted, so there is nothing to configure. Build the INVO-hosted approval instead: approveHosted() on the web, or the QR device grant on consoles and desktop. Both run the passkey on INVO's domain and settle with device_code.
Which operations does 403 PASSKEY_RECOVERY_COOLDOWN block?
It blocks money leaving the account: initiate-send and initiate-transfer (nothing is reserved), and the send and transfer approve calls (an existing reservation stays and is refunded when the approval window lapses). It refuses every factor. Collecting is not affected. The body carries retry_after (an absolute time) and retry_after_seconds; show the time and stop.
Why would approve return DEVICE_APPROVAL_NOT_APPROVED after the player approved on their phone?
The same answer covers every mismatch: the grant was begun with a different flow (for example send_receipt used on approve, or send on a transfer), it was for a different transaction, it belongs to a different player identity than the bearer token, or the code is unknown. Check the flow first, then the transaction id and the token. Otherwise begin a new grant.
Which title's secret must mint the recipient's token, and must it include the phone?
The receiving title's secret, because a player token is scoped to one player in one title. Include player_phone: the send or transfer is addressed to a phone number and only matches a player whose phone matches, and with the phone a first-time recipient is created on the spot (created: true) instead of getting 404 player_not_found.
Why might a transaction still show its old status after the deadline?
Expiry is applied by a sweep that runs on a short cycle, so a refund lands within a few minutes of the deadline rather than at the exact second. Until then the status endpoint reports the pre-expiry status. Treat transfer.refunded, or a later status read, as the signal; do not expire transactions on your own clock.
Peer Send, Stage by Stage
Read the Peer Send, Stage by Stage page
What fields must POST /api/sdk/player-token receive so an unknown player is created instead of 404?
Send player_email and player_phone (E.164), and optionally player_name, with your game secret. With the phone, a player who is not in your title yet is registered and the answer includes created: true. Without it, an unknown player returns 404 with error: "player_not_found" and a hint to pass player_phone.
How do the 201 and 202 responses from initiate-send differ?
201 is a normal initiate: status: "success", transaction_id, verification_expires_at, new_balance, send_details and claim_delivery_planned. 202 is the minor-account branch: status: "pending_guardian_approval" with a guardian_approval block (approval_id, state, expires_at, consent_channel, poll_endpoint, resend_endpoint). In both cases the amount is reserved, but a 202 send cannot be approved until the guardian says yes.
What does identity_rekeyed: true on initiate mean, and which token should I use afterward?
It means this initiate was the first call to put a phone on the sender (for example, a player created without one at checkout), so their INVO identity changed. Any token minted before the initiate no longer matches them. The response also carries identity_id, player_token and player_token_expires_at; use that player_token for every later step. The fields are absent when nothing changed.
Which initiate refusals reserve nothing?
All of them. Every validation, policy, guardian (403 GUARDIAN_REQUIRED), recovery-hold, phone-share, containment, velocity (429) and 503 flow_paused refusal is returned before the reservation step. Only a 201 or 202 reserves funds.
How is claim_delivery_planned (email, none, sms) decided?
"email": the receiver has an email already on file for the receiver phone, and the claim link will go there. "none": you supplied a receiver_player_email that is not on file for that phone, so nothing is sent and the receiver collects in the game. "sms": a legacy value, used only when you supplied no receiver email and none is on file; do not build on it.
Why does polling a device grant to approved without calling approve leave the send unsettled?
The grant only proves who the player is. The transaction advances only when POST /api/sdk/send/{id}/approve is called with that device_code, and that call also re-checks the guardian gate, the recovery hold and the transaction's state. If it is never made, the send stays in pending_pin_verification and is refunded at the end of the approval window. approveHosted() and the server SDK's completeDeviceApproval() make the call for you.
How does a second device/begin behave when the earlier grant is pending versus approved?
If the earlier grant for the same transaction and flow is still pending, the second begin replaces it: the old code is closed and you get a new one. If the earlier grant is already approved and still inside its window, begin returns 409 DEVICE_APPROVAL_ALREADY_PENDING with expires_at; settle with the code you already hold. Grants for different flows on the same transaction (for example send and send_receipt) are tracked separately and do not affect each other.
What should I do for authorization_pending, slow_down, expired_token, access_denied and invalid_grant?
authorization_pending: wait interval seconds and poll again, and if the body has enrollment.state: "awaiting_screen", show the match-code prompt and answer at confirm-enrollment. slow_down: increase your interval and continue. expired_token: the grant timed out; begin a new one if the transaction is still open. access_denied: the player declined; stop and ask before starting again. invalid_grant: the code is unknown or belongs to another player; check that the token and code go together.
How should I handle TRANSACTION_NOT_PENDING, SEND_APPROVE_FAILED and CONFIRM_RECEIPT_FAILED?
400 TRANSACTION_NOT_PENDING is not a failure: the transaction already moved on, usually because your first call worked. Read current_status and continue from there. 400 SEND_APPROVE_FAILED and 400 CONFIRM_RECEIPT_FAILED are faults on INVO's side and ambiguous, because the money may already have moved. Do not retry, refund or re-initiate: read the status endpoint (or wait for the webhook), then send the error_ref to INVO support.
Which two errors justify a single token re-mint at approve?
403 with code: "PLAYER_TOKEN_STALE" (the error field still reads not_send_sender), and 401 with code: "SDK_TOKEN_EXPIRED". Nothing moved in either case: mint a fresh token with the phone and retry once.
What is the difference between flow "send" and "send_receipt"?
send is the sender's grant, used with the sender's token to settle POST /api/sdk/send/{id}/approve. send_receipt is the recipient's grant, used with the recipient's token (minted by the receiving title) to settle POST /api/sdk/send/{id}/confirm-receipt. They share a transaction id but are tracked separately.
What does 409 receiver_not_enrolled_use_claim_code mean?
The recipient's identity has no player row in your (receiving) title, so confirm-receipt has nobody to credit. Minting the recipient's token with player_phone first registers them and avoids this. Otherwise, send them to the hosted claim page (the emailed link) or use POST /api/currency-sends/claim-currency, both of which create the player as part of collecting.
Which windows expire a send, and which refund reasons result?
Two windows. The approval window (verification_expires_at): if nobody approves, the send ends as expired_pin_verification and transfer.refunded fires with reason: "pin_expired" (expiry_reason: "approval_expired"). The claim window (claim_code_expires_at, 24 hours by default): if nobody collects, the send ends as expired_claim, with transfer.claim_expired to both titles and transfer.refunded with reason: "claim_expired".
On a 50.00 send, why is the fee 5.00 and the net 45.00, and when is the fee taken?
Sends carry the same 10% total fee as transfers: sending_game_fee 1.75 (3.5%), receiving_game_fee 1.75 (3.5%) and platform_fee 1.50 (3%), which leaves net_amount 45.00. The fees are calculated and recorded at initiate, but taken only at collect: the full 50.00 is reserved, the recipient gets 45.00 when they collect, and an expiry returns all 50.00 to the sender.
Which SDK methods map to which raw endpoints?
Server SDK (InvoServer in @invonetwork/web-sdk/server, invonetwork for Python):
mintPlayerTokencallsPOST /api/sdk/player-token.initiateSendcallsPOST /api/currency-sends/initiate-send.beginDeviceApprovalcalls/api/sdk/approvals/device/begin.pollDeviceApprovalcalls/poll.confirmDeviceEnrollmentcalls/confirm-enrollment.approveWithDeviceCodecalls/api/sdk/send/{id}/approve, orconfirm-receiptfor the receipt flows.completeDeviceApprovalruns the poll loop and thenapproveWithDeviceCode.getSendStatuscallsGET /api/currency-sends/{id}/status.claimCurrencycallsPOST /api/currency-sends/claim-currency.
Browser InvoClient:
approveHosted()runs the hosted device grant and then calls approve or confirm-receipt.approveSend()calls/api/sdk/send/{id}/approve/webauthn/beginand then approve.confirmReceiptSend()calls the send confirm-receipt webauthn begin and settle.getPendingCollect()callsGET /api/sdk/transfers/pending.
There is no SDK wrapper for POST /api/sdk/send/{id}/resend-claim; call it directly.
Cross-Game Transfer, Stage by Stage
Read the Cross-Game Transfer, Stage by Stage page
What does a 202 with a guardian_approval block on initiate-transfer mean, and how do I poll it?
The initiating player's account is flagged as a minor with a guardian on file. The amount is reserved, but the transfer cannot be approved until the guardian agrees. Poll GET /api/transactions/{id}/approval-status with your game secret (the poll_endpoint in the block) until approval.state is approved, rejected or expired, then produce a factor and call approve as normal. resend_endpoint re-sends the request to the guardian.
Which initiate-transfer refusals (SOURCE_GAME_NOT_LIVE, TARGET_GAME_NOT_LIVE, TRANSFER_POLICY_VIOLATION, PASSKEY_RECOVERY_COOLDOWN) reserve nothing?
None of them reserves anything. The status checks (403 SOURCE_GAME_NOT_LIVE / TARGET_GAME_NOT_LIVE) and the policy check (403 TRANSFER_POLICY_VIOLATION) run before any player or balance work, and 403 PASSKEY_RECOVERY_COOLDOWN rolls back before the reservation. The same is true of every other initiate refusal.
For web, when should I call approveHosted() versus approveTransfer()?
Use approveHosted({ transactionId, flow: "transfer" }) from a click handler. It is the primary web path: it opens INVO's approval page in a popup, sets up a passkey if the player has none, and calls approve for you. approveTransfer() runs the passkey ceremony on your own domain, which for almost every title returns 403 WEBAUTHN_NOT_ENABLED_FOR_TENANT, and new partner domains are not accepted. Use it only if your title already has a verified passkey domain.
When does GET /api/transfers/{id}/status return the claim code?
Only when the caller is the source title, and only once the transfer is approved or completed. The destination title, and any read before approval, gets claim_code: null and claim_code_expires_at: null.
Why does 400 TRANSFER_APPROVE_FAILED with an error_ref forbid retrying, refunding or re-initiating?
It is a catch-all for a fault on INVO's side, and the money can already have moved before the error was produced. A retry could duplicate the action, and a refund or re-initiate could pay out twice. Read GET /api/transfers/{id}/status (or wait for transfer.claim_pending) to learn what happened, then give the error_ref to INVO support.
What is the difference between PLAYER_TOKEN_STALE and not_transfer_sender?
Both are 403 with error: "not_transfer_sender". When code is PLAYER_TOKEN_STALE, the token was minted before the player's phone was added, so it carries their old identity: mint a new token and retry once. Without that code, the token belongs to a different player than the one who started the transfer: mint it for the source player's email, in the source title.
What do expired_pin_verification and expired_claim each trigger?
expired_pin_verification (never approved) releases the reservation and sends transfer.refunded to the source title with reason: "pin_expired", with no transfer.claim_expired. expired_claim (approved, never collected) refunds the source player and sends transfer.claim_expired to both titles plus transfer.refunded with reason: "claim_expired" to the source title.
How do confirm-receipt and claim-transfer differ in credentials, target_currency_id and phone matching?
- Credentials: confirm-receipt uses the player's token minted by the destination title, plus one factor (
device_code,webauthn_assertionor a device signal). claim-transfer uses the destination game secret plus theclaim_code. - Currency: confirm-receipt takes no currency. It credits the transfer's target currency if one is set, otherwise the same currency id in your title, otherwise your title's first currency. claim-transfer requires
target_currency_id. - Phone matching: confirm-receipt checks the phone on the player's row in your title, and a mismatch returns
403 not_intended_receiver. claim-transfer checks the submittedtarget_player_phone, and a mismatch returns403. - Player row: confirm-receipt needs an existing player row and otherwise returns
409 receiver_not_enrolled_use_claim_code. claim-transfer creates the player.
What does GET /api/transactions/{id}/approval-status report?
The guardian approval for a minor's transaction: { "status": "ok", "approval": { "state": "pending" | "approved" | "rejected" | "expired", ... } }. It returns 404 with status: "not_found" when no guardian approval was created for that transaction, and 503 with status: "unavailable" when the state cannot be read right now (retry shortly).
Which device-grant flow does the source title use versus the destination title?
The source title begins the grant with flow: "transfer" and settles at /api/sdk/transfers/{id}/approve. The destination title begins it with flow: "transfer_receipt", using the player's destination-title token, and settles at /api/sdk/transfers/{id}/confirm-receipt.
Are fees stamped at initiate or taken at collect?
Both, at different steps. The fees are calculated and recorded on the transaction at initiate, and fees_preview shows them. Nothing recalculates them later. The money changes hands only at collect: the reserved gross is debited, the recipient gets the net, and the fee shares are distributed. An expiry before collect returns the full gross.
Collecting, Stage by Stage
Read the Collecting, Stage by Stage page
Which endpoint lists a recipient's collectable items, and which field picks the confirm-receipt path?
GET /api/sdk/transfers/pending, called with the recipient's player token minted by your title. Show the items with kind: "receiving_confirm", and choose the endpoint by flow: "send" goes to /api/sdk/send/{transfer_id}/confirm-receipt, and "transfer" goes to /api/sdk/transfers/{transfer_id}/confirm-receipt. Never branch on kind alone.
What does the token mint return for a first-time receiver?
When you send player_email and player_phone, a receiver who is not on INVO yet is created and the 200 returns token, expires_at, identity_id and created: true. They do not need to have bought anything first. If you omit the phone, the answer is 404 player_not_found and they cannot collect.
Which confirm-receipt endpoint does flow "send" use, and which does flow "transfer" use?
flow: "send" uses POST /api/sdk/send/{id}/confirm-receipt, and flow: "transfer" uses POST /api/sdk/transfers/{id}/confirm-receipt. Their device grants use send_receipt and transfer_receipt respectively.
Why does a device grant that polls to approved credit nothing until confirm-receipt is called?
The grant only proves who the recipient is. confirm-receipt is the call that checks the recipient is the addressed one, the phone matches, any consent gate is satisfied and the claim window is still open, and only then credits the balance. Until you call it with that device_code, the value sits in the claim window and is refunded to the sender when the window closes.
What do the approveHosted statuses mean on the receiving side?
"approved": confirm-receipt has already run.result.approval.statusis"completed"and the balance has moved."denied": the player declined."expired": the grant timed out."closed": the player closed the popup and nothing moved."blocked": the popup was blocked. Retry from a fresh click, or fall back to the QR grant."redirected": your page navigated away. Use thedevice_approval.approvedwebhook and a status read as your signal.
What must I do for 202 RECIPIENT_IDENTITY_PENDING versus 403 RECIPIENT_IDENTITY_DECLINED?
202 RECIPIENT_IDENTITY_PENDING means an extra recipient-identity confirmation is in progress. It is not final: tell the player a confirmation was sent and retry once they have answered. 403 RECIPIENT_IDENTITY_DECLINED is final for this attempt: stop, and the value refunds to the sender when it expires. This check is off by default, so most integrations never see either code, but handle them anyway.
What do the confirmed_via values sdk_receipt, hosted_claim and an absent key mean?
On transfer.received, "sdk_receipt" means the value was collected through confirm-receipt (on either rail), and "hosted_claim" means it was collected on INVO's hosted claim page. If the key is absent, it was collected through a legacy claim-code endpoint (claim-currency or claim-transfer). Treat a missing key as "collected by claim code", not as an error.
Which query parameters does GET /api/transfers/inbound-pending require?
Either player_email or player_phone (at least one), with the receiving title's game secret. If both are missing it returns 400 ("player_email or player_phone query parameter is required"), and a bad secret returns 401. It lists only collectable items addressed to your title and never returns the claim code.
Should the claim deadline come from expires_at or claim_code_expires_at?
They are the same deadline. Use expires_at on the pending item in the player-facing collect screen, and claim_code_expires_at from inbound-pending or transfer.claim_pending on your server. Do not use a local timer. For a transfer, the destination title cannot read it from the status endpoint (that field is only returned to the source title).
What does held: true with hold_reason device_requires_elevation or guardian_pending mean?
The item is real but cannot be acted on right now, so show it greyed out with the reason rather than hiding it. device_requires_elevation means the device is not yet cleared for money actions (this applies to INVO wallet sessions, not to tokens minted by your title). guardian_pending appears only on identity_gate items (money the player is sending) while a guardian has not answered. Receiving is never guardian-gated.
What does 400 WRONG_CLAIM_ENDPOINT indicate?
You sent a claim code to the other rail's endpoint: a send's code to claim-transfer, or a transfer's code to claim-currency. The body (error_code: "WRONG_CLAIM_ENDPOINT") names expected_endpoint and expected_body_fields.
How often should I poll the pending list, and why is the webhook only a wake-up?
While a collect screen is open, every 20 to 30 seconds is plenty. Webhooks and push notifications can be delayed or arrive after the player has already collected somewhere else (the hosted page or the INVO app), so treat them as a cue to re-read GET /api/sdk/transfers/pending, which is the authoritative list.
What Your Server Must Call, In Order
Read the What Your Server Must Call, In Order page
What is the exact call order for a send on the sending side and the receiving side?
Sending side: 1. POST /api/sdk/player-token (game secret, with the phone). 2. POST /api/currency-sends/initiate-send. 3. Mint the token again right before approval. 4. The sender approves with flow: "send", through approveHosted() or the QR grant. 5. POST /api/sdk/send/{id}/approve with the device_code. 6. Confirm with transfer.claim_pending or GET /api/currency-sends/{id}/status.
Receiving side: 1. POST /api/sdk/player-token with the receiving title's secret. 2. GET /api/sdk/transfers/pending. 3. Approve with flow: "send_receipt". 4. POST /api/sdk/send/{id}/confirm-receipt. 5. Handle transfer.received.
What is the exact call order for a transfer on each side?
Source title: 1. POST /api/sdk/player-token. 2. POST /api/transfers/initiate-transfer. 3. Re-mint the token. 4. The player approves with flow: "transfer". 5. POST /api/sdk/transfers/{id}/approve, and store the claim_code and claim_code_expires_at. 6. Confirm with transfer.claim_pending or GET /api/transfers/{id}/status.
Destination title: 1. POST /api/sdk/player-token with the destination secret. 2. GET /api/sdk/transfers/pending. 3. Approve with flow: "transfer_receipt". 4. POST /api/sdk/transfers/{id}/confirm-receipt, or claim-transfer as the fallback. 5. Handle transfer.received.
When does POST /api/sdk/send/{id}/resend-claim return CLAIM_DELIVERY_WITHHELD or RESEND_COOLDOWN?
409 CLAIM_DELIVERY_WITHHELD (with claim_delivery: "none") comes back when the receiver email given for the send is not on file for the receiver phone, so no claim link can be sent. Do not retry; the receiver collects in the game. 429 RESEND_COOLDOWN comes back when a resend went out too recently. It carries retry_after in seconds; disable the button for that long.
Which fields must I store from the transfer approve response?
claim_code and claim_code_expires_at (the fallback collect route and its deadline), along with the transaction_id you already hold. Also log claim_delivery, which tells you whether the code was also emailed to the sender. A repeat approve that answers TRANSACTION_NOT_PENDING does not repeat the code, but the source title can read it again from GET /api/transfers/{id}/status.
Why does the transfer status response lack fee, game name and completed_at fields?
The transfer status endpoint is a lean state read: it returns transaction_status, verification_state, amount, net_amount, created_at, game ids, currency_id, order_id, the claim code (source title only, after approval), expiry_reason when expired, and to_phone / to_identity_id for the destination. Take the fee split from fees_preview at initiate, and the credited amounts from gross_amount, fee_breakdown and amount_received on transfer.received. The send status endpoint is richer (fee_amount, game names, claim_info, and completed_at once collected).
Which legacy claim-code routes still work, and what replaces them?
POST /api/currency-sends/claim-currency and POST /api/transfers/claim-transfer still work, as the fallback for a recipient who cannot use a passkey or has no player row yet. Their replacement is confirm-receipt on either rail, plus INVO's hosted claim page for sends. The old PIN routes (/api/currency-sends/verify-sms, /api/transfers/verify-sms and both resend-pin endpoints) are deprecated; approve with the hosted page or a QR grant instead.
Sends & Transfers Error Reference
Read the Sends & Transfers Error Reference page
Which field should my dispatcher read for each code: error_code, code or error?
Read all three, in the order code ?? error_code ?? error. Most SDK refusals use code. The guardian gate, risk holds, the phone-share gate, WRONG_CLAIM_ENDPOINT and CLAIM_LOCKED use error_code. A few older answers (not_send_sender, not_intended_receiver, receiver_not_enrolled_use_claim_code, velocity_limit_exceeded) use only error.
What should my server do on 400 TRANSACTION_NOT_PENDING with current_status?
Treat it as "already happened". Read current_status (for example pending_claim or completed) and reconcile your record to it. Never refund, alert or re-initiate because of it.
Why must I never refund, alert on or re-initiate after SEND_APPROVE_FAILED or TRANSFER_APPROVE_FAILED with an error_ref?
These are catch-alls for a fault on INVO's side, and the money may already have moved: a failure can occur after the transaction is committed. Acting on them can double-pay or double-refund. Check the status endpoint or the webhooks first, then quote error_ref to INVO support.
Why does CONFIRM_RECEIPT_FAILED come back as 400 instead of 500?
So that no internal detail leaks through the response. It still means a fault on INVO's side, not a mistake in your call, and the same request will fail the same way. Do not retry; read the status and report the error_ref.
Why does the hosted claim page return an opaque 404 for transfer claim codes?
The hosted claim page serves peer sends only, because a transfer's code belongs to the sender, who collects in the destination title. It looks up only send codes, and it answers every unknown, reused or non-send code with the same 404, so it cannot be used to probe which codes exist. A transfer that returns 409 receiver_not_enrolled_use_claim_code goes to claim-transfer instead.
How do PHONE_SHARE_APPROVAL_REQUIRED on the API and PHONE_SHARE_REQUIRED on the hosted page differ?
The API answers 409 with error_code: "PHONE_SHARE_APPROVAL_REQUIRED" and the full consent body (approval_id, expires_at, code_sent, already_approved, and existing_account_hints, which shows only masked hints). The hosted claim page deliberately returns a reduced code: "PHONE_SHARE_REQUIRED" with only message, code_sent and expires_at (plus rate_limited / retry_after_seconds when throttled). Match on both names.
What is the correct response to 202 STEP_UP_REQUIRED, and when must I send biometric_verified: true?
The amount crossed a threshold that needs stronger proof. Have the player complete a user-verified factor (passkey or device grant) and call the same endpoint again. When you are using the enrolled-device-key factor (device_fingerprint + device_signal), add biometric_verified: true on that retry. This is returned only while the risk engine is enabled.
Why must I stop on 202 RISK_HOLD instead of retrying?
The action is held for review (status: "held_for_review"), not refused. A retry or re-initiate cannot clear a review and only creates duplicates. Tell the player it is under review and wait for the outcome.
When should I call confirm-enrollment, and what do DEVICE_APPROVAL_NO_ENROLLMENT_PENDING and DEVICE_APPROVAL_ENROLLMENT_ALREADY_DECIDED mean?
Call POST /api/sdk/approvals/device/confirm-enrollment with { device_code, decision: "approve" | "deny" } only when a poll's authorization_pending body carries an enrollment object with state: "awaiting_screen". 409 DEVICE_APPROVAL_NO_ENROLLMENT_PENDING means there was no enrolment prompt to answer. 409 DEVICE_APPROVAL_ENROLLMENT_ALREADY_DECIDED means it was already answered (the body says decided and via); resume polling.
How do I recover from 409 DEVICE_APPROVAL_ALREADY_PENDING if I lost the device code?
The code cannot be handed back, and an approved grant blocks a new begin only until its own expiry. Wait until the expires_at in the 409 body and begin again, as long as the transaction's own window is still open. Next time, persist the device_code before showing the QR.
How should I handle 429 CLAIM_LOCKED with locked_dimension and Retry-After?
Stop and show the wait. locked_dimension says what is locked (phone, email or ip). retry_after_seconds and the Retry-After header say how long. Never loop or auto-retry claim codes.
Why is 200 needs_account_selection not an error?
The claim was valid, but the phone matches more than one player in your title and the email did not identify one. The body carries masked candidates (player_id, email_hint). Let the player pick, then re-submit with target_player_id (transfers) or receiver_player_id (sends). Nothing was credited yet.
Transfer Overview
Read the Transfer Overview page
Why does the status name pending_pin_verification persist, and should my code treat it as a PIN check?
The name is historical and kept so existing integrations do not break. It means "waiting for the sender to approve", which today is done with a passkey or a device approval grant. Treat it as an opaque value and do not build any PIN entry around it.
Which two status fields attribute an inbound transfer to a player?
to_phone and to_identity_id, returned only to the destination title. Match on to_phone. to_identity_id is a stable opaque id that is null when the phone matches more than one of your players, so use it as confirmation, not as the only key.
Which game secret can call the transfer status endpoint?
Either end's: the source title's or the destination title's secret. Any other title gets 404 "Transfer not found or access denied for this game". The two ends see slightly different fields (the claim code goes only to the source, and the attribution fields only to the destination).
How does available-destinations prevent a 403 TRANSFER_POLICY_VIOLATION?
POST /api/transfers/available-destinations applies the same rules initiate enforces: live status, the source's outgoing setting, the destination's incoming setting, and your universal or linked policy, plus the store-inbound rule. If your picker only offers destinations from that list, initiate will not refuse on policy.
How do fees_preview at initiate and fee_breakdown on transfer.received relate?
They carry the same amounts, recorded at initiate, under different names. fees_preview has total_fee, source_game_fee, target_game_fee, invo_fee and net_amount (plus source_rate_pct / target_rate_pct). fee_breakdown has total_fee, losing_game_fee, gaining_game_fee, invo_fee and net_amount_received.
Why should I read fee_amount and net_amount instead of computing fees?
The recorded figures are what the ledger used. Each share is rounded separately and INVO's share is the remainder, so a rate-based calculation can be off by a cent on some amounts. Display and reconcile from the response or webhook fields.
What should my server do on 503 flow_paused?
Transfers (or sends) are paused platform-wide for maintenance, and nothing was reserved. Show a "temporarily unavailable" state and retry later. Do not loop.
How should I handle a same-title transfer that delivers both copies of transfer.claim_pending?
A cross-game transfer cannot be same-title (initiate refuses it), but a same-title peer send can be, and that title receives both the direction: "outbound" and the "inbound" copies. Act on direction, and deduplicate on each delivery's idempotency_key, never on transaction_id, which is the same on both copies.
Is the claim code still a valid collection route for new integrations?
Only as a fallback. Build collection on GET /api/sdk/transfers/pending plus confirm-receipt, and keep claim-transfer behind the 409 receiver_not_enrolled_use_claim_code branch for a player with no account in your title yet.
Available Destinations
Read the Available Destinations page
How often should I call POST /api/transfers/available-destinations, and how long should I cache it?
Call it on startup or on a manual refresh, cache it persistently, and refresh about once a day. Destination lists change rarely. The endpoint is rate limited per caller, so do not call it every time a player opens the transfer screen. Initiate re-checks every rule anyway, so a slightly stale list cannot move money anywhere disallowed.
What happens when source_game_id is missing or not numeric?
400: "source_game_id is required in request body" when it is missing, or "source_game_id must be a valid game ID number" when it is not numeric. A numeric id that does not exist returns 404, and one that is not your own title returns 403 "You can only query destinations for your own game".
What does a 403 "Source game does not allow outgoing transfers" mean for my UI?
Your title has outgoing transfers turned off, so no transfer can start from it. Hide or disable the transfer feature rather than showing an empty picker. Initiate would refuse with the same 403.
How does the list differ when universal_transfers is "yes" versus "no"?
With "yes" (transfer_mode: "universal"), it returns every live title that accepts incoming transfers. With "no" (transfer_mode: "linked"), it returns only titles in your linked_game_ids that are live and accept incoming transfers, and the response also carries linked_game_ids. Your own title is never listed for transfers.
Why are testing and inactive titles excluded?
Initiate requires both titles to be live, so a non-live destination would always be refused. The list only shows titles a transfer can actually reach.
What does an empty linked_game_ids return?
In linked mode with nothing linked, it returns 200 with available_games: [], total_destinations: 0 and message: "No linked games configured for transfers". This is not an error; show a "no destinations" state.
How does accepts_steam_origin_value affect destinations for currency bought through a platform store?
It says whether the destination accepts currency bought through a platform store. Read it rather than assuming a value, and still handle 409 STEAM_VALUE_NON_TRANSFERABLE at initiate. Separately, if your title is not distributed through that store, titles that are distributed through it are omitted from your list entirely, because outside currency can never enter them.
What does a null maximum_transfer mean?
The destination currency has no per-currency maximum set. A platform-wide ceiling on any single transfer still applies, and initiate validates the amount against your sending currency's minimum and maximum.
When should I fall back to cached data after a 500?
On any 500 (or a network failure), keep using your last cached list and retry with exponential backoff. A 400, 403 or 404 is a problem with the request, and cached data will not fix it.
Player-Aware Destinations
Read the Player-Aware Destinations page
What fields does POST /api/players/destinations require, and why is phone mandatory?
email and phone (E.164 preferred; common formats are normalized). Both are required because a player's network-wide identity is defined by the email and phone together. Email alone cannot tell apart accounts that share an email but have different phones.
How is the source tenant identified, and what does 401 mean here?
By the X-Game-Secret-Key header: the calling title is the source. 401 means the header is missing or the secret is not valid.
When does the endpoint return 403 versus an empty destinations array?
403 means your title has outgoing transfers disabled. An empty destinations array (200) means your title is in linked mode with nothing linked, or the player has no presence on any other eligible tenant. Show the empty array as "no other places yet", not as an error.
How does transfer_mode universal versus linked change the results?
universal returns every live tenant that accepts incoming transfers where this player has a player record. linked limits that to tenants in your linked_game_ids. Your own tenant is never included.
How are destinations ordered, and what is the tie-breaker?
The most recently active come first (by the player's latest sent or received transaction on that tenant). Ties are broken by game name, which in the current code sorts in descending (Z to A) order. Destinations with no activity come last.
What does available_balance represent on each destination?
The player's available (not reserved) balance on that tenant, in the destination's default currency. It is null when the player has no balance record in that currency yet.
Can I use this lookup for sends, and whose email and phone do I pass?
Yes. For a transfer, pass the player's own email and phone. For a send to another person, pass the recipient's email and phone, then use the chosen game_id as receiving_game_id. It never lists your own tenant, so a same-title send does not need it.
Does the endpoint ever reveal a player's presence on a tenant I cannot transfer to?
Not on a tenant your transfer policy excludes: results are limited to live tenants that accept incoming transfers and that your universal or linked policy allows. This lookup does not apply the store-inbound rule, though, so a listed destination can still be refused at initiate with 409. Cross-check against available-destinations if your network has store-distributed titles.
Can I call it from user-facing client code?
No. It authenticates with your game secret, which must never be in a client build. Call it from your server and pass the result to the client. The data returned is safe to show to that player.
Initiate Transfer
Read the Initiate Transfer page
Which body fields are required, and what format must amount use?
Required fields: client_request_id, source_player_name, source_player_email, source_player_phone, target_player_phone, target_game_id (numeric) and amount. target_player_email is optional but recommended. Send amount as a positive decimal string such as "100.50".
Why is target_player_email now required, and since when?
It is not required today. It was required from 2026-05-07 so the phone-share consent check could run at initiate, and it has since been made optional, because transfers are routed by phone. Still send it when you have it: when it is present, the phone-share check runs at initiate (before anything is reserved) and it helps pick the right account at collect.
What phone format rules apply to source and target phones?
Both must start with + and the country code. Spaces, dashes and parentheses are stripped, everything after the + must be digits, and the total digit count (country code included) must be 10 to 15. E.164 such as +15555550100 always passes.
What error occurs if target_game_id is my own game?
400 with message: "Self-transfers are not allowed." (no error code). To move currency between two players in your own title, use a peer send to your own receiving_game_id.
What does a 201 response return, and which field is the canonical balance?
status, message, transaction_id, order_id, new_balance, currency_name, verification_method (legacy), verification_expires_at, transfer_details (with fees_preview and transfer_policy) and verification_required (legacy). Top-level new_balance is the canonical figure: the source player's available balance after the reservation.
What does a 202 pending_guardian_approval response contain?
status: "pending_guardian_approval", transaction_id, order_id, the same transfer_details, and a guardian_approval block with approval_id, state: "pending", expires_at, consent_channel ("email" when the guardian has a verified address), poll_endpoint and resend_endpoint.
What does 409 PHONE_SHARE_APPROVAL_REQUIRED contain, and when can I skip the OTP UI because already_approved is true?
The body carries error_code: "PHONE_SHARE_APPROVAL_REQUIRED", message, phone, requesting_email, masked existing_account_hints, next_endpoint, approval_id, expires_at, code_sent and already_approved. A one-time code has already been sent to the current owner of the number. If already_approved is true, the pair was approved in the moment between the check and the response: retry the original call once immediately, without showing the code-entry UI.
How do the phone-share approve, reject and status endpoints work, and how do I tell a rejection from a pending state?
POST /api/wallet/phone-share/approve and POST /api/wallet/phone-share/reject each take { approval_id, otp }, using the code the number's owner received. Retry your original call after an approve. A rejection is final, and the requester must start a fresh request. GET /api/wallet/phone-share/status?phone=...&email=... returns approved: true | false. false covers both pending and rejected, so track the outcome of your own approve or reject call to tell them apart.
What does 409 STEAM_VALUE_NON_TRANSFERABLE mean?
Currency bought through a platform store can only move between titles on that store, and this transfer or send would take it outside. Nothing is reserved. Tell the player the currency can only move to titles on that store, and do not retry the same destination. Read the amounts and fields in the response body rather than assuming them.
What limits apply (velocity cap, daily limit, minimum amount)?
- Minimum and maximum: the amount must be at least the source currency's minimum (default 0.01) and no more than its maximum, if one is set.
- Ceiling: a platform-wide ceiling per transfer applies (default 1,000,000).
- Balance: the player's available balance must cover the amount.
- Velocity: a player can start up to 10 transfers per hour, and up to 1,000,000 units in total per day across the platform. Going over returns
429 velocity_limit_exceeded. - Other 429s: separate request rate limits also return
429.
Which title statuses and flags must be set before a transfer can succeed?
- Status: both titles must be
live. A title in testing returns403 SOURCE_GAME_NOT_LIVEorTARGET_GAME_NOT_LIVE. - Direction flags: the source must allow outgoing transfers and the target must allow incoming transfers.
- Policy: the source's
universal_transfersmust be"yes", or the target must be in itslinked_game_ids. - Currency: the source must have a transferable currency.
- Containment: the store-inbound rule must not block the pair.
- Player tokens: to approve, in-app verification must be enabled for the title. Otherwise
POST /api/sdk/player-tokenreturns403 TENANT_NOT_MIGRATED.
Claim a Transfer (claim-code fallback)
Read the Claim a Transfer (claim-code fallback) page
When should a collect screen use confirm-receipt versus POST /api/transfers/claim-transfer?
Use confirm-receipt by default: the player proves who they are with a passkey or device grant, and no code is involved. Use claim-transfer only when confirm-receipt returns 409 receiver_not_enrolled_use_claim_code (no player row in your title yet), or when the self-claim route is unavailable (503). The hosted claim page does not serve transfers.
Which secret key and body fields does claim-transfer need?
The destination title's X-Game-Secret-Key. Required body fields: claim_code, target_player_name, target_player_email, target_player_phone and target_currency_id (numeric). target_player_id is optional and only used after an account-selection prompt.
What does target_currency_id control?
Which of your title's currencies is credited. It must belong to your title, or the claim fails with 400. If its rate differs from the source currency's, the net is converted at the two rates (rounded down to 0.01).
What does 200 needs_account_selection mean, and how do I resubmit with target_player_id?
More than one player in your title shares the claim phone, and the email did not pick one. The body has status: "needs_account_selection", the claim_code, and candidates (each with player_id and a masked email_hint). Let the player choose, then send the same request again with target_player_id set to the chosen player_id. Nothing was credited yet.
How do I use attempts_remaining on a 400 invalid code, and why should I never retry automatically?
In practice a wrong code usually returns 404 "Invalid claim code or transfer not intended for this game", because the code is looked up together with your title; 400 with attempts_remaining only appears when a code is found but fails verification. If you get attempts_remaining, show it and stop before it reaches zero, because at zero the transfer fails and the sender is refunded.
What does a 403 phone mismatch mean?
The target_player_phone you sent is not the number the transfer was addressed to. This is the check that stops a found code from being redeemed by someone else. Do not retry with other numbers.
What do locked_dimension, retry_after_seconds and Retry-After mean on 429 CLAIM_LOCKED?
locked_dimension names what tripped the lock: phone, email or ip. retry_after_seconds (also sent as retry_after) and the Retry-After header give the wait in seconds. Show it and stop.
Does claim-transfer create a player row?
Yes. If no player in your title matches the email and phone, a player is created with the addressed phone as part of the claim. If the phone is already tied to a different INVO identity, you get 409 PHONE_SHARE_APPROVAL_REQUIRED first.
Player-to-Player Sends
Read the Player-to-Player Sends page
What is the call sequence for a peer send from initiate through approve?
1. Mint the sender's token with POST /api/sdk/player-token (email and phone). 2. POST /api/currency-sends/initiate-send (game secret). 3. Re-mint the token right before approval, or use the player_token from a re-keyed initiate. 4. The sender approves with approveHosted({ flow: "send" }) or a QR grant with flow: "send". 5. POST /api/sdk/send/{id}/approve with the device_code.
Confirm with transfer.claim_pending or the status read.
How does a pending_claim send differ from a completed send?
pending_claim (verification_state: "approved") means the sender approved and the value is waiting to be collected before claim_info.claim_code_expires_at; it can still expire and refund. completed means the recipient was credited. It is final, and the send status adds completed_at, claimed_by_player_name and claimed_by_player_email.
Which status fields are shown only to the receiving game?
to_phone and to_identity_id, so the receiving game can attribute the send to one of its players. to_identity_id is null when the phone matches more than one of its players.
How do I set up a same-game send, and what send_type is reported?
Set receiving_game_id to your own game id. A same-game send skips the cross-game policy check (it only needs your title to be live), and send_details.send_type is "peer_to_peer". A send to another title reports "cross_game".
How does a receiving game list inbound sends without transaction IDs?
Call GET /api/transfers/inbound-pending?player_phone=... (or player_email=...) with your game secret. It returns collectable sends and transfers addressed to that player in your title, with flow, amount, net_amount, source game and claim_code_expires_at, but never the claim code. The transfer.claim_pending webhook with direction: "inbound" tells you as they arrive.
How do I use resend-claim, and what do RESEND_COOLDOWN, CLAIM_DELIVERY_WITHHELD, CLAIM_SMS_SUPPRESSED and NOT_CLAIMABLE mean?
Call POST /api/sdk/send/{id}/resend-claim with the sender's player token and no body, once the send is in its claim window. It re-delivers the same claim link on the original channel, and 200 returns status: "resent", retry_after and claim_delivery. The refusals:
429 RESEND_COOLDOWN: too soon. Waitretry_afterseconds.409 CLAIM_DELIVERY_WITHHELD: the receiver email is not on file for the receiver's phone, so nothing can be sent and the receiver collects in the game.409 CLAIM_SMS_SUPPRESSED: this recipient collects in-app, so there is nothing to resend.400 NOT_CLAIMABLE: the send is not awaiting a claim (not yet approved, or already done).
When does a recipient get the claim link by email, and when must they collect in game?
INVO emails the claim link (to the hosted claim page) when the receiver has an email already on file for the receiver phone. If the receiver email you supplied is not on file for that phone, nothing is sent and the receiver collects in the game with their player token (minted with email and phone).
Why might a minor's send return 202 at initiate or approve?
When the sender's account is flagged as a minor with a guardian on file, initiate returns 202 with a guardian_approval block, and approve keeps returning 202 GUARDIAN_APPROVAL_PENDING until the guardian agrees. Poll GET /api/transactions/{id}/approval-status, then approve as normal. A minor with no usable guardian is refused at initiate with 403 GUARDIAN_REQUIRED.
How does a peer send differ from a cross-game transfer in paths, flow values and who holds the claim code?
- Who: a send goes from one player to a different player (it can be within one title). A transfer moves one player's own currency to another title.
- Paths: sends use
/api/currency-sends/...and/api/sdk/send/.... Transfers use/api/transfers/...and/api/sdk/transfers/.... - Flow values:
send/send_receiptfor sends, andtransfer/transfer_receiptfor transfers. - Claim code: on a send it is never returned to the sender; the recipient is notified (by email link, or in-app), and the sender can only resend that notification. On a transfer the code goes back to the source title in the approve response and its status read.
Send Destinations
Read the Send Destinations page
What does POST /api/currency-sends/available-destinations accept and return?
It takes { "source_game_id": "<your game id>" } with your game secret. It returns status, source_game_id, source_game_name, source_game_icon, source_currency_name, source_currency_icon, universal_transfers, transfer_mode, available_games and total_destinations, plus linked_game_ids in linked mode.
Which available_games fields should a dropdown show?
Show game_name, game_icon and currency_name / currency_symbol (or currency_symbol_url), and keep game_id as the value you pass as receiving_game_id. tenant_type, developer_name and genre are useful secondary text. accepts_steam_origin_value says whether the destination accepts currency bought through a platform store.
Should I check a send amount against the destination's limits before initiating, and whose currency limits apply?
Initiate validates the amount against the sending title's currency (its minimum_transfer_amount, default 0.01, and its maximum if set), plus a platform-wide ceiling per send and the sender's balance and velocity limits. The minimum_transfer / maximum_transfer on each destination describe that destination's currency, not what initiate enforces for your send. Validate against your own currency's limits.
Which ID should a same-title send use as receiving_game_id?
Your own game id, the same id you authenticate as. Your title is not included in this endpoint's available_games list (it shows other titles), but a same-title send is always allowed while your title is live.
Does the endpoint return players or only games?
Only titles (games and platform tenants your title can reach), never players. For a list of places a specific person already has an account, use POST /api/players/destinations with their email and phone.
What errors can it return?
400: missing body, missing or non-numericsource_game_id, or a missing or invalid game secret.403:source_game_idis not your title, or your title does not allow outgoing transfers.404: source title not found.429: rate limited.500: unexpected error. Fall back to cached data.
Initiate Player Send
Read the Initiate Player Send page
What is the difference between setting receiving_game_id to my own title versus another title?
Your own title makes a same-title peer send (send_type: "peer_to_peer"), allowed whenever your title is live, with no linked-games check. Another title makes a cross-game send (send_type: "cross_game"): both titles must be live and your universal or linked policy must allow the destination, otherwise you get 403 CURRENCY_SEND_POLICY_VIOLATION.
Which body fields are required and optional?
Required fields: client_request_id, sender_player_name, sender_player_email, sender_player_phone, receiver_player_phone, receiving_game_id and amount (decimal string). receiver_player_email is optional and recommended.
Why does receiver_player_email matter, and what changes on a phone-only send?
Sends are routed by phone, so a phone-only send still reaches the owner of that number. When you supply the email, the phone-share consent check runs at initiate, before anything is reserved, and the claim link is emailed if that address is on file for the phone (claim_delivery_planned: "email"). If it is not on file, the planned delivery is "none". With no email at all, the planned delivery is the legacy "sms" value, unless an email is already on file for the phone.
How are the fees_preview parts (sending_game_fee, receiving_game_fee, platform_fee, net_amount) derived?
The total fee is 10% of amount. sending_game_fee and receiving_game_fee are each 3.5%, rounded to the cent, and platform_fee is what is left of the 10% (3%, sometimes a cent less after rounding). net_amount is amount minus total_fee. source_rate_pct and target_rate_pct report the side rates. On a same-title send both title shares go to your title.
What does CURRENCY_SEND_POLICY_VIOLATION mean under Universal and Linked policies?
With universal_transfers: "yes", it means the receiving title is not live. With linked transfers, it means you have no linked titles, the receiving title is not in your linked_game_ids, or it is not live. The message names the reason. It never applies to a same-title send.
Why does a send to a title in Testing return 403?
Both titles must be live to move currency, so a receiving title in testing returns 403 with error_code: "TARGET_GAME_NOT_LIVE" (a sending title in testing returns SOURCE_GAME_NOT_LIVE). The receiving title's owner has to switch it to Live in their console, so it may not be in your hands.
What are the amount rules: minimum, maximum, available balance, daily limit and hourly send limit?
- Minimum and maximum: at least your currency's minimum (default 0.01), and no more than its maximum if one is set.
- Ceiling: a platform-wide ceiling per send applies (default 1,000,000).
- Balance: the sender's available balance must cover the amount, otherwise
400. - Velocity: up to 25 sends per hour and 1,000,000 units per day per player, both set by the platform. Going over returns
429 velocity_limit_exceeded.
What does velocity_limit_exceeded mean, and how does it differ from other 429s?
429 with error: "velocity_limit_exceeded" means the sender reached the hourly send count or the daily send amount, and message says which. Other 429s come from request rate limits (per player, per caller and per network; honor Retry-After), from rapid_send_detected (a repeated identical send), or from temporary blocks after repeated failures (which carry retry_after). All of them mean back off, never loop.
What does verification_expires_at represent?
The end of the sender's approval window. If approve has not succeeded by then, the reservation is released back to the sender. Starting a device grant pushes this window out past the grant's own expiry, so read the value from the response instead of assuming a fixed length.
Claim Sent Currency (claim-code fallback)
Read the Claim Sent Currency (claim-code fallback) page
When should I call POST /api/currency-sends/claim-currency instead of confirm-receipt?
Only when confirm-receipt can't serve the recipient. That is mainly 409 receiver_not_enrolled_use_claim_code (no player row in your title), since this route creates the player. A recipient who is not in your title at all is better served by the emailed hosted claim link.
Which request fields are required, and what must receiver_player_phone match?
Required fields: claim_code, receiver_player_name, receiver_player_email and receiver_player_phone, with the receiving title's game secret. receiver_player_phone must be the number the send was addressed to, otherwise the claim returns 403.
What does 200 needs_account_selection contain, and how do I resubmit with receiver_player_id?
The body contains status: "needs_account_selection", claim_code, a message, and candidates (each with player_id and a masked email_hint). Resubmit the same body with receiver_player_id set to the chosen candidate's player_id, or correct receiver_player_email.
What happens to the sender when a claim returns 400 expired?
The sender gets the full reserved amount back. If the sweep has not refunded it yet, the claim attempt itself releases the reservation and fails the send. The recipient cannot collect; the sender has to start a new send.
What does 400 WRONG_CLAIM_ENDPOINT point to?
The code belongs to a cross-game transfer. The body's expected_endpoint is /api/transfers/claim-transfer, and expected_body_fields lists what that endpoint needs, including target_currency_id.
Which success fields should I use, and which balance is canonical?
Use top-level new_balance (the recipient's balance after the credit, which is canonical), currency_name, transaction_id, completion_time and order_id, plus send_details.amount_received (the net). send_details.new_balance repeats the balance for older callers.
Does claim-currency create a player row when none exists?
Yes. If no player in your title matches, one is created with the addressed phone as part of the claim. This is the one thing confirm-receipt cannot do. A phone already tied to a different INVO identity returns 409 PHONE_SHARE_APPROVAL_REQUIRED first.
When should a recipient who isn't in my title be sent to the hosted claim page?
You don't send them yourself: INVO emails them the hosted claim link (https://invo.network/claim) when their email is already on file for the addressed phone. On that page they confirm their email, get a passkey and collect, and you receive transfer.received as normal. If no link could be sent, the sender can use resend-claim (which answers CLAIM_DELIVERY_WITHHELD when the email is not on file), and the recipient collects in your title instead.
Back to the FAQ hub. Something not answered here? Email partners@invo.network.