Skip to content

API Endpoints

The Tech Strategy Tool exposes a RESTful API using ASP.NET Core Minimal APIs. All endpoints are prefixed with /api/ and use JSON (application/json) for request and response bodies.

Authentication

All endpoints except POST /api/auth/login require authentication via the techstrat_session cookie. Mutating requests (POST, PUT, DELETE) also require the X-CSRF-Token: 1 header.

See API Authentication for details.

Endpoint Reference

Authentication

Method Route Auth Description
POST /api/auth/login None Authenticate and create session
POST /api/auth/logout Authenticated Destroy session and clear cookie
GET /api/auth/me Authenticated Return current user info
POST /api/auth/change-password Authenticated Change own password

POST /api/auth/login

Authenticates a user and creates a session cookie.

Request:

{
  "username": "editor",
  "password": "editor"
}

Response (200 — success):

{
  "success": true,
  "userId": "a1b2c3d4-...",
  "username": "editor",
  "role": "editor",
  "error": null
}

Response (200 — failure):

{
  "success": false,
  "error": "Too many failed login attempts. Try again later."
}

GET /api/auth/me

Returns the currently authenticated user's information.

Response (200):

{
  "success": true,
  "userId": "a1b2c3d4-...",
  "username": "editor",
  "role": "editor",
  "error": null
}

Response (401): Not authenticated.

POST /api/auth/change-password

Request:

{
  "currentPassword": "oldpassword",
  "newPassword": "newpassword"
}

New password must be 8-128 characters.

Response (200): Password changed successfully. Response (400): New password too short or too long.


Teams

Method Route Auth Description
GET /api/teams ViewerOrAbove List all teams

GET /api/teams

Returns all teams with basic info (no nested principles/objectives).

Response (200):

[
  {
    "id": "a1b2c3d4-...",
    "name": "Platform Team",
    "color": "#3498db"
  }
]


Principles

Method Route Auth Description
GET /api/teams/{teamId}/principles ViewerOrAbove Get all principles for a team
GET /api/principles/{principleId} ViewerOrAbove Get a single principle

GET /api/teams/{teamId}/principles

Returns the principles view for a team, including all principles with their details.

GET /api/principles/{principleId}

Returns a single principle by ID.


Objectives

Method Route Auth Description
GET /api/teams/{teamId}/objectives ViewerOrAbove Get all objectives for a team
GET /api/objectives/{objectiveId} ViewerOrAbove Get a single objective

GET /api/teams/{teamId}/objectives

Returns the objectives view for a team, including objectives with their linked principles, initiatives, groups, and progress data.

GET /api/objectives/{objectiveId}

Returns a single objective by ID, including its initiatives and linked principles.


Events

Method Route Auth Description
POST /api/events EditorOrAbove Submit an event

This is the primary write endpoint. All state changes go through event submission.

POST /api/events

Request (SubmitEventRequest):

{
  "eventType": "update_principle_title",
  "targetId": "a1b2c3d4-...",
  "data": {
    "title": "API-First Design"
  },
  "lastSeenSequence": 42
}

Field Type Required Description
eventType string Yes One of the 29 event types
targetId GUID No Entity being acted on (parent for create events, null for create_team)
data object Yes Key-value pairs specific to the event type
lastSeenSequence long No For conflict detection on field edits

Response (200) — Applied (SubmitEventResponse):

{
  "sequenceNumber": 43,
  "status": "applied",
  "rejectionReason": null,
  "previousValue": "Old Title",
  "conflictingServerValue": null
}

previousValue contains the field's value before the change (for history). It is only populated on applied field-edit events.

Response (200) — Rejected (conflict):

{
  "sequenceNumber": 43,
  "status": "rejected",
  "rejectionReason": "Conflict: field was modified since your last read",
  "previousValue": null,
  "conflictingServerValue": "Current Server Value"
}

conflictingServerValue contains the current server value that the client is out of sync with. It is only populated on conflict rejections.

Response (200) — No Change:

When the submitted value is identical to the current value (trimmed), the processor detects a no-op. No event is persisted and no SSE notification is broadcast.

{
  "sequenceNumber": 0,
  "status": "no_change",
  "rejectionReason": null,
  "previousValue": null,
  "conflictingServerValue": null
}

Status values: "applied", "no_change" (no-ops), "rejected" (see rejectionReason for details).

previousValue and conflictingServerValue are mutually exclusive

On applied events, previousValue holds the value before the change and conflictingServerValue is null. On conflict rejections, conflictingServerValue holds the current server value and previousValue is null. Both are null for non-field events, no-ops, and non-conflict rejections.

Response (400) — Invalid envelope:

Returned when the request is structurally invalid. These are programmer errors that the legitimate UI never triggers.

  • Empty or missing eventType
  • Unrecognized eventType (not one of the 29 known types)
  • Missing targetId when required (all types except create_team and restore_history)

