Sweep API
Configure when and how HasaPay moves funds from child addresses back to the master wallet (auto-sweeping). Includes org defaults, per-chain overrides, per-address settings, history, and manual trigger.
Authentication: Dual-auth (JWT or HMAC)
Base path: /api/v1/sweep
Sweep modes
HasaPay supports three behaviors, derived from two booleans (auto_sweep_enabled, retain_child_balance):
| Mode | auto_sweep_enabled |
retain_child_balance |
Behavior |
|---|---|---|---|
no_sweep |
false |
true (forced) |
Funds stay on child addresses. No automated movement. |
sweep_retain |
true |
true |
Sweep funds to master, child ledger persists (P2P model — child can still withdraw). |
sweep_clear |
true |
false |
Sweep funds to master, child ledger zeroes (payment-processor model — child is a one-shot receiver). |
Forbidden combination:
auto_sweep_enabled=false+retain_child_balance=falseis rejected. With sweeping off, balances must stay on the child or they'd be unreachable. The server force-corrects toretain=trueif you setauto_sweep=false.
Sweep mechanism (per chain)
The mechanism HasaPay uses for a sweep is auto-detected per token at sweep time — you don't configure it. EVM stablecoins with EIP-2612 permit use a one-tx permit + transferFrom; non-permit ERC-20s use a multi-tx fund_and_approve flow with cached approvals; Tron TRC-20 uses a fund_and_sweep flow (master funds child with TRX, child broadcasts the TRC-20 transfer).
Note:
sweep_modelwas a config knob in earlier versions and was dropped in migration 035. If your client still sends it, it's ignored.
Get org sweep config
GET /api/v1/sweep/config
Returns the org-level defaults plus any per-chain overrides.
Response
{
"data": {
"auto_sweep_enabled": true,
"sweep_threshold_usd": 25,
"sweep_min_native_reserve": "10000000000000000",
"retain_child_balance": false,
"effective_retain_child_balance": false,
"mode": "sweep_clear",
"chain_overrides": [
{
"id": "uuid",
"organization_id": "uuid",
"chain": "tron",
"network": "mainnet",
"auto_sweep_enabled": true,
"sweep_threshold_usd": 50,
"sweep_min_native_reserve": "1000000",
"retain_child_balance": null,
"created_at": "2026-06-08T10:00:00Z",
"updated_at": "2026-06-08T10:00:00Z"
}
]
}
}
| Field | Meaning |
|---|---|
auto_sweep_enabled |
Org default — applied to chains that don't have an override |
sweep_threshold_usd |
Min USD value to trigger a sweep |
sweep_min_native_reserve |
Native amount to leave on child for gas (raw integer in the smallest unit — wei for EVM, sun for Tron) |
retain_child_balance |
Org default policy |
effective_retain_child_balance |
What the server actually uses (after the auto_sweep=false force-correction) |
mode |
Convenience label: no_sweep, sweep_retain, or sweep_clear |
chain_overrides[] |
Per-(chain, network) rows. null field = inherit org default |
Update org sweep config
PUT /api/v1/sweep/config
All fields optional. Send only what you want to change.
Request body
| Field | Type | Description |
|---|---|---|
auto_sweep_enabled |
bool | Master toggle |
sweep_threshold_usd |
float | Min USD to trigger (must be ≥ 0) |
sweep_min_native_reserve |
string | Raw native units to leave on child for gas |
retain_child_balance |
bool | true = sweep_retain, false = sweep_clear |
Response
Returns the updated config plus the recomputed mode.
Per-chain overrides
A row in org_chain_configs overrides the org default for one (chain, network). NULL fields in the row mean "inherit from org default."
Get one chain's override
GET /api/v1/sweep/config/:chain/:network
404 not_found when no override exists — caller should fall back to org defaults from GET /sweep/config.
Upsert chain override
PUT /api/v1/sweep/config/:chain/:network
Idempotent. Same body as the org-level update (auto_sweep_enabled, sweep_threshold_usd, sweep_min_native_reserve, retain_child_balance). The OFF+OFF guard considers effective values — if your override leaves a field NULL, the org default fills in.
Example: set a chain-specific Tron threshold
curl -X PUT https://api.hasapay.com/api/v1/sweep/config/tron/mainnet \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"auto_sweep_enabled": true,
"sweep_threshold_usd": 50,
"sweep_min_native_reserve": "1000000"
}'
Delete chain override (revert to inherit)
DELETE /api/v1/sweep/config/:chain/:network
Idempotent — no error if no row exists. After delete, the chain inherits org defaults again.
Per-address sweep settings
List addresses with their effective sweep settings
GET /api/v1/sweep/addresses?chain=&network=&limit=50&offset=0
Response
{
"data": {
"addresses": [
{
"id": "uuid",
"address": "0x8ba1f109...",
"chain": "ethereum",
"network": "sepolia",
"auto_sweep_enabled": true,
"is_active": true
}
],
"total": 124,
"limit": 50,
"offset": 0
}
}
Update one address's sweep settings
PUT /api/v1/sweep/addresses/:addressId
Request body
| Field | Type | Description |
|---|---|---|
auto_sweep_enabled |
bool | Per-address override; null = inherit chain/org default |
sweep_threshold |
string | Per-address USD threshold; null = inherit |
Manual sweep trigger
POST /api/v1/sweep/addresses/:addressId/trigger
Force a sweep of one address now, regardless of the threshold. Useful for ops use or for a UI "sweep now" button.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
token_symbol |
string | Yes | Which token to sweep (e.g. USDC) |
Response
{
"data": {
"sweep_id": "uuid",
"status": "initiated"
}
}
A successful trigger fires the same sweep.initiated / sweep.completed / sweep.failed webhook sequence as an automatic sweep.
Sweep history
GET /api/v1/sweep/history?chain=&status=&limit=50&offset=0
Query params
| Param | Description |
|---|---|
chain / network |
Filter |
status |
pending, processing, completed, failed |
from_date / to_date |
ISO 8601 |
limit / offset |
Pagination (default 50 / 0) |
Response
{
"data": {
"sweeps": [
{
"id": "uuid",
"organization_id": "uuid",
"source_type": "child_address",
"source_id": "uuid",
"destination_type": "master_wallet",
"destination_id": "uuid",
"chain": "ethereum",
"network": "sepolia",
"amount": "100000000",
"token_symbol": "USDC",
"token_decimals": 6,
"status": "completed",
"trigger_reason": "threshold",
"created_at": "2026-06-09T10:00:00Z",
"updated_at": "2026-06-09T10:05:00Z"
}
],
"total": 312,
"limit": 50,
"offset": 0
}
}
amount is in raw token units — divide by 10^token_decimals for display.
Trigger reasons
| Reason | When |
|---|---|
threshold |
Auto — balance crossed sweep_threshold_usd |
manual |
Triggered via POST /sweep/addresses/:id/trigger |
scheduled |
Scheduled batch sweep (org config-driven) |
cleanup |
Leftover-funds sweep after a Tron TRC-20 sweep |
Tron specifics — fund-and-sweep
TRC-20 sweeps work differently from EVM because child addresses can't pay energy until they have TRX:
- Master estimates TRC-20 sweep cost (energy in SUN).
- Master sends
max(0, needed − child_TRX_balance) × 1.3TRX to the child. - Wait for confirmation.
- Child broadcasts the TRC-20
transfer(master, amount)and pays its own energy from the funded TRX. - Wait for confirmation.
- If
retain_child_balance=falseand leftover TRX > 1, sweep leftover TRX back to master.
Failure handling is documented in the source (internal/service/gasless_orchestrator_tron.go). The relevant ledger groups are fund_for_sweep:{sweepID}, sweep:{sweepID}, and sweep_leftover:{sweepID}.
Native TRX sweeps don't need fund-and-sweep — the child pays bandwidth from the reserve we leave it during the previous sweep (
NativeReserveon the Tron adapter = 1 TRX).
Errors
| Code | Cause |
|---|---|
validation_error |
sweep_threshold_usd negative; or the forbidden OFF+OFF config |
not_found |
Chain override doesn't exist (GET only); address not on this org |
forbidden |
Address belongs to a different org |
internal_error |
Underlying repo/service failure — retryable |