Credentials worker
How `credentials.vendo.run` vends OAuth tokens, when they refresh, and what errors look like.
For providers that use API keys, Vendo can simply inject OPENAI_API_KEY (etc.) into your container's env at deploy time. For providers that use OAuth — Notion, Slack, Google, GitHub, and so on — keys expire. The credentials worker is Vendo's answer to that problem.
It lives at credentials.vendo.run. Your tool asks it for a fresh access token; the worker handles refresh, caching, and credential rotation behind the scenes.
When you talk to it
Two cases:
-
You use the Vendo SDK.
vendo.token("notion")resolves through the credentials worker in Vendo mode. You never see the worker. -
You make your own HTTP call.
GET https://credentials.vendo.run/notionwithAuthorization: Bearer <your VENDO_API_KEY>. Returns:{ "access_token": "ntn_***", "expires_at": "2026-05-20T15:00:00Z", "token_type": "Bearer" }
Same flow either way. The SDK is just a thin wrapper.
What gets cached
For refresh-rotating profiles (composio_managed), the worker caches the freshly-refreshed access token in Cloudflare's Cache API keyed by connection id. TTL is expires_at - 60s (a 60-second safety margin), or a 50-minute fallback when the upstream omits an expiry. On a cache hit, you get the token in <10ms. On a miss, the worker calls Composio's API for a fresh token and re-populates the cache.
For static profiles (oauth_app_install), there's no token expiry, no Cache-API entry, and no refresh — the worker decrypts directly from Postgres on every call.
Don't cache the token in your tool past the next request. Vendo handles refresh; if you hold a token for an hour and try to use it after, you'll get a 401 from the provider.
Credential profiles
The profile is set by the integration in apps_catalog; you don't pick it. The profile drives how the worker resolves the token:
oauth_app_install(e.g. Notion's bot install) — tokens don't expire; the worker decrypts the stored credential and returns it. No refresh, no expiry.composio_managed(most OAuth providers — Slack, Google, GitHub, etc.) — Composio holds the upstream refresh token. The worker calls Composio's API on cache miss and returns a fresh access token. Cache TTL =expires_at − 60s.vendo_managed_pool— Vendo-owned API key pool (the OpenRouter shared pool is the canonical example). Tokens are injected as plain env vars at deploy time; the credentials worker is not on the path. This is what most tools using LLMs are on by default.byok_static— bring-your-own-key static credential. Same env-var injection as the pool; no refresh.webhook_inbound— provider-driven inbound webhook (Telegram bot updates today). Receives athooks.vendo.run; not vended fromcredentials.vendo.run.user_oauth— reserved for future first-party OAuth providers. Returns 500 (profile_unsupported) if you hit it today.
If you're curious which profile a connection uses, check it on the dashboard's Connections tab or via GET /api/connections.
Errors
The worker emits a Vendo-Error-Code response header on every error, with one of these machine-readable codes — match on the header, not the prose. The JSON body uses { error, detail }; the code is in the header.
| Status | Vendo-Error-Code | Meaning |
|---|---|---|
400 | validation_failed | Malformed env-var key passed to /v1/<key>. |
401 | app_unknown | Your VENDO_API_KEY is missing or unknown to the worker. |
401 | app_revoked | The key exists but was revoked. |
401 | app_expired | The key exists but its expires_at has passed. |
401 | connection_needs_reauth | The connection is in needs_reauth state, or Composio reported the upstream account as EXPIRED / INACTIVE / FAILED. The user must re-authorize through vendo.run. |
403 | binding_missing | No active binding for this provider on your app, or tenant mismatch. |
403 | connection_revoked | The connection itself is revoked. |
404 | provider_unknown | Unknown provider slug (you asked for a provider Vendo doesn't support). |
500 | profile_unsupported | Profile branch doesn't dispatch (user_oauth, missing composio_account_id, missing COMPOSIO_API_KEY). |
502 | upstream_error | Composio returned 5xx or a malformed body. Transient — retry with backoff. |
For 401 with connection_needs_reauth: surface this to the user. They'll need to re-authorize through vendo.run — your tool can't recover on its own.
For 502: retry once with backoff. If it persists, the connection is likely broken; tell the user.
Scope: app vs connection
The bearer you send determines what the worker resolves:
- App-scoped key (the default from
${vendo_api_key}) — walksapp_connection_bindingsto find which connection serves this provider. - Connection-scoped key — locked to one connection for one provider. The provider in the URL must match, or you get a 403.
Most tools use app-scoped keys and don't think about this.
Don't store tokens in your filesystem or DB
The whole point of the credentials worker is that Vendo handles token storage and refresh. If you persist a token in your tool's database or on disk, you'll fight the worker every time it rotates.
If you find yourself wanting to do this, you probably want a long-lived API key instead — set up the provider that way in apps_catalog and skip OAuth entirely.