Skip to content

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}/agents is 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.md or 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-001 string 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 operation appears 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`