API documentation

Integrate plannng's retirement projection engine into your platform. This guide covers authentication, data modelling, and running projections via the REST API.

Base URL

https://api.plannng.co/api/v1

Authentication

All API requests (except registration and login) require a Bearer token in the Authorization header.

Register

POST /auth/register

Create a new account. Returns a JWT token and client profile.

{
  "name": "Jane Smith",
  "email": "jane@example.com",
  "password": "securepassword"
}
FieldTypeNotes
namestringrequired
emailstringrequired
passwordstringrequired - 8 to 72 characters

Log in

POST /auth/login

Authenticate with email and password. Returns a JWT token.

{
  "email": "jane@example.com",
  "password": "securepassword"
}

Response:

{
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "client": {
      "id": "c_abc123",
      "name": "Jane Smith",
      "email": "jane@example.com",
      "emailVerified": true,
      "persons": [],
      "createdAt": "2026-01-15T10:30:00Z",
      "updatedAt": "2026-01-15T10:30:00Z"
    }
  }
}

Using the token

Include the token as a Bearer token in subsequent requests:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Refresh token

POST /auth/refresh

Exchange a valid token for a new one with the current subscription tier. Call this after subscription changes take effect.

Current client

GET /auth/me

Returns the authenticated client's profile, including linked persons.

DELETE /auth/me

Permanently delete the authenticated client and all associated data.

Password management

POST /auth/change-password

Change password for the authenticated client. Requires currentPassword and newPassword.

POST /auth/forgot-password

Send a password reset email. Requires email.

POST /auth/reset-password

Reset password using a token from the reset email. Requires token and newPassword.

Email verification

GET /auth/verify-email?token={token}

Verify an email address using the token sent during registration.

POST /auth/resend-verification

Resend the verification email for the authenticated client.

Response format

All successful responses are wrapped in a data envelope:

{
  "data": { ... }
}

Errors return an error object with a machine-readable code and human-readable message:

{
  "error": {
    "code": "not_found",
    "message": "Plan not found"
  }
}

Standard HTTP status codes are used: 200 success, 201 created, 204 no content (deletes), 400 validation error, 401 unauthorized, 403 forbidden, 404 not found, 409 conflict.

Persons

A person represents an individual in the plan - typically the client, and optionally their partner. Each person has their own date of birth, tax jurisdiction, and can be assigned to plan elements.

GET /persons

List all persons for the authenticated client.

POST /persons

Create a new person.

{
  "firstName": "Jane",
  "lastName": "Smith",
  "dateOfBirth": "1980-06-15",
  "taxJurisdiction": "rUK",
  "lsaUsed": "0",
  "otherAnnualIncome": "0"
}
FieldTypeNotes
firstNamestringrequired
lastNamestringrequired
dateOfBirthstringrequired - format YYYY-MM-DD
taxJurisdictionstringrUK or Scotland
lsaUsedstringDecimal - Lifetime Savings Allowance already used
otherAnnualIncomestringDecimal - annual income not modelled as plan elements
GET /persons/{personId}

Get a person by ID.

PUT /persons/{personId}

Update a person. Same fields as create, all required.

DELETE /persons/{personId}

Delete a person.

Plans

A plan is the top-level container for a client's financial scenario. It holds elements, stages, and budgets. Each plan has a target retirement date.

GET /plans

List all plans. Returns each plan with its elements and stages.

POST /plans

Create a new plan.

{
  "name": "Main retirement plan",
  "retirementMonth": 4,
  "retirementYear": 2045
}
FieldTypeNotes
namestringrequired
retirementMonthintegerrequired - 1 to 12
retirementYearintegerrequired - minimum 1900
GET /plans/{planId}

Get a plan by ID, including all elements and stages.

PUT /plans/{planId}

Update a plan's name.

DELETE /plans/{planId}

Delete a plan and all its elements, stages, and budgets.

POST /plans/{planId}/duplicate

Duplicate a plan. Optionally pass {"name": "Copy of plan"} in the body.

Life stages

Stages divide a plan into named time periods - for example "Working", "Semi-retired", "Fully retired". Elements can reference stages for their start and end dates, so moving a stage boundary automatically shifts the elements tied to it.

GET /plans/{planId}/stages

List all stages for a plan, in order.

POST /plans/{planId}/stages

Create a new stage.

