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

Create portfolio

Creates a new yield portfolio (jar) for the specified end user on the given chain and pool. The portfolio starts with zero deposited funds — use POST .../deposit to fund it. Returns a snapshot of the newly created portfolio. Supports both Entra token + user token.

Obtain the token_address and market_address from GET /v1/partner/pools — do not invent or hard-code these values.

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-Tokenstringrequired

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

Idempotency-Keystringrequired

UUID v4. Generate fresh per logical create-portfolio attempt; reuse the same key when retrying a dropped request. See Idempotency-Key.

Content-Typestringrequired

Must be application/json.

Body

namestringrequired

Display name for the portfolio. 1–50 characters.

chain_idstringrequired

Numeric chain ID as a string. Example: "84532" for Base Sepolia.

token_addressstringrequired

Ethereum address of the underlying ERC-20 token (e.g. USDC). Obtain from GET /v1/partner/poolstoken_address.

market_addressstringrequired

Pool or vault contract address. Must come from GET /v1/partner/poolsmarket_address — this is not the same as token_address. For Morpho, this is the MetaMorpho vault address.

protocolstringoptional

Lending protocol identifier. One of "aave" or "morpho". Default: "aave". Must match the protocol associated with market_address.

descriptionstringoptional

Optional description for the portfolio. Maximum 500 characters.

Example request

const baseUrl = process.env.YIELDFORCE_API_BASE_URL ?? 'https://yieldforce.io/api';
 
const res = await fetch(`${baseUrl}/v1/partner/end_users/alice-bunq-id/portfolios`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${entraToken}`,
    'X-User-Token': partnerJwt,
  },
  body: JSON.stringify({
    name: 'USDC vault on Base',
    chain_id: '84532',
    token_address: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
    market_address: '0x6A0935DEF442D92c3456FBb38B888375F022C646',
    protocol: 'morpho',
  }),
});
const data = await res.json();

Response

201 Portfolio created — post-creation snapshot

{
  "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": "0",
  "yield_earned_realized": "0",
  "total_balance": "0",
  "compounding_enabled": true,
  "created_at": "2026-05-20T10:00:00.000Z",
  "updated_at": "2026-05-20T10:00:00.000Z"
}

Response shape differs from list-portfolios

The create response uses flat token_address, token_symbol, and token_decimals fields (not nested under token). The list-portfolios endpoint uses token: { symbol, address, decimals }. Both shapes represent the same data — the difference is intentional to allow independent evolution of read vs write responses.

Errors

400 validation_error — One or more body fields failed validation. Check name length, that chain_id is a numeric string, and that token_address / market_address are valid Ethereum addresses.

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 in your tenant.

422 pool_not_found — The (chain_id, token_address, market_address, protocol) combination does not match any pool registered for your tenant. Verify values from GET /v1/partner/pools.