Refunds
Refund a billed period, in full or in part, yourself, within the limits in section 2. A card subscription is paid only by card, so its refund goes back to the card, partial refunds included. Your revenue is reversed for the refunded portion, you bear the whole refunded amount (Invo’s card fee included), a full refund ends the subscription, and Invo emails the member. The last section covers the other way money comes back: a chargeback the member wins with their card issuer. The clients and raw HTTP helpers the samples use are defined on the overview.
1. POST /api/subscriptions/<subscription_id>/refund
Refunds a billed period. Card rail, plus wallet-covered periods on Steam. Idempotent on client_request_id.
| Field | Type | Req. | Rules | Error |
|---|---|---|---|---|
client_request_id | string | yes | Max 255. Not sub_-prefixed. One per refund; reuse it on retries. | 400 CLIENT_REQUEST_ID_REQUIRED, CLIENT_REQUEST_ID_INVALID, CLIENT_REQUEST_ID_TOO_LONG, CLIENT_REQUEST_ID_RESERVED |
period_seq | integer >= 1 | no | Defaults to the most recent billed (paid or partly refunded) period. The trial period of a paid trial is 1. | 400 INVALID_PERIOD_SEQ, 404 PERIOD_NOT_FOUND |
amount_usd | decimal string > 0 | no | Defaults to the full remaining refundable amount. A partial refund is allowed. | 400 INVALID_AMOUNT, 400 AMOUNT_EXCEEDS_REMAINING (body carries remaining_refundable_usd, already_refunded_usd) |
reason | string, max 500 | recommended | Carried on the receipt, the audit trail and the events. Recommended now; it will become required after a notice period. Without it the refund still runs exactly as before (reason is null on the receipt and the events) and the response carries warnings: [{"code": "REASON_RECOMMENDED", ...}]. Longer than 500 characters is cut to 500, with a REASON_TRUNCATED warning. | none (never refused today) |
Responses
200,status: "refunded": the refund ran. The receipt is in section 4.202,status: "pending_approval": the refund would exceed your game’s daily self-serve limit, so nothing moved and it became a refund request for Invo to review (section 2). Branch onstatus; a202is not a refund. Refunds are authenticated with your game’s secret key, so a refund request your server did not make means that key was used by someone else: rotate your keys.- An error, in the usual
{"message", "error_code", ...}envelope (below).
Refusals
| HTTP | error_code or body | Meaning | What to do |
|---|---|---|---|
| 403 | REFUNDS_NOT_ENABLED | Refunds are turned off for this game. They are on by default for every game; Invo can turn them off for a specific game. | Contact Invo. |
| 404 | SUBSCRIPTION_NOT_FOUND | Not your subscription. | Check the id. |
| 409 | NO_PAID_PERIOD | Nothing has been collected yet; the normal state of a brand-new subscription. | 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’s review. | Wait for subscription.refund_approved or subscription.refund_rejected. |
| 409 | REFUND_REQUEST_REJECTED (+ request_id, decision_reason) | This client_request_id was a refund request Invo rejected. | To try again, use a new client_request_id. |
| 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; see section 7. |
| 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 | Needs manual attention. | Contact Invo. |
| 502 | REFUND_MISMATCH | Needs manual reconciliation. Also returned when a partial card refund finds a processor refund for this request with a different amount, or the 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, same key. |
| 503 | FLOW_PAUSED | Refunds paused for maintenance. | Retry later, same key. |
| 503 | error_code_public: PROCESSOR_NOT_CONFIGURED | The card processor is not configured for this environment. | Contact Invo. |
| 500 | INTERNAL_ERROR | Unexpected. | Retry with the same key. |
| 400 | INVALID_BODY | Body is not a JSON object. | Send an object. |
| 429 | RATE_LIMITED | More than 20 refund calls per minute on one subscription. | Slow down. |
curl -sS -X POST "$BASE/api/subscriptions/SUB_1757155200_A1B2C3D4/refund" \
-H "X-Game-Secret-Key: $GAME_SECRET" \
-H "Content-Type: application/json" \
-d '{
"client_request_id": "refund-2026-11-guild-42-member-7",
"period_seq": 2,
"reason": "member request"
}'
# add "amount_usd": "3.00" for a partial refund; omit period_seq for the most recent billed period
# 200 {"status": "refunded", ...} or 202 {"status": "pending_approval", "request_id": ...}import { isRefundPendingApproval } from "@invonetwork/web-sdk/server";
const r = await invo.subscriptions.refund("SUB_1757155200_A1B2C3D4", {
clientRequestId: "refund-2026-11-guild-42-member-7", // one per refund; reuse on retry
periodSeq: 2, // omit for the most recent billed period
// amountUsd: "3.00", // omit for the full remaining amount
reason: "member request", // recommended; will become required
});
if (isRefundPendingApproval(r)) {
// HTTP 202, over the daily limit: nothing moved; Invo reviews request r.requestId
} else {
// r.isFullRefund, r.cardRefundedUsd, r.subscriptionCanceled,
// r.partnerRevenueReversedUsd, r.processingFeeUsd, r.partnerTotalReversedUsd,
// r.revenueShareAttribution?.netAttributedAmountUsd
}
// r.warnings?.[0]?.code === "REASON_RECOMMENDED" when no reason was sentconst { status, json } = await invo("POST", "/api/subscriptions/SUB_1757155200_A1B2C3D4/refund", {
client_request_id: "refund-2026-11-guild-42-member-7",
period_seq: 2,
reason: "member request",
});
if (status === 200) {
// json.status "refunded"; json.idempotent_replay true on a repeat
} else if (status === 202) {
// json.status "pending_approval"; json.request_id; nothing moved
} else if (json.error_code === "REFUND_WINDOW_CLOSED") {
// json.period_charged_at, json.window_closed_at
} else if (json.error_code === "REFUND_REQUEST_PENDING") {
// json.request_id is waiting for Invo
} else if (json.error_code === "AMOUNT_EXCEEDS_REMAINING") {
// json.remaining_refundable_usd, json.already_refunded_usd
} else if (json.error_code === "STEAM_REFUND_NOT_SUPPORTED") {
// a Steam-charged period; nothing moved
}r = invo.subscriptions.refund(
"SUB_1757155200_A1B2C3D4",
client_request_id="refund-2026-11-guild-42-member-7", # one per refund; reuse on retry
period_seq=2, # omit for the most recent billed period
# amount_usd="3.00", # omit for the full remaining amount
reason="member request", # recommended; will become required
)
if r.is_pending_approval:
pass # HTTP 202, over the daily limit: nothing moved; Invo reviews request r.request_id
else:
pass # r.is_full_refund, r.card_refunded_usd, r.subscription_canceled,
# r.partner_revenue_reversed_usd, r.card_fee_charged_to_partner_usd, r.partner_debited_usdstatus, res = invo("POST", "/api/subscriptions/SUB_1757155200_A1B2C3D4/refund", {
"client_request_id": "refund-2026-11-guild-42-member-7",
"period_seq": 2,
"reason": "member request",
})
if status == 200:
pass # res["status"] == "refunded"; res["idempotent_replay"] True on a repeat
elif status == 202:
pass # res["status"] == "pending_approval"; res["request_id"]; nothing moved
elif res.get("error_code") == "REFUND_WINDOW_CLOSED":
pass # res["period_charged_at"], res["window_closed_at"]
elif res.get("error_code") == "AMOUNT_EXCEEDS_REMAINING":
pass # res["remaining_refundable_usd"], res["already_refunded_usd"]
elif res.get("error_code") == "STEAM_REFUND_NOT_SUPPORTED":
pass # a Steam-charged period; nothing moved2. Refund limits
Refunds are self-serve, inside four limits:
- Enabled per game. Refunds are enabled for every game by default. Invo can turn them off for a specific game, and then
/refundanswers403 REFUNDS_NOT_ENABLED. - 30 days. A period can be refunded for 30 days after it was charged. After that,
409 REFUND_WINDOW_CLOSED. A retry of a refund that already ran still returns its receipt. - A daily limit per game. Self-served refunds per game per UTC day are limited. By default the limit is never lower than the largest single subscription charge (
25000.00USD today), in sandbox and production, so any single period can always be refunded on its own. If Invo raises the per-charge maximum, the limit rises with it. Invo can set a specific limit for your game, and that limit applies as set, even if it is lower. Reaching the limit exactly still runs; a refund that would exceed it does not run and becomes a refund request (below). A single refund larger than the limit always becomes a request. - A reason. Recommended now, required after a notice period (section 1).
Refund requests (the 202)
A refund over the daily limit answers 202. The body carries every key of the 200 receipt, with the values of a refund that has not happened, plus the request, so a decoder built for the receipt keeps working and simply sees that nothing moved:
{
"status": "pending_approval",
"idempotent_replay": false,
"request_id": "SRR20260917150211AB12CD",
"request_status": "pending",
"requested_amount_usd": "40.00",
"requested_at": "2026-09-17T15:02:11+00:00",
"daily_cap_usd": "25000.00",
"executed_today_usd": "24980.00",
"message": "This refund would exceed the game's daily self-serve refund limit. ...",
"refund_key": "subrefund:SUB_...:2:<digest>",
"client_request_id": "refund-2026-11-guild-42-member-7",
"subscription_id": "SUB_...", "period_seq": 2, "transaction_id": null,
"refunded_amount_usd": "0.00", "total_refunded_amount_usd": "0.00",
"remaining_refundable_usd": "40.00", "is_full_refund": false,
"funding_shape": "card", "balance_delta_coins": "0.00", "card_refunded_usd": "0.00",
"processor_refund_reference": null, "processor_refund_adopted": false,
"invo_fee_retained": true, "partner_revenue_reversed_usd": "0.00",
"partner_revenue_reversal_mode": "none", "reason": "member request", "refunded_at": null,
"new_balance": "0.00", "revenue_share_attribution": null,
"initiation": "self_serve", "refund_request_id": "SRR20260917150211AB12CD",
"subscription_canceled": false, "subscription_status": "active",
"processing_fee_usd": "0.00", "processing_fee_basis": "none",
"processing_fee_borne_by": "partner", "processing_fee_transaction_id": null,
"partner_total_reversed_usd": "0.00",
"card_fee_charged_to_partner_usd": "0.00", "partner_debited_usd": "0.00"
}- Branch on
status.pending_approvalmeans nothing has been refunded yet. - The same
client_request_idreturns the same202withidempotent_replay: true. While a request is pending, any other refund of that period answers409 REFUND_REQUEST_PENDING. - You also receive
subscription.refund_requested(section 6). - Approved: the refund runs exactly as if your call had (the 30 days are measured from when you asked). You receive
subscription.refund_approved,subscription.refundedand, for a full refund,subscription.canceled. They are sent together and may arrive in any order. A retry of your key now returns the receipt, withinitiation: "approved_request"andrefund_request_idset. You still bear the whole refunded amount, Invo’s card fee included. Approved refunds do not count toward your daily limit. - Rejected: nothing moves. You receive
subscription.refund_rejectedwithdecision_reason, and your key answers409 REFUND_REQUEST_REJECTED. Use a new key to try again; it is a new attempt, subject to the limit.
Your developer console session can list your requests: GET /api/dev/subscriptions/refund-requests?status=pending|approved|rejected|all&game_id=&limit=&offset= (default pending, limit 1 to 100, default 50) returns items (request_id, game_id, subscription_id, item_id, period_seq, client_request_id, amount_usd, reason, status, created_at, decided_at, decision_reason, transaction_id) and total. It reads only games you own (404 GAME_NOT_FOUND otherwise); 400 INVALID_STATUS and 400 INVALID_LIMIT for bad parameters. This is a console read, authenticated by your console session, not by the game key.
3. What is reversed
Card subscriptions are card only (since 2026-09-17): every period is charged to the card in full and the wallet never moves. So a card-only period is refunded in cash, full or partial:
| Refund of a card-only period | Card | Coins |
|---|---|---|
Full (is_full_refund: true) | The rest of the period’s card charge goes back to the card (card_refunded_usd). | Nothing (balance_delta_coins: "0.00"). |
| Partial | The refunded amount goes back to the card (card_refunded_usd equals refunded_amount_usd). | Nothing. |
Partials and the refund that completes them add up to exactly the card charge. Periods charged before 2026-09-17 (funded from the wallet alone, from the card alone, or both), and Steam periods the wallet covered, are refunded as they always were:
| Refund of an earlier period | Card | Coins |
|---|---|---|
Full (is_full_refund: true) | The whole card charge for that period (card_refunded_usd) goes back to the card. | The wallet is re-credited with the coins that came from the member’s pre-existing balance; coins that the card charge minted are taken back (the member holds the cash instead). balance_delta_coins is signed and can be negative, and can take the wallet below zero if the member has since spent the coins. |
| Partial | Nothing goes back to the card. | The refunded amount is credited to the wallet in coins at the game’s rate. |
- Your revenue is reversed for the refunded portion (
partner_revenue_reversed_usd,partner_revenue_reversal_modefullorprorata). Invo’s fee is retained (invo_fee_retained: true). - You bear the whole refunded amount, Invo’s card fee included (since 2026-09-28), on any refund that returns money to a card, exactly as on a Platform Commerce card refund. Your revenue is reversed, and the rest of the refunded price (Invo’s card fee on that part, net of tax) is deducted from your revenue as its own negative line:
card_fee_charged_to_partner_usd. Across partial refunds, and a partial refund followed by a lost chargeback, the fee is charged at most once in total, so the partials of one charge add up to exactly Invo’s fee on it.partner_debited_usd(andpartner_total_reversed_usd, the same value) is the revenue reversed plus that fee, which equals the refunded price. A coin refund has no fee. - No separate payment processor fee.
processing_fee_usdstays on the receipt and reads"0.00", withprocessing_fee_basis: "none"andprocessing_fee_transaction_id: null. Before 2026-09-28 the payment processor’s fee on the refund was charged to you here, instead of Invo’s card fee. - The international card fee is not returned. The 1.5% charged on a card issued outside the United States stays charged when the period is refunded.
- Example: a 100.00 USD period on the Open tier, refunded in full. Your share was 96.35 (after the 3.65 card fee); the refund debits 100.00, so you end the period at -3.65. On a card issued outside the United States your share was 94.85 (after the 1.50 international card fee), so you end at -5.15. On Merchant of Record, same non-US card: -6.65.
- The attribution is corrected:
revenue_share_attributionon the receipt and the event carriesnet_attributed_amount_usd, the corrected figure to pay your recipient for that renewal. Nothing moves. - A fully refunded period no longer counts toward
paid_through. - A full refund cancels the subscription immediately: status
canceled, no further charges (subscription_canceled: true,subscription_status: "canceled"on the receipt). You receivesubscription.canceledexactly as from the cancel endpoint (canceled_by: "partner"), with the additivecancel_cause: "full_refund". It andsubscription.refundedare sent together and may arrive in any order. A partial refund leaves the subscription live. A subscription that was already canceled or expired is not canceled again. (Before 2026-09-17 a full refund did not cancel.) - Invo emails the member when a refund executes: the amount, where it went (card or balance), and whether the subscription ended. Your
reasonis not included. No email is sent for a replay, a refund request or a refusal. - A paid trial’s trial period refunds like any other period. It is period 1, charged at the trial price, so
period_seq: 1refunds up totrial_amount_usd, in cash, within the same 30 days and daily limit. You bear the whole refunded amount on it, Invo’s card fee included, as on any card refund; on a small trial price that fee is a large share of the refund. A full refund of the trial period ends the subscription, as a full refund of any period does.
4. Receipt (200)
A full refund of a card-only $9.99 period:
{
"status": "refunded",
"idempotent_replay": false,
"refund_key": "subrefund:SUB_...:2:<digest>",
"client_request_id": "refund-2026-11-guild-42-member-7",
"subscription_id": "SUB_...",
"period_seq": 2,
"transaction_id": "TXN_...",
"refunded_amount_usd": "9.99",
"total_refunded_amount_usd": "9.99",
"remaining_refundable_usd": "0.00",
"is_full_refund": true,
"funding_shape": "card",
"balance_delta_coins": "0.00",
"card_refunded_usd": "9.99",
"processor_refund_reference": "<opaque reference>",
"processor_refund_adopted": false,
"invo_fee_retained": true,
"partner_revenue_reversed_usd": "9.49",
"partner_revenue_reversal_mode": "full",
"reason": "member request",
"refunded_at": "2026-10-20T09:12:44+00:00",
"new_balance": "20.00",
"revenue_share_attribution": {
"recipient_player_id": 4242, "percent": "70.00",
"original_attributed_amount_usd": "6.64", "refunded_attributed_amount_usd": "6.64",
"net_attributed_amount_usd": "0.00", "settled_by_invo": false
},
"initiation": "self_serve",
"refund_request_id": null,
"subscription_canceled": true,
"subscription_status": "canceled",
"processing_fee_usd": "0.00",
"processing_fee_basis": "none",
"processing_fee_borne_by": "partner",
"processing_fee_transaction_id": null,
"partner_total_reversed_usd": "9.99",
"tax_refunded_usd": "0.00",
"card_refunded_total_usd": "9.99",
"card_fee_charged_to_partner_usd": "0.50",
"partner_debited_usd": "9.99"
}Open tier, card issued in the United States: Invo’s card fee on 9.99 is 0.50 (3.5% + 0.15) and your revenue was 9.49. The refund reverses the 9.49 and charges you the 0.50, so partner_debited_usd is the whole 9.99.
funding_shapeiswallet,cardormixed. Every card period charged since 2026-09-17 iscard;walletandmixedappear on earlier periods and on wallet-covered Steam periods.- The keys from
initiationtopartner_total_reversed_usdwere added on 2026-09-17. A stored receipt from before that date, returned by a replay, does not carry them. card_fee_charged_to_partner_usdandpartner_debited_usdwere added on 2026-09-28. A stored receipt from before that release, returned by a replay, does not carry them; readpartner_total_reversed_usdthere.- A replay returns the stored receipt, which never includes
new_balanceorrevenue_share_attribution: those two are on the live response only. warnings(a list of{"code", "message"}) appears only when there is something to say (REASON_RECOMMENDED,REASON_TRUNCATED), and is not part of the stored receipt a replay returns.initiationisself_servefor your own call andapproved_request(withrefund_request_idset) when Invo approved a refund request.processor_refund_adopted: truemeans the card refund had already been issued by an earlier attempt that failed before recording it, and this call recorded it; the cash date on your side may differ fromrefunded_at.- A repeated
client_request_idreturns the stored receipt withidempotent_replay: true(or, if the receipt itself was lost, a reduced body withtransaction_idandnote: "This refund was already processed."). revenue_share_attributionisnullwhen the subscription has no share.- What did I earn:
split.partner_revenue_usdonsubscription.renewed, minuspartner_debited_usd(orpartner_total_reversed_usdon a receipt that does not carry it).
5. subscription.refunded
{
"subscription_id": "SUB_...", "item_id": "guild-42-membership", "period_seq": 2,
"transaction_id": "TXN_...", "client_request_id": "refund-2026-11-guild-42-member-7",
"player_email": "member@example.com", "identity_id": "idn_...",
"amount_usd": "9.99", "total_refunded_amount_usd": "9.99", "period_amount_usd": "9.99",
"is_full_refund": true, "reason": "member request", "period_status": "refunded",
"refund": {
"funding_shape": "card", "balance_delta_coins": "0.00", "card_refunded_usd": "9.99",
"processor_refund_adopted": false, "new_balance": "20.00", "invo_fee_retained": true,
"partner_revenue_reversed_usd": "9.49", "partner_revenue_reversal_mode": "full",
"processing_fee_usd": "0.00", "processing_fee_basis": "none",
"processing_fee_borne_by": "partner", "partner_total_reversed_usd": "9.99",
"card_fee_charged_to_partner_usd": "0.50", "partner_debited_usd": "9.99"
},
"revenue_share_attribution": { ...as on the receipt... },
"metadata": {"guild_id": "42"},
"initiation": "self_serve", "refund_request_id": null, "subscription_canceled": true
}amount_usd on the event is the amount refunded by this call; total_refunded_amount_usd is cumulative for the period; period_amount_usd is what the period was billed. refund.processing_fee_*, refund.partner_total_reversed_usd, initiation, refund_request_id and subscription_canceled are additive since 2026-09-17; refund.card_fee_charged_to_partner_usd and refund.partner_debited_usd since 2026-09-28. When subscription_canceled is true, asubscription.canceled with cancel_cause: "full_refund" is sent in the same step; the two are sent together and may arrive in any order. Delivery, signature and dedupe are on the webhooks page.
6. Refund request events
subscription.refund_requested fires when a refund over the daily limit becomes a request:
{
"request_id": "SRR20260917150211AB12CD", "status": "pending",
"subscription_id": "SUB_...", "item_id": "guild-42-membership", "period_seq": 2,
"client_request_id": "refund-2026-11-guild-42-member-7",
"amount_usd": "40.00", "reason": "member request",
"requested_at": "2026-09-17T15:02:11+00:00",
"daily_cap_usd": "25000.00", "executed_today_usd": "24980.00",
"player_email": "member@example.com", "identity_id": "idn_...",
"metadata": {"guild_id": "42"}
}subscription.refund_approved is the same with status: "approved", decided_at and transaction_id (the refund). It is sent in the same step assubscription.refunded (and subscription.canceled on a full refund); they are sent together and may arrive in any order. subscription.refund_rejected is the same with status: "rejected", decided_at and decision_reason. These three events do not carry the common subscription block. reason is null when you sent none. Subscribe to them like any other event.
Prove it in sandbox, end to end
- Run the card recipe through at least one
advance-clock+force-renewalpair, so period 2 is a settled period (funding.card_charged_usdequal to the price on itssubscription.renewed). - For a partial first:
POST .../refundwith a freshclient_request_id,amount_usd: "3.00"and areason. Expect200,is_full_refund: false,card_refunded_usd: "3.00",balance_delta_coins: "0.00",card_fee_charged_to_partner_usdabove zero,partner_debited_usd: "3.00",processing_fee_usd: "0.00",partner_revenue_reversal_mode: "prorata",subscription_canceled: false. The subscription stays live. - Send the same refund without
reasonunder a new key: it still runs, and the response carrieswarningswithREASON_RECOMMENDED. - Now refund the rest (no
amount_usd, a new key). Expectis_full_refund: true,card_refunded_usdequal to the remainder (the card refunds add up to the charge),subscription_canceled: true, and two deliveries,subscription.refundedandsubscription.canceledwithcancel_cause: "full_refund", in either order.GETthe subscription:statusiscanceledandpaid_throughhas moved back to the previous paid boundary. - Replay the last body:
200withidempotent_replay: true, nothing moved. Send another refund for the same period:409 ALREADY_REFUNDED. - To see a
202, ask Invo to set a low refund limit for your sandbox game (for example5.00), then refund more than that in one UTC day on that game. At the default limit a single period always runs, so a 202 needs a lower per-game limit. Expect202withstatus: "pending_approval", arequest_id, nothing refunded, and asubscription.refund_requesteddelivery. Ask Invo to approve or reject it in sandbox to see the other two events.
7. Steam
Refunds of periods that were charged through Steam are not supported yet. /refund on such a period answers 409 STEAM_REFUND_NOT_SUPPORTED before anything moves: no coins move, no revenue is reversed, nothing is returned through Steam. A policy for Steam-charged periods is being settled and will be announced.
A period of a Steam subscription that the wallet covered entirely (funding.steam_charged_usd was 0.00 on its subscription.renewed) is refundable in coins: funding_shape: "wallet", the coins go back to the wallet, your revenue is reversed, no processor fee applies, and subscription.refunded fires. The limits in section 2 apply, and a full refund cancels the subscription (Invo also ends the Steam agreement).
8. Chargebacks on a card renewal
A chargeback is the member disputing a card charge with their card issuer instead of asking you for a refund. It is not something you start and there is no endpoint for it. Each card period (the first charge, every renewal, and the trial charge of a paid trial) is its own card charge, so a chargeback always concerns one period.
Chargeback guarantee. Fraud screening is included in the subscription fee on both tiers; the chargeback guarantee is included on Merchant of Record, and on Open-tier charges made before October 21, 2026 (from that date a chargeback on an Open card charge is yours whatever the reason). On both tiers the card network’s dispute fee on a card dispute opened from October 21, 2026 is passed through to you at cost, except when the guarantee covers the chargeback. When the member’s bank raises the chargeback as fraud (the cardholder says they did not authorize the payment), Invo covers it: your share of the period is not reversed, nothing is held while the dispute is open, and no dispute fee is charged to you. purchase.disputed carries guarantee_covered: true, and developer_revenue_reversed_usd reads 0.00. The guarantee covers INVO card charges only; purchases through a third-party storefront or payment rail are unaffected. A chargeback that is not coded as fraud (for example product not received) is yours on both tiers and works as the table below describes. See Tiers.
| Stage | What happens |
|---|---|
| Opened | Your share of the period is held from your earnings while the dispute is open, so it cannot be paid out (it is counted in dispute_holds_usd on GET /api/withdrawals/available-balance), unless the chargeback guarantee covers the dispute. You receive purchase.disputed for the period’s card charge (its order_id is the mint_order_id on that period’s subscription.renewed). |
| Won | The hold is released. purchase.disputed again, with dispute_status: "won". |
| Lost | The card network has taken the money back, so Invo reverses that period as if it had been refunded in full: your share of the period is reversed (netted from your payout if it was already paid; the hold is released at the same moment, so it is taken only once), and the period stops counting toward paid_through. No other period is touched, and the member’s coin wallet is not debited for the coins the renewal spent (any small rounding remainder still in the wallet is taken back). You receive purchase.disputed with dispute_status: "lost", order_type: "subscription_renewal", subscription_id, period_seq, developer_revenue_reversed_usd, guarantee_covered, card_fee_charged_to_partner_usd and partner_debited_usd. A lost chargeback costs you exactly what a refund of the same period does, on every charge: your share is reversed and Invo’s card fee on the lost amount is charged to you (card_fee_charged_to_partner_usd); partner_debited_usd is the two together. processing_fee_usd is still sent and reads 0.00. On both tiers the card network’s dispute fee on a dispute opened from October 21, 2026 is also passed through to you at cost, with no Invo administrative fee, reported as dispute_fee_usd (a string, or null) and dispute_fee_charged_to_partner (true, false or null); it is credited back if the dispute is won and the network returns it, and a chargeback the guarantee covers carries no dispute fee (see Tiers). When the chargeback guarantee covers it, your share is not reversed and nothing is charged to you. |
A lost chargeback does not cancel the subscription. Unlike a full refund, which you ask for, nobody decided to end the membership, so it stays live and the next renewal is charged on schedule. No subscription.refunded and no subscription.canceled are sent. If you do not want to keep billing a member who charged back, cancel the subscription yourself with POST .../cancel and at_period_end: false.
- Read the chargeback from
purchase.disputed: matchsubscription_idandperiod_seq, and subtractpartner_debited_usd(your share reversed plus Invo’s card fee) from what you recorded for that period onsubscription.renewed. It is0.00whenguarantee_coveredistrue, andnullwhere it does not apply (for example on a settlement Invo replays); key your books ondispute_idso a figure is recorded once. - A period that was already refunded in full, or a period charged before 2026-09-17 that was paid partly from the member’s wallet, is not reversed automatically; Invo reviews it by hand.
- Keep your consent record complete: it is the evidence a disputed recurring charge is argued from.