Two kinds of credential are in play, pointing in opposite directions. Mixing them up is how a provider key ends up in a browser bundle.
your application ──ml_vat_…──▶ ManyLayers ──sk-…──▶ OpenAI
gateway provider
credential credential
| Provider credential | Gateway credential |
|---|
| Looks like | sk-proj-…, sk-ant-…, an AWS key pair | ml_pat_…, ml_vat_…, ml-… |
| Issued by | OpenAI, Anthropic, … | ManyLayers |
| Held by | ManyLayers | your application or person |
| Used to call | the provider | ManyLayers |
| Configured in | Gateway → Providers | Gateway → Access and Gateway → API Keys |
A provider credential is never sent to your applications, and a gateway credential is never sent to a provider. The gateway drops the caller’s Authorization header and sets the provider’s own in its place, so a ManyLayers token cannot reach OpenAI even by accident.
Gateway credentials
Every request to https://app.manylayers.io/v1 carries one of these as Authorization: Bearer <token>. Three kinds exist:
| Kind | Prefix | Acts as | Created in |
|---|
| Personal access token | ml_pat_ | You, with your role, in the one workspace it was issued for | Gateway → Access → Personal Access Tokens |
| Virtual account token | ml_vat_ | An application. A virtual account reaches only the models and provider accounts you list, never its creator’s access. | Gateway → Access → Virtual Accounts |
| API key | ml- | A team, or a person an administrator issued it to; optionally bound to a workspace | Gateway → API Keys |
Use a virtual account token for anything running in production, and a personal access token for scripts, notebooks and local development.
- A token’s plaintext is shown once, when it is issued. Only a SHA-256 hash is stored, and nothing returns the token again. Rotate or revoke it from the same page; revoking takes effect immediately.
- Every personal access token expires, and your organization’s credential policy sets the longest lifetime. A virtual account token’s expiry is optional unless the policy limits it, and it can rotate on a schedule with a grace period for the old token.
- A virtual account is created with explicit
allowed_models and allowed_providers. Leaving either empty gives access to none.
- Tokens are issued from a signed-in console session, never from another token.
What a client receives
Only a token and the base URL:
MANYLAYERS_API_KEY=ml_vat_xxxxxxxx
MANYLAYERS_BASE_URL=https://app.manylayers.io/v1
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.MANYLAYERS_API_KEY,
baseURL: process.env.MANYLAYERS_BASE_URL,
});
const response = await client.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Hello" }],
});
The model name is a gateway model name. Whether OpenAI, Anthropic, Gemini or another provider serves it is workspace configuration, and changing it does not change this code.
Keep the token server-side. It carries your workspace’s spend and model access; a token in a browser bundle is a token anyone can use. There is no browser-safe variant, so a public client needs its own backend to hold it.
Where a provider credential lives
Both options are set when you add or edit an account in Gateway → Providers (the credential field of the account’s setup, or Credentials & prices on an existing account).
Reference (recommended)
API key
The secret stays outside ManyLayers and the database holds only its name:${OPENAI_API_KEY}
${vault:secret/data/llm#openai}
${ENV_VAR} reads the gateway process’s environment, so it suits deployments where you run the gateway. ${vault:path#key} reads HashiCorp Vault (KV v2) using VAULT_ADDR and VAULT_TOKEN. A reference must be exactly one reference, and the variable or secret must resolve when you save it. For AWS accounts the key pair can be two references joined by a colon, such as ${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}. A database dump then contains no credential, and rotation happens where the secret already lives.This field refuses a secret: sk-proj-… and ${sk-proj-…} are both rejected at entry, because a secret stored as if it were a reference would be sent upstream verbatim and fail as an unexplained 401 much later. Paste the provider key into the console. ManyLayers seals it with AES-256-GCM before it is written and stores the ciphertext plus a masked hint for display (for example sk-proj-…4a2B). The key is never shown again and never returned by any API. A pasted key is limited to 4,096 bytes, and one that looks like a reference (${…}) is rejected so you put it in the reference field instead.When the account uses a provider’s default endpoint, the key is checked against that provider’s format: OpenAI sk-, Anthropic sk-ant-, xAI xai-, Cerebras csk-, and Gemini AIza… or the newer AQ.… AI Studio keys. Admin keys for OpenAI and Anthropic are refused because they cannot call models.Self-hosted deployments. Sealing needs an encryption key. Set connectors.encryption_key in gateway.yaml or MANYLAYERS_ENCRYPTION_KEY; if neither is set, the gateway generates one into data/credentials.key (or the path in MANYLAYERS_ENCRYPTION_KEY_FILE) on first start. Back that file up, because credentials sealed with it cannot be read without it, and set MANYLAYERS_ENCRYPTION_KEY explicitly when running more than one replica, since replicas that each generate their own file cannot read each other’s credentials. If the gateway has no key, pasting a key is refused with encryption_not_configured and you can use a reference instead.A sealed key is only as protected as the key that seals it. A stolen database is useless on its own, but a host compromise that can read the configuration can read both. Use a reference where your deployment allows one.
Some provider types take more than one credential method: AWS accounts take an access key pair, an assumed role or a Bedrock API key; Vertex takes an API key (express mode), a service account, an external account, or the gateway’s own Google identity. See Providers.
Resolution at request time
token → workspace → gateway model → provider account → credential → provider API
A credential becomes a usable secret in one place, the model resolver. Test Credentials and Test Connection use the same code and the same credential row, so a passing test means a working request. Resolutions are cached for 30 seconds and dropped immediately when a provider, credential or model changes, so a rotation takes effect at once.
Test Connection
POST https://app.manylayers.io/admin/gateway/providers/{id}/test (with Authorization: Bearer <personal access token>) makes one authenticated call to the provider and names the stage that failed:
| Stage | Meaning |
|---|
endpoint | the base URL is missing or unusable |
credential | no active credential, or the reference did not resolve |
reachability | the endpoint could not be reached: a network problem, not a key problem |
authentication | the provider rejected the credential: it resolved, but the value is wrong or revoked |
upstream | the provider answered an error |
unsupported | this provider type cannot be probed from the console |
ok | the provider answered |
The resolved secret never appears in the result. To check individual models, see Model selection.
Workspace isolation
A workspace is the isolation boundary. Providers, credentials and models belong to one. A personal access token and a virtual account token are bound to the workspace they were issued for; an API key is either bound to one workspace or team-scoped.
A workspace-bound token resolves only that workspace’s providers and models. Another workspace’s models are not listed by GET /v1/models and are not callable with it, and its provider configuration is not readable. A provider in another workspace answers 404, not 403, so a guessed id confirms nothing. See Model resolution.