Skip to content

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.

  1. POST /perps/withdraw/payload — returns EIP-712 typed data and the nonce embedded in it.
  2. 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 503 on 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

FieldTypeRequiredDescription
amountstringYesUSDC amount to withdraw as a decimal string, e.g. "250". The venue deducts its withdrawal fee (about $1) from it.
destinationstringNoArbitrum 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

FieldTypeRequiredDescription
amountstringYesExactly the amount returned by step 1
destinationstringYesExactly the destination returned by step 1
noncenumberYesExactly the nonce returned by step 1
signatureobjectYes{ 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

StatuserrorTypeMeaningWhat to do
400Invalid body (non-positive amount, malformed address, nonce or signature)Fix the request
403READ-scoped key, or a portfolio of another organization (Organization API)Use a WRITE-scoped key for a portfolio of your organization
422PERPS_ACCOUNT_NOT_CONFIGUREDThe portfolio has no perps accountCheck Account Status
422WITHDRAW_DESTINATION_NOT_ALLOWEDdestination is not one of the portfolio's Arbitrum trading vaultsOmit destination, or pass one of the portfolio's Arbitrum trading vault addresses
422WITHDRAW_DESTINATION_UNAVAILABLEThe portfolio has no Arbitrum trading vault to receive the USDCCreate an Arbitrum trading vault for the portfolio in the app
422WITHDRAW_INSUFFICIENT_BALANCEThe venue reports an insufficient withdrawable balanceCheck withdrawable on /perps/account and lower the amount
422WITHDRAW_SIGNATURE_INVALIDThe venue could not match the signature to the accountSign typedDataJson exactly as returned with the account owner wallet, then resubmit
422WITHDRAW_NONCE_INVALIDThe venue rejected the nonce (stale or already used)Request a new payload and sign it
422WITHDRAW_REJECTEDThe venue rejected the withdrawal for another reasonRequest 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.