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:
Response (200 — success):
{
"success": true,
"userId": "a1b2c3d4-...",
"username": "editor",
"role": "editor",
"error": null
}
Response (200 — failure):
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:
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):
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
targetIdwhen required (all types exceptcreate_teamandrestore_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: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
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):
| 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 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):
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:
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:
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.