Skip to content

Credentials & Keys

Store API keys, OAuth2 tokens, and secrets once. Reference them anywhere in your workflows with credentials:// URIs. The engine resolves them at dispatch time — secrets never appear in sequence definitions.

Why Credentials

Hard-coding API keys in workflow definitions is a security risk and an operational headache. Orch8's credential system solves this:

  • Secrets never stored in sequences — definitions stay portable and safe to version-control
  • Rotate without redeploying — update a credential once, all workflows pick it up immediately
  • Tenant isolation — credentials are scoped per tenant by default
  • OAuth2 auto-refresh — the engine handles token refresh loops automatically
  • API redaction — secret values are never returned by the API, preventing accidental exposure

How It Works

The credential lifecycle has three stages:

Step 1

Store

Create a credential via the API with a unique ID, kind, and secret value.

Step 2

Reference

Use credentials://<id> in any step parameter where a secret is needed.

Step 3

Resolve

At dispatch time, the engine replaces URIs with actual values before calling the handler.

The resolution is recursive — it walks the entire JSON params object for a step, replacing any string matching the credentials:// pattern. This means credentials can appear anywhere: headers, body, query params, nested objects.

Credential Types

Three kinds of credentials cover all common auth patterns:

KindValue ShapeUse CaseAuto-Refresh
api_keySingle opaque token stringBearer tokens, API keys, PATsNo
oauth2JSON: access_token, refresh_token, expires_atGoogle, GitHub, Slack OAuthYes
basicJSON: username, passwordLegacy HTTP Basic AuthNo

Registry API

Full CRUD for credential management. All endpoints are tenant-scoped.

Create Credential

POST /credentials

{
  "id": "openai-prod",
  "name": "OpenAI Production Key",
  "kind": "api_key",
  "value": "sk-proj-abc123...",
  "tenant_id": "tenant-1"
}

Response returns the credential metadata without the value field.

Create OAuth2 Credential (with auto-refresh)

POST /credentials

{
  "id": "google-calendar",
  "name": "Google Calendar OAuth",
  "kind": "oauth2",
  "value": "{\"access_token\": \"ya29.xxx\", \"expires_at\": \"2024-01-15T10:00:00Z\"}",
  "refresh_token": "1//0abc...",
  "refresh_url": "https://oauth2.googleapis.com/token",
  "tenant_id": "tenant-1"
}

List Credentials

GET /credentials?tenant_id=tenant-1

Response:
[
  {
    "id": "openai-prod",
    "name": "OpenAI Production Key",
    "kind": "api_key",
    "tenant_id": "tenant-1",
    "enabled": true,
    "has_refresh_token": false,
    "created_at": "2024-01-10T08:00:00Z",
    "updated_at": "2024-01-10T08:00:00Z"
  }
]

Note: value and refresh_token are never returned. Only has_refresh_token: boolean indicates presence.

Update Credential

PATCH /credentials/openai-prod

{
  "value": "sk-proj-newkey456...",
  "enabled": true
}

Delete Credential

DELETE /credentials/openai-prod

ID constraints: alphanumeric plus - and _, max 255 characters. Choose descriptive IDs like stripe-live, anthropic-team-a.

Referencing in Workflows

Use the credentials://<id> URI scheme anywhere in step parameters. The engine resolves them recursively before dispatch.

Simple API Key Reference

{
  "type": "llm_call",
  "params": {
    "provider": "openai",
    "model": "gpt-4o",
    "api_key": "credentials://openai-prod",
    "messages": [
      { "role": "user", "content": "Hello" }
    ]
  }
}

At dispatch, credentials://openai-prod is replaced with the actual key value.

Field-Level Access (OAuth2)

{
  "type": "http_request",
  "params": {
    "url": "https://api.example.com/data",
    "headers": {
      "Authorization": "Bearer credentials://google-calendar/access_token"
    }
  }
}

Use credentials://<id>/<field> to extract a specific field from JSON-valued credentials.

Nested References

{
  "type": "http_request",
  "params": {
    "url": "https://api.stripe.com/v1/charges",
    "headers": {
      "Authorization": "Bearer credentials://stripe-live"
    },
    "body": {
      "metadata": {
        "webhook_secret": "credentials://stripe-webhook-secret"
      }
    }
  }
}

