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.

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.

MethodRouteAuthDescription
POST/api/auth/loginNoneAuthenticate and create session
POST/api/auth/logoutAuthenticatedDestroy session and clear cookie
GET/api/auth/meAuthenticatedReturn current user info
POST/api/auth/change-passwordAuthenticatedChange own password

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."
}

Returns the currently authenticated user’s information.

Response (200):

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

Response (401): Not authenticated.

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.


MethodRouteAuthDescription
GET/api/teamsViewerOrAboveList all teams

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

Response (200):

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

MethodRouteAuthDescription
GET/api/teams/{teamId}/principlesViewerOrAboveGet all principles for a team
GET/api/principles/{principleId}ViewerOrAboveGet a single principle

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

Returns a single principle by ID.


MethodRouteAuthDescription
GET/api/teams/{teamId}/objectivesViewerOrAboveGet all objectives for a team
GET/api/objectives/{objectiveId}ViewerOrAboveGet a single objective

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

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


MethodRouteAuthDescription
POST/api/eventsEditorOrAboveSubmit an event

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

Request (SubmitEventRequest):

{
"eventType": "update_name",
"targetId": "a1b2c3d4-...",
"targetType": "principle",
"data": {
"name": "API-First Design"
},
"lastSeenSequence": 42
}
FieldTypeRequiredDescription
eventTypestringYesOne of the recognized event types
targetIdGUIDNoEntity being acted on (parent entity for create_entity, null for create_entity with teams)
targetTypestringConditionalRequired for create_entity, delete_entity, update_name, update_description. Values: "Team", "Group", "Principle", "Objective", "Initiative"
dataobjectYesKey-value pairs specific to the event type
lastSeenSequencelongNoFor 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 in EventTypes.All)
  • Missing targetId when required (all types except create_entity, restore_history, and import_team)
  • Missing targetType for generic events (create_entity, delete_entity, update_name, update_description)

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


MethodRouteAuthDescription
GET/api/history/entity/{entityId}ViewerOrAboveGet event history for a specific entity
GET/api/history/allAdminOnlyPaginated global event log

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.

Returns paginated global event log. Query parameters:

ParameterTypeDefaultDescription
fromint(latest)Starting sequence number
limitint100Number 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: 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

MethodRouteAuthDescription
GET/api/admin/usersAdminOnlyList all users
POST/api/admin/usersAdminOnlyCreate a user
PUT/api/admin/users/{userId}/roleAdminOnlyUpdate user role
DELETE/api/admin/users/{userId}AdminOnlyDelete user
POST/api/admin/rebuild-historyAdminOnlyRebuild all history associations and checkpoints

Response (200): Array of UserDto:

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

Request (CreateUserRequest):

{
"username": "newuser",
"password": "securepassword",
"role": "editor"
}
FieldConstraints
usernameRequired, max 50 characters
passwordRequired, 8-128 characters
roleRequired, 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.

Request:

{
"role": "admin"
}

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

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.


MethodRouteAuthDescription
GET/api/sseViewerOrAboveServer-Sent Events stream

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


MethodRouteAuthDescription
GET/api/teams/{teamId}/exportViewerOrAboveExport team strategy as PPTX
GET/api/teams/{teamId}/export/jsonAdminOnlyExport team as JSON (for import)
POST/api/teams/importAdminOnlyImport 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
}
MethodRouteAuthDescription
GET/NoneRedirects to /strategy/
GET/healthNoneHealth check endpoint

Returns a simple health check response:

{
"status": "healthy"
}

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

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

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.