Core concepts

The handful of ideas that make Numeratica different — read these once and the rest of the API is obvious.

On this page: Authentication · Determinism & result_id · Seeds · Error model · Versioning · Idempotency · Not advice · Rate limits

Authentication & API keys

Every /v1 endpoint requires an API key; health probes (/readyz) and self-serve signup (/v1/signup) are public. Get a free key in seconds — no card. Send the key as a bearer token, or in the X-API-Key header — both are accepted:

# either of these:
-H "Authorization: Bearer nmr_sk_test_xxx"
-H "X-API-Key: nmr_sk_test_xxx"
Keep keys server-side. A key in client-side JavaScript or a public repo is compromised — treat it like a password and rotate immediately if exposed. Separate test and live keys are on the roadmap.

Determinism & result_id

Numeratica calculations are pure functions. Every response carries an engine_version and a result_id, and the same inputs always produce the same answer and the same id — byte for byte.

The result_id is a content hash:

result_id = "<prefix>_" + sha256( canonical_json({ params, engine }) )[:16 hex]
# e.g.  mc_5445a80e48d88d60   (prefix "mc" = Monte Carlo)

Seeds

Costs and fees

Projections are gross of costs by default. Pass annual_fee_rate (a decimal — 0.01 is 1%) to model the all-in drag of an advisory fee plus fund expense ratios; it is applied as a reduction to the expected return each year, so it compounds inside the return distribution rather than acting as a separate withdrawal.

This matters more than it looks. On our documented example — age 40 to 65 to 95, $500k starting, $30k/yr in, $60k/yr out, 7% return / 12% volatility — a 1% annual fee moves success probability from 89.8% to 75.1% and cuts the median ending balance by about 61%. For an already-retired household the same fee can turn a median ending balance of ~$178k into zero. If you surface these numbers to end users, either pass a realistic rate or tell them the figures are gross of fees.

The same input is accepted by every endpoint that simulates returns — /v1/retirement/monte-carlo, /v1/retirement/withdrawal-strategy, /v1/retirement/accumulation-projection, /v1/retirement/savings-target and /v1/debt-vs-invest. Costs bite hardest in decumulation, where there is no contribution stream to absorb them: a 65-year-old drawing 4.5% from $1.5M goes from 61.9% success at no fee to 47.2% at 1%.

Stochastic engines (such as the retirement Monte Carlo) draw random paths. To make a run exactly reproducible, pass a seed:

Error model

Errors share one envelope — a stable machine-readable code plus a human message:

{
  "error": {
    "code": "missing_field",
    "message": "field \"current_age\" is required"
  }
}
CodeHTTPWhen
unauthorized401Missing or invalid API key.
invalid_json400Body isn't valid JSON, or contains an unknown field.
missing_field400A required field is absent (the message names it).
invalid_params400A value is out of its allowed range or internally inconsistent.
too_many_simulations400A simulation count exceeds the engine's cap.
no_solution400A root-finder couldn't converge for the given inputs.
non_finite_result400The calculation produced a non-finite value (rejected before returning).
free_tier_limit400An input exceeds the free tier's cap (e.g. the Monte Carlo simulation count on the free plan).
method_not_allowed405Wrong HTTP method (calc endpoints are POST).
rate_limited429Over your per-minute budget — see Rate limits.
deadline_exceeded503The calculation hit its 25-second compute deadline.
internal_error500An unexpected server error (should be rare; please report).

Validation is strict and specific: unknown JSON fields are rejected (so typos surface immediately), and missing_field / invalid_params name the exact problem. Input caps keep every call bounded, so deadline_exceeded should never appear in normal use.

Versioning & deprecation

There are two version surfaces:

That makes engine changes detectable, not silent: watch engine_version (or a changed result_id for known inputs) to know exactly when an engine moved.

Deprecation. We rarely remove anything — additive changes stay in /v1, and a breaking change ships as a new /v2 surface that runs beside /v1 rather than replacing it. If we ever retire a stable (GA) endpoint or field, we give at least 12 months' notice on the wire — RFC 8594 Deprecation and Sunset response headers plus a changelog entry — and the old surface keeps working for the full window. Endpoints explicitly labeled beta carry a shorter window so we can iterate. This is separate from routine kept-current data updates (e.g. a new tax year's tables): those change outputs and are signaled by an engine_version / result_id bump, not a deprecation.

Continuity. Determinism is your safety net — a cached (inputs, seed, engine_version) → result_id stays valid indefinitely, and every kept-current value traces to a public primary source (see Trust), so you can always reproduce a result. Enterprise customers who need a stronger guarantee can ask about source/data escrow, released on defined triggers (e.g. discontinuation without a successor or company wind-down).

Idempotency

Because calculation POSTs are pure and side-effect-free, they are inherently idempotent: sending the same body twice returns the same result and the same result_id, and changes nothing on our side. Retry freely after a network error — no idempotency key required. (Stateful operations like billing, when they arrive, will use explicit idempotency keys.)

Informational, not financial advice

Every response includes a disclaimer. Numeratica returns calculation-engine output — it is not financial, tax, or investment advice, and it does not account for an individual's full circumstances. You are responsible for how results are presented to end users and for any advice layered on top.

"disclaimer": "Calculation engine output for informational purposes only; not financial advice."

Rate limits & quotas

Each API key has a per-minute request budget (a token bucket). Full keys get 200 requests/minute; free keys get 15/minute. Exceed it and the API returns 429 rate_limited with a Retry-After header (in seconds):

HTTP/1.1 429 Too Many Requests
Retry-After: 1

{ "error": { "code": "rate_limited", "message": "rate limit exceeded; retry after 1s" } }

Next