---
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 |
| `AppointmentsController` | `v1/appointments` | `AgencyApiBase` | 3 | Details |

## 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}` | `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` | `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` | `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}` | `AppointmentsController` | `GetAppointment` | None in code | `200 OK` | `404`, `500` | — |
| `GET /v1/appointments` | `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` | `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 |

## Related documentation
- [API index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Authentication and authorization](authentication-authorization.md)
- Dependencies
- Configuration
- Operations and processes
- [Business rules](business-rules.md)
- Data model
- [Operational runbook](operational-runbook.md)
- Tech debt

## 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`
