Subscription error reference
Every code every subscription route can return, grouped by route. All bodies are {"message": "...", "error_code": "..."} unless a different shape is shown. Where a body carries status too, branch on status first. Match on error_code, never on message.
Authentication and gates (every route)
| HTTP | Code or body | Meaning | Remedy |
|---|---|---|---|
| 401 | {"message": "X-Game-Secret-Key header is required"} | No key. | Send the header. |
| 401 | {"message": "Invalid game secret key"} | Unknown or rotated-out key. | Use the current key for this environment. |
| 403 | {"message": "Game '...' is not active"} | Game suspended. | Contact Invo. |
| 403 | GAME_NOT_LIVE | Create refused; game is testing. | Make the game live in the console. |
| 503 | {"status": "error", "error": "flow_paused", "message": "..."} | Maintenance pause (create, amount, payment-method, Steam init, card setup). | Retry later with the same idempotency key. |
| 503 | FLOW_PAUSED | Maintenance pause (refund, sandbox clock). | Retry later. |
| 429 | {"error": "rate_limit_exceeded", "message": "...", "retry_after": n} | Rate limited. | Honour Retry-After. |
| 429 | RATE_LIMITED | Rate limited (refund, sandbox clock). | Slow down. |
| 429 | {"error": "spending_limit_exceeded", "message": "..."} | Member spending limit (Steam init). | Retry later. |
| 404 | SUBSCRIPTION_NOT_FOUND | Not your subscription, or no such id. | Check the id and the key. |
| 400 | INVALID_BODY | Body is not a JSON object. | Send an object. |
| 400 | INVALID_FIELD_VALUE | A value the database refused (length, characters). | Fix the value. |
| 500 | INTERNAL_ERROR | Unexpected. | Retry with the same idempotency key. |
Create (/subscribe, /steam/init)
| HTTP | Code | Meaning | Remedy |
|---|---|---|---|
| 400 | CLIENT_REQUEST_ID_REQUIRED, CLIENT_REQUEST_ID_INVALID, CLIENT_REQUEST_ID_TOO_LONG | Missing, not a string, or over 255 chars. | Send a string of at most 255 chars. |
| 400 | CLIENT_REQUEST_ID_RESERVED | Key begins with sub_. | Do not begin keys with sub_. |
| 400 | PLAYER_EMAIL_INVALID, PLAYER_EMAIL_TOO_LONG, PLAYER_NAME_REQUIRED, PLAYER_NAME_TOO_LONG, PLAYER_PHONE_TOO_LONG | Identity field invalid. | Fix the field. |
| 400 | ITEM_ID_REQUIRED, ITEM_ID_TOO_LONG, ITEM_NAME_TOO_LONG | Item field invalid. | Fix the field. |
| 400 | AMOUNT_REQUIRED, AMOUNT_INVALID, AMOUNT_TOO_SMALL, AMOUNT_TOO_LARGE | Price invalid. | 0.01 to 999999.99; keep at or below 500.00 to be chargeable. |
| 400 | INTERVAL_INVALID, INTERVAL_COUNT_INVALID, INTERVAL_COUNT_OUT_OF_RANGE | Interval invalid. | month or year; count 1 to 36 (1 on Steam). |
| 400 | TRIAL_DAYS_INVALID, TRIAL_DAYS_OUT_OF_RANGE, TRIAL_END_INVALID, TRIAL_END_IN_PAST, TRIAL_END_TOO_FAR | Trial invalid. | 1 to 365 days; ISO 8601 in the future. |
| 400 | METADATA_INVALID, METADATA_TOO_LARGE | Metadata invalid. | JSON object, at most 8192 bytes, no NUL. |
| 400 | CONSENT_AT_INVALID, CONSENT_INVALID, CONSENT_DISCLOSED_AMOUNT_INVALID, CONSENT_DISCLOSED_INTERVAL_INVALID | Consent field invalid. | Fix the consent field. |
| 400 | REVENUE_SHARE_INVALID, REVENUE_SHARE_PERCENT_OUT_OF_RANGE, REVENUE_SHARE_RECIPIENT_REQUIRED, REVENUE_SHARE_RECIPIENT_IS_SUBSCRIBER, REVENUE_SHARE_RECIPIENT_NOT_FOUND | Revenue share invalid. | Name an existing player in this game who is not the subscriber; percent 0 to 100. |
| 400 | CURRENCY_NOT_CONFIGURED | The game has no currency. | Configure the game currency. |
| 400 | PAYMENT_METHOD_INVALID, PAYMENT_METHOD_EXPIRED | Card id invalid or card expired. | Pass an integer card id of an unexpired card. |
| 404 | PAYMENT_METHOD_NOT_FOUND | The card is not this player’s in this game, or the player does not exist yet. | Save a card for the player first. |
| 409 | WALLET_ONLY_SUBSCRIPTION | wallet_only: true combined with a card, or sent on Steam. | Do not combine wallet_only: true with a card; on Steam do not send wallet_only. |
| 409 | ACTIVE_SUBSCRIPTION_EXISTS (+ subscription_id) | The player already has a live subscription to this item, on either rail. | Use the live subscription. Same item id across rails is intended. |
| 409 | CONCURRENT_REQUEST | Two creates for a new player collided. | Retry with the same key. |
| 409 | CLIENT_REQUEST_ID_CONFLICT | Key already used for a different player or item. | Use a key unique per subscription and player. |
| 409 | IDEMPOTENT_REPLAY_MISMATCH (+ mismatched_fields) | Same key, same player and item, different material terms. | Use /amount or /payment-method to change terms, or a new key. |
| 409 | REVENUE_SHARE_EXISTS | Share already set. | Nothing; the share is set once at create. |
| 409 | PHONE_SHARE_APPROVAL_REQUIRED (+ approval_id, expires_at, next_endpoint) | The phone belongs to another identity. | Complete the approval flow in the body, then retry. |
| 400 | DATA_CONFLICT | A value the database refused. | Retry with a new key after fixing the data. |
| 400 | STEAMID_REQUIRED, INVALID_USERSESSION, MISSING_PLAYER_IP, STEAM_INTERVAL_COUNT_UNSUPPORTED, CARD_FIELDS_NOT_ALLOWED, TRIAL_NOT_SUPPORTED_ON_STEAM, AMOUNT_TOO_SMALL_FOR_STEAM, STEAM_REQUEST_INVALID | Steam field rules. | See the Steam road. |
| 403 | STEAM_RAIL_NOT_ENTITLED, RAIL_NOT_AVAILABLE_TO_PLATFORM | Title not enabled for Steam / rail not available to your tenant type. | Enable Steam for the title in the console; contact Invo. |
| 409 | STEAM_CHANNEL_REQUIRED, STEAM_ACCOUNT_LOCKED, STEAM_CHARGE_UNPLANNABLE, PARTNER_BILLING_NOT_SET_UP, PARTNER_RAIL_SUSPENDED, PARTNER_CREDIT_UNAVAILABLE | Steam gates. | See the Steam road. |
| 409 | STEAM_AUTHORIZATION_PENDING (+ subscription_id) | A Steam subscription for this player and item is waiting for authorisation and this request carries different terms. | Finalize, cancel or wait out the pending row, or send the same terms to get it back. |
| 503 | STEAM_NOT_CONFIGURED, STEAM_UNAVAILABLE, {"message": "Steam subscriptions are not enabled."} | Steam configuration missing / Steam down / rail off. | Configure Steam; retry with a new key; contact Invo. |
| 502 | STEAM_ERROR | Steam could not start the subscription; row abandoned. | Retry with a new key. |
| 500 | CLAIM_FAILED, ORDER_FAILED | Invo could not open the first period. | Retry with a new key. |
Finalize (/steam/finalize)
| HTTP | Code or body | Meaning | Remedy |
|---|---|---|---|
| 400 | SUBSCRIPTION_ID_REQUIRED | Missing body field. | Send the id. |
| 409 | {"status": "not_authorized", "steam_status": "..."} | The member has not completed authorisation yet. | Retry after they have. |
| 409 | NOT_A_STEAM_SUBSCRIPTION | Not a Steam subscription. | Nothing to finalize. |
| 409 | SUBSCRIPTION_TERMINAL | Canceled or expired. | Start a new one. |
| 409 | NO_STEAM_AUTHORIZATION | No open Steam authorisation. | Start a new subscription. |
| 409 | STEAM_APP_CHANGED | Started against a different Steam application. | Contact Invo. |
| 503 | {"status": "service_unavailable"} | Steam unavailable. | Retry. |
| 502 | {"status": "steam_error"} | Could not verify the authorisation. | Retry. |
Read, list, cancel, amount, payment-method
| HTTP | Code or body | Meaning | Remedy |
|---|---|---|---|
| 400 | PLAYER_EMAIL_REQUIRED, PLAYER_EMAIL_TOO_LONG | List without a valid email. | Send the email. |
| 400 | STATUS_INVALID | Unknown status in the list filter. | Use the statuses on the overview or live. |
| 400 | PAGINATION_INVALID | limit outside 1 to 100 or offset outside 0 to 100000. | Fix the parameter. |
| 409 | SUBSCRIPTION_TERMINAL | Amount or payment-method on a canceled or expired row. | Start a new subscription. |
| 409 | STEAM_REAUTHORIZATION_REQUIRED (+ current_amount_usd, requested_amount_usd) | Price increase on Steam. | Start a new Steam subscription at the new price; cancel this one. |
| 400 | PAYMENT_METHOD_REQUIRED | No card id in the body. | Send the id. |
| 400 | WALLET_ONLY_CONFLICT | wallet_only: true together with a card. | Send one or the other. |
| 409 | WALLET_ONLY_SUBSCRIPTION | Card attached to a wallet-only subscription without wallet_only: false. | Pass wallet_only: false. |
| 409 | NOT_A_CARD_SUBSCRIPTION | Payment-method on Steam. | Not available on Steam. |
| 409 | PAYMENT_METHOD_UNAVAILABLE | The card was removed concurrently. | Pick another card. |
| 404 | PAYMENT_METHOD_NOT_FOUND | Not this player’s card in this game. | Use a card from /player-cards. |
| 400 | PAYMENT_METHOD_EXPIRED, PAYMENT_METHOD_INVALID | Expired or malformed id. | Use an unexpired card id. |
Refund (/refund)
| HTTP | Code or body | Meaning | Remedy |
|---|---|---|---|
| 400 | CLIENT_REQUEST_ID_REQUIRED, CLIENT_REQUEST_ID_INVALID, CLIENT_REQUEST_ID_TOO_LONG, CLIENT_REQUEST_ID_RESERVED | Key invalid. | One key per refund, at most 255 chars, not sub_-prefixed. |
| 400 | INVALID_PERIOD_SEQ | Not an integer at or above 1. | Fix. |
| 404 | PERIOD_NOT_FOUND | No such period on this subscription. | Check the sequence. |
| 400 | INVALID_AMOUNT | Not a decimal string above zero. | Fix. |
| 400 | AMOUNT_EXCEEDS_REMAINING (+ remaining_refundable_usd, already_refunded_usd) | More than is left to refund. | Refund at most the remaining amount. |
| 409 | NO_PAID_PERIOD | Nothing collected yet. | Nothing to refund. |
| 409 | PERIOD_NOT_REFUNDABLE (+ period_status) | That period was never collected. | Pick a paid period. |
| 409 | ALREADY_REFUNDED (+ refunded_amount_usd, amount_usd) | Fully refunded already. | Nothing to do. |
| 409 | STEAM_REFUND_NOT_SUPPORTED | The period was charged through Steam. Refused before anything moves. | Not supported yet. Wallet-covered periods of a Steam subscription are refundable. |
| 409 | MINT_ORDER_MISSING, NO_PAYMENT_REFERENCE | The card charge’s record cannot be found. | Contact Invo. |
| 409 | TENANT_ROWS_MISSING | The period’s settlement rows cannot be found for your game. | Contact Invo. |
| 502 | REFUND_LOOKUP_FAILED, REFUND_MISMATCH | Needs manual attention. | Contact Invo. |
| 502 | PROCESSOR_ERROR | The card processor refused or failed. | Retry with the same key; contact Invo if it persists. |
| 503 | {"error": "service_unavailable"} | Temporary. | Retry shortly. |
| 503 | error_code_public: PROCESSOR_NOT_CONFIGURED | Card processor not configured for this environment. | Contact Invo. |
Card capture (/setup-intent, /setup-intent/confirm, purchase save_card)
| HTTP | Code or body | Meaning | Remedy |
|---|---|---|---|
| 400 | MISSING_SETUP_REFERENCE, INVALID_SETUP_REFERENCE | Reference missing or outside the character rules. | Send 1 to 200 chars of A-Z a-z 0-9 . _ : - |
| 404 | PLAYER_NOT_FOUND | The player does not exist in this game. | Create the player first. |
| 409 | SETUP_REFERENCE_REUSED | Same reference, different parameters. | Use a new reference. |
| 400 | INVALID_PAYMENT_METHOD, CARD_DECLINED, SETUP_FAILED | The card could not be saved. | Ask for another card. |
| 400 | RAW_CARD_NOT_SUPPORTED | Raw card numbers were sent. | Send a tokenised card. |
| 400 | {"status": "still_requires_action"} | The client has not finished confirming. | Confirm with the card element, then call confirm again. |
| 500 | CARD_PERSIST_FAILED | Authorised but not recorded. | Retry /setup-intent/confirm with the same setup_intent_id. |
| 500 | SETUP_CONFIRMATION_FAILED | Confirm failed unexpectedly. | Retry confirm with the same id. |
| 400 | INVALID_SAVE_CARD | save_card was not a JSON boolean (purchase path). | Send true, not "true". |
Sandbox clock tools
| HTTP | Code or body | Meaning | Remedy |
|---|---|---|---|
| 401 | SANDBOX_CLOCK_UNAUTHORIZED | Missing, wrong or never-issued clock key. | Reveal the key in the sandbox console (Game Settings) and send X-Sandbox-Clock-Key. |
| 400 | INVALID_INTERVALS, INVALID_OUTCOME | Body value out of range. | intervals 0 to 60; outcome card_declined, insufficient_funds or error. |
| 409 | SUBSCRIPTION_NOT_LIVE | Terminal or pending row. | Use a live subscription (finalize a pending Steam row first). |
| 409 | ATTEMPT_IN_FLIGHT | An unresolved attempt. | Wait about 30 minutes. |
| 409 | AUTH_CHALLENGE_OPEN | A challenge is open. | Complete it or let it expire. |
| 409 | AMOUNT_OVER_CEILING | Price above 500.00 USD. | Lower the price. |
| 409 | PERIOD_TOO_FAR_AHEAD | Forced twice without advancing. | Call advance-clock. |
| 409 | NO_CHARGEABLE_PERIOD | Nothing due. | Call advance-clock. |
| 409 | WINDOW_ALREADY_PAID | The window is behind the paid boundary. | Create a fresh subscription. |
| 500 | CLAIM_FAILED | Could not claim the row. | Retry. |
| 503 | FLOW_PAUSED | Billing paused. | Retry shortly. |
| 404 | {"message": "Not found"} | You are outside sandbox, or the path has /api in it. | Use https://sandbox.invo.network/sandbox/subscriptions/<id>/... |
Reporting (/api/subscriptions/reporting/...)
| HTTP | Code or body | Meaning | Remedy |
|---|---|---|---|
| 400 | WINDOW_INVALID | start or end missing, unparseable, or end not after start. | Send ISO 8601 dates, end after start. |
| 400 | WINDOW_TOO_LONG | Range over 400 days. | Narrow the range. |
| 400 | WINDOW_TOO_LARGE (+ counts) | More than 20,000 settled or refunded renewals in the range. | Run the report in parts. |
| 400 | RECIPIENT_INVALID | recipient_player_id not an integer. | Fix. |
| 400 | SUBSCRIPTION_ID_INVALID | subscription_id over 50 chars. | Fix. |
| 400 | PAGINATION_INVALID | limit or offset out of range. | Fix. |
| 400 | STATUS_INVALID | Unknown period status in the history filter. | Use pending, paid, failed, forgiven, refunded. |
| 404 | SUBSCRIPTION_NOT_FOUND, PERIOD_NOT_FOUND | Not on your game. | Check the id. |
Webhook verification (SDK)
Thrown by verifyWebhook / verify_webhook rather than returned by the API:
| Code | Meaning | Remedy | |
|---|---|---|---|
WEBHOOK_SIGNATURE_MISSING | No X-Invo-Signature header. | Check the request reached you unmodified. | |
WEBHOOK_SECRET_MISSING | You passed no secret. | Pass the signing secret from registration. | |
WEBHOOK_TIMESTAMP_EXPIRED | The signed timestamp is more than 300 seconds from now. | Check your clock; do not queue deliveries before verifying. | |
WEBHOOK_SIGNATURE_INVALID | No v1 value matched. | Verify the raw body bytes, not a re-serialised object; during a rotation pass both secrets. | |
WEBHOOK_MALFORMED | The body is not a valid envelope. | Check for truncation or a proxy rewriting the body. |