# Add a saved provider connection

Store a provider key once as a named connection, check it against a model, activate it, grant its use, and replace it.

Source: https://tokentriage.com/docs/build/provider-connections

---
import Callout from '../../../components/Callout.astro';
import Details from '../../../components/Details.astro';
import NextSteps from '../../../components/NextSteps.astro';

Your developers need traffic to reach a provider. They don't need the provider's API key. Add it once
as a named connection in the workspace, and they pick it from a list by name.

Two facts before the first step. A saved connection's secret is stored encrypted and can't be read
back: later reads show its label, its version number and its status, and nothing derived from the
secret itself — no masked hint, no last four characters, not even a length. Your applications hold a
TokenTriage runtime key and no provider secret at all.

You need `connection_manager` or `workspace_admin` on the workspace. [Who can do what in your
company](/docs/build/organizations-and-access) is where those grants come from.

**Availability.** The **Provider connections** page is wired to the operations described here: add,
replace the key, check it, activate it, rename, disable, and grant or revoke its use. Two things
this page tells you plainly rather than promising: a disabled connection cannot be turned back on,
and the cost of a check is reported after the call rather than estimated before it.

## 1. Add the connection

Provider connections belong to a workspace, and there is one page for them. Reach it from the
application's **Connect this application** action, or from the workspace address directly. Nothing
is pre-filled from the application you came from.

Four fields sit in **Add a provider connection**, and you fill them in one form:

- **Connection name.** Something a colleague can pick out of a list, such as
  `IT / Development / Provider X`. Names are what a gateway's connection picker shows, so include the
  environment.
- **Provider.** One supported provider per connection, chosen from a list. The destination address
  is derived from the provider, so there is no field for a URL: a stored key cannot be pointed at an
  arbitrary host.
- **The provider account this key belongs to.** Optional, and your own words. It is stored as you
  write it and is not checked against the provider, because an opaque key cannot prove which billing
  account it belongs to.
- **Provider API key.** A password field with no reveal control. It is stored encrypted, it is not
  shown again, and it is not sent back to your browser. Replace it if you lose it.

Behind that one form are two operations: create the row, then deposit the key into it. When the
create succeeds and the key write is refused, the page says so in those words — the connection is on
the server and holds no credential, and you deposit a key from **Replace key** on its row. Adding it
a second time would leave you with two.

Saving grants nothing. The row's own line says it: nothing can dispatch through the connection until
you check the key with the provider, activate it, and grant it to an application and an environment.

## 2. Check the key, then activate it

Checking and activating live in the **Replace key** panel on the connection's row, and that panel
runs three steps in this order and no other.

1. **Store this key as a candidate.** The version comes back as `candidate`, written and not in use.
2. **Check this key.** Type a model the provider account is allowed to use into **Model to check
   with**, then check. This is a real request to the provider and it can be billed to you.
3. **Activate version N for this connection.** Offered only when the check came back accepted.

The panel checks the version it wrote, and it is the only place a check can be started. So the key
you deposited on the add form is checked by pasting it into this panel once more: that deposit is
the next candidate version, and activating it is what puts a credential into use. A freshly added
connection with nothing done here reads "No version has been activated", which is accurate rather
than a fault.

Four outcomes, and only two of them are a verdict on your key:

| What the page says | What it means | What to do |
|---|---|---|
| the provider accepted this credential | a request reached the provider with this key and the provider answered it | activate the version |
| the provider refused this credential | the provider rejected the key: wrong, revoked there, or not permitted to use the model checked | replace the key |
| the provider answered with an error | the request arrived and the provider failed it, which says nothing about your key | check again later |
| the key store could not be reached | your key was never unsealed and never presented, so nothing about it changed | retry once the key store answers |

Reading either of the last two as a bad key is what sends somebody to replace a working production
credential at two in the morning, so the page names them apart.

Each check is recorded against the version it ran against, with the model, the time and the cost
attributed to it. That cost prints as an integer of nano-USD, with a line saying a billion of them
is one dollar. When no cost was attributed, the page says so and adds that this is not the same as
the call having been free. Successful OpenAI checks use the provider's complete token usage and
the same price snapshot as the cost engine. Missing or inconsistent usage, an unavailable price,
or a rate that would require an estimate leaves the cost unknown. Earlier unknown checks are
not reconstructed from later checks. Keep their reserved exposure until it can be reconciled.

The credential in use does not move until you activate. Activation carries the revision you read, so
two people activating at once get one winner and one refusal rather than a lost update.

You can leave and reopen the page after checking a candidate. Its result stays visible before
activation, including a failed check or an unknown cost. Reading it does not send another provider
request. The credential-version label distinguishes that result from the version currently in use.

An accepted check says one model answered once. It isn't a claim that every model on that account
works, or that a future quota will hold.

## 3. Let an application use it

