Credit
Credit
Credit a user wallet for an Opinion sell/redemption payout or a Polyflight win/refund. The same path also records a Polyflight losing result with an exactly zero amount and no balance change.
Path
POST /credit
Request body
{
"external_user_id": "operator-user-123",
"amount_minor": 15000,
"currency_code": "IDR",
"idempotency_key": "mini_app_redemption_item:77:credit",
"reason": "mini_app_redemption",
"metadata": {
"redemptionItemId": 77,
"marketId": 51,
"resolveDeadlineTs": "2026-06-30T12:00:00.000Z"
}
}
Sell payouts use the same shape with reason = "mini_app_sell_order" and metadata such as orderId, marketId, outcome, and resolveDeadlineTs.
There is no direction field in the REST v1 JSON body. The path POST /credit is the direction.
external_user_id is required here just like debit. OpenPoly gets it from the stored operator user linked to the Mini App session or redemption item.
| Request field | Type | Requirement | Notes |
|---|---|---|---|
external_user_id | string | Required | Stable operator-side user ID. |
amount_minor | integer | Required | Positive payout/refund amount in minor units; exactly 0 only for a valid Polyflight loss credit. |
currency_code | string | Required | Current REST v1 value: IDR. |
idempotency_key | string | Required | Credit operation key. |
reason | string | Required | Opinion reason or mini_app_polyflight_win / mini_app_polyflight_loss / mini_app_polyflight_refund. |
metadata | object | Required | Always present, possibly empty; fields are additive. |
Required credit metadata varies by flow:
| Flow | Reason | Required metadata | Optional additive metadata |
|---|---|---|---|
| Sell | mini_app_sell_order | orderId, marketId | outcome, resolveDeadlineTs |
| Redemption | mini_app_redemption | redemptionItemId, marketId | resolveDeadlineTs |
resolveDeadlineTs is an optional ISO-8601 string-or-null field. Current sell and redemption requests include the related market's resolution deadline when configured, otherwise null. Redemption credit does not include orderId. Do not reject unknown additive metadata fields.
Polyflight credits
| Effect | Amount | Key |
|---|---|---|
| Win | Stored potentialPayoutMinor | mini_app_polyflight_ticket:{ticketId}:win |
| Loss acknowledgement | Exactly 0 | mini_app_polyflight_ticket:{ticketId}:loss |
| Cancelled-ticket refund | Full original stakeMinor | mini_app_polyflight_ticket:{ticketId}:refund |
All Polyflight credits use native IDR without an FX lookup. Wins include metadata.operation: "win" and metadata.result: "won"; losses use reason mini_app_polyflight_loss, metadata.operation: "loss", and metadata.result: "lost". Both carry the immutable ticket context and pinned transport, with no original-debit fields. A refund retains its original-debit references. See Native wallet contract for the complete zero-amount request and acknowledgement examples, not Opinion market/order fields.
Zero is valid only for that Polyflight loss contract; do not accept arbitrary zero credits. Record the losing result durably and idempotently without changing the balance. A loss obligation tracks delivery, not money owed; it causes no monetary accounting entries and emits no loss payout pending/processed callback. Historical losses are not backfilled, and already-planned/dispatched request evidence is unchanged.
Response body
Same response shape as debit:
{
"external_user_id": "operator-user-123",
"status": "succeeded",
"currency_code": "IDR",
"amount_minor": 15000,
"idempotency_key": "mini_app_redemption_item:77:credit",
"remote_reference": "wallet-op-456",
"balance_minor": 115000
}
For a loss, succeeded acknowledges the recorded losing result. An optional balance_minor is the unchanged current balance and remote_reference remains optional. Timeout, pending, invalid responses, and manual resolution follow the same safety contract as monetary credits; zero does not authorize blind replay or a new key.
Status values
Response field requirements:
| Response field | Type | Requirement | OpenPoly behavior |
|---|---|---|---|
status | string | Required | Must be succeeded, failed, or pending. |
external_user_id | string | Optional | Falls back to the request value. Full-contract tests reject a mismatch. |
idempotency_key | string | Optional | Falls back to the request key. |
remote_reference | string | Optional | Preserved when non-empty. |
balance_minor | integer | Optional | Post-operation balance, preserved for reconciliation. |
failure_reason | string | Optional | Recommended when status = failed. |
amount_minor, currency_code | mixed | Optional echoes | Not consumed from the response; OpenPoly keeps the request values. |
succeededfailedpending
Rules
- payout amount already includes OpenPoly rounding rules
- credit must be idempotent by
idempotency_key - response should echo
external_user_idwhen available - direct mutation responses must not return
unknown;pending, transport failure, and invalid HTTP2xxresponses require manual review - without a valid contract body, HTTP
408,429, and5xxare ambiguous while another4xxis failed; a valid REST v1 status body is honored even on non-2xx - do not blindly re-credit with a new idempotency key after timeout or manual review
- for a Polyflight win or refund, even an explicit
failedresponse leaves the owed obligation in manual review; it does not erase the debt - for a Polyflight loss, an explicit
failedor ambiguous result likewise requires reconciliation of the existing zero-amount delivery; the ticket remains lost and no payout is owed
