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:
Store
Create a credential via the API with a unique ID, kind, and secret value.
Reference
Use credentials://<id> in any step parameter where a secret is needed.
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:
| Kind | Value Shape | Use Case | Auto-Refresh |
|---|---|---|---|
| api_key | Single opaque token string | Bearer tokens, API keys, PATs | No |
| oauth2 | JSON: access_token, refresh_token, expires_at | Google, GitHub, Slack OAuth | Yes |
| basic | JSON: username, password | Legacy HTTP Basic Auth | No |
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-prodID 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_urlwith 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):
| Priority | Method | Example |
|---|---|---|
| 1 (highest) | api_key param | "api_key": "credentials://openai-prod" |
| 2 | api_key_env param | "api_key_env": "MY_OPENAI_KEY" |
| 3 (lowest) | Default env var | OPENAI_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.