# Hosted egress reference

> Routes, roles, limits, the provider catalog, API errors and gateway answers of the AI egress gateway hosted by OpenWAF Cloud.

The hosted gateway is the open-source [OpenWAF Egress](https://developers.openwaf.dev/egress/) run by OpenWAF Cloud at `https://egress.openwaf.dev`, with agents, targets and keys managed per organization instead of a configuration file.

## Gateway URL

```text
https://egress.openwaf.dev/v1/agent/<agent name>/proxy/<target id>/<provider path>
Authorization: Bearer OWE_TOKEN_…
```

`<provider path>` and the query string are appended to the target's base URL: `…/proxy/openrouter/chat/completions` goes to `https://openrouter.ai/api/v1/chat/completions`. Each agent's `base_urls` gives the URL for every target it may call.

Compared with a self-hosted gateway:

- The agent is identified by its token, and the URL must name that token's agent; `/v1/proxy/<target>/…` and the `X-OpenWAF-Agent` header are not used.
- The agent's token is never forwarded upstream; the target's auth header carries the provider key instead.
- A target without a provider key is refused instead of being called without one.
- Besides the check before the call, the address actually connected to is checked again against private and reserved ranges.
- Changes to agents, targets and keys reach the gateway within seconds; a provider key is cached for up to five minutes.

The checks, detectors, body limits and timeouts are those of the open-source gateway: see the [security model](https://developers.openwaf.dev/egress/security-model/) and the [fixed limits](https://developers.openwaf.dev/egress/configuration/#fixed-limits).

## Gateway errors

The gateway's own errors are JSON with an `error` code and a `message`; the provider's answers pass through unchanged.

| Status | `error` | Cause |
|---|---|---|
| 401 | `token_required` | No `Authorization` header |
| 401 | `invalid_token` | Unknown token, a rotated one, or the agent is disabled or deleted |
| 401 | `unauthorized_agent` | The URL does not name the token's agent |
| 403 | `egress_blocked` | The target is not among the agent's allowed targets (another agent of the organization may call it) |
| 403 | `ssrf_blocked` | The target resolves to a private or reserved address |
| 403 | `injection_detected` | A prompt-injection detector matched the body (**Block prompt injection** on) |
| 403 | `data_leak_blocked` | A data-leak pattern matched the body (**Block data leaks** on) |
| 404 | `unknown_target` | No agent of the organization may call that target id, or it does not exist |
| 413 | `body_too_large` | Over 8 MiB, or over 1 MiB of text to inspect |
| 502 | `credential_resolution_failed` | The target has no provider key, or it could not be read |
| 502 | `upstream_error` | The provider could not be reached |
| 503 | `policy_unavailable` | The gateway has no current policy for the request; retry later |

The `403` and `413` answers appear in [blocked requests](https://developers.openwaf.dev/egress/cloud/blocked-requests/). The [troubleshooting page](https://developers.openwaf.dev/egress/troubleshooting/) explains each code in detail.

## Management API

All routes are under `https://dash.openwaf.dev/v1/orgs/{org}` with an API token (`Authorization: Bearer owf_…`) or the dashboard session. Reads need any role of the whole organization; writes need `admin` of the whole organization. Members and tokens limited to a client or a zone get `403`. Writes take an optional `X-Change-Reason`; they are not versioned configuration and refuse `dry_run`. The TypeScript and Python SDKs include every route except the stream.

| Route | What it does |
|---|---|
| `GET /egress` | `{enabled, gateway_url, credentials_available}`: whether AI egress is on for the organization, readable even when it is not |
| `GET /egress/targets` | The catalog, then the custom targets, each with its key's metadata (`credential`: fingerprint, `updated_at`, `updated_by`, or `null`) |
| `POST /egress/targets` | Add a custom target: `id`, `name`, `base_url`, `auth_header`, `auth_prefix` |
| `DELETE /egress/targets/{target}` | Delete a custom target and its key |
| `PUT /egress/targets/{target}/credential` | Set or replace the key: `{"secret": "…"}`; answers the target with the key's fingerprint |
| `DELETE /egress/targets/{target}/credential` | Remove the key |
| `GET /egress/agents`, `GET /egress/agents/{id}` | Agents, with `base_urls` per allowed target and `token_prefix` |
| `POST /egress/agents` | Create an agent: `name`, `description`, `allowed_targets`, `enforce_injection`, `block_dlp`, `disabled`; answers `{agent, token}` |
| `PATCH /egress/agents/{id}` | Change `description`, `allowed_targets` (replaced whole), `enforce_injection`, `block_dlp` or `disabled` |
| `POST /egress/agents/{id}/rotate-token` | New token; answers `{agent, token}` |
| `DELETE /egress/agents/{id}` | Delete an agent |
| `GET /egress/events` | Blocked requests, newest first, with the filters of the security events |
| `GET /egress/events/stream` | Blocked requests as Server-Sent Events |

Reading and deleting agents, targets and keys works even when AI egress is off for the organization; every other write, and the events, answer `403 feature_disabled`. The answers that carry a token or set a key have `Cache-Control: no-store`.

## Limits and formats

| Item | Rule |
|---|---|
| Agent name | `^[a-z0-9][a-z0-9-]{0,62}$`, unique in the organization, cannot change |
| Description | Up to 500 characters |
| Allowed targets | 1 to 32 target ids per agent |
| Token | `OWE_TOKEN_` and 32 random bytes in base64url, shown once; only its SHA-256 and its prefix (`OWE_TOKEN_` and 6 characters) are kept |
| Custom target id | `^[a-z][a-z0-9-]{1,31}$`, not a catalog id |
| Custom target name | Up to 100 characters |
| Custom target URL | `https://`, port 443, public DNS name, no IP, credentials, query or fragment |
| Auth header, prefix | A header name of up to 64 characters; a prefix of up to 32 printable characters |
| Provider key | 1 to 8,192 characters, no spaces or control characters |

## API errors

| Status | `code` | When |
|---|---|---|
| 403 | `feature_disabled` | AI egress is not enabled for the organization |
| 403 | `forbidden` | The role is too low, or the member or token is limited to a client or a zone |
| 404 | `not_found` | No such agent or target in the organization |
| 409 | `conflict` | An agent or a custom target with that name or id already exists |
| 409 | `target_in_use` | Deleting a custom target that an agent still allows |
| 422 | `invalid` | A field breaks the rules above (`field` names it), or deleting a catalog target |
| 502 | `credential_store` | The credential store did not accept the change; try again |
| 503 | `unavailable` | Provider keys cannot be stored right now (`credentials_available` is `false`) |

## Catalog

| Id | Provider | Base URL | Key header |
|---|---|---|---|
| `openrouter` | OpenRouter | `https://openrouter.ai/api/v1` | `Authorization: Bearer` |
| `openai` | OpenAI | `https://api.openai.com/v1` | `Authorization: Bearer` |
| `anthropic` | Anthropic | `https://api.anthropic.com/v1` | `x-api-key` |
| `huggingface` | Hugging Face | `https://router.huggingface.co/v1` | `Authorization: Bearer` |
| `groq` | Groq | `https://api.groq.com/openai/v1` | `Authorization: Bearer` |
| `mistral` | Mistral AI | `https://api.mistral.ai/v1` | `Authorization: Bearer` |
| `gemini` | Google Gemini (OpenAI-compatible) | `https://generativelanguage.googleapis.com/v1beta/openai` | `Authorization: Bearer` |
| `deepseek` | DeepSeek | `https://api.deepseek.com/v1` | `Authorization: Bearer` |
| `together` | Together AI | `https://api.together.xyz/v1` | `Authorization: Bearer` |
| `fireworks` | Fireworks AI | `https://api.fireworks.ai/inference/v1` | `Authorization: Bearer` |
| `cohere` | Cohere | `https://api.cohere.com/v2` | `Authorization: Bearer` |
| `perplexity` | Perplexity | `https://api.perplexity.ai` | `Authorization: Bearer` |
| `xai` | xAI | `https://api.x.ai/v1` | `Authorization: Bearer` |

## MCP tools

`egress_status` and `egress_events` read; `egress_create_agent`, `egress_update_agent`, `egress_rotate_token`, `egress_delete_agent`, `egress_add_target`, `egress_remove_target` and `egress_remove_credential` write, with `confirm=false` to check first and no `expected_version`. There is no tool to set a provider key. See the [MCP tools](https://developers.openwaf.dev/mcp/tools/#write-tools).
