Refunds
Refund a billed period, in full or in part. The refund puts the member back where they were: card money goes back to the card, wallet coins go back to the wallet, your revenue is reversed for the refunded portion and the attribution is corrected. 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. | 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 | no | Carried on the receipt and the event. |
Other refusals
| HTTP | error_code or body | Meaning | What to do |
|---|---|---|---|
| 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 | STEAM_REFUND_NOT_SUPPORTED | The period was charged through Steam. Refused before anything moves. | Not supported yet; see section 5. |
| 409 | MINT_ORDER_MISSING, NO_PAYMENT_REFERENCE | The card charge’s record cannot be found. | 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, 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 periodconst receipt = 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",
});
// receipt.isFullRefund, receipt.cardRefundedUsd, receipt.balanceDeltaCoins,
// receipt.partnerRevenueReversedUsd, receipt.revenueShareAttribution?.netAttributedAmountUsdconst { 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 (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
}receipt = 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",
)
# receipt.is_full_refund, receipt.card_refunded_usd, receipt.balance_delta_coins,
# receipt.partner_revenue_reversed_usd, receipt.revenue_share_attribution.net_attributed_amount_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 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. What is reversed
A renewal can have been funded from the wallet alone, from the card alone, or both. The refund puts the member back where they were:
| Refund | 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 10:1 peg. |
- Partials that add up to the full amount compose correctly: the completing refund returns the card charge and reclaims the coins handed out earlier.
- 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). - 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. The subscription itself is not cancelled; cancel it separately if that is what the member wants.
3. Receipt (200)
{
"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": "mixed",
"balance_delta_coins": "20.00",
"card_refunded_usd": "7.99",
"processor_refund_reference": "<opaque reference>",
"processor_refund_adopted": false,
"invo_fee_retained": true,
"partner_revenue_reversed_usd": "8.69",
"partner_revenue_reversal_mode": "full",
"reason": "member request",
"refunded_at": "2026-11-10T09:12:44+00:00",
"new_balance": "20.00",
"revenue_share_attribution": {
"recipient_player_id": 4242, "percent": "70.00",
"original_attributed_amount_usd": "6.08", "refunded_attributed_amount_usd": "6.08",
"net_attributed_amount_usd": "0.00", "settled_by_invo": false
}
}funding_shapeiswallet,cardormixed.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.
4. 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": "mixed", "balance_delta_coins": "20.00", "card_refunded_usd": "7.99",
"processor_refund_adopted": false, "new_balance": "20.00", "invo_fee_retained": true,
"partner_revenue_reversed_usd": "8.69", "partner_revenue_reversal_mode": "full"
},
"revenue_share_attribution": { ...as on the receipt... },
"metadata": {"guild_id": "42"}
}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. Delivery, signature and dedupe are on the webhooks page.
5. 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, exactly like a wallet-funded card period: funding_shape: "wallet", the coins go back to the wallet, your revenue is reversed, and subscription.refunded fires.