# List Every API Endpoint in a Phoenix App

Yes. An agent reads a Phoenix router scope by scope, maps each API route to its pipeline, auth plug and controller, checks the OpenAPI spec and returns a table.

[Run this on Rush](https://cloud.getrush.ai/run/api-endpoint-inventory/phoenix/), in a Project that has your repository.

## What the agent does

A Phoenix router says most of what an inventory needs, if it is read as code rather than as a list. In an application like plausible/analytics, `lib/plausible_web/router.ex` is 764 lines with 241 route macros, organised into pipelines and scopes: `:external_api` accepts JSON and nothing else, `:api` adds `fetch_session` and `PlausibleWeb.AuthPlug`, `:internal_stats_api` adds `AuthorizeSiteAccess`, `NoRobots` and `InternalStatsApiVersion`, `:public_api` accepts JSON and is paired on each scope with `PlausibleWeb.Plugs.AuthorizePublicAPI`, and `:plugins_api` adds `OpenApiSpex.Plug.PutApiSpec` with the spec module `PlausibleWeb.Plugins.API.Spec`. The agent runs `mix phx.routes` for the flat list and then reads the file to recover what the flat list drops: the `pipe_through` chain, the `assigns` such as `api_scope: "stats:read:*"` and `api_context: :site`, the `private` flags such as `allow_consolidated_views`, and the `on_ee do` blocks that exist only in the enterprise build.

Each scope becomes a section of the inventory. `/api/v1/stats` has four `ExternalStatsController` actions (`realtime_visitors`, `aggregate`, `breakdown`, `timeseries`) behind the public API plug with scope `stats:read:*`; `/api/v2/query` is the `ExternalQueryApiController`, and `/api/docs/query` is the same action behind `:docs_stats_api` with a session instead of a key; `/api/v1/sites` is `ExternalSitesController` from `extra/lib/plausible_web/controllers/api/`, split between `sites:read:*` and `sites:provision:*` scopes and wrapped in `on_ee`; `/api/plugins/v1` is the Plugins API with `SharedLinks`, `Goals`, `CustomProps`, `TrackerScriptConfiguration` and, on EE, `Funnels`, authenticated by `AuthorizePluginsAPI` with a token; `/api/stats/:domain/*` is the internal stats API the dashboard uses; and the unauthenticated group under `/api` holds `POST /event`, the ingest endpoint, `GET /error`, the legacy `GET /health` and the `/system/health/live` and `/system/health/ready` probes from `SystemController`, plus the Paddle and Postmark webhooks.

For the authentication column the agent reads the plugs, not just their names. `lib/plausible_web/plugs/authorize_public_api.ex` documents that `api_scope` is matched by prefix against the key's `scopes`, that `stats:read:*` and `sites:read:*` are implicit for every key, that a `stats:read:*` route also needs a valid `site_id`, and that every request is rate limited per key with an hourly limit through `PlausibleWeb.Api.RateLimit`; `authorize_plugins_api.ex` extracts a token and answers through `PlausibleWeb.Plugins.API.Errors.unauthorized`. Those facts go into the row for each route, so a reader sees not only "API key" but which scope, which context, and what happens over the limit.

Finally it checks the source against the documentation the app publishes: `GET /api/plugins/spec/openapi` renders the OpenApiSpex spec, `GET /api/docs/query/schema.json` is the stats query schema, and the agent lists routes present in the router but absent from either and vice versa. It can start the app with `mix setup` against the PostgreSQL and ClickHouse the `test` alias expects and probe the unauthenticated routes to record status codes, and it says in the inventory which rows were probed. It changes no code and opens no pull request.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Write endpoints.md for this Phoenix application. Run mix phx.routes, then read lib/plausible_web/router.ex in full and every pipeline and plug it names under lib/plausible_web/plugs, plus the on_ee blocks and the controllers under extra/. For every route under /api, /api/v1, /api/v2, /api/docs, /api/plugins, /.well-known and /system, record method and path, controller and action with file:line, the pipelines it passes through, the authentication it requires (session, API key with which api_scope and api_context, plugins token, or none), the rate limit that applies, whether it is enterprise-only, and the private flags set on its scope. Group the inventory by scope. Compare the router against the Plugins API OpenAPI spec and the query schema endpoint and list the differences. If PostgreSQL and ClickHouse can be started in the sandbox, run mix setup, start the server and probe the unauthenticated routes, recording the status codes and marking which rows were probed. Do not change code, push or open a pull request.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is `endpoints.md` as an artifact on the session. It starts with the pipelines and what each plug in them does, then one table per scope: method and path, `controller.action` with a `router.ex` line, pipelines, authentication with the scope string, rate limit, an EE marker, and a probed status code where one was taken. A closing section lists routes in the router that the OpenAPI spec does not describe and spec paths with no route, and the commands the agent ran.

The transcript at `GET /o/{org}/sessions/{id}/messages` holds the `mix phx.routes` output, every file read and, if the server was started, each probe with its response, so a reader can check any row against the source. The repository is not changed and the session lists no pull request.

Before relying on the inventory, a reviewer spot-checks the enterprise rows against an EE build, confirms the implicit scopes in `AuthorizePublicAPI` match what the key management UI grants, and decides whether the legacy `GET /api/health` route, which the router comment marks for removal, should still be listed as public.

## Run it on every router change on main

Create an automation in the Project with this prompt and a `github` trigger on `pull_request.closed.merged`, with `branch_patterns` matching your default branch. The trigger adds the pull request event after the prompt; the agent regenerates the full inventory rather than a diff, and the artifact on each run's session is the current one. A `config.labels` filter such as `api` keeps the automation to pull requests that touch the router.

From your own server, the same run starts with `POST /o/{org}/sessions` and this body, with a `request_id`, one UUID kept per merge commit, so a retry returns the first run. Read the artifact from the session or from the `run.completed` webhook's `session_url`.

## Why run it on Rush?

An inventory of this router means reading 764 lines of scopes with nested `pipe_through` chains, two auth plugs with their own scope rules, enterprise-only blocks and controllers that live under `extra/`, and a server that needs PostgreSQL and ClickHouse before a single route can be probed. On Rush the agent does that in its own sandbox and the commands and their output stay with the session, so a row that says "probed: 401" points at the request that produced it.

The result is a cited document rather than a memory of the router, and the same request runs against several branches or several Phoenix services at once, each in its own sandbox. When the router changes, the next run replaces the document and the previous one stays on its session for comparison.

## Frequently asked questions

### Why read the router when mix phx.routes already lists everything?

`mix phx.routes` prints method, path and controller but not the `assigns` that set `api_scope` and `api_context`, the `private` flags, the pipeline order or the `on_ee` blocks. Those decide who can call a route, and the agent reads them from `router.ex` and the plugs under `lib/plausible_web/plugs`.

### Does it include enterprise-only routes?

Yes, marked as such. Routes inside `on_ee do` blocks, like `/api/v1/sites` and the Plugins API funnels, are listed with an EE column so a community edition deployment knows they are absent.

### Will it include the dashboard's internal endpoints?

The internal stats API under `/api/stats/:domain` and the segments and annotations routes are in the inventory as session-authenticated rows, because they are endpoints the running app serves. The report labels them internal so they are not mistaken for the public API.

### Can it probe routes that need a key?

Only if you give it one through a Project secret, and the prompt here does not ask for that. Without a key it probes the unauthenticated routes and records the `401` the public API plug returns elsewhere, which is itself a useful row.
