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"} | Title suspended. | Contact Invo. |
| 403 | GAME_NOT_LIVE | Create refused; title is testing. | Set the title live in the console, in sandbox or production alike. |
| 403 | PAYMENTS_PAUSED | Create or Steam init refused; INVO has paused payments on the account. Existing subscriptions keep renewing. | Contact INVO support. |
| 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, "limit_type": "rate_limit"} | Rate limited (every route with a per-minute or per-hour limit, including the card-setup mint). | 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 the per-charge ceiling of 25000.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 | TRIAL_AMOUNT_REQUIRES_TRIAL | trial_amount_usd sent without trial_days or trial_end. | Send a trial length with it, or drop it for no trial. |
| 400 | TRIAL_AMOUNT_NOT_SUPPORTED_FOR_FUNDING | trial_amount_usd with wallet_only: true, or on /steam/init. | Paid trials are card subscriptions only. |
| 400 | TRIAL_AMOUNT_INVALID | trial_amount_usd is not an amount, is below 0.50 (the card minimum), or is not less than amount_usd. | Send a decimal string from 0.50 up to, but not including, amount_usd. |
| 400 | TRIAL_AMOUNT_BELOW_FEE_FLOOR | A new paid trial whose trial_amount_usd would leave you nothing after Invo’s card fee on the trial charge. Nothing was created. The message names the smallest trial price accepted for your account. A subscription already created replays as before. | Raise trial_amount_usd to at least the price the message names. |
| 400 | PAYMENT_METHOD_REQUIRED | A paid trial and the member has no usable saved card. Nothing was created. (On /payment-method: no card id sent.) | Save the member’s card first, then subscribe. |
| 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_DISCLOSED_TRIAL_AMOUNT_INVALID, CONSENT_DISCLOSED_TRIAL_DAYS_INVALID, CONSENT_DISCLOSED_TRIAL_END_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 title who is not the subscriber; percent 0 to 100. |
| 400 | CURRENCY_NOT_CONFIGURED | The title has no currency. | Configure the title 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 title, 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 (amount_usd, interval, interval_count, wallet_only, funding_rail, trial_amount_usd). | 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 title. | Use a card from /player-cards. |
| 400 | PAYMENT_METHOD_EXPIRED, PAYMENT_METHOD_INVALID | Expired or malformed id. | Use an unexpired card id. |
Refund (/refund)
Not an error: 202 with status: "pending_approval" (the refund exceeded the daily self-serve limit and became a refund request; nothing moved), and warnings codes REASON_RECOMMENDED and REASON_TRUNCATED on a 200 or 202. See Refunds.
| HTTP | Code or body | Meaning | Remedy |
|---|---|---|---|
| 403 | REFUNDS_NOT_ENABLED | Refunds are turned off for this game (they are on by default). | Contact Invo. |
| 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 | REFUND_WINDOW_CLOSED (+ period_charged_at, refund_window_days, window_closed_at) | The period was charged more than 30 days ago. | It can no longer be refunded here. A retry of a refund that already ran still returns its receipt. |
| 409 | REFUND_REQUEST_PENDING (+ request_id) | A refund request for this period is waiting for Invo review. | Wait for subscription.refund_approved or subscription.refund_rejected. |
| 409 | REFUND_REQUEST_REJECTED (+ request_id, decision_reason) | This key was a refund request Invo rejected. | Use a new client_request_id to try again. |
| 409 | CONCURRENT_REQUEST | Two refund requests for one period were filed at the same moment. | Retry with the same key. |
| 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. |
| 500 | TENANT_ROWS_MISSING | The period’s settlement rows cannot be found for your title. Nothing moved. | Contact Invo. |
| 502 | REFUND_LOOKUP_FAILED, REFUND_MISMATCH | Needs manual attention. REFUND_MISMATCH also covers a partial card refund that found a processor refund for this request with a different amount, or a charge already fully refunded outside this flow. | Contact Invo; do not retry in a loop. |
| 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. |
Hosted card page (POST /api/checkout/card-setup-sessions)
| HTTP | Code or body | Meaning | Remedy |
|---|---|---|---|
| 400 | INVALID_PLAYER_EMAIL | player_email missing or not an email. | Send the member’s email. |
| 400 | INVALID_INPUT | success_url or cancel_url uses a script scheme; or, for a new member, player_name is not text, over 255 characters or has no visible characters, or player_phone is not text or over 30 characters. | Use https or an app deep link; send a real name and phone. |
| 404 | PLAYER_NOT_FOUND | The member does not exist in this title and no player_name was sent. Nothing was created. | Send player_name to create the member with their card (card road). |
| 409 | PHONE_SHARE_APPROVAL_REQUIRED | Creating the new member needs the approval of whoever already holds player_phone. Same body as on /subscribe. Nothing was created. | Handle it as on /subscribe, then call again. |
| 503 | PLAYER_CREATE_FAILED | The new member could not be created. Nothing was created and no link was minted. | Call again. |
| 401 | {"message": "..."} | Key missing, unknown, disabled or out of grace. | Send the current key for this environment. |
| 403 | {"message": "Game '...' is not active"} | Title neither live nor testing. | Contact Invo. |
| 503 | {"status": "error", "error_code": "<configuration code>", "message": "Card setup is not available right now."} | Card setup is not available in this environment; the code names an Invo-side configuration condition. | Retry later; contact Invo if it persists. Do not branch on the code. |
| 429 | {"error": "rate_limit_exceeded", "message": "...", "retry_after": n, "limit_type": "rate_limit"} | Over 2000 mints per minute on one key, or 120 per minute from one IP. Retry-After header set. | Honour Retry-After. |
The 404 body also carries "status": "error". What the member can see on the page itself (never returned to your server; listed so support can recognise them): “This card link has expired. Ask for a new one.” (the 10-minute session lapsed, or the link is malformed; mint another), “Your card has not been authorised yet. Please try again.” (the issuer step did not finish; the same link keeps working), “We could not save your card. Please try again in a moment.” (a save failure after authorisation; the same link keeps working), “This link can no longer be used. Ask for a new card link.” (a save failure after which the link could not be handed back; mint a new one), “Only a card can be saved for a subscription. Ask for a new link and enter a card.” (terminal; mint a new link), and “This card link has already been used.” (the link was spent and no card is recorded under it; mint a new link). After a successful save, reopening the link within its 10 minutes shows the “Finish saving card” step; a click returns the same card, saves nothing twice, and redirects the member to your success_url again.
Card capture, own form (/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 title. | 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 the 25000.00 USD per-charge ceiling. | Lower the price. The ceiling is per charge, so an annual plan is measured on the whole annual amount. |
| 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 | {"error": "Resource not found", "error_id": "ERR_...", "timestamp": "..."} | You are outside sandbox, or the path has /api in it. The same body as any unknown path. | 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 title. | 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. |