{
  "name": "Semi-retired",
  "startMonth": 1,
  "startYear": 2040,
  "endMonth": 3,
  "endYear": 2045,
  "isRetirement": false,
  "afterStageId": 1
}
FieldTypeNotes
namestringrequired
startMonthintegerrequired - 1 to 12
startYearintegerrequired - minimum 1900
endMonthinteger1 to 12
endYearintegerminimum 1900
isRetirementbooleanMarks this stage as the retirement stage
afterStageIdintegerInsert after this stage in the ordering
GET /plans/{planId}/stages/{stageId}

Get a stage by ID.

PUT /plans/{planId}/stages/{stageId}

Update a stage. Requires name, startMonth, and startYear.

DELETE /plans/{planId}/stages/{stageId}

Delete a stage.

Plan elements

Elements are the building blocks of a plan. Each element represents a financial instrument or cash flow in the client's life. There are five types:

  • income - employment, rental, or other income with start and end dates
  • expense - living costs, bills, and other outgoings
  • investment - ISA, GIA, or savings accounts, each with their own tax treatment
  • pension - PCLS drawdown or UFPLS, with crystallisation and tax-free lump sum handling
  • asset - non-income holdings like property or vehicles, tracked for net worth
GET /plans/{planId}/elements

List all elements for a plan.

POST /plans/{planId}/elements

Create a new element.

Example: ISA investment

{
  "name": "Stocks and shares ISA",
  "type": "investment",
  "subType": "ISA",
  "personId": "p_abc123",
  "startingValue": "85000",
  "startMonth": 4,
  "startYear": 2026,
  "growthRate": {
    "mode": "percentage",
    "period": "annual",
    "value": "5.0"
  },
  "contribution": {
    "amount": "1000",
    "period": "monthly"
  },
  "drawdownOrder": 2
}

Example: PCLS drawdown pension

{
  "name": "Workplace pension",
  "type": "pension",
  "subType": "PCLS_DRAWDOWN",
  "personId": "p_abc123",
  "startingValue": "320000",
  "startMonth": 4,
  "startYear": 2026,
  "growthRate": {
    "mode": "percentage",
    "period": "annual",
    "value": "4.5"
  },
  "contribution": {
    "amount": "500",
    "period": "monthly",
    "endMonth": 3,
    "endYear": 2045
  },
  "pclsPercentage": "25",
  "pclsTargetId": "elem_isa_123",
  "drawdownOrder": 3,
  "drawdownStartMonth": 4,
  "drawdownStartYear": 2045
}

The pclsPercentage sets the tax-free lump sum as a percentage of the fund at crystallisation. pclsTargetId is the element that receives the lump sum (for example, an ISA or savings account). Alternatively, use pclsAmount for a fixed amount.

Example: employment income

{
  "name": "Salary",
  "type": "income",
  "personId": "p_abc123",
  "startingValue": "65000",
  "startMonth": 4,
  "startYear": 2026,
  "endMonth": 3,
  "endYear": 2045,
  "growthRate": {
    "mode": "percentage",
    "period": "annual",
    "value": "2.5"
  }
}

Common fields

FieldTypeNotes
namestringrequired
typestringrequired - income, expense, investment, pension, asset
subTypestringFor investments: ISA, GIA, SAVINGS. For pensions: PCLS_DRAWDOWN, UFPLS. For expenses: BUDGET
personIdstringThe person this element belongs to
startingValuestringrequired - decimal as string
startMonthintegerrequired - 1 to 12
startYearintegerrequired
endMonthinteger1 to 12
endYearintegerWhen the element ends
growthRateobjectrequired - see below
contributionobjectRegular contribution - see below
drawdownOrderintegerPriority for drawdown (lower = drawn first)

Growth rate

Every element requires a growth rate. All three fields are required.

FieldTypeNotes
modestringpercentage or absolute
periodstringmonthly or annual
valuestringDecimal as string - e.g. "5.0" for 5% annual

Contribution

FieldTypeNotes
amountstringrequired - decimal as string
periodstringrequired - monthly or annual
endMonthinteger1 to 12 - when contributions stop
endYearintegerWhen contributions stop

Stage-linked dates

Instead of fixed dates, element start, end, contribution end, drawdown start, and liquidation dates can be linked to a stage boundary. Use the *StageId and *StageEdge fields:

