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
Section titled “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
Section titled “Endpoint Reference”Authentication
Section titled “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
Section titled “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
Section titled “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
Section titled “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.
| Method | Route | Auth | Description |
|---|---|---|---|
| GET | /api/teams | ViewerOrAbove | List all teams |
GET /api/teams
Section titled “GET /api/teams”Returns all teams with basic info (no nested principles/objectives).
Response (200):
[ { "id": "a1b2c3d4-...", "name": "Platform Team", "color": "#3498db" }]Principles
Section titled “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
Section titled “GET /api/teams/{teamId}/principles”Returns the principles view for a team, including all principles with their details.
GET /api/principles/{principleId}
Section titled “GET /api/principles/{principleId}”Returns a single principle by ID.
Objectives
Section titled “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
Section titled “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}
Section titled “GET /api/objectives/{objectiveId}”Returns a single objective by ID, including its initiatives and linked principles.
Events
Section titled “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
Section titled “POST /api/events”Request (SubmitEventRequest):
{ "eventType": "update_name", "targetId": "a1b2c3d4-...", "targetType": "principle", "data": { "name": "API-First Design" }, "lastSeenSequence": 42}| Field | Type | Required | Description |
|---|---|---|---|
eventType | string | Yes | One of the recognized event types |
targetId | GUID | No | Entity being acted on (parent entity for create_entity, null for create_entity with teams) |
targetType | string | Conditional | Required for create_entity, delete_entity, update_name, update_description. Values: "Team", "Group", "Principle", "Objective", "Initiative" |
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).
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 inEventTypes.All) - Missing
targetIdwhen required (all types exceptcreate_entity,restore_history, andimport_team) - Missing
targetTypefor generic events (create_entity,delete_entity,update_name,update_description)
See Event Types for the complete catalog of event types and their data fields.
History
Section titled “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}
Section titled “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_entity for a principle affecting objectives). Falls back to TargetId-based filtering for pre-migration data with no associations.
GET /api/history/all
Section titled “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) |
Response: Array of HistoryEntryDto:
[ { "sequenceNumber": 3, "timestamp": "2026-01-15T10:25:00Z", "actorName": "editor", "eventType": "create_entity", "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_name", "status": "applied", "description": "Changed principle name to 'Security First v2'", "fieldName": null, "oldValue": "Security First", "newValue": "Security First v2", "isTransitive": false, "canReapply": true }]isTransitive:truefor cascaded effects (e.g., objective affected by principle deletion),falsefor direct actionsoldValue/newValue: populated for field edits when a previous event exists for the same field (viaprevious_sequencein the association)canReapply:falsefor transitive entries and rejected events
| 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
Section titled “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
Section titled “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
Section titled “PUT /api/admin/users/{userId}/role”Request:
{ "role": "admin"}Role must be viewer, editor, or admin. Returns 400 for unrecognized roles.
POST /api/admin/rebuild-history
Section titled “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.
| 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 & Import
Section titled “Export & Import”| Method | Route | Auth | Description |
|---|---|---|---|
| GET | /api/teams/{teamId}/export | ViewerOrAbove | Export team strategy as PPTX |
| GET | /api/teams/{teamId}/export/json | AdminOnly | Export team as JSON (for import) |
| POST | /api/teams/import | AdminOnly | Import team from JSON export |
GET /api/teams/{teamId}/export returns a PowerPoint file (application/vnd.openxmlformats-officedocument.presentationml.presentation) containing the team’s principles and objectives.
GET /api/teams/{teamId}/export/json returns a TeamExportDto JSON document capturing the team’s state as an ordered sequence of events. This format is designed for import.
POST /api/teams/import accepts a TeamExportDto body and replays it as a new team with freshly generated IDs. Returns ImportResultDto:
{ "newTeamId": "a1b2c3d4-...", "newTeamName": "Platform Team", "eventsImported": 42}Utility
Section titled “Utility”| Method | Route | Auth | Description |
|---|---|---|---|
| GET | / | None | Redirects to /strategy/ |
| GET | /health | None | Health check endpoint |
GET /health
Section titled “GET /health”Returns a simple health check response:
{ "status": "healthy"}Error Responses
Section titled “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
Section titled “CSRF Protection”All mutating requests (POST, PUT, PATCH, DELETE) must include the header:
X-CSRF-Token: 1The 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.