Admin-only events

Team-level events (create_team, update_team_name, update_team_color, delete_team) and restore_history require the admin role. The endpoint returns 403 for non-admin users attempting these events.

See Event Types for the complete catalog of event types and their data fields.


History

Method Route Auth Description
GET /api/history/entity/{entityId} ViewerOrAbove Get event history for a specific entity
GET /api/history/all AdminOnly Paginated global event log

GET /api/history/entity/{entityId}

Returns all events that affected the given entity, using the history_associations table. This includes create events (where TargetId is the parent, not the entity itself) and cascaded effects (e.g., delete_principle affecting objectives). Falls back to TargetId-based filtering for pre-migration data with no associations.

GET /api/history/all

Returns paginated global event log. Query parameters:

Parameter Type Default Description
from int (latest) Starting sequence number
limit int 100 Number of events to return (max 500)

GET /api/history/entity/{entityId}

Response: Array of HistoryEntryDto:

[
  {
    "sequenceNumber": 3,
    "timestamp": "2026-01-15T10:25:00Z",
    "actorName": "editor",
    "eventType": "create_principle",
    "status": "applied",
    "description": "Created principle 'Security First'",
    "fieldName": null,
    "oldValue": null,
    "newValue": null,
    "isTransitive": false,
    "canReapply": true
  },
  {
    "sequenceNumber": 5,
    "timestamp": "2026-01-15T10:30:00Z",
    "actorName": "editor",
    "eventType": "update_principle_title",
    "status": "applied",
    "description": "Changed principle title to 'Security First v2'",
    "fieldName": null,
    "oldValue": "Security First",
    "newValue": "Security First v2",
    "isTransitive": false,
    "canReapply": true
  }
]

  • isTransitive: true for cascaded effects (e.g., objective affected by principle deletion), false for direct actions
  • oldValue/newValue: populated for field edits when a previous event exists for the same field (via previous_sequence in the association)
  • canReapply: false for transitive entries and rejected events

Admin

Method Route Auth Description
GET /api/admin/users AdminOnly List all users
POST /api/admin/users AdminOnly Create a user
PUT /api/admin/users/{userId}/role AdminOnly Update user role
DELETE /api/admin/users/{userId} AdminOnly Delete user
POST /api/admin/rebuild-history AdminOnly Rebuild all history associations and checkpoints

GET /api/admin/users

Response (200): Array of UserDto:

[
  {
    "id": "a1b2c3d4-...",
    "username": "admin",
    "role": "admin",
    "createdAt": "2026-01-15T10:30:00Z"
  }
]

POST /api/admin/users

Request (CreateUserRequest):

{
  "username": "newuser",
  "password": "securepassword",
  "role": "editor"
}

Field Constraints
username Required, max 50 characters
password Required, 8-128 characters
role Required, must be viewer, editor, or admin

Response (201): UserDto of the created user. Response (400): Validation failure (empty username, short password, invalid role). Response (409): Username already exists.

PUT /api/admin/users/{userId}/role

Request:

{
  "role": "admin"
}

Role must be viewer, editor, or admin. Returns 400 for unrecognized roles.

Self-modification

The API rejects attempts to delete your own account or change your own role with a 400 response.

POST /api/admin/rebuild-history

Rebuilds all history associations and checkpoints by replaying the entire event stream. During rebuild, the live system continues processing events but temporarily suspends association persistence and checkpoint saves.

Response (200):

{
  "eventsProcessed": 547,
  "checkpointsSaved": 6,
  "associationsSaved": 892
}

Response (409): A rebuild is already in progress.


SSE

Method Route Auth Description
GET /api/sse ViewerOrAbove Server-Sent Events stream

See Real-Time Updates (SSE) for the SSE protocol and wire format.


Export

Method Route Auth Description
GET /api/teams/{teamId}/export ViewerOrAbove Export team strategy as PPTX

Returns a PowerPoint file (application/vnd.openxmlformats-officedocument.presentationml.presentation) containing the team's principles and objectives.

Utility

Method Route Auth Description
GET / None Redirects to /strategy/
GET /health None Health check endpoint

GET /health

Returns a simple health check response:

{
  "status": "healthy"
}

Error Responses

Status Code Meaning
400 Bad request (validation error, self-modification attempt)
401 Not authenticated
403 Insufficient permissions or missing CSRF token
404 Entity not found
500 Internal server error

All error responses include a JSON body with a message field describing the error.

CSRF Protection

All mutating requests (POST, PUT, PATCH, DELETE) must include the header:

X-CSRF-Token: 1

The value is always 1 — this is a simple CSRF mitigation that verifies the request was made from JavaScript (browsers cannot set custom headers on cross-origin form submissions). Missing this header results in a 403 response.