{
  "startDateStageId": 2,
  "startDateStageEdge": "start",
  "endDateStageId": 3,
  "endDateStageEdge": "end"
}

When a stage's dates change, all elements linked to it shift automatically.

Liquidation

Elements can have a one-off liquidation event - for example, selling a property or crystallising a pension.

FieldTypeNotes
liquidationMonthinteger1 to 12
liquidationYearintegerWhen the liquidation occurs
liquidationPercentstringPercentage to liquidate (e.g. "100")
liquidationAmountstringFixed amount to liquidate
liquidationTargetIdstringElement ID to receive the proceeds
gainPercentagestringCapital gain percentage for CGT calculation (GIA only)

Pension-specific fields

FieldTypeNotes
pclsPercentagestringTax-free lump sum as % of fund (typically "25")
pclsAmountstringFixed tax-free lump sum amount (alternative to percentage)
pclsTargetIdstringElement ID to receive the PCLS
drawdownStartMonthintegerWhen drawdown begins (1 to 12)
drawdownStartYearintegerWhen drawdown begins
GET /plans/{planId}/elements/{elementId}

Get an element by ID.

PUT /plans/{planId}/elements/{elementId}

Update an element. Same required fields as create.

DELETE /plans/{planId}/elements/{elementId}

Delete an element.

Budgets

A budget is a named collection of expense line items attached to a plan. When an expense element has subType: "BUDGET" and a budgetId, the engine uses the budget's total as the expense amount.

GET /plans/{planId}/budgets

List all budgets for a plan.

POST /plans/{planId}/budgets

Create a new budget.

{
  "name": "Monthly living costs",
  "items": [
    {
      "name": "Mortgage",
      "amount": "1200",
      "growthRate": {
        "mode": "percentage",
        "period": "annual",
        "value": "0"
      },
      "endMonth": 6,
      "endYear": 2040
    },
    {
      "name": "Utilities",
      "amount": "250",
      "growthRate": {
        "mode": "percentage",
        "period": "annual",
        "value": "3.0"
      }
    }
  ]
}

Each budget item has a name, amount (decimal as string), and its own growthRate. Items can optionally have an end date, after which they stop contributing to the total.

GET /plans/{planId}/budgets/{budgetId}

Get a budget by ID.

PUT /plans/{planId}/budgets/{budgetId}

Update a budget. Replaces the full item list.

DELETE /plans/{planId}/budgets/{budgetId}

Delete a budget.

Projections

A projection runs the deterministic engine against a plan - a month-by-month simulation using fixed growth rates across the full time range.

POST /plans/{planId}/projections
{
  "startMonth": 4,
  "startYear": 2026,
  "endMonth": 3,
  "endYear": 2070
}
FieldTypeNotes
startMonthintegerrequired - 1 to 12
startYearintegerrequired - 1900 to 2200
endMonthintegerrequired - 1 to 12
endYearintegerrequired - 1900 to 2200
overridesobjectWhat-if overrides - see below

Response

The response contains a summary, an array of monthlySnapshots, and effectiveDates for each element.

{
  "data": {
    "summary": {
      "totalMonths": 528,
      "finalNetWorth": "1245000.00",
      "finalInflationAdjustedNetWorth": "780000.00",
      "totalIncomeGenerated": "2850000.00",
      "totalExpensesIncurred": "1920000.00",
      "totalTaxPaid": "485000.00",
      "totalTaxFreeIncome": "120000.00",
      "totalDrawdown": "340000.00"
    },
    "monthlySnapshots": [
      {
        "date": "2026-04",
        "totalNetWorth": "405000.00",
        "inflationAdjustedNetWorth": "405000.00",
        "totalIncome": "5416.67",
        "totalExpenses": "3200.00",
        "totalTax": "890.00",
        "totalTaxFreeIncome": "0",
        "netIncomeAfterTax": "1326.67",
        "netCashFlow": "1326.67",
        "totalDrawdown": "0",
        "remainingLSA": "1073100.00",
        "elements": [
          {
            "elementId": "elem_123",
            "name": "Stocks and shares ISA",
            "type": "investment",
            "value": "86000.00"
          }
        ],
        "personTaxDetails": [
          {
            "personId": "p_abc123",
            "personName": "Jane Smith",
            "taxableIncome": "5416.67",
            "taxFreeIncome": "0",
            "netIncome": "4526.67",
            "taxDue": "890.00",
            "bands": [
              {
                "bandName": "Basic rate",
                "rate": "0.20",
                "income": "4370.83",
                "tax": "874.17"
              }
            ]
          }
        ]
      }
    ],
    "effectiveDates": [
      {
        "elementId": "elem_123",
        "name": "Stocks and shares ISA",
        "type": "investment",
        "startDate": "2026-04",
        "endDate": "2070-03",
        "contributionEndDate": "2070-03",
        "drawdownStartDate": "2045-04"
      }
    ]
  }
}

