Sandbox Testing
The Invo Sandbox environment provides a safe testing space to integrate and validate your title's currency operations without affecting real player data or processing actual payments. Perfect for development, testing, and QA workflows.
API paths take the sandbox prefix. Page links do not.
Every API call in sandbox goes to https://sandbox.invo.network/sandbox/..., which is why the SDK base URL carries the /sandbox and why every sample on this site shows it. Leave it off and the call returns 404, which is indistinguishable from an endpoint that does not exist. That is the single most common sandbox mistake.
Links Invo hands you for a PERSON to open are different. Hosted checkout and hosted card setup are served at the host root, so they come back as https://sandbox.invo.network/checkout?session=... and https://sandbox.invo.network/card-setup?session=..., with no /sandbox in them. Always use the URL exactly as returned rather than assembling one from your base URL. Invo knows which environment minted it; a URL you build yourself does not.
Safe Testing Environment
Test all API operations without risk to production data or real financial transactions.
Separate Everything
A separate database, separate keys, separate game IDs. Nothing you do in sandbox can reach your production data.
Realistic Simulation
Full API functionality with simulated payments, sender approval, and transfer operations.
One Click From Your Console
Open sandbox from the INVO console and your account is there, with a sandbox copy of each of your titles. No second sign-up.
Nearly Full Parity
Everything works the same way, with one exception worth planning around: cross-game transfers and subscriptions need a live title. See below.
Test Scenarios
Built-in error scenarios and edge cases to validate your error handling logic.
Sandbox Environment
The INVO Network sandbox environment uses a separate base URL and requires the /sandbox prefix for all API routes. Test with fake payments and approvals without real money.
What differs from Production
- • Same code, same fees. Sandbox runs the release Production runs, and its responses and webhooks report the Production fees.
- • Test approval shortcuts. The test-only tools on this page settle approvals without a real device or passkey. They do not exist in Production.
- • No company verification for platform status. In Sandbox you can claim platform status without completing company verification; Production requires it.
- • Smaller capacity. Sandbox is sized for integration testing, not load tests. See Rate Limits for the Production limits.
Production Environment
https://invo.network/api/* and /auth/*Sandbox Environment (Testing)
https://sandbox.invo.network/sandbox/api/*// Sandbox API call, same single-header auth as production
const SDK_KEY = process.env.INVO_SDK_KEY; // ivsdk_test_<random>
const r = await fetch(
'https://sandbox.invo.network/sandbox/api/player-balances/player/by-email/test@example.com',
{ headers: { 'X-Game-Secret-Key': SDK_KEY } }
);
console.log(await r.json());Sandbox vs Production: what changes
- • Path prefix. Sandbox is
/sandbox/api/...; production is/api/.... - • SDK key. Different key per environment, and new keys say which:
ivsdk_test_for sandbox (test mode),ivsdk_live_for production. Sandbox keys do not work against production and vice versa; a prefixed key sent to the other server returns401 KEY_FOR_OTHER_ENVIRONMENTwith a message naming the right server. Older keys without a prefix keep working in their own environment. - • Database. Fully isolated; sandbox players, balances, and transactions live in their own database.
- • Card processing. Sandbox accepts test cards only. No real money moves regardless of amount.
- • Game IDs and keys are different. A title in sandbox has its own
game_idand its own keys, deliberately. Nothing is shared, so a value copied from one environment cannot work in the other, and an ID in a log line or a support ticket can only ever mean one environment. - • Going live is self-serve in both environments. You set a title from Testing to Live yourself in the console, and in sandbox you need to before testing transfers, sends or subscriptions, because a Testing title cannot receive a cross-game transfer or a send, or sell a subscription. No INVO review is needed.
- • One thing does cross, and only when you ask. You can copy a sandbox title into production so you do not rebuild it by hand. It carries the title's own description and settings, never a key, an ID, a fee, a rate or any player data. See Your account across the two environments below.
Your account across the two environments
You have one INVO account. Sandbox is reached from it, and the two environments share nothing except who you are.
Opening sandbox
Click Open sandbox in the INVO console. Your sandbox account is created the first time, and you are signed in. There is no second password: the sandbox account is entered from your INVO console, so there is one credential to look after rather than two.
If you signed up in sandbox first
Every sandbox developer has a production account. When you finish signing up in sandbox, we create your production account with the same sign-in and link the two, or, if a production account already exists for the same verified email address, we link to it. Both signups prove the address (an emailed code, or a verified sign-in provider), so nothing is asked of you and nothing changes in how you sign in. A link also copies the identity and bank details you entered in sandbox into production once, where they wait for review: sandbox never marks anything verified in production.
Your titles
Each of your production titles gets a sandbox copy with the same name, artwork, fees and settings, and its own game ID and its own keys. Settings are copied once, when the copy is made, and the two then go their own way: changing a fee in production does not change it in sandbox, and neither does the reverse. Nothing you do in sandbox can affect a production title.
A title you built in sandbox first
Titles move the other way too, so you do not have to rebuild in production what you already built and tested. Every title (game or platform) you create in sandbox is copied into production under your linked production account, automatically and once, including titles you made before the accounts were linked, and we email you when it exists. A copied platform keeps its platform type in production once your identity verification is approved there; until then it arrives as a game. If a copy is refused (for example, the name is already taken in production) or fails, press Copy to production on the title in the sandbox console to ask again. A title is never copied twice. After the copy lands the two are independent. There is no sync in either direction, and editing the sandbox title copies nothing, so from that point on you maintain each one where it lives.
What crosses
- • The name, developer and publisher names, description, genre, platform and title URL.
- • The icon, the poster and the currency symbol artwork. We take our own copy into production storage, so the production title never points at a sandbox asset. If one cannot be fetched the title is still created without it and we tell you which to re-upload.
- • The default currency: its name, starting amount, symbol, type, whether it is transferable, and its minimum and maximum transfer amounts.
- • The transfer policy (whether the title accepts universal transfers).
What never crosses
- • Every key and secret. Production mints entirely new ones, starting
ivsdk_live_andwhsec_live_. The two environments share no secrets, so there is nothing to carry across even in principle. - • Every identifier. Production mints its own
game_id, so an ID in a log line or a ticket can only ever name one environment. - • Fees and currency rate. The production title gets the network defaults and the platform peg. Sandbox is the lower-trust environment and these decide what money is worth.
- • Linked titles. Those name sandbox titles, which do not exist in production.
- • Per-title flags and distribution settings. Each is a production decision with its own door; a sandbox value is not evidence about any of them.
- • Anything identity, KYC or bank, with the title. Those cross once, when the accounts link, and are reviewed in production.
- • Players, balances, items, webhook subscriptions, subscriptions and ledger history. Nothing else with a game ID crosses. A copied webhook subscription would deliver sandbox test events to your production endpoint.
The copy arrives in Testing, and going live stays your decision. A copied title lands in Testing exactly as a title you create by hand does. When you are ready, set it to Live yourself from the title's settings in the production console. Until then, remember that a Testing title cannot be a cross-game transfer destination, cannot receive a currency send and cannot sell subscriptions.
Plan for this one: a title starts in Testing, and a Testing title cannot be a cross-game transfer destination, cannot receive a currency send and cannot sell subscriptions. In sandbox, switch it to Live yourself before you test any of those. Do the same in production when you are ready to launch.
Test Data & Scenarios
The sandbox comes pre-loaded with test data to help you get started quickly:
Test Players
Test Card Numbers
Subscription time travel
A monthly billing cycle cannot be validated by waiting a month. Sandbox therefore exposes four endpoints that push one subscription through the real renewal machinery on demand: the same funding, dunning and step-up code that runs on a live billing day, described in full on Renewals, Funding & Dunning. Nothing here is a mock: a forced renewal really runs the money path, against sandbox test cards, and produces the same webhooks your production integration will receive.
These endpoints do not exist in production
They are not merely disabled, permission-gated or hidden behind something we could switch on for you. The routes are never registered on the production service at all. Calling any of them against https://invo.network returns a plain 404, indistinguishable from any other unknown path, and there is no production credential that changes that.
So keep time travel inside your test harness. Never ship code that calls it, and never make a production code path conditional on it. In production the billing clock is ours and webhooks are how you learn it moved.
Two credentials, not one
Every call carries your usual sandbox X-Game-Secret-Key and a second header, X-Sandbox-Clock-Key. The second one exists because force-renewal genuinely charges a card: a leaked game secret should not also be a charge-on-demand button. Both are required on all four endpoints, and presenting one without the other is refused.
- The clock key is per title, and you issue it yourself from the sandbox developer API:
GET /sandbox/api/dev/games/{game_id}/sandbox-clock-key, documented immediately below. A console screen for it is on the way; until it ships, that endpoint is how you get one. - It is deliberately not retrievable with the game secret. The issuing endpoint authenticates with your developer dashboard session, never with
X-Game-Secret-Key. If the game secret could fetch the clock key, the game secret would once again be sufficient on its own and the second factor would be decorative. That constraint is the entire reason this credential exists. - It is a sandbox credential only. There is no production counterpart, because there is no production endpoint to present it to.
- Rotation has two modes and the difference matters. A routine rotation leaves the previous key working for a 7-day grace so a test run already in flight does not break; a leaked key needs the immediate mode, which revokes the old value on the spot. See the rotate endpoint below, and Partner Credentials & Key Handling.
- Server-side only. Never in a browser bundle, never in a repository, never in chat.
Getting and rotating your clock key
Two endpoints on the sandbox developer API, both authenticated with the dashboard session token your console login issues, Authorization: Bearer <token>, the same credential the dashboard's own pages use. The game secret is not accepted here and never will be; you must be signed in as the developer account that owns the title. Both live only on the sandbox service: calling them on production returns 409 SANDBOX_ONLY and issues nothing, because a production clock key could never authenticate anything.
| Method | Path | Purpose |
|---|---|---|
| GET | /sandbox/api/dev/games/{game_id}/sandbox-clock-key | Reveal this title's clock key. The first call mints it. Keys are created only for titles that ask, so newly_issued is true exactly once. Repeat calls return the same value. |
| POST | /sandbox/api/dev/games/{game_id}/sandbox-clock-key/rotate | Mint a new key. Body {"immediate": false} (the default) parks the old one for 7 days; {"immediate": true} revokes it now. |
DEV=https://sandbox.invo.network/sandbox/api/dev
GAME_ID=1234
# Reveal (mints on the first call). Dashboard session token, NOT the game secret.
curl -s "$DEV/games/$GAME_ID/sandbox-clock-key" \
-H "Authorization: Bearer $INVO_DASHBOARD_TOKEN"
# → { "success": true, "game_id": 1234,
# "header": "X-Sandbox-Clock-Key",
# "sandbox_clock_key": "ivclk_…",
# "issued": true, "newly_issued": true,
# "rotated_at": "2026-08-14T11:02:57+00:00",
# "previous_key_active": false, "previous_key_valid_until": null,
# "note": "Send this in the X-Sandbox-Clock-Key header IN ADDITION to your …" }
export INVO_SANDBOX_CLOCK_KEY=ivclk_… A leaked key needs immediate: the default does not revoke it
The default rotation is a hygiene rotation: it mints a new key and keeps the old one valid for a 7-day grace window, so a test rig mid-run does not hard-break. That grace is exactly wrong for a key that has escaped. Rotating the ordinary way and assuming it is dead leaves a charge-on-demand credential live for a week.
# Routine: new key now, old key valid for 7 more days.
curl -X POST "$DEV/games/$GAME_ID/sandbox-clock-key/rotate" \
-H "Authorization: Bearer $INVO_DASHBOARD_TOKEN"
# LEAKED KEY: revoke the old value immediately, no grace.
curl -X POST "$DEV/games/$GAME_ID/sandbox-clock-key/rotate" \
-H "Authorization: Bearer $INVO_DASHBOARD_TOKEN" \
-H "Content-Type: application/json" \
-d '{"immediate": true}'The rotate response returns the new key once and tells you which mode ran: immediate, previous_key_active and previous_key_valid_until. The previous key's value is never returned. If you have lost it, rotate. immediate must be a real boolean; a string is a 400 INVALID_IMMEDIATE, deliberately, so a quoted "false" cannot silently trigger the emergency path.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | INVALID_IMMEDIATE | Rotate only. immediate was not a JSON boolean. |
| 401 | none | No valid dashboard session. Note this is a session failure: sending X-Game-Secret-Key here does not authenticate anything. |
| 403 | none | You are signed in, but this title is not yours, or it is not active. An inactive title is never minted for. |
| 409 | SANDBOX_ONLY | You called production. Clock keys are issued only by the sandbox API, because the endpoints that consume them exist only there. Nothing was issued or changed. |
| 500 | ISSUE_FAILED / ROTATE_FAILED | The key could not be minted or rotated. Nothing changed; retry. |
Watch the path. These four sit at /sandbox/subscriptions/…. There is no /api segment, unlike every other subscription call, which is /sandbox/api/subscriptions/…. A 404 on your first attempt is almost always this.
BASE=https://sandbox.invo.network/sandbox/subscriptions
SUB=SUB_1786012800_A1B2C3D4
# 1. Pretend a month went by: the NEXT period becomes due.
curl -X POST "$BASE/$SUB/advance-clock" \
-H "X-Game-Secret-Key: $INVO_SDK_KEY" \
-H "X-Sandbox-Clock-Key: $INVO_SANDBOX_CLOCK_KEY" \
-H "Content-Type: application/json" \
-d '{"intervals": 1}'
# 2. Bill it now instead of waiting for the scheduled renewal.
curl -X POST "$BASE/$SUB/force-renewal" \
-H "X-Game-Secret-Key: $INVO_SDK_KEY" \
-H "X-Sandbox-Clock-Key: $INVO_SANDBOX_CLOCK_KEY"
# Repeat 1 and 2 to run the subscription across as many consecutive
# periods as you want to test.The loop, and why it is a loop
advance-clock then force-renewal, repeated, is the intended pattern and it runs for as many consecutive periods as you like. Each pass bills a new, later period, and no period is ever billed twice. That is how you get a year of renewal history (trial conversion, a decline, a recovery, an expiry) into a few minutes of testing.
Calling force-renewal repeatedly on its own does not work, deliberately. Each success moves the subscription onto the next, not-yet-due period, and billing ahead is capped at one period; a further call without moving the clock is refused with PERIOD_TOO_FAR_AHEAD. Say a period went by, then charge it.
/sandbox/subscriptions/{subscription_id}/advance-clock
Makes the next period due, right now. No money moves here. The charge happens on the next scheduled renewal, or immediately if you follow with force-renewal.
Request body (optional)
| Field | Type | Notes |
|---|---|---|
intervals | integer | 0 to 60, default 1. 1 simulates one billing interval, one month on a monthly subscription. 0 means "become due now" and nothing else, which is how you trigger a first charge or an early dunning retry without touching the period window. |
Response (200)
{
"status": "ok",
"action": "advance-clock",
"intervals": 1,
"intervals_applied_to_window": 0,
"window_moved": false,
"window_already_elapsed": false,
"before": { ...clock fields before... },
"after": {
"subscription_id": "SUB_1786012800_A1B2C3D4",
"status": "active",
"period_seq": 3,
"current_period_start": "2026-09-13T09:14:22+00:00",
"current_period_end": "2026-10-13T09:14:22+00:00",
"next_charge_at": "2026-08-14T11:02:57+00:00",
"trial_end": null,
"cancel_at_period_end": false,
"wallet_only": false,
"has_payment_method": true,
"amount_usd": "9.99"
},
"note": "The subscription is now due..."
}before and after carry the same eleven clock fields, so you can diff exactly what the call changed.
It arms the next period. It does not rewind onto a paid one
"A month went by" is expressed by making the subscription due, not by dragging its period window backwards. The window stays exactly where the last successful renewal left it (one interval after the period just paid) and the next renewal bills that one. This is what makes the advance-and-bill loop repeatable: nothing moves backwards, so nothing can be re-billed.
The window does still move across periods nobody has paid for, which is how you elapse a trial so it converts, or reproduce a long gap in billing. intervals_applied_to_window tells you how many of the intervals you asked for the window actually gave up. 0 is the ordinary answer once anything has been collected, and the advance is carried entirely by the due marker. window_already_elapsed: true means the current window is fully in the past, so the next renewal will record it as forgiven and bill the following one.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | INVALID_INTERVALS | Not an integer, or outside 0 to 60. |
| 409 | SUBSCRIPTION_NOT_LIVE | Already canceled or expired; there is no clock left to advance. |
| 409 | WINDOW_ALREADY_PAID | The subscription's window already sits behind what the member is paid through, so arming it would bill a period twice. Nothing is changed. Start from a fresh subscription. The response echoes paid_through and requested_period_start so you can see the overlap. Only checked when intervals is 1 or more: with intervals: 0 the window is not touched at all, so the same overlap is caught one step later, by the renewal, as NO_CHARGEABLE_PERIOD. Either way nothing is billed twice. |
| 500 | INTERNAL_ERROR | The clock could not be advanced. Nothing was changed; retry. |
/sandbox/subscriptions/{subscription_id}/force-renewal
Runs the renewal now: on a card subscription the card is charged the full price (the balance is never used), then mint, settle, split the fee, emit subscription.renewed(and purchase.completed for the mint leg, which you must not grant from). This one really charges a card, a sandbox test card, which is the point. No request body.
Response (200)
{
"status": "ok",
"action": "force-renewal",
"period_seq": 3,
"attempt_no": 1,
"amount_usd": "9.99",
"amount_coins": "99.90",
"engine_stats": { "renewed": 1 },
"subscription": { ...clock fields after the run... }
}engine_stats is the renewal engine's own counters for this one run, so a failed renewal is as legible as a successful one. Exactly one outcome counter is set, and it is one of renewed, declined, insufficient, auth_required, containment, ambiguous, settle_failed, errors, abandoned (the subscription was cancelled or ended under the claim, so nothing was charged and no retry was consumed) or refused_over_ceiling (the price failed the per-renewal ceiling; held, not dunned). periods_forgiven may appear alongside the outcome when the run had elapsed windows to catch up on. Treat any counter you did not expect as a real outcome rather than switching on a fixed set of two or three.
Which outcome you get is decided by the card on the subscription. Pick from the test cards above to steer it, or use force-failure when you want a specific outcome regardless of card.
Refusals are the two shared sets below: opening a renewal and all four endpoints.
/sandbox/subscriptions/{subscription_id}/force-failure
Synthesizes a failed renewal and runs the real dunning ladder: the retry budget arithmetic, the retry spacing, the move to past_due, the eventual expired, and the member-facing webhooks that go with each. No card is touched and no money moves, which makes this the endpoint to build your failure handling against.
Request body (optional)
| Field | Type | Notes |
|---|---|---|
outcome | string | One of card_declined (default), insufficient_funds, error. Anything else is a 400 INVALID_OUTCOME. For the step-up branch use force-auth-challenge. |
failure_code | string | Free text, truncated at 100 characters. Surfaces on subscription.payment_failed. Defaults to outcome. |
failure_message | string | Free text, truncated at 2000 characters. The human-readable reason on the event. |
Response (200)
{
"status": "ok",
"action": "force-failure",
"period_seq": 3,
"attempt_no": 2,
"outcome": "card_declined",
"failure_code": "card_declined",
"retry_budget_used": 1,
"retry_schedule_days": [2, 3, 2],
"retries_remaining": 2,
"containment_failure_code": "…",
"subscription": { ...clock fields after the run... }
}attempt_no and retry_budget_used are different numbers on purpose: not every failure consumes retry budget. Call this repeatedly to walk a subscription all the way to expired and verify your revocation path. containment_failure_code is echoed back so you can pass it as failure_code to reproduce the origin-containment branch: held rather than retried, exempt from the retry budget, and it never expires the member.
Refusals are the two shared sets below (this endpoint opens a real claim, so it can return every refusal in opening a renewal too), plus 400 INVALID_OUTCOME.
/sandbox/subscriptions/{subscription_id}/force-auth-challenge
Forces the step-up authentication path, the behaviour that surprises every integrator. The subscription moves to awaiting_authentication, the dunning clock pauses, and no retry budget is consumed. No card is touched. Optional body: failure_message (string, truncated at 2000 characters).
Response (200)
{
"status": "ok",
"action": "force-auth-challenge",
"period_seq": 3,
"attempt_no": 2,
"auth_challenges_used": 1,
"max_auth_challenges": 2,
"downgraded_to_decline": false,
"retry_budget_used": 0,
"subscription": { ...clock fields after the run... }
}This call does not emit the step-up event, and returns no link
It drives the state machine only. The 200 carries no confirmation URL, and subscription.authentication_required does not fire from this request. The challenge object and its one-time link are minted by a separate pass that runs on the billing schedule, and only for a challenge that has a real payment behind it. A synthesized challenge has no payment attached, so no link is issued for it at all: an honest link is not something we can fabricate.
So use this endpoint to test the state half: the pause, the retained access, the untouched retry budget, your awaiting_authentication handling. To exercise the link and the event end to end, run a real force-renewal against the 3D Secure test card above. Do not go debugging your webhook configuration because nothing arrived after this call.
What the ceiling looks like when you hit it
Call this repeatedly and the engine stops honouring the challenge past max_auth_challenges. A card that challenges forever would otherwise loop free of the retry budget. The signal is in the numbers, not in a flag:
auth_challenges_usedstops rising and stays pinned atmax_auth_challenges.retry_budget_usedstarts incrementing. It sits at0for every honoured challenge, because a challenge is not a payment judgement.subscription.statusmoves fromawaiting_authenticationtopast_due, then toexpiredonce the budget is spent, with the ordinarysubscription.payment_failedevents along the way.
Do not assert on downgraded_to_decline. It is reported from a count taken after the downgrade has already rewritten the attempt, so in practice it stays false even on the calls that were downgraded. Assert on the status and on retry_budget_used instead. That ceiling is worth exercising. It is what stops a member sitting in awaiting_authentication forever.
Refusals are the two shared sets below (this endpoint opens a real claim, so it can return every refusal in opening a renewal too). AUTH_CHALLENGE_OPEN is the one to expect here: once a challenge object exists for the period, the next call is refused until it is completed or expires.
Refusals shared by the three that open a renewal
force-renewal, force-failure and force-auth-challenge all open a real claim (the same period record and attempt record the scheduled renewal opens) before they do anything else. So all three return every refusal below, not just the one that spends money. Write your handling once.
| HTTP | Code | Meaning |
|---|---|---|
| 409 | PERIOD_TOO_FAR_AHEAD | You are trying to open a period more than one interval ahead of now without moving the clock. Call advance-clock first. Carries period_start, max_period_start and max_future_intervals. |
| 409 | ATTEMPT_IN_FLIGHT | A previous attempt on this period has not resolved yet, so whether money moved is not yet known and a second attempt is not allowed. has_processor_payment_reference tells you whether a payment was already opened. Wait for it to settle. |
| 409 | AUTH_CHALLENGE_OPEN | A step-up challenge is live for this subscription and the member still holds the link; opening another attempt would start a second payment. Complete it or wait for challenge_expires_at. This is the refusal you meet when you call force-auth-challenge again while a challenge is still open. |
| 409 | AMOUNT_OVER_CEILING | The price is above the absolute per-renewal ceiling. Refused, not clamped; no retry budget is spent. |
| 409 | NO_CHARGEABLE_PERIOD | No billable period can be derived: usually the current window overlaps one that was already invoiced, or the current cycle is already terminal. |
| 409 | SUBSCRIPTION_NOT_LIVE | Already canceled or expired. |
| 500 | CLAIM_FAILED | The claim could not be opened. Nothing was charged. |
A cancelled subscription is retired, not charged, and that is a 200
If cancel_at_period_end is set, all three return 200 with "action": "retired-at-period-end" and "retired": true. Nothing was billed, no attempt was recorded, and no failure was synthesized. That is the production behaviour at the cancellation boundary, reproduced. Branch on action, not on the status code.
Refusals shared by all four
| HTTP | Code | Meaning |
|---|---|---|
| 401 | SANDBOX_CLOCK_UNAUTHORIZED | X-Sandbox-Clock-Key is missing or not valid for this title. Three cases collapse into this one response: header absent, key wrong, and no key has ever been issued for this title. That is deliberate, so the refusal cannot be used to enumerate which tenants have provisioned the second factor. There is consequently no "not configured" state to detect: if you have not issued a key yet, that is indistinguishable from a typo. Issue or re-reveal one (see above) before assuming anything else is wrong. |
| 401 | none | X-Game-Secret-Key is missing or invalid. Reported separately from the clock key so a two-credential handshake is debuggable. |
| 403 | none | The title is not active. |
| 404 | none | Unknown subscription_id in this title. Every lookup is tenant-scoped, so another partner's subscription reads as absent. A 404 on every call means you are on production, or missing the path note above. |
| 429 | RATE_LIMITED | These are hand-driven test tools, not a traffic surface. Roughly 10 calls per minute per subscription and 60 per minute per source address. |
| 503 | FLOW_PAUSED | Subscription billing is paused platform-wide for maintenance. All four honour it, including advance-clock. Arming a charge during a pause would only fire it the moment the pause lifted. Retry shortly. |
What time travel will not do for you
- • It cannot move a subscription's clock behind a period that has already been paid. That is refused with
WINDOW_ALREADY_PAID, and the period window is never rewound across a paid boundary even when you ask for more intervals than are available. Billing one calendar period twice is not reachable from this surface. If you need a clean clock, create a new subscription. - • It cannot un-bill anything. A forced renewal is a real renewal, with a real ledger entry and real webhooks. Refund it the ordinary way if you need it reversed (see Subscription Refunds; a full refund also cancels the subscription).
- • It cannot be reached in production, with any credential, ever. See the top of this section.
Testing Checklist
Use this checklist to ensure comprehensive testing of your integration:
Basic Operations
Error Scenarios
Ready for Production?
Once you've thoroughly tested your integration in sandbox, switch to production:
- • Update base URL:
https://invo.network - • Remove /sandbox prefix: Use
/api/*instead of/sandbox/api/* - • Get the title into production: It is copied there for you, once, automatically; press Copy to production in the sandbox console only to ask again after a refused or failed copy. You do not recreate it by hand. It arrives in Testing with new keys of its own.
- • Use production credentials: Get your production game_secret from console.invo.network. Sandbox keys never work in production, and the copy does not carry any across.
- • Environment detection: Read the environment from the key prefix (
ivsdk_live_orivsdk_test_), so going live is swapping the key. The Web and Python SDKs do this for you. - • Test with real cards. Real money will be charged in production.
// Environment configuration
const INVO = {
production: {
baseURL: 'https://invo.network',
apiRoute: '/api'
},
sandbox: {
baseURL: 'https://sandbox.invo.network',
apiRoute: '/sandbox/api'
}
};
const SDK_KEY = process.env.INVO_SDK_KEY; // ivsdk_live_... or ivsdk_test_...
// New keys name their environment. Older ivsdk_ keys have no prefix,
// so fall back to your own setting for those.
const env = SDK_KEY.startsWith('ivsdk_live_') ? 'production'
: SDK_KEY.startsWith('ivsdk_test_') ? 'sandbox'
: (process.env.NODE_ENV === 'production' ? 'production' : 'sandbox');
const cfg = INVO[env];
// Read a player's balance
const url = `${cfg.baseURL}${cfg.apiRoute}/player-balances/player/by-email/${encodeURIComponent('test@example.com')}`;
const r = await fetch(url, { headers: { 'X-Game-Secret-Key': SDK_KEY } });