Example: API endpoint catalog¶
endpoints.md is the hub for an API's HTTP surface. It explains how a route is
composed in this particular service, lists every controller, and carries one
catalog row per endpoint that links into the per-controller detail pages. Doc
gen produces one per API at docs/apis/<api-slug>/endpoints.md.
What reviewers should check¶
- Every route in the catalog is fully resolved —
[RoutePrefix]combined with[Route], or the convention template with the controller name substituted. A bare[Route]fragment such as{id}/agentsis a contract violation. - Each Endpoint cell links to that endpoint's H2 anchor on the controller's page
(
endpoints/<controller-slug>.md#<verb>-<route-slug>), never to an on-page anchor and never to the top of the controller page. - The catalog is complete. Count the H2 sections on each controller page against
the Endpoints column of the controller catalog; a missing row usually means an
action was skipped because it had no
[Route]attribute of its own. - The Auth column links into
authentication-authorization.mdor records "None in code". An unprotected endpoint is a finding, not an omission to be tidied away. - The Rules column cites business rules as links. A bare
BR-001string is a contract violation. - Per-endpoint parameter, DTO, and status-code detail is not duplicated here — it belongs on the controller page. This page is a catalog.
## Endpoints by operationappears only when an endpoint really maps to a generated operation document, and is omitted otherwise.
Example¶
The document below is a complete, correct api-endpoints.md for a fictional
API.
View rendered example — the same document as a real page, so you can compare rendering as well as source.
---
title: Endpoints
summary: Endpoint catalog for the REST API, with per-controller detail pages.
status: draft
owner: Agency Systems Team
last_reviewed: 2026-09-10
---
# Endpoints
`SAIF.Agency.Api` exposes ten endpoints across three controllers, one per
resource: agencies, agents, and the appointments that connect them. Nine are
reads; the only write is the agency contact update, and the only create is the
appointment POST. Every route is an attribute route composed from the
controller's `[RoutePrefix]` and the action's `[Route]`, so the version segment
`v1` is part of the prefix rather than a header or a query parameter.
This page is the endpoint catalog and hub. Each controller's endpoints are documented in detail on its own page under `endpoints/`.
## Route composition
| Aspect | Finding |
| --- | --- |
| Routing mode | Attribute routes (`MapHttpAttributeRoutes()`) |
| Convention template | — (the default `api/{controller}/{id}` route is commented out in `WebApiConfig.Register`) |
| Versioning | `[RoutePrefix("v1/agencies")]`, `[RoutePrefix("v1/agents")]`, `[RoutePrefix("v1/appointments")]` |
| Base path | `/agency` (IIS virtual directory) |
A resolved route is the controller's `[RoutePrefix]` followed by the action's
`[Route]` fragment: `[RoutePrefix("v1/agencies")]` plus `[Route("{agencyNumber}/agents")]`
gives `GET /v1/agencies/{agencyNumber}/agents`. An action whose `[Route]` is
empty resolves to the prefix itself, which is how the search endpoint
`GET /v1/agencies` is produced. Callers prepend the base path, so the absolute
PROD address of that route is
`https://agencyapi.example.saif/agency/v1/agencies/{agencyNumber}/agents`.
## Controller catalog
| Controller | Route prefix | Base | Endpoints | Documentation |
| --- | --- | --- | --- | --- |
| `AgenciesController` | `v1/agencies` | `AgencyApiBase` | 4 | [Details](endpoints/agencies.md) |
| `AgentsController` | `v1/agents` | `AgencyApiBase` | 3 | [Details](endpoints/agents.md) |
| `AppointmentsController` | `v1/appointments` | `AgencyApiBase` | 3 | [Details](endpoints/appointments.md) |
## Endpoint catalog
| Endpoint | Controller | Action | Auth | Success | Errors | Rules |
| --- | --- | --- | --- | --- | --- | --- |
| [`GET /v1/agencies/{agencyNumber}`](endpoints/agencies.md#get-v1agenciesagencynumber) | `AgenciesController` | `GetAgency` | [AgencyRead](authentication-authorization.md#roles-and-resources) | `200 OK` | `400`, `404`, `500` | [BR-001](business-rules.md#br-001-agency-number-must-be-six-digits), [BR-003](business-rules.md#br-003-terminated-agencies-are-excluded-unless-explicitly-requested) |
| [`GET /v1/agencies`](endpoints/agencies.md#get-v1agencies) | `AgenciesController` | `SearchAgencies` | [AgencyRead](authentication-authorization.md#roles-and-resources) | `200 OK` | `400`, `500` | [BR-002](business-rules.md#br-002-agency-search-must-be-bounded-by-a-producer-code), [BR-003](business-rules.md#br-003-terminated-agencies-are-excluded-unless-explicitly-requested) |
| [`GET /v1/agencies/{agencyNumber}/agents`](endpoints/agencies.md#get-v1agenciesagencynumberagents) | `AgenciesController` | `GetAgentsForAgency` | [AgencyRead](authentication-authorization.md#roles-and-resources) | `200 OK` | `400`, `404`, `500` | [BR-001](business-rules.md#br-001-agency-number-must-be-six-digits), [BR-004](business-rules.md#br-004-an-agency-roster-returns-only-agents-with-an-active-appointment) |
| [`PUT /v1/agencies/{agencyNumber}/contact`](endpoints/agencies.md#put-v1agenciesagencynumbercontact) | `AgenciesController` | `UpdateAgencyContact` | [AgencyContactWrite](authentication-authorization.md#roles-and-resources) | `204 No Content` | `400`, `404`, `409`, `500` | [BR-005](business-rules.md#br-005-contact-details-must-be-a-deliverable-email-address-and-phone-number), [BR-006](business-rules.md#br-006-contact-details-may-only-be-changed-on-an-active-agency), [BR-007](business-rules.md#br-007-every-accepted-contact-change-writes-an-audit-row), [BR-008](business-rules.md#br-008-a-contact-update-must-carry-the-expected-agency-version) |
| [`GET /v1/agents/{agentNumber}`](endpoints/agents.md#get-v1agentsagentnumber) | `AgentsController` | `GetAgent` | [AgencyRead](authentication-authorization.md#roles-and-resources) | `200 OK` | `400`, `404`, `500` | [BR-009](business-rules.md#br-009-an-agent-is-returned-as-licensed-only-while-the-license-is-active) |
| [`GET /v1/agents`](endpoints/agents.md#get-v1agents) | `AgentsController` | `SearchAgents` | [AgencyRead](authentication-authorization.md#roles-and-resources) | `200 OK` | `400`, `500` | [BR-009](business-rules.md#br-009-an-agent-is-returned-as-licensed-only-while-the-license-is-active) |
| [`GET /v1/agents/{agentNumber}/appointments`](endpoints/agents.md#get-v1agentsagentnumberappointments) | `AgentsController` | `GetAppointmentsForAgent` | [AgencyRead](authentication-authorization.md#roles-and-resources) | `200 OK` | `400`, `404`, `500` | [BR-010](business-rules.md#br-010-an-agent-holds-at-most-one-active-appointment-at-an-agency) |
| [`GET /v1/appointments/{appointmentId}`](endpoints/appointments.md#get-v1appointmentsappointmentid) | `AppointmentsController` | `GetAppointment` | None in code | `200 OK` | `404`, `500` | — |
| [`GET /v1/appointments`](endpoints/appointments.md#get-v1appointments) | `AppointmentsController` | `SearchAppointments` | None in code | `200 OK` | `400`, `500` | [BR-003](business-rules.md#br-003-terminated-agencies-are-excluded-unless-explicitly-requested) |
| [`POST /v1/appointments`](endpoints/appointments.md#post-v1appointments) | `AppointmentsController` | `CreateAppointment` | None in code | `201 Created` | `400`, `409`, `500` | [BR-009](business-rules.md#br-009-an-agent-is-returned-as-licensed-only-while-the-license-is-active), [BR-010](business-rules.md#br-010-an-agent-holds-at-most-one-active-appointment-at-an-agency) |
`AppointmentsController` carries no authorization attribute at controller or
action level, including on `POST /v1/appointments`, which creates a record. See
[Authentication and authorization](authentication-authorization.md) for the
consequence.
## Request flow diagram
The following diagram visualizes how a request reaches a handling action.
```mermaid
flowchart TD
REQ[Client request] --> ROUTE{Attribute route match}
ROUTE -->|GET /v1/agencies/agencyNumber| GET1["AgenciesController.GetAgency"]
ROUTE -->|GET /v1/agencies| GET2["AgenciesController.SearchAgencies"]
ROUTE -->|GET /v1/agencies/agencyNumber/agents| GET3["AgenciesController.GetAgentsForAgency"]
ROUTE -->|PUT /v1/agencies/agencyNumber/contact| PUT1["AgenciesController.UpdateAgencyContact"]
ROUTE -->|No match| NF[404 Not Found]
GET1 --> AUTH{SaifAuthorize role check}
GET2 --> AUTH
GET3 --> AUTH
PUT1 --> AUTH
AUTH -->|No| DENY[401 Unauthorized]
AUTH -->|Yes| OP[AgencyService]
OP --> OK[200 OK or 204 No Content]
OP --> ERR[400, 404, or 409 via AgencyApiExceptionFilter]
```
## Endpoints by operation
Endpoints that delegate to a documented processing workflow link to it here.
| Endpoint | Operation |
| --- | --- |
| [`PUT /v1/agencies/{agencyNumber}/contact`](endpoints/agencies.md#put-v1agenciesagencynumbercontact) | [OP-001](operations/update-agency-contact.md) |
## Related documentation
- [API index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Authentication and authorization](authentication-authorization.md)
- [Dependencies](dependencies.md)
- [Configuration](configuration.md)
- [Operations and processes](operations-processes.md)
- [Business rules](business-rules.md)
- [Data model](data-model.md)
- [Operational runbook](operational-runbook.md)
- [Tech debt](tech-debt.md)
## Sources
- `src/SAIF.Agency.Api/App_Start/WebApiConfig.cs`
- `src/SAIF.Agency.Api/Controllers/v1/AgenciesController.cs`
- `src/SAIF.Agency.Api/Controllers/v1/AgentsController.cs`
- `src/SAIF.Agency.Api/Controllers/v1/AppointmentsController.cs`
- `src/SAIF.Agency.Api/Controllers/AgencyApiBase.cs`