References work at any nesting depth. The resolver walks the entire params object recursively.

OAuth2 Auto-Refresh

For OAuth2 credentials with a refresh_url, the engine runs a background refresh loop that automatically renews tokens before they expire.

Refresh Loop

  • Polls every 60 seconds
  • Refreshes tokens expiring within 5 minutes
  • POSTs to refresh_url with grant_type=refresh_token
  • Updates stored credential on success

Failure Handling

  • Logs warning on refresh failure
  • Retries on next 60s cycle
  • Credential remains usable until actual expiry
  • 5s connect / 30s request timeouts

Setup Example

POST /credentials

{
  "id": "slack-bot",
  "name": "Slack Bot Token",
  "kind": "oauth2",
  "value": "{\"access_token\": \"xoxb-xxx\", \"expires_at\": \"2024-01-15T12:00:00Z\"}",
  "refresh_token": "xoxr-1-xxx",
  "refresh_url": "https://slack.com/api/oauth.v2.access",
  "tenant_id": "tenant-1"
}

Once created, the engine automatically refreshes this token. Your workflows always get a valid access token from credentials://slack-bot/access_token.

Security Model

Tenant Isolation

Credentials are scoped to a tenant_id. A workflow in tenant A cannot resolve credentials belonging to tenant B. Global credentials (empty tenant_id) are accessible to all tenants.

API Redaction

The value and refresh_token fields are write-only. The API never returns secret material — only metadata and a has_refresh_token boolean.

Dispatch-Time Resolution

Secrets are resolved only when a step is about to execute. They are never stored in the sequence definition, instance state, or logs.

Disabled Credentials

Setting enabled: false immediately prevents resolution. Any workflow referencing a disabled credential will fail with a permanent error — useful for emergency revocation.

ID Validation

Credential IDs are restricted to alphanumeric, hyphen, and underscore (max 255 chars) to prevent injection via crafted URIs.

LLM Provider Keys

The built-in llm_call handler has special key resolution logic. You can provide keys in three ways (in priority order):

PriorityMethodExample
1 (highest)api_key param"api_key": "credentials://openai-prod"
2api_key_env param"api_key_env": "MY_OPENAI_KEY"
3 (lowest)Default env varOPENAI_API_KEY, ANTHROPIC_API_KEY

Per-Tenant LLM Keys

In multi-tenant setups, each tenant stores their own LLM API key as a credential. The workflow references it via the credential system — the engine ensures tenant isolation.

{
  "type": "llm_call",
  "params": {
    "provider": "anthropic",
    "model": "claude-sonnet-4-20250514",
    "api_key": "credentials://anthropic-key",
    "messages": [
      { "role": "user", "content": "Summarize this document." }
    ]
  }
}

Multi-Provider Failover

Use separate credentials per provider in failover chains:

{
  "type": "llm_call",
  "params": {
    "providers": [
      {
        "provider": "anthropic",
        "api_key": "credentials://anthropic-key",
        "model": "claude-sonnet-4-20250514"
      },
      {
        "provider": "openai",
        "api_key": "credentials://openai-key",
        "model": "gpt-4o"
      }
    ],
    "messages": [
      { "role": "user", "content": "Hello" }
    ]
  }
}

Best Practices

Use descriptive IDs

Name credentials by service + environment: stripe-live, openai-dev, google-calendar-team-a. This makes sequences self-documenting.

One credential per concern

Don't bundle multiple secrets into one credential. Use separate entries for API keys, webhook secrets, and OAuth tokens — rotation is simpler.

Prefer credentials:// over env vars

Environment variables work for single-tenant self-hosted setups. For multi-tenant or cloud deployments, always use the credential system for proper isolation.

Disable before delete

When rotating keys, disable the old credential first (PATCH enabled: false). Verify no workflows are failing, then delete.

Use OAuth2 for expiring tokens

Don't manually rotate access tokens. Set up auto-refresh and let the engine handle renewal.

Audit via list endpoint

Periodically GET /credentials to review what's stored. The response shows enabled/disabled state and timestamps without exposing secrets.