Internal Transfer (Perp ↔ Spot)
Atomic move of USDT between the user's perp margin account and the spot wallet. No on-chain transaction is involved — both sides are maintained by the backend.
Authentication Required
JWT only. API-key callers receive 403 API Key permission denied.
Authorization: Bearer <JWT>
POST /spot/transfer
Request Body
{
"direction": "perp_to_spot",
"token": "USDT",
"amount": "100"
}
| Field | Type | Required | Description |
|---|---|---|---|
direction | string | Yes | perp_to_spot or spot_to_perp. |
token | string | Yes | MVP only supports USDT. |
amount | string / number | Yes | Decimal amount (e.g. "100" = 100 USDT). Must be positive. |
Response — 200 OK
{
"direction": "perp_to_spot",
"token": "USDT",
"amount": "100",
"perp_balance_after": "4900",
"spot_balance_after": "5100"
}
| Field | Description |
|---|---|
perp_balance_after | Perp available balance after the transfer. |
spot_balance_after | Spot available balance after the transfer. |
Atomicity
The transfer happens inside a single DB transaction with FOR UPDATE row locks on both sides. Either both balances move or neither does — concurrent transfers from the same user serialize.
Error Responses
| HTTP | error | Cause |
|---|---|---|
400 | invalid direction: <value> | direction not one of the two allowed values. |
400 | unsupported token: <symbol> | Token other than USDT requested. |
400 | amount must be positive | amount ≤ 0. |
400 | insufficient balance | The source side does not have amount available. |
403 | API Key permission denied: transfers not allowed | Caller authenticated via API Key. |
500 | internal error | Database error — see server logs. |
Code Example
import requests
BASE = "https://api-sepolia.p99.world/api/v1"
JWT = "your_jwt_token"
resp = requests.post(
f"{BASE}/spot/transfer",
headers={"Authorization": f"Bearer {JWT}", "Content-Type": "application/json"},
json={"direction": "perp_to_spot", "token": "USDT", "amount": "100"},
)
print(resp.status_code, resp.json())