Integration materials generated from this source version
Refunds and historical discounts / 退款与历史折扣
Workflow and authority
- Read the order and original items. Customer users use
GET /v1/customer-portal/orders/{order_id}; tenant operators useGET /v1/orders/{order_id}. - Preview with
POST /v1/orders/{order_id}/refund-preview. This does not reserve money or create a refund. - Persist the exact selection, preview token, reason and application-generated idempotency key. Submit
POST /v1/refund-requests. - An independent tenant operator with
billing.manageapproves or rejects throughPOST /v1/refund-requests/{request_id}:approveor:reject. The requester cannot approve their own request. Portal users cannot approve, including customer owners. - Approval creates a durable pending execution. Read
GET /v1/refund-requests/{request_id}and, oncerefund_idexists,GET /v1/refunds/{refund_id}. Approval is not a completed cash refund. - Read
GET /v1/refunds/{refund_id}/effectsseparately. Cash statusSUCCEEDEDis not proof that coupons, entitlements, subscriptions or purchase benefits finished processing.
Customer OWNER users can request and withdraw their own unreviewed request; VIEWER users are read-only. Operators need billing.view for reads and billing.manage for writes. A write-only key should not be used as a polling client. Customer account selection requires membership, not an arbitrary caller-supplied ID.
Choose the intended policy
| Mode | Input | Amount source | Business meaning |
|---|---|---|---|
RETURN_ITEMS | Original order-line IDs and decimal quantities | Server allocates the original paid net amount, including historical discounts | Return purchased items; the server decides attributable reversals |
COMPENSATION | Positive decimal cash amount | Caller proposes, server validates against remaining captured cash | Keep purchase benefits and coupon policy |
LEGACY_CASH | Positive decimal cash amount | Compatibility with historical amount-only refunds | Do not use as a shortcut around item attribution |
Do not mix cash and item selections, recalculate using current catalogue prices, restore a coupon locally, or refund the undiscounted price. For a historical order with subtotal 100, discount 20, paid 80 and two equally priced items, returning one item previews 40. Unequal lines, fractional quantities, rounding, previous refunds and active holds can change the allocation. Use the returned amount and token; do not hard-code this example's arithmetic.
Wire example
The following are request shapes, not embedded credentials or runnable customer data. Supply real UUIDs from your authenticated order read. Monetary amounts and quantities are strings. Allocation amounts are integer minor-unit strings. Protobuf int64 versions are JSON strings; do not convert them to JavaScript number.
POST /v1/orders/{order_id}/refund-preview
{"mode":"RETURN_ITEMS","items":[{"order_line_id":"<original-line-uuid>","quantity":"1"}]}POST /v1/refund-requests, with Idempotency-Key: <persisted-business-key>:
{"order_id":"<order-uuid>","mode":"RETURN_ITEMS","items":[{"order_line_id":"<original-line-uuid>","quantity":"1"}],"reason":"Customer returned one item","preview_token":"<preview_token>","idempotency_key":"<same-persisted-business-key>"}POST /v1/refund-requests/{request_id}:approve from a different authorized principal:
{"reason":"Reviewed original payment and returned item"}GET /v1/refund-requests?order_id=<order-uuid>&limit=20&cursor=<url-encoded-next_cursor> returns {data,next_cursor}. An omitted/empty data array is empty, not an error. Stop when the cursor is absent/empty. Do not restart from page one repeatedly to find old requests.
SDK usage from this source version
TypeScript, trusted server/BFF only:
import { NexusClient } from "@rainlib/nexus-sdk";
const nexus = new NexusClient({
baseUrl: process.env.NEXUS_API_URL!,
bearerToken: userAccessToken,
tenantId, environmentId, customerAccountId,
});
const selection = { orderId, mode: "RETURN_ITEMS" as const, items: [{ orderLineId, quantity: "1" }] };
const preview = await nexus.refunds.preview(selection);
const command = { ...selection, reason: "Returned item", previewToken: preview.previewToken, idempotencyKey: businessKey };
// Application responsibility: durably save command BEFORE sending it.
const request = await nexus.refunds.request(command);
const latest = await nexus.refunds.getRequest(request.id);
if (latest.refundId) {
const cash = await nexus.refunds.get(latest.refundId);
const effects = await nexus.refunds.effects(latest.refundId);
// Display cash, recovery and effect status independently.
}Go provides PreviewRefund, RequestRefund, GetRefundRequest, ListRefundRequests, ApproveRefundRequest, RejectRefundRequest, WithdrawRefundRequest, GetRefund, ListRefundEffects, and RetryRefundEffect on *nexus.Client. Configure Options.EnvironmentID and, for a human portal session, Options.CustomerAccountID plus BearerToken. Use another operator client for approval. New methods require the matching built/released SDK; a version number in source is not proof of registry publication.
Failure and retry rules
- A stale preview must be refreshed before a new request. Once submission may have reached the server, retain the original command/key until its outcome is known; do not mint a replacement key because a timeout or later rejection occurred.
- SDK retries of keyed requests reuse serialized body, key and scope within a bounded time budget. Exhaustion still means the outcome may be unknown. Applications own durable recovery across process restarts.
- Approve/reject/withdraw and effect retry have no invented idempotency key and are not automatically replayed by these SDK methods. Read the current request after an unknown response. Never send the opposite approval decision to “undo” an uncertain write.
PENDINGholds remain reserved during network failures orREVIEW_REQUIRED. Do not release them or call Stripe yourself.- Effect retry rechecks the business source; it is not another cash refund and cannot waive consumed or reserved entitlement checks.
Stripe webhook setup
Send Stripe snapshot events to the raw endpoint POST /v1/payment-connections/{connection_id}/webhooks/stripe. Configure that connection's signing secret through the deployment secret resolver. Use original bytes and Stripe-Signature, not the generic JSON wrapper route. Do not substitute a publishable key or API secret for a webhook signing secret.
Subscribe to refund.created, refund.updated, refund.failed; compatibility charge.refund.updated is also recognized. Dispute created/updated/closed/funds_withdrawn/funds_reinstated events enter channel review. Do not rely only on aggregate charge.refunded for refund-object processing. A successful receipt acknowledges durable handling, not cash success. Duplicate event IDs are isolated per payment connection.
Current limits — do not conceal these in a customer integration
External refunds, disputes, conflicting money facts and late failures enter review. Unknown-order facts block refunds on that payment connection until attribution is resolved. Dedicated review/unfreeze and external adjustment workflows are not complete. Zero-cash cancellation, mixed-mode manual adjustments, complete subscription-period refunds, actual marketing gift reversal, accounting/invoice and notification closure still require work. Do not label these paths automatic or production-ready. Test the actual coupon checkout and refund integration separately from synthetic payment tests.
中文要点
先读取原订单商品,再由服务端预览历史折后金额;申请前持久化原命令与幂等键。申请人与审批人必须不同,客户门户不可审批。审批、资金退款、恢复状态、权益/赠品后置处理分别展示。请求超时不代表未执行,不得换键重退。渠道外部退款和争议仍需人工复核,尚未形成自动调账/解冻闭环。