/api/qmail/local/can-send
GETPreflight whether the payment wallet can fund a send: estimated storage fees (per-server stripe formula) plus required inbox fee. Local wallet/pool only — does not contact RAIDA for payment. Handler: api_handle_qmail_can_send.
Description
Computes wallet_required = sum of per-server upload fees + required inbox fee, then compares against wallet balance plus locked pool value. Returns can_send, a stable reason code, and a user-facing message.
Storage-fee estimate uses the same interim formula as CMD 70 / CMD 75 (0.01 CC per MiB of per-server stripe data, ceil to the next 0.01 CC, welfare free under the configured allowance, default 1 MiB). Prefer files= (file sizes in bytes) when available; it mirrors orchestrator striping math. Fallback is stripe_bytes (same stripe size on every server). If neither is present, upload fees are estimated as zero (welfare / free upload).
Inbox fee: explicit inbox_fee / required_inbox_fee if provided; otherwise max fee from to / cc / bcc contact rows (default when unknown). If still zero (compose-level check with no recipients), the configured default inbox fee is used so an empty Default wallet is blocked before the user writes a message.
estimated_server_count comes from qmail_health_get_healthy_servers (fallback: configured server count, then 1). Preflight does not apply the orchestrator’s cooldown filter or expand-to-configured when healthy < required data stripes. Default upload_lockers equals this estimate; override with upload_lockers or servers.
Parameters
| Param | Type | Required | Description |
|---|---|---|---|
wallet_path |
string | No (default Default) |
Wallet name (e.g. Default) or full path; wallet is accepted as an alias. Must exist. The Mail wallet cannot fund payments when a fee is due (can_send: false, reason: MAIL_WALLET_NOT_ALLOWED). |
upload_lockers |
integer | No | Number of upload lockers / servers for the fee sum (0…QMAIL_MAX_SERVERS). Alias: servers. Default: estimated_server_count. |
files |
string (CSV) | No | Comma-separated file sizes in bytes (e.g. 2621440,1048576). When present and non-empty, wins over stripe_bytes. Strict unsigned decimal CSV: no signed numbers, no scientific notation, no empty items, no leading/trailing/consecutive commas. Max 64 files. Empty files= is HTTP 400. |
stripe_bytes |
integer | No | Per-server stripe size in bytes when files is absent. Same fee applied to every locker. Strict unsigned parse: leading +/- rejected (HTTP 400). Absent or unused → zero stripe fee (welfare estimate). |
inbox_fee |
number / integer | No | Explicit required inbox fee in decimal CC (e.g. 10, 0.25) or legacy whole-CC integer. Alias: required_inbox_fee. When set, recipient lists are not used for fee amount. |
to, cc, bcc |
string | No | Recipient address lists (comma/semicolon-separated) used only when inbox_fee is not supplied. Handler takes the max contact inbox fee (or default for unknown contacts). Counts appear as recipient_count / unknown_fee_count. |
Response
{
"command": "can-send",
"success": true,
"request_id": "…",
"can_send": true,
"reason": "OK",
"message": "Ready to send mail",
"required_upload_lockers": 10,
"estimated_server_count": 10,
"required_inbox_fee": 10,
"upload_locker_fee_cc": 0.03,
"default_inbox_fee_cc": 10,
"inbox_fee_explicit": false,
"recipient_count": 0,
"unknown_fee_count": 0,
"upload_available": 0,
"inbox_fee_available": 3,
"inbox_fee_available_for_amount": 3,
"wallet_required": 10.3,
"locked_available_value": 30,
"locked_value": 30,
"wallet_balance": 114.5,
"combined_wallet_value": 144.5,
"low_balance": false
}| Field | Type | Description |
|---|---|---|
command | string | Always "can-send". |
success | bool | true when the preflight ran (not an HTTP error). |
request_id | string | Correlation id for logs (always present). |
can_send | bool | true when combined wallet + locked value ≥ wallet_required (or free send on Mail wallet). |
reason | string | Stable code: OK, NO_SEND_FUNDS, MAIL_WALLET_NOT_ALLOWED. |
message | string | User-facing explanation. |
required_upload_lockers | int | Locker count reported for the send (minimum 1, even when upload_lockers=0 zeroes the fee sum). |
estimated_server_count | int | Healthy-server count. Used as the default when upload_lockers/servers is not supplied; an explicit override also drives the files= striping math. |
required_inbox_fee | number | Exact decimal CC inbox fee included in the total. |
upload_locker_fee_cc | number | Average per-server upload fee (decimal CC). Historical field name; not a fixed whole-CC charge. |
default_inbox_fee_cc | number | Configured default inbox fee (decimal CC). |
inbox_fee_explicit | bool | true when inbox_fee / required_inbox_fee was supplied. |
recipient_count | int | Resolved recipients from to/cc/bcc (0 when fee is explicit or lists omitted). |
unknown_fee_count | int | Recipients that fell back to the default inbox fee. |
upload_available | int | Pool rows typed upload currently available (often low between per-send funding). |
inbox_fee_available | int | Inbox-fee lockers available in the pool. |
inbox_fee_available_for_amount | int | Available inbox-fee lockers funded at or above required_inbox_fee. |
wallet_required | number | Exact decimal CC total: sum of per-server upload fees + inbox fee. |
locked_available_value | number | Locked pool value in AVAILABLE status (decimal CC). |
locked_value | number | Total locked pool value (decimal CC). |
wallet_balance | number | Bank + fracked balance (decimal CC). |
combined_wallet_value | number | wallet_balance + total locked value. |
low_balance | bool | UI hint from pool status (replenish may starve). |
Worked fractional example
Request: wallet_path=Default&stripe_bytes=2621440&upload_lockers=10&inbox_fee=10
- 2 621 440 bytes = 2.5 MiB per server → 2.5 × 0.01 = 0.025 → ceil to 0.03 CC per server.
- Upload total: 10 × 0.03 = 0.3 CC.
- Inbox fee: 10 CC (explicit).
wallet_required= 10.3;upload_locker_fee_cc= 0.03;inbox_fee_explicit=true.
{
"command": "can-send",
"success": true,
"request_id": "…",
"can_send": true,
"reason": "OK",
"message": "Ready to send mail",
"required_upload_lockers": 10,
"estimated_server_count": 10,
"required_inbox_fee": 10,
"upload_locker_fee_cc": 0.03,
"default_inbox_fee_cc": 10,
"inbox_fee_explicit": true,
"recipient_count": 0,
"unknown_fee_count": 0,
"upload_available": 0,
"inbox_fee_available": 3,
"inbox_fee_available_for_amount": 3,
"wallet_required": 10.3,
"locked_available_value": 30,
"locked_value": 30,
"wallet_balance": 114.5,
"combined_wallet_value": 144.5,
"low_balance": false
}With files=2621440 instead of stripe_bytes, per-server stripe size is derived from orchestrator striping (data_stripes = max(1, server_count - 1)), then the same fee formula is applied to every server.
Errors
| HTTP | Message |
|---|---|
| 400 | Invalid wallet_path |
| 400 | Invalid upload_lockers parameter |
| 400 | Invalid inbox_fee parameter |
| 400 | Invalid files parameter — empty files=, signed numbers (+/-), scientific notation, empty CSV items, overflow, or fee estimate failure |
| 400 | Invalid stripe_bytes parameter — signed input, non-decimal junk, or overflow |
| 404 | Wallet not found |
| 500 | Failed to check send funding |
Mail-wallet payment rejection is a successful JSON body (success: true, can_send: false, reason: MAIL_WALLET_NOT_ALLOWED), not HTTP 400.
Example
# Stripe-based fractional estimate (2.5 MiB/server × 10 + 10 CC inbox)
curl "http://localhost:8081/api/qmail/local/can-send?wallet_path=Default&stripe_bytes=2621440&upload_lockers=10&inbox_fee=10"
# File sizes CSV (strict unsigned decimals)
curl "http://localhost:8081/api/qmail/local/can-send?wallet_path=Default&files=2621440,1048576&upload_lockers=10"
# Compose-level check (default inbox fee, zero storage estimate)
curl "http://localhost:8081/api/qmail/local/can-send?wallet_path=Default"Related
- /api/qmail/local/inbox-fee — contact inbox fee by serial number only (not a send-cost calculator).
- /api/qmail/local/locker-pool/status — pool counts, locked values, wallet balance.
- CMD 70 payment & pricing — storage fee formula, welfare, claim-by-download.