Refund
Return a completed spend, reversing the posting that recorded it.
Source src/operations/refund.ts#L54 · refundsrc/operations/registry.ts#L45 · refund
Refund
refund reverses a completed spend. It returns the buyer the full price they paid, then unwinds the sale account by account.
You name the order, not the buyer or the amount — the handler reads those off the recorded sale. Pass the orderId from the original spend. A reason for the audit trail is optional:
const outcome = await economy.submit({
kind: "refund",
idempotencyKey: "idem_refund_1",
actor: { kind: "system", service: "support" },
orderId: "ord_1",
reason: "changed mind",
});
// → outcome.status === "committed"
The buyer gets the full price back. Each seller is debited only up to the balance they still hold. Any uncollectable remainder is booked as a debt to the platform.
Parameters
The payload is one refund variant of Operation, tagged by kind. The fields:
| Field | Type | Default | Description |
|---|---|---|---|
kind | "refund" | — | Selects this operation. |
idempotencyKey | string | — | Makes a retried request run at most once. A repeat with the same key returns the earlier result. |
actor | Principal | — | Who is asking. Must be system or operator — see Authorization. |
orderId | string | — | The order to reverse, from the original spend. A blank or whitespace-only value throws. |
reason | string | — (omitted) | Optional note recorded in the reversing transaction's metadata. |
Returns
refund returns an Outcome:
committed— the reversal posted;transactionis the reversing posting.duplicate— the order was already reversed (by an earlier refund or aclawback);transactionis the recorded reversal.rejected— no sale was found for the order. The onlyRejectionCodehere isUNKNOWN_ORDER.
Postings
The reversal rebuilds the sale's double-entry posting in mirror image. It applies the opposite of each change the sale made.
To do that, it splits the original sale's legs into three groups:
- Raise in full — the buyer's
spendableandpromoaccounts the sale debited, plus any platform account the sale drew down. Raising an account never pushes a balance below zero, so these apply with no cap. - Claw back, capped — each seller's
earnedbalance the sale credited, andREVENUEif it took a fee. The clawback is limited to what that account still holds. If a seller already spent or paid out their cut, only the part still there comes back. - Receivable — the total that couldn't be clawed back is credited to
SYSTEM.RECEIVABLE, the platform's IOU, so debits and credits still cancel.
The shortfall is always denominated in CREDIT. A sale only moves CREDIT, so the receivable that stands in for an uncollectable part matches.
Authorization
refund is platform-initiated only. An end user (actor.kind === "user") may never run it: a self-serve refund debits a seller's earned balance, which is a fraud vector. Only a system service or a human operator may call it — see actors and authorization.
A user actor is declined with an UNAUTHORIZED fault before any work begins.
Reason codes
refund returns one RejectionCode:
| Code | When |
|---|---|
UNKNOWN_ORDER | No sale was recorded for orderId. |
A blank or whitespace-only orderId is not a rejection — it throws a MALFORMED_OPERATION fault. That way a malformed request surfaces as a client error instead of degrading to a silent UNKNOWN_ORDER.
Preconditions and invariants
A refund and an order-tied clawback both reverse the same sale, so only one may run. Before posting, refund claims a second, order-scoped key, reversed:<orderId>. That claim makes the two paths mutually exclusive per order. Whoever claims first reverses; the other path returns the recorded transaction as duplicate.
The order-scoped claim is recorded in the same database transaction as the reversal. If the refund rolls back, the claim rolls back too, so the order can still be reversed later.
After the reversal commits, refund revokes the buyer's entitlement to the SKU — for a gift, the recipient's. That revocation runs in the same database transaction, and it is ownership a UI checks through read.entitled. It is a no-op if that user was never granted the SKU; for a sale predating ownership-at-purchase, nothing is revoked.
The reversing posting balances to zero across each currency, exactly as the sale did, so conservation holds.