Wallet Transfer
Let registered customers send wallet money to each other. Two-factor verified by OTP — secure by default.
Setting up?
Configure transfer fees, daily caps, and Twilio SMS credentials under Notifications Settings (SMS section). KYC gating under KYC Settings.
What It Does
| For | Means |
|---|---|
| Customer | Send money to another customer's wallet. OTP confirms the move (SMS + email) |
| Admin | Configurable fee per transfer. Full audit log of every move. KYC-gateable |
How Customers See It
- On the My Wallet page → click Send
- Pick recipient (autocomplete by email or username)
- Type amount + optional note
- Submit → OTP sent to email + SMS (if phone on file)
- Enter the 6-digit OTP
- Done — sender debited, recipient credited, both get confirmation

Setup
Wallet → Settings → Transfer
| Setting | Default | What it does |
|---|---|---|
| Enable | OFF | global toggle |
| Min amount | 1 | smallest transfer |
| Max amount per request | empty | largest single transfer |
| Daily cap | empty | per-user per-day limit |
| Fee type | none | none / fixed / percent |
| Fee amount | 0 | value when fee is fixed/percent |
| Who pays fee | sender | always the sender; not configurable |
The same Settings tab in admin contains the Transfer Fees section:


OTP Delivery
| Channel | Default | Notes |
|---|---|---|
| always sent | uses your store's mailer | |
| SMS | sent if Twilio configured AND customer has phone on file | optional but recommended |
OTP is valid for 10 minutes. Customers can re-request after 30 seconds.
If Twilio fails for any reason (downtime, geo block), the email OTP still works — transfer flow never breaks because of an SMS issue.
See SMS Notifications for Twilio setup.

KYC Gating
Add transfer to your KYC required-features list to require approved KYC before any transfer:
Wallet → Settings → KYC → Required features → ✓ Transfer
Customer with non-approved KYC sees a "Verify identity to send" card instead of the form. See KYC.
Lock a Customer From Transferring
Sometimes you want to block transfers for one specific customer (account recovery, fraud investigation) without disabling the feature site-wide:
Users → [user] → Wallet card → Lock transfers → writes the per-user lock flag.
Customer-side: the Send tile is disabled with a tooltip; server-side blocks any submitted form.
Customers Can Self-Lock Too
In /wallet-central/settings/ → Disable transfers (security) toggle. Useful if a customer suspects compromise. Toggleable back on anytime.
Transfer Fee
Configurable as fixed or percent of amount:
| Type | Result |
|---|---|
none | no fee, customer sends 100% |
fixed | flat amount, e.g. 2.50 per transfer |
percent | 1 = 1% of transfer amount |
Fee is debited from the sender's wallet at the same instant as the transfer amount. Fee row is logged separately for accounting.
Common Scenarios
Customer says OTP didn't arrive
- Check spam folder
- Confirm Twilio credentials in Wallet → Settings → Notifications → SMS
- Confirm customer has a phone number on file (
Users → [user] → Phone) - Email OTP should still arrive — if neither, check WC mailer config
Customer A sends to wrong recipient B
Transfers are atomic and final. Recover by manually debiting B + crediting A from their user profile Wallet cards (with audit notes).
Disable transfers store-wide
Toggle "Enable" → OFF in Transfer settings. Customer Send tile shows "Transfers are currently disabled."
When Something Goes Wrong
| Problem | Fix |
|---|---|
| OTP not received | Check spam; confirm Twilio creds; check Twilio console logs |
Code expired | 10-min window — re-request |
KYC required | Recipient or sender lacks approved KYC and transfer is gated |
Recipient invalid | Email / username not found among registered customers |
| Send tile greyed out | KYC not approved OR transfer-locked flag set |
For developers — REST endpoint, hooks, OTP storage
REST endpoint
POST /wp-json/wkwc_wallet/v1/verify_otp/{otp}
Headers:
X-WP-Nonce: <nonce>
Body:
recipient_id, amount, note
Response on success:
{
"success": true,
"transfer_id": 1234,
"balance_after": 750.00
}
Errors: invalid_otp / expired_otp / insufficient_balance / kyc_required / recipient_invalid.
See REST API.
Atomic move
WKWP_Wallet_Transfer::verify_otp_and_send:
- Match OTP against payment-requests row
- Confirm not expired and not consumed
- DB transaction
- Debit sender amount + fee
- Credit recipient amount
- Insert two ledger rows linked by
reference=transfer:#<request_id> - Mark request consumed
- Commit
- Fire
wkwp_wallet_transfer_completed
On any failure → rollback.
OTP storage
Bcrypt-hashed in wkwc_wallet_payment_requests (never stored plaintext). Verified via password_verify().
Hooks
| Hook | Type | When |
|---|---|---|
wkwp_wallet_transfer_otp_generated | action | OTP row written |
wkwp_wallet_transfer_completed | action | both ledger rows written |
wkwp_wallet_transfer_failed | action | rollback |
wkwp_wallet_transfer_fee | filter | mutate fee per transfer |
wkwp_wallet_transfer_otp_template_sms | filter | override SMS template |
