Wallet
Panel Guide
Operations & Intervention

Panel — Operations & Intervention

Who calls these endpoints?Merchant panel
Base path
/api/panel/v1/wallet
Identity
Panel session (JWT) — Authorization: Bearer <panel access token>. The token comes from panel login; an s2s signing key does NOT open these endpoints.
Authorization
RBAC permission: wallet:operation:read · wallet:operation:interveneFour-eyes: the requester and the approver cannot be the same person — this is enforced by identity comparison, not by a role gate.
Cross-surface
READ ONLY is partially mirrored: GET operations, GET operations/:id, /receipt, GET topups/:id, GET withdrawals/:id also exist in the s2s API; the s2s POST topups/:id/refund endpoint runs on your own approval flow. The four-eyes request and approval endpoints have NO s2s equivalent: */intervention-requests, cash-out/:id/hold-requests, suspense-funds/:id/match-requests|return, topups/:id/refund-requests, GET suspense-funds, ops-health, bill-payments, operations/:id/receipt.pdf.

This section does two jobs: monitoring (what happened) and intervention (what to do about stuck money). Interventions are not executed directly: one operator opens a request and another person approves it.

Monitoring

GET operations                      # filters: endUserId, walletAccountId, status, operationType
GET operations/:id
GET operations/:id/receipt
GET operations/:id/receipt.pdf
GET operations/export
GET topups/:id
GET withdrawals/:id
GET bill-payments
GET bill-payments/:id

What the operation fields mean: Operations.

On a support call, look at status first, then failureCode and fraudDecision. Those three answer "what happened to my transaction" almost every time.

Ops-health — stuck and pending money at a glance

GET ops-health

Permission: wallet:operation:read

The counters on a list screen only count the page currently fetched; ops-health answers the same question server-side, across the whole tenant. When the KPI on the operations screen and this endpoint disagree, this one is authoritative.

FieldWhat it reports
operations.postingTotal / postingStuckOperations sitting in POSTING, and those past the threshold
operations.oldestPostingAgeSecondsAge of the oldest POSTING record
operations.awaitingReconThe reconciliation queue awaiting an operator decision
operations.heldOperations held by a risk/compliance lock
operations.reversingOperations with a reversal or card top-up refund in progress; the system completes them on its own
operations.failedRecentFAILED count over the last 24 hours
suspense.unmatched + unmatchedAmountMinor + oldestUnmatchedAgeSecondsUnmatched incoming money
cashout.held + heldAmountMinorCash-out codes held for review
bulk.processingBatchesBulk payout batches left half-processed

The response also carries generatedAt, postingStuckMinutes (default 5) and failedLookbackHours (24).

⚠️

Wire this endpoint into a monitoring dashboard. When postingStuck and oldestPostingAgeSeconds rise together, the money path is stuck somewhere — it shows up here before the user complaints do.

Amounts are minor-unit strings — do not convert them to numbers and sum them.

Four-eyes requests

Every intervention that touches money takes two steps:

  1. Request. An operator requests the intervention. The request is stored permanently; the requester is taken from your session.
  2. Approval. A person other than the requester approves it. Only then is the action executed.

The request body has no person field. If you add requesterPrincipal or approverPrincipal to the body, the request is rejected with 400. If the requester tries to approve their own request, it is rejected with 403 WALLET_FOUR_EYES_VIOLATION; the comparison uses the stored requester on the request record.

GET  intervention-requests                 # status, kind, targetId, limit, cursor
GET  intervention-requests/:id
POST intervention-requests/:id/approve     # { reason? }
POST intervention-requests/:id/reject      # { reason }
Request type (kind)Endpoint that opens itPermission to requestPermission to decide
OPERATIONPOST operations/:id/intervention-requests · POST withdrawals/:id/intervention-requestswallet:operation:intervenewallet:operation:intervene
CASHOUT_HOLDPOST cash-out/:id/hold-requestswallet:operation:intervenewallet:operation:intervene
SUSPENSE_MATCHPOST suspense-funds/:id/match-requestswallet:operation:intervenewallet:operation:intervene
TOPUP_REFUNDPOST topups/:id/refund-requestswallet:operation:intervenewallet:operation:intervene
KYC_OVERRIDEPOST end-users/:id/kyc-level/override-requestswallet:enduser:kyc-overridewallet:enduser:kyc-approve
LEGAL_HOLD_RELEASEPOST legal-holds/:id/release-requestswallet:legal-hold:managewallet:legal-hold:approve

Request statuses:

StatusMeaning
PENDINGAwaiting approval. Only one open request of the same type may exist for the same record; a second one gets 409 WALLET_INTERVENTION_REQUEST_CONFLICT
APPROVEDApproved, being executed. A short-lived intermediate state
EXECUTEDApproved and applied (decidedBy, executedAt, resultRef)
FAILEDApproved but could not be applied; failureCode and failureMessage give the reason. The request is not approved again — open a new one if needed
REJECTEDRejected; no money moved. The requester can withdraw their own request this way if they hold the decision permission