Each monthly snapshot includes per-element values, per-person tax breakdowns with band detail, and aggregate totals. The inflationAdjustedNetWorth reports net worth in today's purchasing power.

Monte Carlo simulations

A simulation runs the Monte Carlo engine - multiple stochastic iterations with randomised monthly returns, aggregated into percentile bands. Simulations run asynchronously; you submit a job and poll for results.

Submit a simulation

POST /plans/{planId}/simulations

Returns 202 Accepted with a job ID.

{
  "startMonth": 4,
  "startYear": 2026,
  "endMonth": 3,
  "endYear": 2070,
  "simulations": 1000,
  "annualVolatility": 15.0,
  "targetMonth": 3,
  "targetYear": 2060
}
FieldTypeNotes
startMonthintegerrequired - 1 to 12
startYearintegerrequired
endMonthintegerrequired - 1 to 12
endYearintegerrequired
simulationsintegerrequired - 10 to 10,000
annualVolatilitynumberrequired - 0 to 100 (percentage)
targetMonthinteger1 to 12 - for success rate calculation
targetYearintegerTarget date for success rate
overridesobjectWhat-if overrides - see below

Response:

{
  "data": {
    "jobId": "job_xyz789",
    "type": "simulation",
    "status": "pending"
  }
}

Poll for results

Use the jobs endpoint to check status and retrieve results once complete. The result contains P10/P25/P50/P75/P90 percentile bands for net worth at each month, plus a success rate if a target date was provided.

Jobs

Simulations and other long-running operations are processed as background jobs. Poll the job endpoint to check status.

GET /jobs

List all jobs for the authenticated client.

GET /jobs/{jobId}

Get the status and result of a specific job.

{
  "data": {
    "jobId": "job_xyz789",
    "planId": "plan_abc123",
    "type": "simulation",
    "status": "completed",
    "createdAt": "2026-04-12T14:00:00Z",
    "startedAt": "2026-04-12T14:00:01Z",
    "completedAt": "2026-04-12T14:02:15Z",
    "result": { ... }
  }
}

Job statuses: pending, running, completed, failed. If failed, the response includes an errorMessage.

What-if overrides

Both projections and simulations accept an overrides object. This lets you run scenarios without modifying the underlying plan data - change an element's value, shift a stage boundary, or adjust a person's income, then compare the results.

{
  "startMonth": 4,
  "startYear": 2026,
  "endMonth": 3,
  "endYear": 2070,
  "overrides": {
    "elementOverrides": [
      {
        "elementId": "elem_123",
        "startingValue": "100000",
        "growthRate": {
          "mode": "percentage",
          "period": "annual",
          "value": "4.0"
        }
      }
    ],
    "stageOverrides": [
      {
        "stageId": 2,
        "startMonth": 1,
        "startYear": 2043
      }
    ],
    "personOverrides": [
      {
        "personId": "p_abc123",
        "otherAnnualIncome": "5000"
      }
    ]
  }
}

Element overrides

Override any element field for the duration of the run. Only elementId is required; include only the fields you want to change. Use remove* flags (e.g. removeContribution: true) to clear optional fields.

Stage overrides

Shift stage boundaries. Requires stageId; include startMonth, startYear, endMonth, endYear as needed.

Person overrides

Adjust a person's lsaUsed or otherAnnualIncome for the run.

Subscription and usage

API access is metered by subscription tier. Check your current tier and daily usage.

GET /subscription

Get the authenticated client's subscription details, including tier and expiry date.

GET /subscription/usage

Get daily usage per feature.

{
  "data": [
    {
      "feature": "projection",
      "dailyUsed": 12,
      "dailyLimit": 100
    },
    {
      "feature": "simulation",
      "dailyUsed": 3,
      "dailyLimit": 20
    }
  ]
}

Questions about the API? Get in touch at business@plannng.co