Withdraw from Perps
Moves USDC from the perps account back to the portfolio's Arbitrum trading vault. The withdrawal is authorized by the wallet that owns the perps account, not by the API key: you request the venue's typed data, sign it with that wallet, and submit the signature. Definitive never holds a key that can withdraw from a perps account.
POST /perps/withdraw/payload— returns EIP-712 typed data and the nonce embedded in it.POST /perps/withdraw— submits your signature over that exact typed data.
Endpoints
Portfolio API: POST https://ddp.definitive.fi/v2/portfolio/perps/withdraw/payload and POST https://ddp.definitive.fi/v2/portfolio/perps/withdraw
Organization API: Replace
/v2/portfolio/with/v2/organization/portfolios/{portfolioId}/. See Portfolio vs. Organization API.
Both require a WRITE-scoped API key (all POST endpoints do). Returns
503on deployments where perps account access is not yet enabled — see Availability.
Signer
Sign with the wallet that owns the perps account — its address is accountOwnerAddress on /perps/account/status. The venue recovers the signer from the signature and treats that wallet as the account being withdrawn from, so sign only with the account owner wallet.
Step 1 — Request the payload
POST /perps/withdraw/payload
| Field | Type | Required | Description |
|---|---|---|---|
| amount | string | Yes | USDC amount to withdraw as a decimal string, e.g. "250". The venue deducts its withdrawal fee (about $1) from it. |
| destination | string | No | Arbitrum address that receives the USDC. Defaults to the portfolio's Arbitrum trading vault; when set it must be one of the portfolio's Arbitrum trading vaults. |
const payload = await AuthHelpers.signAndSend({
path: "/v2/portfolio/perps/withdraw/payload",
method: "POST",
body: { amount: "250" },
apiKey: process.env.API_KEY,
apiSecret: process.env.API_SECRET,
});Response:
{
typedDataJson: "{\"types\":{...},\"primaryType\":\"HyperliquidTransaction:Withdraw\",...}",
nonce: 1757370000000,
amount: "250",
destination: "0x1234..."
}Sign typedDataJson exactly as returned (eth_signTypedData_v4 or your library's typed-data signing). The domain is HyperliquidSignTransaction with chain id 421614 on both mainnet and testnet; the primary type is HyperliquidTransaction:Withdraw. Split the 65-byte signature into r and s (32-byte hex strings) and v (27 or 28).
import { parseSignature } from "viem";
const typedData = JSON.parse(payload.typedDataJson);
const signature = await account.signTypedData({
domain: typedData.domain,
types: typedData.types,
primaryType: typedData.primaryType,
message: typedData.message,
});
const { r, s, v } = parseSignature(signature);Step 2 — Submit the signature
POST /perps/withdraw
| Field | Type | Required | Description |
|---|---|---|---|
| amount | string | Yes | Exactly the amount returned by step 1 |
| destination | string | Yes | Exactly the destination returned by step 1 |
| nonce | number | Yes | Exactly the nonce returned by step 1 |
| signature | object | Yes | { r, s, v } — r/s as 0x-prefixed 32-byte hex, v as 27 or 28 |
The venue verifies the signature against a withdrawal rebuilt from these three values, so any difference from step 1 (including formatting) invalidates the signature.
const json = await AuthHelpers.signAndSend({
path: "/v2/portfolio/perps/withdraw",
method: "POST",
body: {
amount: payload.amount,
destination: payload.destination,
nonce: payload.nonce,
signature: { r, s, v: Number(v) },
},
apiKey: process.env.API_KEY,
apiSecret: process.env.API_SECRET,
});Response:
{
success: true,
amount: "250",
destination: "0x1234...",
nonce: 1757370000000
}USDC arrives in the destination vault on Arbitrum, typically within about five minutes.
Errors
| Status | errorType | Meaning | What to do |
|---|---|---|---|
| 400 | — | Invalid body (non-positive amount, malformed address, nonce or signature) | Fix the request |
| 403 | — | READ-scoped key, or a portfolio of another organization (Organization API) | Use a WRITE-scoped key for a portfolio of your organization |
| 422 | PERPS_ACCOUNT_NOT_CONFIGURED | The portfolio has no perps account | Check Account Status |
| 422 | WITHDRAW_DESTINATION_NOT_ALLOWED | destination is not one of the portfolio's Arbitrum trading vaults | Omit destination, or pass one of the portfolio's Arbitrum trading vault addresses |
| 422 | WITHDRAW_DESTINATION_UNAVAILABLE | The portfolio has no Arbitrum trading vault to receive the USDC | Create an Arbitrum trading vault for the portfolio in the app |
| 422 | WITHDRAW_INSUFFICIENT_BALANCE | The venue reports an insufficient withdrawable balance | Check withdrawable on /perps/account and lower the amount |
| 422 | WITHDRAW_SIGNATURE_INVALID | The venue could not match the signature to the account | Sign typedDataJson exactly as returned with the account owner wallet, then resubmit |
| 422 | WITHDRAW_NONCE_INVALID | The venue rejected the nonce (stale or already used) | Request a new payload and sign it |
| 422 | WITHDRAW_REJECTED | The venue rejected the withdrawal for another reason | Request a new payload and retry; contact support if it persists |
The venue classifications are best-effort; when in doubt, request a fresh payload, sign it, and resubmit promptly.
Notes
- Withdrawing the entire balance also clears the account's trading approval at the venue. Order placement in the app then asks for setup again; withdrawals are unaffected.
- The withdraw endpoints share the perps rate limit of 20 requests/second per API key.