POST/v1/partner/end_users/{external_id}/withdraw

Withdraw

Withdraws funds from the specified portfolio back to the end user's wallet. If the portfolio's underlying token is not USDC, the funds are automatically swapped back to USDC via 1inch. If the withdrawal amount matches the full portfolio balance (within a rounding threshold), the full-withdrawal path is taken which avoids dust. This is a synchronous HTTP call — the connection is held open for 5–30 seconds while the on-chain transaction confirms. Supports both Entra token + user token.

Returns a post-withdrawal snapshot of the portfolio. For the on-chain transaction hash or fee details, use GET .../transactions.

Idempotency-Key is required. See Idempotency.

Path parameters

external_idstringrequired

Your stable identifier for the end user.

Headers

Authorizationstringrequired

Entra M2M access token. Format: Bearer <token>. See Entra authentication.

X-User-Tokenstringoptional

Partner-minted JWT for the end user. Required in user-scoped. The sub claim must equal external_id. See User authentication.

Idempotency-Keystringrequired

UUID v4 uniquely identifying this logical withdrawal operation. If the connection drops and you retry, send the same key. See Idempotency.

Content-Typestringrequired

Must be application/json.

Body

portfolio_idstringrequired

UUID of the portfolio to withdraw from. Returned by POST .../portfolios as portfolio_id.

amount_minorstringrequired

Amount to withdraw in the portfolio's token minor units (wei string). For USDC (6 decimals), "10000000" = 10 USDC. Must be a numeric string — no decimals. To withdraw the full balance, pass the current total_balance from GET .../portfolios.

Example request

import { randomUUID } from 'node:crypto';
 
const baseUrl = process.env.YIELDFORCE_API_BASE_URL ?? 'https://yieldforce.io/api';
 
const res = await fetch(`${baseUrl}/v1/partner/end_users/alice-bunq-id/withdraw`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${entraToken}`,
    'X-User-Token': partnerJwt,
    'Idempotency-Key': randomUUID(), // generate once per logical operation
  },
  body: JSON.stringify({
    portfolio_id: 'jar_01HZ4KXQM5E8WRTYN3P7VBJD6F',
    amount_minor: '10000000', // 10 USDC
  }),
  // Set HTTP client timeout to at least 60s
});
const data = await res.json();

Response

200 Withdrawal confirmed — post-withdrawal portfolio snapshot

{
  "status": "confirmed",
  "portfolio": {
    "portfolio_id": "jar_01HZ4KXQM5E8WRTYN3P7VBJD6F",
    "name": "USDC vault on Base",
    "description": null,
    "protocol": "morpho",
    "chain_id": "84532",
    "token_address": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
    "market_address": "0x6A0935DEF442D92c3456FBb38B888375F022C646",
    "token_symbol": "USDC",
    "token_decimals": 6,
    "deposited": "0",
    "yield_earned": "42000",
    "yield_earned_realized": "42000",
    "total_balance": "42000",
    "compounding_enabled": true,
    "created_at": "2026-05-20T10:00:00.000Z",
    "updated_at": "2026-05-20T10:10:00.000Z"
  }
}

Note: the response status code for a successful withdrawal is 200 (not 201) — the portfolio already exists, it is being modified.

Connection drop recovery

Same recovery strategy as deposit: retry with the same Idempotency-Key first, then poll GET .../transactions for a recent withdraw event on this portfolio_id if the retry also times out. Do not generate a new key unless you intend a second, separate withdrawal.

Full-withdrawal detection: if you pass an amount_minor equal to the full total_balance, the backend detects this and uses a more efficient on-chain full-withdrawal path. The resulting deposited and total_balance fields in the response may show small rounding residuals from yield; these are expected.

Errors

400 validation_errorportfolio_id is missing or amount_minor is not a valid numeric string.

400 idempotency_key_required — The Idempotency-Key header is missing.

401 token_missing — No Authorization: Bearer header on the request.

401 invalid_entra_token — Token is expired, malformed, or has an unexpected audience.

401 invalid_user_token — The X-User-Token failed signature verification or is expired.

403 sub_url_mismatch — The sub claim in X-User-Token does not match external_id in the URL.

404 end_user_not_found — No user with this external_id exists, or the portfolio does not belong to this user.

409 idempotency_key_in_use_with_different_params — An Idempotency-Key was reused with a different request body. Generate a new key for a different logical operation.

422 insufficient_portfolio_balance — The requested amount_minor exceeds the portfolio's current total_balance.

504 on_chain_timeout — The on-chain transaction did not confirm within the allowed window. Poll GET .../transactions to check whether it eventually landed.