VendoVendo Docs
Infrastructure

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:

  1. You use the Vendo SDK. vendo.token("notion") resolves through the credentials worker in Vendo mode. You never see the worker.

  2. You make your own HTTP call. GET https://credentials.vendo.run/notion with Authorization: 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 at hooks.vendo.run; not vended from credentials.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.

StatusVendo-Error-CodeMeaning
400validation_failedMalformed env-var key passed to /v1/<key>.
401app_unknownYour VENDO_API_KEY is missing or unknown to the worker.
401app_revokedThe key exists but was revoked.
401app_expiredThe key exists but its expires_at has passed.
401connection_needs_reauthThe connection is in needs_reauth state, or Composio reported the upstream account as EXPIRED / INACTIVE / FAILED. The user must re-authorize through vendo.run.
403binding_missingNo active binding for this provider on your app, or tenant mismatch.
403connection_revokedThe connection itself is revoked.
404provider_unknownUnknown provider slug (you asked for a provider Vendo doesn't support).
500profile_unsupportedProfile branch doesn't dispatch (user_oauth, missing composio_account_id, missing COMPOSIO_API_KEY).
502upstream_errorComposio 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}) — walks app_connection_bindings to 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.

  • The proxy — provider proxies use the same bindings to inject credentials transparently.
  • Sandbox — env vars and tokens in customize sandboxes.

On this page