---
title: "Project a household you send (nothing stored)"
method: POST
path: "/api/v1/project"
tags: ["project"]
---

# Project a household you send (nothing stored)

`POST /api/v1/project`

## Request

- Base URL: `https://worthune.com`
- URL: `https://worthune.com/api/v1/project`
- Auth: one of:
  - HTTP bearer
  - API key in header `X-Api-Key`

The stateless counterpart to POST /api/v1/households/{id}/project: same engine (spec v0.11.0), same evidence record. Nothing is stored: no household, no id, no row. Any key works, a free sandbox key included; the call shares the organization's hourly quota. Monte Carlo is clamped to 500 paths.

## Request body

- AnonymousProjectionRequest: Expense labels are stripped before the engine sees the document; nothing is stored.
  - `household` Household, required: Household object model v0.2.0 (docs/household-schema-spec.md). Validation rejects, never clamps, and returns every issue with a path. Cross-field rules: ids are unique per collection; every ownerId references a member; exactly one primary member and at most one spouse (a spouse requires married_joint); costBasis only on taxable accounts; holdings fractions sum to 1; a socialSecurity block only on social_security streams; endYear never precedes startYear.
    - `schemaVersion` '0.2.0', required
    - `filingStatus` 'single' | 'married_joint', required: MFS and head-of-household are not modeled (the tax module approximates them with Single ceilings and discloses it).
    - `state` 'AL' | 'AK' | 'AZ' | 'AR' | 'CA' | 'CO' | 'CT' | 'DE' | 'FL' | 'GA' | 'HI' | 'ID' | 'IL' | 'IN' | 'IA' | 'KS' | 'KY' | 'LA' | 'ME' | 'MD' | 'MA' | 'MI' | 'MN' | 'MS' | 'MO' | 'MT' | 'NE' | 'NV' | 'NH' | 'NJ' | 'NM' | 'NY' | 'NC' | 'ND' | 'OH' | 'OK' | 'OR' | 'PA' | 'RI' | 'SC' | 'SD' | 'TN' | 'TX' | 'UT' | 'VT' | 'VA' | 'WA' | 'WV' | 'WI' | 'WY' | 'DC': Two-letter USPS code (or DC). Optional: without it the household gets federal-only tax treatment, stated in the response assumptions.
    - `members` Member[], required
      - `id` string, required: Unique within members.
      - `birthYear` integer, required
      - `relationship` 'primary' | 'spouse' | 'dependent', required
      - `sex` 'male' | 'female': Needed only for longevity-aware Monte Carlo (NCHS sex-specific life table) and for pension-election / claiming strategies; a run that needs it rejects (never guesses) when an adult member lacks it.
    - `accounts` Account[], required
      - `id` string, required: Unique within accounts.
      - `wrapper` 'taxable' | 'traditional' | 'roth' | 'hsa' | 'plan529' | 'cash' | 'annuity', required
      - `ownerId` string, required: Must reference an existing member id (joint taxable accounts: the primary member).
      - `balance` number, required
      - `holdings` Holdings: Asset-class fractions that must sum to 1 (tolerance 1e-9). Omitted means the account is projected as one blended asset.
        - `us_equity` number
        - `intl_equity` number
        - `bonds` number
        - `cash` number
        - `real_assets` number
        - `other` number
      - `costBasis` number: Taxable accounts only (rejected on any other wrapper): total cost basis for gains treatment.
    - `liabilities` Liability[], required
      - `id` string, required: Unique within liabilities.
      - `kind` 'mortgage' | 'auto' | 'student' | 'credit_card' | 'personal', required
      - `balance` number, required
      - `annualRate` number, required: Annual nominal rate as a fraction (0.065 = 6.5%).
      - `monthlyPayment` number, required
    - `incomes` IncomeStream[], required
      - `id` string, required: Unique within incomes.
      - `ownerId` string, required: Must reference an existing member id.
      - `kind` 'salary' | 'self_employment' | 'rental' | 'pension' | 'social_security', required
      - `annualAmount` number, required
      - `startYear` integer: Calendar year, inclusive. Omitted = already flowing.
      - `endYear` integer: Calendar year, inclusive; must not precede startYear. Omitted = lifelong.
      - `growthRate` number: Annual nominal growth as a fraction. Omitted = the run's defaultIncomeGrowth.
      - `socialSecurity` SocialSecurityBlock: Only on kind = social_security. When present the engine COMPUTES the benefit from the §215(a) formula (the ssa-pia model): annualAmount is ignored and startYear defaults to the year the owner reaches claimAge.
        - `aime` number, required: Average Indexed Monthly Earnings, USD/month.
        - `claimAge` integer, required: Whole-year claiming age.
        - `fullRetirementAge` 66 | 67, required
    - `expenses` ExpenseStream[], required
      - `id` string, required: Unique within expenses.
      - `kind` 'living' | 'goal', required
      - `label` string: The only free-text field in the schema. Stripped before computing on the keyless routes (/project, /book-scan) so no identity reaches the engine or the record hash.
      - `annualAmount` number, required
      - `startYear` integer
      - `endYear` integer: Must not precede startYear.
      - `inflationAdjusted` boolean, required: True: the amount is in today's dollars and rides the inflation assumption.
  - `assumptions` ProjectionAssumptions, required: Unknown keys are rejected — the accepted set is exactly: annualReturn, inflationRate, defaultIncomeGrowth, socialSecurityCola, returnsByWrapper.
    - `annualReturn` number, required: Nominal annual return applied to every account balance (one blended rate).
    - `inflationRate` number, required: Annual inflation applied to inflation-adjusted expense streams.
    - `defaultIncomeGrowth` number, required: Growth for income streams that carry no growthRate of their own.
    - `socialSecurityCola` number: COLA applied to COMPUTED social_security benefits from each benefit's start year. Defaults to inflationRate; pass 0 for a flat benefit.
    - `returnsByWrapper` object: Per-wrapper returns; unlisted wrappers keep the blended annualReturn.
      - `taxable` number: Return for taxable accounts, replacing annualReturn (and any sampled path) for that wrapper.
      - `traditional` number: Return for traditional accounts, replacing annualReturn (and any sampled path) for that wrapper.
      - `roth` number: Return for roth accounts, replacing annualReturn (and any sampled path) for that wrapper.
      - `hsa` number: Return for hsa accounts, replacing annualReturn (and any sampled path) for that wrapper.
      - `plan529` number: Return for plan529 accounts, replacing annualReturn (and any sampled path) for that wrapper.
      - `cash` number: Return for cash accounts, replacing annualReturn (and any sampled path) for that wrapper.
      - `annuity` number: Return for annuity accounts, replacing annualReturn (and any sampled path) for that wrapper.
  - `horizon` ProjectionHorizon, required
    - `startYear` integer, required: First projected calendar year, an integer in [1900, 2150] on every surface.
    - `years` integer, required: Number of years to project.
  - `policy` ProjectionPolicy: Execution choices, hashed into the evidence record — a policy run never hashes like a default run. Unknown keys are rejected.
    - `withdrawalOrder` string[]: A permutation of cash, taxable, traditional, roth — the order deficits are drawn from. Default: cash → taxable → traditional → roth.
    - `rothConversions` object[]: Scheduled Roth conversions: each year's amount moves from traditional accounts (document order) into the first roth account, taxed as ordinary income the same year. A schedule with no roth destination is a 422 from the engine.
      - `year` integer, required
      - `amountUsd` number, required
  - `monteCarlo` AnonymousMonteCarloConfig: returnVolatility is not read on the keyless route (engine default 0.15 applies).
    - `seed` integer: Defaults to 20260901 when omitted.
    - `simulations` integer: Requested paths; clamped to [1, 500] on this keyless surface (default 300). The effective count is echoed in monteCarloConfig.
    - `longevity` boolean: Sample death years from the NCHS life table; every adult member then needs `sex` (422 otherwise).

