For AI agents: the complete documentation index is at llms.txt. Markdown versions are available by appending .md or sending Accept: text/markdown.
Reflex Logo
Docs Logo
Enterprise

/

Event Handler Api

New in reflex-enterprise v0.7.1.

Event Handler API Plugin

rxe.EventHandlerAPIPlugin exposes your app's registered event handlers as HTTP POST endpoints and auto-generates an OpenAPI 3 specification for them. This turns any Reflex app into a machine-driveable API without writing a single route by hand — great for CLI scripts, end-to-end tests, or external integrations that need to drive the same logic the frontend uses.

Requires reflex >= 0.9.0 and reflex-enterprise. The plugin only works with rxe.App.

For driving the app from an LLM agent, Auto MCP publishes the same handlers over the Model Context Protocol instead of REST. The two plugins share one implementation — the same handler filtering, naming, authentication, and rate limits — so they can be enabled together and will agree.

Endpoints

When the plugin is enabled, the following routes are added to the backend:

PathPurpose
POST /_reflex/auth/tokenIssues a bearer token bound to a fresh, server-generated session. Rate limited per client IP.
POST /_reflex/event/<state_name>/<handler_name>One endpoint per @rx.event handler on every application state class. Streams state deltas as newline-delimited JSON.
POST /_reflex/retrieve_stateReturns the full root state .dict() for the token's session without re-hydrating client storage.
GET /_reflex/events/openapi.yamlAuto-generated OpenAPI 3 specification describing every endpoint above.
GET,HEAD /.well-known/api-catalogRFC 9727 API catalog pointing at the OpenAPI spec (RFC 9264 Linkset).

Handler argument names and type annotations are introspected to build each requestBody schema, and the docstring's first line becomes the endpoint summary. Handlers registered as page on_load triggers are listed in the description field of the spec so API consumers can tell which endpoint is invoked when a given page is "visited".

Configuration

Add the plugin to the plugins list of rxe.Config in rxconfig.py:

Your app must use rxe.App() (not rx.App()):

OptionDefaultPurpose
api_version"1.0.0"info.version in the generated spec.
contact / license_infoNoneinfo.contact / info.license objects.
call_rate_limit / call_rate_window60 / 60.0Per-session-token cap on API calls across all endpoints. Per-handler override via rxe.event(rate_limit=...).
token_rate_limit / token_rate_window10 / 60.0Per-client-IP cap on POST /_reflex/auth/token grants.
anonymous_sessionsTrueWhether this plugin wires the anonymous token endpoint.
anonymous_session_ttl3600Anonymous token lifetime in seconds. There is no refresh — an expired token means a fresh session.
trusted_proxy_hops0Number of trusted reverse proxies, used to resolve the real client IP for the token endpoint's per-IP limit. 0 ignores X-Forwarded-For.
token_storeautoToken storage. Defaults to Redis when the app is configured for Redis, otherwise in-process.

Setting a rate limit to 0 disables it, which is not recommended in production.

