Connect OAuth services, service accounts, and MCP servers for your workspace. Credentials supply tokens and keys to apps and agents at run time.
→ Extend credentials — declare requirements in inf.yml
→ Connectors overview — MCP connectors and agent tools
Requires API key scopes credentials:read (list, capabilities, check) and credentials:write (connect, disconnect, call tools).
Paths under
/integrations(the name before 2026-09-23) answer410 Gonewith aLinkheader naming the/credentialspath.
Connect stores non-OAuth credentials (service accounts, MCP bearer tokens) and OAuth tokens in encrypted JSON on the credential row — tokens are never returned in API responses. Disconnect loads stored tokens, calls provider cleanup when supported, then soft-deletes the connection.
MCP tool proxy (/mcps/{slug}/tools) is not part of the credentials namespace.
Vaults
Credentials and secrets are grouped into vaults. Workspace- and user-scoped credentials are linked to the workspace's default vault when they are created. Workspace-scoped secrets are linked on create as well. Use GET /vaults/{id}/credentials and GET /vaults/{id}/secrets to list members, and the POST/DELETE membership endpoints to manage links without deleting the underlying credential or secret.
Capabilities catalog
GET /credentials/capabilities
Returns credential keys your apps can declare (for example google.sheets, google-sa.files). Each capability includes provider, type, OAuth scopes, and whether it is available on this server.
1curl https://api.inference.sh/credentials/capabilities \2 -H "Authorization: Bearer inf_your_key"Response:
1{2 "capabilities": [3 {4 "key": "google.sheets",5 "provider": "google",6 "type": "oauth",7 "scopes": ["https://www.googleapis.com/auth/spreadsheets"],8 "display_name": "Google Sheets",9 "available": true10 }11 ]12}Use this to build setup UIs or validate inf.yml credential keys before deploy.
Credential configs
GET /credentials/configs
Returns the merged credential view: the provider catalog joined with your workspace's connection state in one response. Each registered provider (OAuth, BYOK, service accounts) appears once with catalog metadata (display names, descriptions, icons, docs links, BYOK form fields, has_managed, connection_scope) and, when connected, a nested credential object with live status, account info, and OAuth scopes. accounts lists every login you can use for the provider. credential is accounts[0]: the login GET /credentials/{provider} returns without identifier, which is the connected login a run would use when there is one, otherwise the first login on the resolution ladder (for example a disconnected key login that still needs its keys), so a setup UI can prompt for them. The other entries follow the ladder (your own before the workspace's, each owner's default before its other accounts, then oldest). A disconnected or expired personal login is not shown as the active login while a connected workspace key is what runs use. OAuth providers may also include an app object — the OAuth application row (client id/secret), separate from the login in credential. Each entry has a unique slug (for example google, google-sa, gcp, slack). Providers whose default connection_scope is user include your personal connection in credential when you are authenticated.
Connected MCP servers are appended as standalone rows at the end of the array: provider is mcp, slug is the server's slug, and credential.account_identifier is the server URL.
Requires credentials:read.
1curl https://api.inference.sh/credentials/configs \2 -H "Authorization: Bearer inf_your_key"Response (array of config objects):
| Field | Type | Description |
|---|---|---|
slug | string | URL-friendly identifier for this provider (for example google, google-sa, gcp, slack). Used in the web app's detail page URLs (/credentials/{slug}). Defaults to provider when not set. |
provider | string | Provider key used in POST /credentials connect calls (for example google, google-sa, gcp, slack) |
type | string | Credential type (oauth, service_account, …) |
name | string | Display name |
short_name | string | Short label |
description | string | Provider description |
icon_url | string | Logo URL |
how_it_works | string[] | Setup steps shown in the UI |
instructions | string | Markdown from the auth provider on how to get what it asks for (links allowed). Omitted when empty. See Setup instructions |
docs_url | string | Link to provider docs |
expires_after_days | integer | For api_key providers: how many days a saved key counts as valid (0 or omitted = never). See API key lifetime |
secret_fields | array | BYOK form fields (key, label, placeholder, sensitive, optional, pattern). When a field has a pattern, saving a value that does not match it is refused (see Pasted-key fields). For catalogued api_key providers, keys declared in deploying apps' credentials: secrets lists are merged onto the platform template and surfaced here. |
allows_byok | boolean | Workspace can supply its own credentials instead of a managed login. Always true for api_key providers. |
available | boolean | Provider is registered on this server |
has_managed | boolean | Platform has managed credentials configured (workspaces can use without BYOK) |
company | object | The company this credential slug belongs to (CompanySummary): slug, name, icon_url, website, docs_url, terms_url, privacy_url (the last two when the catalog records them). Google's google, google-sa and google-gemini entries all carry company.slug: "google". provider (the string) is the credential slug. |
connection_scope | string | Default owner for new connections: user (each member connects their own account) or team (shared workspace connection), plus org / platform where applicable. Drives connect-button copy — see Managed credentials and grant type. |
app | object | OAuth app credential (grant: "credentials") when logins go through a configured client id/secret (workspace, org, or platform). Omitted when the provider does not use an app row or none is set up yet. Configure with PUT /credentials/{provider}/app. |
hook_url | string | Where the configured app receives provider webhook deliveries — for example the GitHub App webhook URL to paste in GitHub's app settings (https://api.inference.sh/triggers/hooks/github/{app id}, for the platform's app and a workspace-owned GitHub App alike). Omitted when the provider's apps do not deliver events to a hook. |
auth_provider_id | string | The auth provider's id when it is one the team defined — enables edit/remove in the UI |
credential | object | The login in accounts[0]: the one a run resolves to when possible, else the first login on the ladder; omitted when there is none |
accounts | array | Every login the resolution ladder can return for this provider, including when there is only one: your personal and workspace logins, org-scope logins shared across your organization, and (in the platform workspace only) platform-scope logins. Same objects as GET /credentials/{provider} returns, the login in use first and the rest in ladder order. Omitted when there is none |
Native providers (OAuth, BYOK, service accounts) appear once per credential slug. MCP servers are not in the provider catalog, but MCP entries (type: "mcp", provider: "mcp", slug = the workspace's server slug when it has one) are included for every server the caller has connected, and for every server host whose OAuth app is set up but nobody has connected yet: that entry carries app, no credential, secret_fields for the app's client id and secret, and connection_scope: "user". A connected MCP entry also carries app when its host has one. To browse or connect new MCP servers, use the MCP servers API or POST /credentials with provider: "mcp".
For MCP browse and connect UIs, authenticated GET /mcps and GET /mcp-servers responses carry connection_status for each server_url. Use those endpoints instead of merging this configs array with MCP server lists.
Provider names are globally unique for native providers, and each maps to exactly one connection type. MCP supports several connections per workspace under the one provider mcp, told apart by account_identifier (the server URL); name one with ?identifier={server_url} on get and disconnect. Use slug for dashboard links and provider for POST /credentials, GET /credentials/{provider}, and DELETE /credentials/{provider}.
JavaScript SDK: client.credentials.getConfigs()
Company catalog
GET /companies · GET /companies/{slug}
The companies behind credential slugs (company.slug on a config). A company has one or more auth providers (Google: google, google-sa, google-gemini, …). Public: no API key required. GET /companies takes an optional category query parameter (ai_ml, media, search, cloud, social, tools). GET /companies/{slug} returns one entry, or 404 for an unknown slug.
Before INF-1079 these were GET /providers and GET /providers/{slug}. Those paths are no longer served; use GET /companies and GET /companies/{slug}.
1curl https://api.inference.sh/companies?category=ai_ml2curl https://api.inference.sh/companies/googleEach entry (CompanyDTO):
| Field | Type | Description |
|---|---|---|
slug | string | Company slug (google, openai) |
display_name | string | Display name |
description | string | Short description |
icon_url | string | Logo URL |
docs_url | string | Documentation link |
website | string | Company home page |
terms_url | string | Terms of service |
privacy_url | string | Privacy policy |
supported_types | string[] | Credential types the company offers |
category | string | Catalog category |
status | string | active, beta, or deprecated |
data_residency, legal, partner_config, metadata | object | Structured catalog data. website reads metadata.website; terms_url and privacy_url read legal |
created_at, updated_at | string | ISO timestamps |
Available providers
GET /credentials/available
Returns a simple list of provider names that can be connected on this server (for example google, google-sa, gcp, slack). Requires credentials:read.
1curl https://api.inference.sh/credentials/available \2 -H "Authorization: Bearer inf_your_key"JavaScript SDK: client.credentials.listAvailable()
List connected credentials
GET /credentials or POST /credentials/list
Cursor-paginated list of credentials connected for your workspace (and per-user connections when applicable). Use this when you need to page through all connections or look up a single provider by key. For setup UIs that show catalog metadata alongside connection state, prefer GET /credentials/configs, which returns the merged view in one call.
1curl https://api.inference.sh/credentials \2 -H "Authorization: Bearer inf_your_key"CLI equivalent: belt credentials list (add --limit and --cursor to page; table mode prints next page: / prev page: hints with the cursor value). The table's columns are company, slug (credential / provider slug), sign-in, status, owner, and keys, and it also lists OAuth apps set up for a slug nobody has connected yet. With --json, stdout is the current page's items array; --json --apps prints {"items": [...], "apps": [...]}. The CLI calls POST /credentials/list and reads each row's company from configs.
GET /credentials/{provider} returns a single credential by provider key (for example google, google-sa, gcp, slack, mcp). When a provider has several accounts, ?identifier= names one (for MCP, the server URL); without it you get the default.
Optional identifier selects one login when several accounts are connected for the same provider. Pass the provider's stable account_id (for example Google's sub, an X user id, or an MCP server URL) or the display label in account_identifier (email, @handle, …). Prefer account_id when the credential object includes it — labels can change and legacy logins may have been created before ids were stored. Omit identifier to get the default login for that provider.
1curl https://api.inference.sh/credentials/google \2 -H "Authorization: Bearer inf_your_key"34curl 'https://api.inference.sh/credentials/google?identifier=user%40company.com' \5 -H "Authorization: Bearer inf_your_key"67curl https://api.inference.sh/credentials/google-sa \8 -H "Authorization: Bearer inf_your_key"CLI equivalent: belt credentials get <provider> (for example belt credentials get google, belt credentials get google-sa, or belt credentials get mcp). The CLI calls GET /credentials/{provider} and best-effort merges GET /credentials/configs so table output can show the provider name, linked company (company), website, and docs.
JavaScript SDK: client.credentials.get(provider) — same response as the REST endpoint.
Credential object fields
Each connected credential (CredentialDTO) includes:
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier for this credential row. Stays the same when you reconnect, complete OAuth again, or grant additional scopes; disconnect removes the row and the next connect issues a new id. A login without an account that is folded into an older duplicate also loses its id |
scope | string | team (shared workspace connection), user (your personal connection), org (shared across all workspaces in the organization), or platform (managed by the platform). There is no agent scope: agents use the same vault logins as apps |
grant | string | For managed OAuth providers: credentials (OAuth app row — never a login) or token (a connection: OAuth login, API key, service account, MCP authorization). See Managed credentials and grant type. |
app_credential_id | string | When this row is a login (grant: "token") that signed in through an OAuth app, the id of that app credential row |
provider | string | Provider key |
type | string | Credential type |
status | string | connected, disconnected, expired, or error (see below; pending is no longer written) |
display_name | string | User-facing label |
scopes | string[] | Granted OAuth scopes, in the provider's own names (for example https://www.googleapis.com/auth/gmail.send, Mail.Read) |
allowed_scopes | string[] | On an OAuth app row (grant: "credentials"): every scope a login through this client may request. Absent when any scope may be (a workspace's own client by default). See Allowed scopes |
account_id | string | Provider-stable account key (Google sub, Slack user id, MCP server URL, …). Logins are matched, deduplicated, and selected by this value when present |
account_identifier | string | Display label for the account (email, @handle, hostname). The provider can change it; reconnecting updates the label on the same account_id |
account_name | string | Display name for the account |
expires_at | string | ISO timestamp. For OAuth logins, when the access token expires (the platform refreshes it when it has a refresh token). For api_key logins of a provider with expires_after_days, when the saved key stops counting as valid; set on each key save. Omitted when the key never expires |
error_message | string | Human-readable detail when status is expired or error (for example "No refresh token available") |
is_primary | boolean | Whether this login is the default account for its owner and provider. Lookups that omit identifier use the default (after preferring your own user login). MCP rows are separate servers, not accounts of one provider — each MCP connection is its own row and is_primary does not pick a default among them |
vault_id | string | Optional vault reference for encrypted secret storage |
metadata | object | Provider-specific configuration (env var mapping, allowed hosts, …) |
secrets | array | The secrets linked to a key-based login, as CredentialSecretRef objects: id (secret row id), key (vault key name, such as OPENAI_API_KEY), updated_at (when the value last changed). Never the values. Filled on api_key logins in GET /credentials/configs; omitted elsewhere. Reveal a value with GET /secrets/{id}/reveal |
A credential row exists only once it holds a login (OAuth sign-in completed) or a key login was started by saving a key. The API creates no empty placeholder rows: not when an agent run is blocked on a provider (the blocked tool waits in the chat until you connect or save keys), not when you start OAuth. Saving a key creates a disconnected login that turns connected once its keys are stored. Deploying an app whose declared keys are missing creates a login only when deploy can adopt unlinked vault secrets onto it; otherwise the next run asks for the keys and saving them creates the login. The pending status (a keyless placeholder login) is no longer written, and the remaining placeholder rows were retired on 2026-10-28. Runs that require a credential use only connected logins.
Raw tokens are never returned. Workspace-scoped credentials (scope: "team") are shared connections created by connect flows; user-scoped rows (scope: "user") are personal OAuth logins (grant: "token"); org-scoped rows (scope: "org") are shared across all workspaces in the organization; platform-scoped rows (scope: "platform") are platform keys inference.sh provides (managed credentials when has_managed is true on the provider config). When several rows match at run time, secrets passed with the task come first, then your own user connection, then the workspace team default login (is_primary: true, else oldest), then org, then a platform key. A platform-scope login is on the lookup ladder only in the platform workspace; other workspaces get platform keys through the run-time access list below, never as a login they can read or name. Each platform key has an access list deciding which other workspaces' runs may use it: which apps and functions, store-reviewed versions only, on cloud infrastructure only. When no key admits the run, the requirement stays unmet and POST /run returns 412; add your own key for that provider.
Multiple accounts per provider
OAuth and other account-based providers can have several logins for the same owner (for example two Google accounts on one workspace). Connecting another account adds a new login instead of replacing the first.
| Concept | Behavior |
|---|---|
| Default login | The owner's first login for a provider becomes the default (is_primary: true). Additional accounts connect alongside it with is_primary: false. |
Lookup without identifier | GET /credentials/{provider}, add-scopes, disconnect and run-time resolution walk one ladder: your user login, then the workspace team login, then org, then (platform workspace only) platform. Each rung uses that owner's default login (is_primary, else oldest created_at). Runs skip logins that are not connected or whose key lifetime has passed, and then fall back to a platform key whose access list admits the run. Without identifier, GET /credentials/{provider} returns the login a run would use when there is one, otherwise the first login on the ladder (for example a disconnected key login) so you can finish setup. GET /credentials/configs makes the same choice for credential. |
Lookup with identifier | Get, disconnect, set-default and add-scopes walk the same ladder, matching the named account_id or account_identifier on each rung. A teammate's personal login is never returned. A workspace naming the account of a platform login gets 404 not_found. |
| Choose account at connect | Set another_account: true on POST /credentials for OAuth providers that distinguish accounts. The consent URL asks the provider to show its account picker (for example Google prompt=select_account consent, Microsoft prompt=select_account) instead of reusing the remembered account. |
| Account identity | Connect and add-scopes match an existing login by account_id when the provider supplies one. A login saved before ids were stored is found once by its label and then keeps the id. Reconnecting with the same id updates account_identifier when the label changed (for example a renamed X handle). The same label on a different account_id creates a second login. |
identifier in requests | Use account_id when present on the row you mean; fall back to account_identifier only when no id was stored yet. The web app shows emails and handles in the UI but sends the stable id on disconnect, set-default, add-scopes, and permission changes. |
| Set default | PUT /credentials/{provider}/default?identifier=... makes that login the default (identifier is account_id or account_identifier). Requires permission to manage the login (same rules as disconnect). |
| Disconnect default | When the default login is disconnected, the owner's oldest remaining login for that provider becomes the new default. |
| Add scopes | POST /credentials/{provider}/scopes?identifier=... targets one account (account_id or account_identifier). Completing OAuth for a different account (different account_id) during an add-scopes flow returns already_exists (409). |
| Duplicate detection | When a login without account_id later learns its account and another login for the same owner already has that id, the API folds the duplicate into the existing login (the older row is kept; the newer row is retired without revoking tokens at the provider, so the kept login's grant stays valid). Labels refresh when the login is next used. |
MCP servers remain one row per server URL (provider: "mcp", account_identifier = the server URL). Each MCP connection is its own server, not another account of a shared provider slug.
Only platform admins can replace or remove a platform credential, and only org admins an org credential. A shared team credential needs the workspace admin role unless you created it. When allows_byok is true on the provider config, a workspace can override an inherited credential with its own: OAuth providers via POST /credentials, and api_key providers by saving a workspace secret with the provider field set (see Create secret). For api_key providers the credential's status is connected while at least one linked key secret exists and disconnected after the last one is removed.
API key lifetime
An api_key auth provider (or an app's credentials: entry) can set expires_after_days. It is copied to expires_after_days on GET /credentials/configs.
| Value | Behavior |
|---|---|
0 or omitted | Keys do not expire by age; expires_at stays unset |
1–3650 | Each time a key is saved for the provider (create, replace, update, attach or adopt), expires_at is set to that many days ahead. Saving again restarts the count |
Once expires_at has passed, the login reads as expired, its key is not injected at run time, and POST /credentials/check and 412 responses ask for a new key (your {name} key expired on {date}; add a new one), with the provider's instructions on the action. A login with no expires_at never expires this way.
OAuth logins use the same expires_at field for the access token, unrelated to expires_after_days.
Managed credentials and grant type
Each config entry exposes connection_scope — who owns new connections by default. Use it for setup copy:
connection_scope | Meaning | Web app UI |
|---|---|---|
team | One shared workspace connection (or a ready-to-use platform token) | connect {name} |
user | Each member authorizes their own account | connect your {name} account; detail pages use your account / disconnect your account |
When the provider signs in through an OAuth application, app holds that app row (grant: "credentials" — client id/secret only, not a login). credential holds the login (grant: "token" for OAuth connections). Completed OAuth stores which app was used on the login as app_credential_id.
grant on CredentialDTO (including nested app and credential objects) distinguishes app rows from connection rows:
grant | Row kind | Typical scope |
|---|---|---|
credentials | OAuth application (client credentials) | team, org, or platform |
token | Connection (OAuth login, API key, service account, MCP authorization) | user, team, org, or platform |
grant=credentials is only valid for type: "oauth" app rows. Per-user OAuth providers use connection_scope: "user" on the config; each member's login appears in credential with scope: "user".
Check scope on the connected CredentialDTO to distinguish personal vs workspace connections in list and detail UIs.
When a provider signs in through an OAuth app, new logins use the first complete app in this order: the workspace's own (scope: "team"), then the organization's (scope: "org"), then the platform's (scope: "platform"). GET /credentials/configs returns that choice in app. Refresh tokens always use the app that issued the login (app_credential_id on the login row).
Configure OAuth app
OAuth providers whose config includes secret_fields (for example SLACK_CLIENT_ID / SLACK_CLIENT_SECRET) require an OAuth application row before POST /credentials can return an auth_url. The app holds client credentials only (grant: "credentials"); user and workspace logins stay separate (grant: "token" in credential).
Set or update app
PUT /credentials/{provider}/app
Requires credentials:write and the workspace admin role (platform operators on the platform workspace when setting a platform app). The body is a CredentialAppRequest:
| Field | Description |
|---|---|
values | Map of vault key names from secret_fields (and optional app keys) to new values. Keys omitted from the map, or sent as blank strings, keep their stored values on update. |
allowed_scopes | Optional. Replaces the scopes a login through this app may request (see Allowed scopes). [] allows any scope; omitted keeps the stored list. |
The app row's status becomes connected once both the client id and client secret keys are non-empty. Register the provider's redirect URI in the third-party console (for example https://app.inference.sh/settings/secrets/oauth/{slug}).
1curl -X PUT https://api.inference.sh/credentials/slack/app \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{5 "values": {6 "SLACK_CLIENT_ID": "123.456",7 "SLACK_CLIENT_SECRET": "abc..."8 }9 }'Response: CredentialDTO for the app row (grant: "credentials").
| Error | HTTP | When |
|---|---|---|
validation_error | 400 | Provider does not sign in through an OAuth app, or a key in values is not part of that provider's app |
team_role_required | 403 | Caller is not a workspace admin (or platform operator for a platform app) |
Remove app
DELETE /credentials/{provider}/app
Deletes the caller's own OAuth app row for the provider (workspace or platform scope) and removes linked vault secrets. Requires the same admin roles as PUT. Responds 400 validation_error when one or more logins still reference the app — disconnect those connections first — and 404 when you have no app row of your own for the provider.
Allowed scopes
An OAuth app can carry allowed_scopes: the list of scopes a login through that client may request. The platform's clients are restricted to the scopes the provider has approved them for (the platform's Google client, for example, to the scopes it passed Google's verification with); a workspace's or org's own client has no list and is unrestricted unless its admin sets one.
- Empty or absent: any scope. This is what a workspace's own client gets by default.
- Set: every scope a connect or add-scopes request names, after the auth method's
required_scopesare merged in, must be on the list. Scopes are compared in the method's wire form, so an alias (gmail.send) and the provider's own form (https://www.googleapis.com/auth/gmail.send) name the same scope, on the list and in the request. The method'srequired_scopes(identity and refresh) are never refused.
The check runs before any authorize URL is built. On connect, the app is the one a new login uses: the caller's workspace client, else its org's, else the platform's. On add scopes, it is the app that issued the login (app_credential_id); a login with no stored app uses the one a new connect would take. Widening a login never moves it to another client, because a refresh token works only with the app that issued it. A request for a scope outside that app's list is refused with 400 validation_error:
1{2 "error": "validation_error",3 "detail": "scope https://www.googleapis.com/auth/youtube.readonly is not approved for the platform's Google client; connect through your own OAuth app or ask an admin"4}Either set up your own OAuth app for the provider with PUT /credentials/{provider}/app (logins then go through it, with no list) or ask a platform admin to approve the scope. Platform admins edit the platform client's list in the admin console or as data through PUT /admin/credentials/{provider} with allowed_scopes (the response is the app row); a workspace admin edits their own client's list in the vault's OAuth app dialog (allowed scopes) or through PUT /credentials/{provider}/app.
A deploy whose inf.yml asks the platform's client for a scope outside its list is accepted with a unapproved_platform_scope deploy warning; the connect is where it is refused.
Optional app keys
Some providers take extra keys on the OAuth app (the auth method's app_keys: signing secrets, bot tokens, tenant ids). Store them in the same values map on PUT /credentials/{provider}/app as the client id and secret, for example SLACK_SIGNING_SECRET and SLACK_BOT_TOKEN for Slack (Slack guide).
When the API needs one of these keys (channel webhook verification, outbound messages, provider-specific OAuth steps), it reads them from the OAuth app in use for the provider: the app a login was made through, else the one a new login would use (workspace, org, then platform). When the provider has such an app, its keys are the only source. Only when there is no app does the API fall back to a workspace secret with the same key name, then to keys on the caller's personal (scope: "user") login for the provider.
Connect a credential
POST /credentials
Starts OAuth, creates a service account, or registers an MCP server connection.
OAuth providers
1{2 "provider": "google",3 "type": "oauth",4 "scopes": ["https://www.googleapis.com/auth/spreadsheets"]5}To connect a second Google account on the same workspace, set another_account: true so the provider shows its account chooser:
1{2 "provider": "google",3 "type": "oauth",4 "scopes": ["https://www.googleapis.com/auth/spreadsheets"],5 "another_account": true6}Client credentials. When the slug's auth method is oauth2 with flow: client_credentials, there is no browser step: the API exchanges the slug's client id and secret at the token endpoint and the response carries credential with status: "connected". The token is re-minted before it expires. api_key and http methods are not connected here: save their fields with POST /secrets and provider set to the slug (the same path as catalogued API-key providers; see Secrets API).
Response when the user must complete OAuth in a browser:
1{2 "auth_url": "https://accounts.google.com/o/oauth2/v2/auth?...",3 "state": "state_abc123",4 "code_verifier": "..."5}Open auth_url, then complete the flow with POST /credentials/oauth/complete:
1{2 "provider": "google",3 "type": "oauth",4 "code": "authorization_code",5 "state": "state_abc123",6 "code_verifier": "..."7}Some providers add extra query parameters on the redirect (QuickBooks realmId, Shopify shop, and so on). Pass them in optional params on the complete request (every key except code and state, which stay top-level). Auth method templates read them as {{callback.*}} — see Callback query parameters on OAuth complete.
Omit code_verifier when you started connect with the same state — the API stores the PKCE verifier from POST /credentials and claims it when completing OAuth. Pass code_verifier explicitly when your client held it locally and did not use the server-stored challenge.
| Error | HTTP | When | Meaning |
|---|---|---|---|
already_exists | 409 | Connect | Non-OAuth provider already has a connected login for this owner (service account, API key, and similar single-slot types) |
already_exists | 409 | Complete OAuth | Add-scopes flow completed with a different account than the login you started from — restart add-scopes for the intended account or connect the other account with another_account: true |
invalid_grant | 400 | Complete OAuth | OAuth state or PKCE challenge expired — restart connect |
invalid_request | 400 | Connect, Complete OAuth, add-scopes | Unknown provider; the detail names the slug (there is no provider called "nemox") |
validation_error | 400 | Connect, add scopes | A requested scope is not on the OAuth app's allowed scopes (scope … is not approved for the platform's Google client; connect through your own OAuth app or ask an admin) |
Account identity on OAuth complete
When the slug's auth method says how to learn the account (identifier or identity_hook), completing OAuth must yield a non-empty account. If the provider answers the identity lookup without one (a 401 or 403, or a 200 with no matching field), the connect fails and nothing is saved. The response is 500 internal_error with the generic detail failed to complete OAuth flow. The usual cause is an OAuth app or grant missing the scope the userinfo or lookup endpoint needs: fix the app permissions or scopes and connect again.
Temporary lookup failures are different. When the identity lookup times out, cannot reach the provider, or gets 408, 429, or 5xx, and the lookup URL is the auth method's own (not one named by the token response), the login is saved without an account. The API asks the provider again when the login is next used, at most every 15 minutes, and then fills account_id, account_identifier, account_name, and metadata. When the lookup URL came from the token response, a temporary failure fails the connect like a refusal.
Auth methods that do not define identifier or identity_hook can still connect without an account id (unusual for user OAuth).
When reconnecting an expired or error credential, you can call POST /credentials with the same provider and an empty scopes array (or omit scopes). The API carries forward previously granted scopes in the OAuth consent URL so the user restores all permissions in one step instead of starting from zero.
Completing OAuth (POST /credentials/oauth/complete) for an existing login — reconnect, incremental scope grant, or MCP re-authorization — updates that row in place. The credential id, vault membership links, and anything that references the id stay put. New tokens, scopes, account fields, and status change. Only disconnect removes the row; the next connect creates a new id. One exception: a login saved without an account that later learns an account another login of the same owner already has is folded into the older login, and the newer id is retired (see Multiple accounts per provider).
Required scopes on connect
OAuth slugs resolve through an auth provider and its oauth2 auth method. When you pass scopes on connect, the API still merges the method's required_scopes into the authorize URL. Those permissions are not optional: they let the platform read account id and display name after token exchange (and request refresh tokens when the method defines them).
Without them the provider can refuse the identity lookup, and then the connect fails and nothing is saved (see Account identity on OAuth complete). Built-in providers (Google, Microsoft, X, Discord, GitLab, LinkedIn, Linear, Atlassian, Xero, and others) declare required_scopes on their platform row. A team's own auth provider should set required_scopes in auth_methods when identity templates depend on specific grants.
The same merge applies on POST /credentials/{provider}/scopes: requested scopes plus required_scopes go to the provider.
Connection scope
Optional connection_scope on POST /credentials sets who owns the resulting credential. It is separate from scopes, which are OAuth permission scopes.
1{2 "provider": "google",3 "type": "oauth",4 "scopes": ["https://www.googleapis.com/auth/spreadsheets"],5 "connection_scope": "team"6}connection_scope | Who can use it | Requirement |
|---|---|---|
user | Only the connecting user | Any member |
team | The whole workspace | Workspace admin |
org | All workspaces in the organization | Org admin, calling from the organization workspace |
platform | Other workspaces' runs that the key's access list admits (store-reviewed versions on cloud) | Platform admin, calling from the platform workspace |
Omit connection_scope to use the provider's default: the scope the provider declares (user for MCP), user for grant=credentials providers, team otherwise.
Choose connection_scope at connect time, or on POST /secrets when saving a BYOK API key with provider. To change who owns an existing connection without revoking tokens, use Change connection scope.
For OAuth flows the chosen scope is stored server-side with the OAuth state and applied when oauth/complete runs. The completing client does not resend connection_scope.
| Error | HTTP | When |
|---|---|---|
team_role_required | 403 | connection_scope: "team" without workspace admin, or "org" without org admin |
validation_error | 400 | An unknown connection_scope, org when the workspace is not part of an organization, or org / platform requested from a workspace other than the organization or platform workspace. detail says which. |
forbidden | 403 | connection_scope: "platform" without platform admin |
Service accounts
Google Sheets/Docs/Drive service accounts use the google-sa provider (separate from Google OAuth's google and GCP WIF's gcp):
1{2 "provider": "google-sa",3 "type": "service_account"4}Returns credential when the connection is created immediately (no redirect). Declare google-sa.files in inf.yml (replaces the former google.sa and interim gcp.sa capability keys). Existing workspace connections that used provider: "google" or provider: "gcp" with auth: "service_account" were migrated to google-sa in a one-time database update, so no reconnect is required.
Custom MCP servers
Connect an arbitrary HTTPS MCP endpoint (same flow as belt mcp connect):
1{2 "provider": "mcp",3 "type": "mcp",4 "metadata": {5 "server_url": "https://mcp.example.com"6 }7}For bare origins, the API may resolve the MCP path via /.well-known/mcp-server-card or UCP discovery — see Browsing connectors.
CLI equivalent: belt credentials connect <provider> (for example belt credentials connect google). The CLI resolves the provider from GET /credentials/configs. OAuth providers open a browser and poll until connected. api_key providers are not connected through this endpoint — the CLI collects each declared key and saves it with POST /secrets (belt secrets set <KEY> --provider <provider>). MCP servers use belt mcp connect <slug> instead. belt creds and belt integrations are aliases for belt credentials.
Bearer token / API key connect
When the remote MCP server accepts a pre-issued bearer token (API key, personal access token, or similar) instead of OAuth, pass it in metadata.bearer_token. The API skips OAuth discovery, stores the token as an encrypted workspace secret, and returns credential immediately:
1{2 "provider": "mcp",3 "type": "mcp",4 "metadata": {5 "server_url": "https://mcp.example.com",6 "bearer_token": "your-api-key-or-pat",7 "display_name": "My MCP Server"8 }9}| Metadata field | Required | Description |
|---|---|---|
server_url | Yes | MCP endpoint or bare origin (resolved via discovery when needed) |
bearer_token | Yes (for this flow) | Token sent as Authorization: Bearer on MCP requests |
display_name | No | Label in the connected-apps list (defaults from server introspection or hostname) |
The connect response omits the token. Use GET /credentials/mcp?identifier={server_url} or belt credentials get mcp to confirm the connection is connected before calling tools.
→ Browsing connectors — API key servers
Connect errors
When the API can't sign in to the server, connect answers 400 with a validation_error whose detail says what went wrong:
| Situation | detail (example) |
|---|---|
server_url missing, not absolute, or not https | server_url must use https (got "http") |
| The URL can't be reached | Could not reach https://mcp.example.com. Check the URL is the server's MCP endpoint. |
| The URL answers, but not as an MCP server with OAuth discovery (for example a web page or a settings page) | https://app.example.com/settings/mcp did not answer as an MCP server with OAuth sign-in. Check the URL is the MCP endpoint (not a web page), connect with a token, or set up its OAuth app by hand with the authorize and token URLs. |
| The authorization server publishes no usable metadata | The sign-in server for https://mcp.example.com (https://auth.example.com) did not publish usable OAuth metadata. Set up its OAuth app by hand with the authorize and token URLs. |
1{2 "type": "https://api.inference.sh/errors/validation_error",3 "title": "Bad Request",4 "status": 400,5 "detail": "https://app.example.com/settings/mcp did not answer as an MCP server with OAuth sign-in. ..."6}When OAuth discovery doesn't work for a server, set its OAuth app up by hand with PUT /credentials/mcp/app and connect again.
MCP OAuth client identity
When the remote MCP server requires OAuth, the API acts as a public OAuth client (PKCE, no client secret). Client identity is resolved in this order:
- BYOK:
oauth_client_idon a workspace MCP server registration, or a platform-managed secret keyed by hostname - Client ID Metadata Document (CIMD, SEP-991): when the authorization server advertises
client_id_metadata_document_supported, the API uses the metadata document URL on the app origin asclient_id(no Dynamic Client Registration) - Dynamic Client Registration (DCR): registers a new client when the server exposes a registration endpoint and CIMD is unavailable
The CIMD document is public (no API key). It is served on the app origin, so client_id shares an origin with client_uri and the redirect URI (some authorization servers require that):
1GET https://app.inference.sh/.well-known/mcp-client.jsonThe app serves this path by proxying the document the API builds at GET https://api.inference.sh/.well-known/mcp-client-app.json (same JSON body and Cache-Control; 502 with Cache-Control: no-store when the API does not answer).
1{2 "client_id": "https://app.inference.sh/.well-known/mcp-client.json",3 "client_name": "inference.sh",4 "client_uri": "https://app.inference.sh",5 "redirect_uris": ["https://app.inference.sh/settings/secrets/oauth/mcp"],6 "grant_types": ["authorization_code", "refresh_token"],7 "response_types": ["code"],8 "token_endpoint_auth_method": "none"9}The client_id URL must match the document location. On self-hosted deployments, set APP_URL on the API to the public app base URL (for example https://app.example.com), and serve /.well-known/mcp-client.json on that origin from the API's /.well-known/mcp-client-app.json. If APP_URL is unset, CIMD is disabled and the connect flow falls back to DCR or BYOK credentials.
GET https://api.inference.sh/.well-known/mcp-client.json (the same document with the API origin as client_id) stays for MCP connections registered under that id before; they refresh with it.
When CIMD and DCR are unavailable (for example Microsoft Entra ID), connections need a registered OAuth application for the MCP server's host — see Configure MCP OAuth app below.
Authorization server profiles
MCP OAuth does not use an auth provider row. After discovery, the API selects an authorization server profile from the authorization server's metadata (issuer host, then authorize URL host, then token URL host). Unmatched servers use the standard profile (discovered endpoints, RFC 8707 resource on authorize, no scopes on refresh).
Profiles encode provider-specific rules: how authorize and token URLs are built from app fields, whether a resource parameter is sent, whether scopes are resent on refresh, extra app form fields, admin consent links, and how error responses are classified. The profile name is stored on the OAuth flow state and on the connection's metadata.as_profile after connect. Profile names are listed under auth_server_profiles in GET /auth-providers/hooks.
Today the API registers microsoft_entra for Microsoft Entra ID authorities (login.microsoftonline.com and related hosts). Entra apps may include a tenant_id field for single-tenant registrations; multi-tenant platform apps use the organizations tenant segment. Entra does not use the resource parameter and repeats scopes on token refresh.
Configure MCP OAuth app
Some MCP servers sign in through an authorization server that offers neither CIMD nor dynamic client registration. Others publish no OAuth discovery metadata at all. For both, configure an OAuth application per MCP host (not per workspace MCP registration row); for the second, the app also carries the authorize and token URLs (endpoints by hand). An app can be set for any server, not only one that needs it: connections then sign in through it instead of registering a client. Resolution order for a new connection matches native providers: workspace app, then org app, then platform app.
Requires credentials:read (get status) or credentials:write (create or update). Who may manage an app depends on manage_scopes in the status response (workspace admin, org admin from the org workspace, or platform operator).
Get MCP OAuth app status
GET /credentials/mcp/app?server_url={url}
Returns how connections to that MCP server sign in, for connect and admin UIs.
Response (MCPOAuthAppStatus):
| Field | Description |
|---|---|
server_url | Echo of the query URL (after normalization) |
account | MCP host used as the app key (for example mcp.dev.azure.com) |
app | OAuth app a new connection would use (team, org, or platform), if any |
apps | Every app configured for this host that the caller may use |
manage_scopes | Which scopes (team, org, platform) the caller may configure; empty means ask an admin |
redirect_url | Redirect URI the OAuth app must register (MCP callback) |
authorization_server | Issuer from discovery, when known |
needs_app | true when discovery found no DCR and an app must be configured before connect |
app_fields | Extra non-secret keys the authorization server's profile requires on the app (for Entra: tenant_id for single-tenant apps) |
admin_consent_url | Link a Microsoft admin opens once to approve the app for their tenant, when the profile supports admin consent and an app exists |
scopes | OAuth scopes the next connection will request |
recommended_headers | Static MCP request headers suggested for this host from the public directory entry (when set), for example toolset filters |
Each app in app / apps (MCPOAuthAppDTO) includes client_id, has_secret, scope, status, and fields (values for app_fields). Secrets are never returned.
1curl "https://api.inference.sh/credentials/mcp/app?server_url=https%3A%2F%2Fmcp.dev.azure.com%2Fcontoso" \2 -H "Authorization: Bearer inf_your_key"Set MCP OAuth app
PUT /credentials/mcp/app
Body (MCPOAuthAppRequest):
| Field | Description |
|---|---|
server_url | MCP server URL (host selects the app row) |
scope | Owner: team (default — caller's workspace), org, or platform |
client_id | OAuth client id |
client_secret | OAuth client secret; omit or send blank to keep the stored secret |
fields | Values for keys in app_fields, plus the endpoints by hand keys |
Only keys declared by the active authorization server profile, and the endpoint keys below, are accepted. The profile validates field values (for example Entra tenant id format).
Endpoints by hand
For a server whose OAuth metadata is missing or wrong, put the authorization server's endpoints on the app. With both endpoints set, connecting skips discovery and signs in through them.
fields key | Description |
|---|---|
authorization_endpoint | The authorize URL, for example https://auth.example.com/oauth/authorize. https (http only on localhost) |
token_endpoint | The token URL, for example https://auth.example.com/oauth/token. https (http only on localhost) |
scopes | Scopes to request, space or comma separated. Optional |
A key sent with an empty value clears it; a key left out keeps its stored value. Clearing both endpoints puts the server back on discovery.
1curl -X PUT https://api.inference.sh/credentials/mcp/app \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{5 "server_url": "https://mcp.example.com/mcp",6 "client_id": "your-client-id",7 "client_secret": "your-client-secret",8 "fields": {9 "authorization_endpoint": "https://auth.example.com/oauth/authorize",10 "token_endpoint": "https://auth.example.com/oauth/token",11 "scopes": "read write"12 }13 }'Connections made this way send the client id and secret to the token URL with HTTP Basic auth, and use PKCE. They send no RFC 8707 resource parameter and don't check an iss on the redirect, since there is no discovered issuer to check against. The server URL still has to be the MCP endpoint: tools are listed and called there with the token.
Remove MCP OAuth app
DELETE /credentials/mcp/app?server_url={url}&scope={team|org|platform}
Removes the app for that host and scope. Refused while active logins still reference the app.
Vault keys for MCP apps use the MCP_ prefix: MCP_CLIENT_ID, MCP_CLIENT_SECRET, and MCP_<FIELD> for each entry in app_fields (for example MCP_TENANT_ID) and each endpoint by hand (MCP_AUTHORIZATION_ENDPOINT, MCP_TOKEN_ENDPOINT, MCP_SCOPES).
OAuth callback without an authorization code
POST /oauth-callback/notice
Public endpoint (no API key). The web app's MCP OAuth callback uses this when the redirect carries no code — typically an error from the authorization server or a Microsoft admin consent landing that approves the app without connecting a user.
Body (OAuthCallbackNoticeRequest):
| Field | Description |
|---|---|
provider | Provider key (mcp for MCP connectors) |
state | OAuth state from the redirect, when present |
params | Other query parameters from the redirect (error codes, descriptions, Entra admin_consent flags, and so on) |
Response (OAuthCallbackNotice):
| Field | Description |
|---|---|
outcome | error (sign-in failed) or approved (admin approved the app for their organization; no credential was created) |
message | User-facing explanation |
admin_consent_url | Optional link to send to an admin when consent is still required |
command | Optional one-time shell command an admin can run (for example creating a service principal) when directory setup data provides a resource application id |
Set default account
PUT /credentials/{provider}/default?identifier={account_id_or_label}
Makes one login the default for its owner and provider. Other logins for the same provider stay connected with is_primary: false. Requires credentials:write and permission to manage the login (same as disconnect).
1curl -X PUT 'https://api.inference.sh/credentials/google/default?identifier=user%40company.com' \2 -H "Authorization: Bearer inf_your_key"Response: the updated CredentialDTO with is_primary: true.
identifier is required. Omitting it returns a validation error.
Add OAuth scopes
POST /credentials/{provider}/scopes
Request additional scopes for an existing OAuth credential (incremental authorization). Optional identifier query parameter selects which account to extend (same as GET /credentials/{provider}); omit it to use the default login.
1{2 "scopes": ["https://www.googleapis.com/auth/drive.readonly"]3}Response (same shape as Connect a credential):
1{2 "auth_url": "https://accounts.google.com/o/oauth2/v2/auth?...",3 "state": "st_abc123",4 "code_verifier": ""5}Redirect the user to auth_url to approve the new scopes, then complete the flow with POST /credentials/oauth/complete (or your app's OAuth callback handler). Omit code_verifier when the API returned a non-empty value from connect or add-scopes — it is stored with the state challenge.
The authorize URL and the OAuth complete use the OAuth app that issued the login (app_credential_id), not whichever app a new connect would pick today, so token refresh stays on the client that issued the refresh token. A login saved before apps were recorded uses the app a new connect would take. If the issuing app row was deleted, add scopes answers 400 validation_error (the {provider} app this login was made through is gone; connect again).
The requested scopes are held to that app's allowed scopes: a scope the platform's client is approved for does not help when your workspace's app issued the login, and the other way round.
Incremental vs full re-authorization
Providers that support incremental authorization (for example google, microsoft, slack, salesforce, discord) return a consent URL that adds only the requested scopes. Existing grants are preserved.
Providers that do not support incremental auth (for example x) fall back to a full re-authorization URL that includes all existing scopes plus the new ones. The user must approve the combined scope list again.
Account pre-selection (login_hint)
When the credential has an account_identifier (email or username from the original connect), add-scopes auth_url values for Google and Microsoft include login_hint set to that identifier. The provider can skip the account chooser and go straight to consent for the same account. Custom OAuth auth providers can set account_hint_param (for example login_hint) to get the same behavior.
login_hint is not sent on the initial POST /credentials connect URL, only when re-authorizing for more scopes. It is omitted when account_identifier is empty — which is why OAuth complete refuses to save a login when the scheme expects an account id but the provider did not supply one (see Account identity on OAuth complete). A login saved during a temporary lookup outage gets account_identifier once it is next used, so a later add-scopes round trip can send login_hint.
Change connection scope
PUT /credentials/{provider}/scope
Rescopes an existing connection in place — same credential row and tokens, new owner level. Requires credentials:write. The same access rules apply as on connect: you must be allowed to use the connection at its current scope, and you must meet the admin requirements for the target scope (team needs workspace admin, org needs org admin, and so on).
Body:
1{2 "scope": "team"3}scope | Who can use the connection after the change |
|---|---|
user | Only the member who connected it |
team | The whole workspace |
org | All workspaces in the organization |
platform | All workspaces on the platform |
MCP connections: Pass identifier as a query parameter with the MCP server_url (same as Disconnect):
1curl -X PUT 'https://api.inference.sh/credentials/mcp/scope?identifier=https%3A%2F%2Fmcp.linear.app%2Fsse' \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{"scope":"team"}'Rules:
- Only the member who connected a row may narrow a shared connection to
user(personal). A workspace admin cannot re-scope someone else's team connection to their own user row (403forbidden). - A member without workspace admin cannot widen a personal connection to
team. - When the workspace already has a shared connection for the same provider account, rescoping your personal row to that level returns 409
conflict— disconnect the duplicate or pick a different target scope. - Call from the workspace that owns the connection. From any other workspace the API returns 400
validation_error. - The credential's default vault membership is updated to match the new scope.
Response: CredentialDTO for the updated connection.
| Error | HTTP | When |
|---|---|---|
team_role_required | 403 | Target scope needs admin and the caller does not have it |
forbidden | 403 | Caller cannot access the connection at its current scope, or narrows someone else's shared row to user |
conflict | 409 | Target scope already has a shared row for this account |
validation_error | 400 | Missing or unknown scope, a target scope this workspace cannot connect at, or the call comes from a workspace that does not own the connection |
not_found | 404 | No matching connection for provider (and identifier when required) |
Disconnect
DELETE /credentials/{provider}
Revokes tokens with the provider when supported, then soft-deletes the workspace's connection for that provider. Vault membership links for the credential are removed as well. Apps that require the credential will fail until it is reconnected.
When the disconnected login was the default (is_primary: true), the owner's oldest remaining login for that provider becomes the new default.
An optional identifier query parameter selects one connection by account_id or account_identifier. belt mcp disconnect uses it with the MCP server URL (the stable id for MCP logins):
1curl -X DELETE 'https://api.inference.sh/credentials/mcp?identifier=https%3A%2F%2Fmcp.linear.app%2Fsse' \2 -H "Authorization: Bearer inf_your_key"With identifier, the API walks the same ladder as a lookup without one, matching the named account on each rung (see Multiple accounts per provider). Platform-scope logins are on that ladder only in the platform workspace.
CLI equivalent: belt credentials disconnect <provider> (for example belt credentials disconnect google or belt credentials disconnect google-sa).
Check requirements (pre-flight)
POST /credentials/check
Validate secrets and credentials before calling POST /run. Returns 200 with a structured result (not HTTP 412):
1{2 "secrets": [3 { "key": "OPENAI_API_KEY", "optional": false }4 ],5 "credentials": [6 { "key": "google.sheets", "optional": false }7 ]8}Response:
1{2 "satisfied": false,3 "errors": [4 {5 "type": "secret",6 "key": "OPENAI_API_KEY",7 "message": "Missing secret: OPENAI_API_KEY",8 "action": { "type": "add_secret" }9 },10 {11 "type": "credential",12 "key": "google.sheets",13 "message": "connect Google Account to use this app",14 "action": {15 "type": "connect",16 "provider": "google",17 "provider_name": "Google Account",18 "scopes": ["https://www.googleapis.com/auth/spreadsheets"],19 "scope_descriptions": {20 "https://www.googleapis.com/auth/spreadsheets": "View and manage your spreadsheets"21 }22 }23 },24 {25 "type": "scope",26 "key": "google.sheets",27 "message": "Google Account connected but missing permissions: [https://www.googleapis.com/auth/spreadsheets]",28 "action": {29 "type": "add_scopes",30 "provider": "google",31 "provider_name": "Google Account",32 "scopes": ["https://www.googleapis.com/auth/spreadsheets"],33 "scope_descriptions": {34 "https://www.googleapis.com/auth/spreadsheets": "View and manage your spreadsheets"35 }36 }37 }38 ]39}Each errors[] entry has type (secret, credential, or scope), key, message, and optional action (add_secret, connect, add_scopes).
action.type | Meaning | Web app deep link |
|---|---|---|
add_secret | User must add a workspace secret | vaults → secrets |
connect | User must connect a credential | https://app.inference.sh/credentials/{slug} |
add_scopes | User must grant additional OAuth scopes | https://app.inference.sh/credentials/{slug}?grant={scope}, with grant repeated for each scope in action.scopes (permission grant links) |
action field | When present | Purpose |
|---|---|---|
provider | connect, add_scopes | Provider key for API connect calls and CLI (for example google, google-sa, gcp, slack) |
provider_name | connect, add_scopes | Human-readable label for UI copy (for example Google Account, Slack) — also used in message |
scopes | connect, add_scopes | OAuth scopes to request or add |
scope_descriptions | connect, add_scopes | Map of scope key → friendly display name from provider capabilities (for UI lists without hardcoded labels) |
instructions | connect, add_scopes, add_secret | The provider's setup instructions (markdown, links allowed), when it has them. Show them for step-by-step guidance; message is the one-line summary |
Use provider (not provider_name) when calling POST /credentials or belt credentials connect. Use provider_name or message when showing setup prompts to end users.
For dashboard links, resolve {slug} from GET /credentials/configs — match provider and the capability type from GET /credentials/capabilities. For most providers slug equals provider; Google OAuth uses google, the Sheets/Docs/Drive service account uses google-sa, and GCP WIF uses gcp. → Credentials overview — detail page
When requirements are missing at run time, POST /run and POST /apps/run return 412 with a plain JSON body (not problem+json):
1{2 "satisfied": false,3 "errors": [ /* the same RequirementError objects as check */ ]4}Other routes refused for incomplete setup (for example an agent message whose run needs a credential) answer 412 application/problem+json with code requirements_not_met; the same list is the problem's top-level errors member and is repeated under meta.errors. Use check when you want the list without submitting a task (it always answers 200).
CLI: belt app run and belt agent run catch 412 responses and print categorized hints — belt secrets set for missing secrets, belt credentials connect for OAuth providers, belt mcp connect for MCP requirements, or connect at: https://app.inference.sh/vaults when the requirement names no provider. Next to belt credentials connect <provider> the CLI also prints or: https://app.inference.sh/vaults/<provider>.
→ Secrets · CLI setup · Troubleshooting
MCP tool proxy
After connecting an MCP server (provider: "mcp"), call tools over HTTP:
| Endpoint | Method | Scope | Purpose |
|---|---|---|---|
/mcps/{slug}/tools | GET | credentials:read | List tools exposed by the server |
/mcps/{slug}/tools/{tool} | POST | credentials:write | Invoke a tool (body is forwarded to the MCP server) |
{slug} is the MCP server slug (for example linear from the connector directory). Tool calls are recorded in your workspace's credential history.
CLI equivalent: belt mcp tools <slug> and belt mcp run <slug>:<tool>.
→ Connectors browsing · MCP server
JavaScript SDK
1import { inference } from '@inferencesh/sdk';23const client = inference({ apiKey: 'inf_your_key' });45const caps = await client.credentials.getCapabilities();6const { auth_url } = await client.credentials.connect({7 provider: 'google',8 type: 'oauth',9 scopes: ['https://www.googleapis.com/auth/spreadsheets'],10});11await client.credentials.checkRequirements({12 secrets: [{ key: 'OPENAI_API_KEY' }],13 credentials: [{ key: 'google.sheets' }],14});→ SDK overview — client.credentials namespace
Related
- REST overview — authentication and response format
- Entitlements — connector limits on your plan
- Agent connector tools — use credentials from agents