Refund a payment
Refund a paid order through the CN Payment API. The same flow applies to eligible WeChat, Alipay, and activated Huawei purchases: retrieve the order, submit one refund command, and track that refund until execution finishes. Your app does not call a native payment SDK to refund.
A refund request can require review before money is returned. Keep the request status separate from the provider's execution status in your app.
1. Authorize the refund and retrieve the order
Your app or support tool calls a refund endpoint you own, for example POST /api/orders/shop_order_1042/refunds. Authenticate the caller, check order ownership and refund permissions, and apply your business policy. Calculate the permitted amount on your backend.
Retrieve the saved platform order using a Payment API key with payments:read:
curl --fail-with-body \
'https://pay.appconnect.cn/payment/v1/orders/ord_demo_001' \
--header "Authorization: Bearer $PAYMENT_API_KEY"
Check that the identity matches your business order and the status is paid. Read version, paid_amount_minor, refunded_amount_minor, pending_refund_amount_minor, and refund_request_deadline_at.
The unreserved principal is:
paid_amount_minor - refunded_amount_minor - pending_refund_amount_minor
For a fully paid CNY 10.00 order with no refunds in progress, a full refund is 1000. A CNY 2.50 partial refund is 250. For a later refund, calculate the remaining amount again. Do not refund pending amounts twice.
This arithmetic is a local precheck. The platform revalidates capacity, deadline, risk, and policy when it receives the request. If your integration also uses the Business API, its refund-eligibility endpoint provides eligible, blocked_reasons, refundable_amount_minor, and order_version; it is an additional eligibility workflow, not the checkout's payment authority.
2. Persist and submit the refund request
Create a durable refund-attempt record with the platform order ID, intended amount, reason, request bytes, and a new idempotency key. Use a different key for each deliberately separate partial refund, and the same key for retries of one request.
Save this as refund.json, using the current order version. This example refunds CNY 2.50:
{
"amount_minor": 250,
"reason": "customer_request",
"expected_order_version": 4
}
curl --fail-with-body --request POST \
'https://pay.appconnect.cn/payment/v1/orders/ord_demo_001/refunds' \
--header "Authorization: Bearer $PAYMENT_API_KEY" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: refund-shop-order-1042-01' \
--data-binary @refund.json
The key needs refunds:write. Allowed reasons are customer_request, duplicate, fraud, service_issue, consumer_complaint, and other.
A successful response contains the refund in data. Save data.id before returning a response to the caller. It is the stable refund identifier throughout review and provider execution; execution_id may be null initially.
A 2xx response acknowledges the request. Show Refund requested or its current review state, not Refund complete.
Handle retries and conflicts
If the response is lost, replay the same saved bytes with the same idempotency key on the same CN route. Do not submit the same command through the Business API as a recovery shortcut. Follow a returned operation reference if the request is still running.
After a definitive VERSION_CONFLICT, retrieve the order again and recalculate the remaining principal. If a new command is still appropriate, persist a revised body and a new key. Never change the body associated with the old key, and never create a replacement command while the original outcome remains uncertain.
3. Retrieve the refund result
Use refunds:read:
curl --fail-with-body \
'https://pay.appconnect.cn/payment/v1/refunds/ref_demo_001' \
--header "Authorization: Bearer $PAYMENT_API_KEY"
Or use the sample HTTP client in your backend:
const { data: refund } = await payments.getRefund(savedRefund.platformRefundId);
Verify id, app_id, order_id, currency, and amount_minor against the saved refund attempt. Keep the latest refund version. Do not compare its version number with the order's version.
| Review or execution state | Customer-facing handling |
|---|---|
Review received or under_review | Refund requested; awaiting review. |
Review needs_materials | Ask for the required information through your support workflow. |
Review awaiting_funds | Request is waiting; keep tracking the same refund. |
Review approved, execution not yet complete | Refund processing. Approval does not mean money has been returned. |
Review rejected or cancelled | Explain the reviewed result; do not show a successful refund. |
Execution accepted, submitting, or processing | Refund processing. |
Execution unknown | Result is being confirmed. Continue reconciliation; do not submit another refund. |
Execution succeeded | Refund complete. |
Execution failed or cancelled | Show the confirmed outcome and route to your support/retry policy. |
The reference lists all refund fields and statuses. Receive refund.succeeded and refund.failed through your verified webhook handler, and keep background queries for requests still under review or awaiting execution.
4. Update your app and product records
Expose your own authenticated refund-status endpoint. It can return a business refund ID, amount, currency, review status, and execution status. Check ownership before returning those values; the mobile app does not query the platform with your key.
After successful execution, retrieve the order again to update its refunded and pending amounts. Apply your product policy once: revoke an eligible unlock, adjust unused credits, or record a return. A financial refund does not automatically change your entitlements. Decide explicitly how partial refunds affect the product and how to handle already-consumed goods.
Refund policy and fees
The default ordinary request window is 180 × 24 hours after authoritative paid_at. A valid request must arrive before refund_request_deadline_at; the deadline is exclusive. An accepted request keeps its original valid_received_at while waiting for review.
The target default automatic workflow requires an original payment of at most CNY 500, a customer-wide rolling automatic-refund amount within CNY 5,000, sufficient eligible funds, and successful risk checks. Requests outside those conditions go to review rather than being automatically rejected. These defaults are configurable; use the policy attached to the order. Provider execution limits can differ.
amount_minor is customer principal. Fees are accounted for separately in economics.estimate and economics.actual. See Fees and balances for a worked example.