Open **Who may use it** on the row. One grant names one application, one environment and one
purpose, all three, and a connection with no grant serves nothing however healthy it is. Production
is another grant you make explicitly, not a box that arrives ticked.

The purpose list is fixed in the product: application traffic, playground runs, comparisons, quality
judging and connection checks. A purpose nobody wrote a resolver for is a grant nobody can honour.

**Which connections a model choice may use** on the same page answers the eligibility question for
one application, environment, model and purpose. All four are required, because the server declines
a question that leaves one of them open. The panel filters rather than annotates: a connection you
may not use here is absent from the answer, not greyed out.

That panel reports; it does not bind. Binding happens in the gateway builder's **Provider
connections** section, where each upstream slot carries a **Connection for …** picker offering only
the connections already granted for that application, environment and purpose. A slot nobody has
chosen for reads **No connection bound**, which is what `connection_slot_unbound` reports at release
time.

The saved configuration holds a reference to the connection. It holds no secret, so a configuration
diff, an export or a screenshot of the builder carries nothing worth stealing.

One connection can serve several gateways in the same application when its grants allow it, so a
three-gateway application needs one key rather than three.

## 4. Replace the key

Replacing a key is the same three steps as step 2, on a connection that already has an active
version: store the replacement as a candidate, check it, activate it. The credential in use keeps
working the whole time, so there is nothing to book a quiet evening for.

Existing TokenTriage runtime keys and deployed gateway releases keep working across a replacement. A
release pins the connection and its provider account context, while each new dispatch resolves
whichever secret version is active at that moment. Every attempt records the version that served it,
so a cost figure stays attributable afterwards. The version the new one displaced is retired.

Two things replacing a key doesn't do:

- It can't revoke the old key at the provider. Do that in the provider's own console once the overlap
  you want has passed.
- It doesn't survive a rollback in reverse. Rolling a gateway back to an earlier release doesn't
  revive a retired secret version, a disabled connection or a revoked grant.

If the replacement key belongs to a different provider account, project or destination, make a new
reviewed connection instead. A different account is a different connection, and calling it a
replacement would claim the checked behaviour carried over when it didn't.

## 5. Rename it, or take it out of use

**Rename** changes the display name and nothing else: the stored key, its versions and every grant
stay as they were. A deployment whose store implements no rename answers that it cannot do it at all,
which is a fact about the build rather than a complaint about the name you typed, and retrying gets
the same answer.

**Disable** stops new dispatch. The panel says what that costs before you confirm it: requests that
would have gone through this connection start being refused, while the connection, its stored
versions, its recorded checks and its grants are kept, and nothing that references it is edited.
Cost reconciliation for work already done continues.

A runtime request refused by saved-connection custody returns `custody_disabled` with
`retryable: false`. Select an active eligible connection, save the binding and deploy the gateway
before sending another request. Requests already admitted can still finish and incur cost.

<Callout type="warning">

Disabling is one way. This lane publishes no operation that turns a disabled connection back on, and
the page offers no control for one. When the workspace needs that provider again, add a new
connection, deposit a key, check it, activate it and grant it.

</Callout>

The initial design target is that a disable, a revocation or an access change stops new provider
dispatch within 30 seconds, using invalidation plus an expiry bound rather than an open-ended stale
refresh. That target has to be measured across workers, restarts and missed notifications before it
is published as a guarantee, so treat it as the design intent for now.

Two limits on any revocation, whoever builds it. Requests already sent to a provider can't be
recalled, including streams in flight. And stopping the provider's own key needs the provider's
console; disabling a connection stops TokenTriage from using it and nothing more.

## When a request fails on its credential

Three failures look alike in a log and need different responses, so they're distinguished on the
wire.

| What you see | What happened | What to do |
|---|---|---|
| provider authentication failed | the provider rejected the active secret version | replace the key: store a candidate, check it, activate it. Check whether the key was revoked provider-side |
| key service unavailable | the wrapping-key service couldn't be reached and no valid cached handle remained | retry. This is a dependency failure, not a bad key, and no paid call was made, so no provider charge exists |
| connection disabled | somebody disabled the connection, or a grant behind it was revoked | a disabled connection stays disabled: bind the model to another eligible connection, or add a new connection and grant it. A revoked grant can be granted again |

Two behaviours follow from keeping them apart. A credential failure isn't reported as an ordinary
model failure, so a retry loop can tell broken configuration from an outage. And TokenTriage doesn't
try a different key after an authentication failure: probing keys hides the broken configuration and
multiplies the charges.

<Details summary="Why a failed decrypt and an unavailable key service are separate answers">

Corrupt ciphertext, a wrong address context and a rejected provider key are configuration faults: a
retry produces the same answer. An unreachable wrapping service is transient, and the answer is
retryable.