The backend serves the API on the Reflex backend port (default http://localhost:8000 in dev, or the deploy_url in production). If you're running production with --single-port, the API is instead reachable on the frontend port (default http://localhost:3000).

Authentication

Every endpoint requires an app-issued bearer token in the Authorization header:

The token identifies a client session, and that session is generated by the server — a caller can neither pick nor see the underlying Reflex client token, so a credential can only ever address its own session, never a browser session or another client's.

Anonymous session tokens

POST /_reflex/auth/token mints one, bound to a fresh, empty session:

Reuse the same token across calls if you want subsequent requests to see the effects of earlier ones (e.g. create a ticket, then list tickets). Request a new one to get a fresh, independent session — anonymous tokens have no refresh, so an expired token simply means a new session.

The endpoint is rate limited per client IP (token_rate_limit, default 10 per minute), because every grant seeds a server-side session that consumes memory. Set anonymous_sessions=False so this plugin does not wire it.

Authenticated tokens

An anonymous session carries no user identity, so with an configured only auth=False handlers and vars are reachable through one.

To act as a signed-in user, add and complete its OAuth 2.1 flow. The access token it issues works on these REST endpoints as well, and the session it is bound to gets the full enforcement stack: the per-event gate, callable auth= checks, delta filtering, and AuthUserState.current().

Auth checks can tell the surfaces apart — ctx.surface is "event_api" for a request that arrived here — and inspect the token's granted scopes through ctx.token_scopes. See surface-aware auth checks.

Rate limiting

Every call is counted against the presenting session token: call_rate_limit per call_rate_window (default 60 per minute), tracked per process. Exceeding it returns 429 with a Retry-After header. Browser (websocket) events are never rate limited by this mechanism.

Individual handlers can override their own budget:

An overridden handler is counted in its own per-token bucket; everything else shares the token's default bucket.

What is exposed

Only your application's own event handlers get endpoints. Framework and auth handlers — every OIDC provider (including your own OIDCAuthState subclasses), the login/logout/callback dispatchers, the page guard, AuthUserState, and other reflex / reflex_enterprise internals — are withheld, along with generated set-var handlers. API clients authenticate through the token endpoint or the OAuth flow instead of by posting to the login handlers.

The same goes for the Pages section of the generated spec: it lists your app's pages, not the auth machinery's (/login, /callback, /logout, /forbidden, the OIDC popup pages, and the MCP consent page are all omitted, along with their dynamic route variables).

Discovering the API

The plugin publishes the OpenAPI spec at a well-known location per RFC 9727. Any compliant client can discover it from the catalog:

Response (RFC 9264 Linkset):

Fetch the spec directly:

Browse it with any OpenAPI viewer (Swagger UI, Redoc, Scalar, the JetBrains HTTP client, etc.) pointed at that URL. Its info.description documents the token endpoint, a document-level BearerToken security requirement covers every operation, and each operation documents its 401 and 429 responses.

Response shape

Event handler endpoints return the state deltas produced by the handler as newline-delimited JSON (application/x-ndjson). Each line is one delta; the stream ends when the handler finishes:

Var names come back clean: the framework's internal _rx_state_ field-marker suffix is stripped from the streamed deltas, from /_reflex/retrieve_state, and from the Auto MCP surfaces alike.

For one-shot clients that just want the final state, consume the stream to completion and then fetch the full state:

Unlike the built-in hydrate event, /_reflex/retrieve_state does not reset client-storage vars (rx.Cookie, rx.LocalStorage, rx.SessionStorage). Use it whenever you want to read state without modifying it.

State reads redact the session's server-side client_token / session_id from the returned router var, so the session token never reaches the client, and framework/auth states are dropped from the returned dict.

The tickets demo app

The reflex-enterprise repository includes a ready-to-run IT-ticketing demo under demos/tickets/ that exercises every feature of the plugin. Its rxconfig.py is the minimal reference setup:

The state class exposes typical CRUD handlers — create_ticket, update_ticket, set_status, delete_ticket, seed, clear_all, plus list/filter/sort/pagination helpers and a load_tickets on-load handler. Here's a trimmed excerpt:

Expand

Because the state's full name is tickets___tickets____ticket_state, the generated handler routes live at:

The state name is built from the Python module path (dot separators become ___) followed by the class name — inspect the generated openapi.yaml if you are unsure of the exact path for a given handler.

curl examples

Assume a dev server running on http://localhost:8000:

Discover the API.

Retrieve the full state dict.

Seed some sample tickets.

Load the first page of tickets into the session. This mirrors the on_load handler the frontend runs when a browser hits /:

Create a ticket. title is required; description, priority, and assignee are optional (the server applies the same defaults as in the Python signature):

Change a ticket's status.

Partial update. update_ticket treats empty strings as "leave unchanged":

Delete a ticket.

Clear the board.

Example OpenAPI excerpt

The generated spec groups handlers under an OpenAPI tag matching the state class name. Here's the entry for create_ticket:

Expand

Driving the app from an LLM

Because the OpenAPI spec is self-describing (summaries, parameter types, defaults, on-load references, and how to obtain a token), most LLM agents with HTTP tool access can drive a Reflex app end-to-end without any extra glue code. Give them the spec URL and a natural-language task:

Use the API exposed at http://localhost:8000/_reflex/events/openapi.yaml to drive the application.

Create a new ticket assigned to Masen for investigating RegistrationContext issues in reflex CI.

A well-equipped agent will:

  1. GET /_reflex/events/openapi.yaml and parse the operations.
  2. POST /_reflex/auth/token for a session bearer credential.
  3. Call POST /_reflex/event/.../create_ticket with a body like {"title": "Investigate RegistrationContext issues in Reflex CI", "assignee": "Masen", "priority": "medium"}.
  4. Optionally call /_reflex/retrieve_state to confirm the ticket landed.

Other prompts that work well with the tickets demo:

Using the Reflex API at http://localhost:8000, seed the database, then close every ticket currently assigned to bob.

Via http://localhost:8000/_reflex/events/openapi.yaml, page through every ticket and summarize which assignees have the largest open backlog.

Using the Reflex API at http://localhost:8000, create three high-priority tickets for the following issues, then show me the resulting state: <list of issues>

Dynamic route variables

If any of your pages use dynamic route segments (e.g. /tickets/[ticket_id]), the plugin surfaces those as optional query parameters on every endpoint so the state can read them via self.router:

They appear under components.parameters.route_<name> in the OpenAPI spec and are referenced from every operation's parameters list.

Security considerations

  • The bearer token is a session credential, not a user identity. An anonymous token means "some client, its own session" — nothing more. Anyone who can reach the token endpoint can obtain one, so put the API behind your normal perimeter (reverse proxy, VPN, IP allowlist) before exposing it outside trusted networks, and serve it over HTTPS so tokens aren't readable in transit.
  • Every application event handler is exposed by default. Framework and auth handlers are withheld, but yours are not. Gate privileged handlers with checks — which can also require an OAuth scope or reject the "event_api" surface outright — or run the plugin only where API access is appropriate.
  • Rate limits are per process. In a multi-worker deployment each worker enforces its own budget; size call_rate_limit and token_rate_limit accordingly.
  • Configure Redis for multi-worker deployments. The default in-process token store does not survive a restart and is not shared across workers, so clients would have to re-authenticate whenever they land on a different one.
  • Handlers that return rx.redirect(...) work over the API, but the redirect is emitted as a state delta rather than an HTTP 3xx — the client sees the URL change, not a browser redirect. This is usually what you want for programmatic clients.
Built with Reflex