If, after approval, a system failure prevents execution from completing, the request does not stay in APPROVED: it is picked up automatically within a few minutes. If the action was not applied, it is applied with the approver's record; if it was, the request closes as EXECUTED. If it cannot be proven that this request applied the action, the request becomes FAILED with failureCode: WALLET_INTERVENTION_OUTCOME_UNVERIFIED — check the record's history and open a new request if needed.

Preconditions are checked twice: when the request is opened (does the action make sense in the record's current state) and at approval (the state may have changed in between). Opening a request does not move money.

In the panel, the approval queue is the Four-Eyes Approvals screen (Dört-Göz Onayları) in the wallet operations menu. On a request you opened, the Approve button is disabled and the reason is shown.

Suspense funds

Money that arrives but cannot be attributed goes to suspense.

GET  suspense-funds                       # filter by status
POST suspense-funds/:id/match-requests    # { instructionId, reason } — four-eyes request
POST suspense-funds/:id/return            # { reason }
ActionWhat it does
MatchOpens a request to bind the funds to a deposit instruction; once approved, the user is credited
ReturnSends the money back to the sender
⚠️

Matching moves money and is hard to undo. Before approving, confirm the sender details are consistent with the instruction holder; when in doubt, reject the request and return the funds. Reclaiming a transfer credited to the wrong person may be impossible once they have spent it.

Operation and withdrawal interventions

Open a request to finalize a stuck or held operation. A withdrawal is an operation; both endpoints create the same request type (OPERATION).

POST operations/:id/intervention-requests
POST withdrawals/:id/intervention-requests
FieldRequiredRule
action✅reject | approve | force_reverse | recon_close
reason✅3–1000 characters
ActionAccepted statusOnce approved
rejectHELD, INITIALIZED, CHALLENGEDFAILED — blocked funds are released
approveHELDThe money-out that passed risk review is executed
force_reverseCOMPLETED, AWAITING_RECONThe money path is reversed — see below
recon_closeAWAITING_RECONCOMPLETED — the bank is taken to have sent the money
🚫

Once approved, all four actions are irreversible. There is no "undo" endpoint for a wrong action; correcting it requires a new money movement.

reason is mandatory and cannot be shorter than 3 characters — but length is not the point: this field is the audit record, and values like "test" or "ok" explain nothing in an audit.

⚠️

Never resolve an AWAITING_RECON operation without seeing the real outcome on the bank side. Reversing it on the assumption that "it probably did not go through" causes a double payout if it did.

How a reversal (force_reverse) proceeds

A reversal starts by moving the operation to REVERSING before any money moves. A concurrent action that loses this claim (for example a bank result arriving at the same moment, or another intervention) never reaches the ledger; the money is reversed only once.

Ledger answerOperation statusWhat to do
Reversal acceptedREVERSEDNothing; the wallet.*.reversed event is sent
Reversal rejected (e.g. insufficient balance on the receiving side)AWAITING_RECON (failureCode: WALLET_REVERSAL_FAILED)No money moved. Resolve the cause and request the reversal again, or request recon_close based on the bank outcome
Outcome unknown (timeout)REVERSINGWait. The system retries the reversal automatically with the same ledger key and completes it; no second reversal is created

The automatic bank refund (when the bank rejects a withdrawal) follows the same order. If a bank rejection collides with a concurrently approved recon_close, only one is applied and the conflict is raised to platform monitoring as a money-integrity alert.

Held cash-out codes

POST cash-out/:id/hold-requests           # { action, reason }

Opens a request for a cash-out code stuck in risk review (HELD). The action set is different:

FieldRequiredRule
action✅approve | reject
reason✅3–1000 characters
ActionOnce approved
approveHELD → ACTIVE; the code continues within its own TTL
rejectHELD → RELEASED; the blocked funds return to the user

Top-up refunds

POST topups/:id/refund-requests           # { reason }

Opens a request to refund a completed card top-up; the refund is executed at approval.

⚠️

Check the user's balance before approving — if the funds were spent, the wallet cannot be debited and the refund is not executed.

Commission

GET business-accounts/:id/commission
PUT business-accounts/:id/commission

Commission configuration for a business account. Schedule versioning: Panel — Limit & Fee Schemes.

Intervention discipline

  1. Read before you touch. The operation history (status transitions) answers nearly every question.
  2. Write a reason. The reason field is the audit record and the only later answer to "why was this done".
  3. Wait when uncertain. AWAITING_RECON is a waiting state, not a decision.
  4. Read the request before approving. The requester and the approver are taken from their sessions and cannot be the same person; the value of the second pair of eyes lies in the approver actually reviewing the request.