A cached credential handle may serve a request during a wrapping-service outage, but only inside its
bounded authority and key lifetime. A cache miss fails as a dependency error rather than falling back
to a revoked version, another connection or the caller's own key.

</Details>

## Saved connections and request-supplied keys are two modes

Storage claims only make sense per mode, so say which mode you mean.

**Saved connection** is the mode this page describes, and it is the one a gateway starts in.
TokenTriage holds the provider key encrypted and attaches it for the upstream call. Your application
sends its TokenTriage runtime key and no provider secret. A cross-provider fallback resolves that
target's own bound connection rather than forwarding one provider's key to another.

**Request-supplied** is a separate mode, selected in configuration. The request carries two
separately identified credentials, the TokenTriage key and a provider key, and the supplied provider
value isn't persisted. An ordinary call can't switch into that mode by adding a header, because the
source mode belongs to the gateway's configuration and not to the caller.

Request-supplied mode isn't a route to arbitrary multi-provider fallback or to durable jobs that
poll later. Use saved connections for those targets, or the combination is reported as unsupported.

A blanket "no provider keys are stored" sentence describes request-supplied mode only. In saved
mode, the accurate statement is that the key is stored encrypted, isn't readable back through any
API, export or log, and is used only for requests the connection's scope permits.

## Save a candidate with the CLI or SDK

Automation uses the same connection and version operations as the page. Give the management key
`connections.manage` on the workspace. Keep that key in `TOKENTRIAGE_MANAGEMENT_KEY`; never put
it or the provider credential in a command argument. Set these non-secret values for your company:

```console
export TOKENTRIAGE_BASE_URL=https://app.tokentriage.com
export TOKENTRIAGE_ORG=org_YOUR_COMPANY
export TOKENTRIAGE_WORKSPACE=t_YOUR_WORKSPACE
```

Create the connection first. Its response contains the connection ID but no credential:

```console
tokentriage cloud --base-url "$TOKENTRIAGE_BASE_URL" \
  --org "$TOKENTRIAGE_ORG" --workspace "$TOKENTRIAGE_WORKSPACE" --json \
  connection create --name "IT / Development / OpenAI" --provider openai
```

Set `TOKENTRIAGE_CONNECTION` to that response's `data.id`. Store the key at the CLI's masked prompt:

```console
tokentriage cloud --base-url "$TOKENTRIAGE_BASE_URL" \
  --org "$TOKENTRIAGE_ORG" --workspace "$TOKENTRIAGE_WORKSPACE" --json \
  connection write-secret --connection "$TOKENTRIAGE_CONNECTION"
```

For a secret-manager pipe, add `--secret-stdin` to that command. Do not combine it with
`--management-key-stdin`: one input stream cannot carry both credentials.

The runnable Python example (`sdk/python/examples/save_provider_connection.py`)
and TypeScript example (`sdk/ts/examples/save_provider_connection.ts`)
use the SDK's single-use `ConnectionSecret` wrapper. From a repository checkout with the SDK
installed, choose one:

```console
# Python asks for a masked key when run interactively.
python sdk/python/examples/save_provider_connection.py

# TypeScript reads stdin. Here the file is an existing protected secret source.
npx tsx sdk/ts/examples/save_provider_connection.ts < /secure/provider-key
```

With `TOKENTRIAGE_CONNECTION` set, each program writes a new candidate on that connection.
Without it, the program first creates a connection named `IT / Development / OpenAI`; set
`TOKENTRIAGE_CONNECTION_NAME` to choose another name. Output contains only the connection ID,
version and state, for example:

```json
{"connection":"pc_EXAMPLE","version":2,"state":"candidate"}
```

Saving a candidate makes no provider call and does not activate it. Return to **Replace key** to
check that exact version, review its outcome and cost, then activate it as described above. A check
is a separate paid action; an unknown cost is not zero. Grant the application and environment
separately before use.

A credential write is not replayable: repeating it creates another version. If the response is lost,
read the connection and its versions before another write. The example programs never retry it.

## What this page doesn't cover

- The authentication schemes each provider uses, and what a descriptor carries: [Authentication
  schemes](/docs/build/providers-auth).
- Configuring upstream credentials in a self-hosted deployment, which is a different mechanism: [How
  credentials are configured](/docs/build/providers-credentials).
- Binding a connection to a model choice inside a draft, and promoting it: [Configure, test and
  promote a gateway](/docs/build/gateway-builder).

<NextSteps items={[
  { title: 'Configure, test and promote a gateway', href: '/docs/build/gateway-builder', description: 'Bind the connection to a model choice, test the draft, then promote it.' },
  { title: 'Who can do what in your company', href: '/docs/build/organizations-and-access', description: 'The grants that let somebody add, use or disable a connection.' },
  { title: 'Authentication schemes', href: '/docs/build/providers-auth', description: 'How each provider expects its credential to arrive.' }
]} />