## Response `200`

The projection, optional Monte Carlo, the record, and the storage note.

- object
  - `ok` unknown, required
  - `projection` ProjectionResult, required: Projection spec v0.11.0 (docs/model-specs/household-projection.md).
    - `rows` YearRow[], required
      - `year` integer, required
      - `primaryAge` integer, required
      - `grossIncome` number, required
      - `rmd` number, required: Traditional-account withdrawals forced by the Uniform Lifetime Table.
      - `rothConversion` number: Present only when a scheduled conversion executed (> 0).
      - `federalTax` number, required
      - `stateTax` number, required
      - `expenses` number, required
      - `debtService` number, required
      - `netCashFlow` number, required: Negative = deficit.
      - `withdrawals` number, required: Deficit withdrawals taken this year.
      - `accountBalances` object, required: End-of-year balance by account id.
      - `liabilityBalances` object, required: End-of-year balance by liability id.
      - `netWorth` number, required
    - `endingNetWorth` number, required
    - `ranOutOfMoney` boolean, required
    - `ranOutInYear` integer, nullable, required
    - `assumptionsApplied` string[], required: The simplifications that applied to THIS run, stated plainly.
  - `monteCarlo` HouseholdMonteCarloResult
    - `simulations` integer, required
    - `seed` integer, required
    - `successRatePct` number, required: Share of paths that never fail to cover a deficit, in percent.
    - `endingNetWorth` Percentiles, required
      - `p10` number, required
      - `p25` number, required
      - `p50` number, required
      - `p75` number, required
      - `p90` number, required
      - `mean` number, required
    - `yearlyNetWorthBands` object[], required
      - `year` integer, required
      - `p10` number, required
      - `p25` number, required
      - `p50` number, required
      - `p75` number, required
      - `p90` number, required
    - `medianRuinYear` integer, nullable, required
    - `longevity` object: Present only when the run was longevity-aware.
      - `table` 'nchs.us-life-table.2024', required
      - `survivalConditionedSuccessRatePct` number, required
      - `perMember` object[], required
        - `id` string, required
        - `probAliveAtHorizonEndPct` number, required
      - `notes` string[], required
  - `monteCarloConfig` object: The effective Monte Carlo configuration (after clamping); present with monteCarlo.
    - `seed` integer, required
    - `simulations` integer, required
    - `longevity` boolean, required
  - `record` DecisionRecord, required: Evidence record v2. Verify with POST /api/v1/records/verify or the SDKs' verifyRecord.
    - `sha256` string, required: SHA-256 (hex) over the canonical JSON of {model, specVersion, inputs, outputs} — keys sorted recursively, no whitespace.
    - `fields` unknown, required
    - `howToVerify` string, required
    - `model` string, required: The hashed model identity — a catalog model name, "household-projection", or "decision-<strategy>".
    - `specVersion` string, required
    - `inputs` object: The exact hashed inputs, present on records whose envelope does not otherwise expose them (household projections, decisions). Point-model run responses echo `inputs` at top level instead; a verify response's record covers the inputs the caller sent in the request.
    - `signature` RecordSignature: Proves Worthune minted the record's hash. Absent when the deployment has no signing key; a stored decision is signed only when its hash is recomputed from the stored inputs.
      - `alg` 'Ed25519', required
      - `keyId` string, required: Matches a key at GET /api/v1/records/keys.
      - `value` string, required: base64url signature.
      - `message` string, required: What was signed: "worthune-record:v1:" + sha256. Rebuild it from the hash when verifying.
  - `storage` object, required
    - `stored` unknown, required
    - `note` string, required
  - `upgrade` object, required
    - `note` string, required
    - `pricing` string, required

## Other responses

- `400`: Malformed JSON, missing household/assumptions/horizon, or a bad policy (errors carry paths).
- `401`: No key presented (Authorization: Bearer wk_… or X-Api-Key).
- `402`: Unrecognized or revoked API key.
- `403`: The key predates organization support (contact support for a free rotation).
- `422`: The household document failed validation, longevity was requested without member sex, or the engine rejected the policy.
- `429`: Hourly per-org stateful quota, a per-route durable limit, or the 120 requests/minute per-IP burst backstop.
- `503`: Billing is not configured on this deployment; household resources are disabled rather than served without tenancy (production fail-closed).

---

[API](https://skmtc.dev/worthune/apis/worthune-api.md) · [All operations](https://skmtc.dev/worthune/apis/worthune-api/llms.txt) · [OpenAPI document](https://skmtc.dev/worthune/apis/worthune-api/revisions/9f2241072555?raw)
