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.

FieldTypeReq.RulesError
client_request_idstringyesMax 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_seqinteger >= 1noDefaults to the most recent billed (paid or partly refunded) period.400 INVALID_PERIOD_SEQ, 404 PERIOD_NOT_FOUND
amount_usddecimal string > 0noDefaults 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)
reasonstring, max 500noCarried on the receipt and the event.

Other refusals

HTTPerror_code or bodyMeaningWhat to do
404SUBSCRIPTION_NOT_FOUNDNot your subscription.Check the id.
409NO_PAID_PERIODNothing has been collected yet; the normal state of a brand-new subscription.Nothing to refund.
409PERIOD_NOT_REFUNDABLE (+ period_status)That period was never collected.Pick a paid period.
409ALREADY_REFUNDED (+ refunded_amount_usd, amount_usd)Fully refunded already.Nothing to do.
409STEAM_REFUND_NOT_SUPPORTEDThe period was charged through Steam. Refused before anything moves.Not supported yet; see section 5.
409MINT_ORDER_MISSING, NO_PAYMENT_REFERENCEThe card charge’s record cannot be found.Contact Invo.
502REFUND_LOOKUP_FAILED, REFUND_MISMATCHNeeds manual attention.Contact Invo.
502PROCESSOR_ERRORThe card processor refused or failed.Retry with the same key; contact Invo if it persists.
503{"error": "service_unavailable"}Temporary.Retry shortly, same key.
503FLOW_PAUSEDRefunds paused for maintenance.Retry later, same key.
503error_code_public: PROCESSOR_NOT_CONFIGUREDThe card processor is not configured for this environment.Contact Invo.
500INTERNAL_ERRORUnexpected.Retry with the same key.
400INVALID_BODYBody is not a JSON object.Send an object.
429RATE_LIMITEDMore than 20 refund calls per minute on one subscription.Slow down.
curl
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
Node, @invonetwork/web-sdk
const 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?.netAttributedAmountUsd
Node, raw HTTP
const { 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
}
Python, invonetwork
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_usd
Python, raw HTTP
status, 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 moved

2. 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:

RefundCardCoins
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.
PartialNothing 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_mode full or prorata). Invo’s fee is retained (invo_fee_retained: true).
  • The attribution is corrected: revenue_share_attribution on the receipt and the event carries net_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_shape is wallet, card or mixed.
  • processor_refund_adopted: true means 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 from refunded_at.
  • A repeated client_request_id returns the stored receipt with idempotent_replay: true (or, if the receipt itself was lost, a reduced body with transaction_id and note: "This refund was already processed.").
  • revenue_share_attribution is null when 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.