The credential vault is Crewship’s encrypted store for API keys, OAuth tokens, and secrets. It covers the full lifecycle: CRUD on credentials, live validation (test before save and re-test stored values), zero-downtime rotation with a configurable grace window, an append-only audit timeline, and per-agent assignment. Values are protected at rest with AES-256-GCM and are never returned in API responses.
All credential endpoints require authentication and workspace context unless otherwise noted.
Endpoints
| Method | Endpoint | Purpose |
|---|
GET | /api/v1/credentials | List credentials with assignment info |
POST | /api/v1/credentials | Create a credential |
GET | /api/v1/credentials/{credentialId} | Get credential metadata |
PATCH PUT | /api/v1/credentials/{credentialId} | Partial-update a credential |
DELETE | /api/v1/credentials/{credentialId} | Soft-delete a credential |
POST | /api/v1/credentials/test | Validate a value without storing it |
POST | /api/v1/credentials/{credentialId}/test | Re-test a stored credential |
GET | /api/v1/credentials/{credentialId}/audit | Read the credential audit timeline |
POST | /api/v1/credentials/{credentialId}/rotate | Rotate with grace overlap |
GET | /api/v1/credentials/{credentialId}/rotations | List rotation history |
DELETE | /api/v1/credential-rotations/{rotationId} | End a grace window early |
GET | /api/v1/credentials/default-env-var | Default env var name for a provider |
Managing Credentials
Core CRUD for the vault — create, read, update, and soft-delete credential records. Secret values are encrypted on write and omitted from every response.
List Credentials
GET /api/v1/credentials?workspace_id={workspaceId}
Returns all credentials in the workspace with agent assignment info.
Auth: Session or CLI token + workspace membership
Query Parameters: (parseListPagination in internal/api/credentials.go:81)
| Parameter | Type | Default | Description |
|---|
workspace_id | string | — | Required workspace context. |
limit | integer | 100 | Page size. Capped at 500; values <= 0 fall back to the default. |
offset | integer | 0 | Row offset for pagination. Negative values clamp to 0. |
Ordering is type ASC, created_at DESC, id ASC, so paging with limit/offset is stable across requests.
Response: 200 OK
[
{
"id": "cred_abc",
"name": "anthropic-primary",
"description": "Main Anthropic API key",
"type": "AI_CLI_TOKEN",
"provider": "ANTHROPIC",
"status": "ACTIVE",
"scope": "WORKSPACE",
"crew_id": null,
"crew_ids": [],
"account_label": "Production",
"account_email": "team@company.com",
"token_expires_at": "2025-12-31T00:00:00Z",
"last_checked_at": "2024-01-15T10:00:00Z",
"last_error": null,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-15T10:00:00Z",
"_count_agent_credentials": 5,
"agent_names": ["Backend Dev", "Frontend Dev", "QA Engineer"],
"mcp_used": false
}
]
Response Fields
| Field | Type | Description |
|---|
id | string | Credential ID |
name | string | Display name |
description | string? | Description |
type | string | Credential type |
provider | string | Provider identifier |
status | string | Current status |
scope | string | WORKSPACE or CREW |
crew_id | string? | Legacy single crew ID |
crew_ids | string[] | All associated crew IDs |
account_label | string? | Account label |
account_email | string? | Account email |
token_expires_at | string? | Token expiry timestamp |
last_checked_at | string? | Last health check timestamp |
last_error | string? | Last error message |
_count_agent_credentials | integer | Number of agent assignments |
agent_names | string[] | Names of assigned agents |
mcp_used | boolean | Whether used by MCP bindings |
username | string? | USERPASS cleartext identifier (null for all other types) |
last_used_at | string? | Latest USE event recorded by the audit timeline |
last_used_ips | string[] | Ring-buffer (max 5) of recent caller IPs |
tags | string[] | Free-form tag labels (always non-null in JSON) |
created_by_actor_type | string? | Attribution (v98): user, agent, or system |
created_by_actor_id | string? | Attribution (v98): the actor id, null for system rows |
provisioned_for_service | string? | <crew>/<service> tag for Crewship-managed (AUTO_MANAGED) rows; null otherwise |
created_at | string | ISO 8601 timestamp |
updated_at | string | ISO 8601 timestamp |
Create Credential
POST /api/v1/credentials?workspace_id={workspaceId}
Auth: OWNER, ADMIN, or MANAGER role (canRole(role, "create") in internal/api/credentials_mutate.go:66)
Request Body:
| Field | Type | Required | Default | Description |
|---|
name | string | Yes | — | Display name (1-255 characters) |
value | string | Conditional | — | Secret value (required unless type is OAUTH2, or pending = true) |
description | string | No | null | Description |
type | string | No | "SECRET" | Credential type |
provider | string | No | "NONE" | Provider identifier |
scope | string | No | "WORKSPACE" | "WORKSPACE" or "CREW" |
crew_id | string | No | null | Legacy single crew ID |
crew_ids | string[] | No | [] | Associated crew IDs (auto-sets scope to CREW) |
tags | string[] | No | [] | Free-form tag labels |
account_label | string | No | null | Account label |
account_email | string | No | null | Account email |
username | string | No | null | Cleartext identifier half of USERPASS credentials (required when type is USERPASS) |
token_expires_at | string | No | null | Token expiry timestamp |
security_level | integer | No | 1 | Security level (1-3) |
refresh_token | string | No | null | OAuth refresh token |
oauth_client_id | string | No | null | OAuth client ID (OAUTH2 type only) |
oauth_client_secret | string | No | null | OAuth client secret |
oauth_auth_url | string | No | null | OAuth authorization URL |
oauth_token_url | string | No | null | OAuth token URL |
oauth_scopes | string | No | null | OAuth scopes (space-separated) |
pending | boolean | No | false | Create a placeholder credential (status PENDING, no real value) used by crewship apply -f manifest slots |
{
"name": "anthropic-primary",
"type": "AI_CLI_TOKEN",
"provider": "ANTHROPIC",
"value": "sk-ant-api03-xxxxx",
"scope": "WORKSPACE",
"description": "Main Anthropic API key"
}
The value is encrypted with AES-256-GCM before storage using the format v1:{base64(IV||AuthTag||Ciphertext)}.
Response: 201 Created — credential object (without the encrypted value).
| Status | Condition |
|---|
400 | Missing name, missing value (non-OAuth2), invalid crew_id, invalid type/USERPASS without username |
403 | Insufficient role (requires MANAGER, ADMIN, or OWNER) |
409 | Credential with this name already exists in the workspace |
Get Credential
GET /api/v1/credentials/{credentialId}?workspace_id={workspaceId}
Returns credential metadata (never the encrypted value).
Response: 200 OK — credential object with crew_ids, agent_names, and mcp_used.
| Status | Condition |
|---|
404 | Credential not found |
Update Credential
PATCH /api/v1/credentials/{credentialId}?workspace_id={workspaceId}
PUT /api/v1/credentials/{credentialId}?workspace_id={workspaceId}
Both methods behave as partial update.
Auth: OWNER, ADMIN, or MANAGER role (canRole(role, "update") in internal/api/credentials_mutate.go:319)
Updatable Fields:
| Field | Type | Description |
|---|
name | string | Display name |
description | string | Description |
value | string | New secret value (re-encrypted; resets status to ACTIVE and records an inline-rotate audit event) |
type | string | Credential type |
provider | string | Provider identifier |
scope | string | WORKSPACE or CREW |
crew_id | string | Legacy single crew ID |
crew_ids | string[] | Associated crew IDs (replaces existing) |
tags | string[] | Free-form tag labels (empty array / null clears the column) |
account_label | string | Account label |
account_email | string | Account email |
username | string | USERPASS cleartext identifier |
token_expires_at | string | Token expiry timestamp |
security_level | integer | Security level (1-3) |
Note: The status field cannot be updated via this endpoint. Status changes are managed by the credential monitor and OAuth refresh worker.
Response: 200 OK — updated credential object.
| Status | Condition |
|---|
400 | No fields to update, invalid crew_id |
403 | Insufficient role |
404 | Credential not found |
Delete Credential
DELETE /api/v1/credentials/{credentialId}?workspace_id={workspaceId}
Soft-deletes the credential and clears it from all MCP bindings.
Deleting a credential removes it from every MCP binding that references it.
Auth: OWNER or ADMIN role
Response: 200 OK
| Status | Condition |
|---|
403 | Insufficient role |
404 | Credential not found |
Validating Credentials
Probe a value against the provider’s live API — either an unsaved value before storing it, or an existing stored credential.
Test Credential
POST /api/v1/credentials/test
Validates a credential value against the provider’s API without storing it. Useful for checking a key before saving. Rate-limited to 60 requests/minute per IP.
Auth: Session or CLI token (no workspace context needed)
Request Body: (internal/api/credentials_test_endpoint.go:229)
| Field | Type | Required | Description |
|---|
provider | string | No | Provider to test against (drives which API is probed) |
type | string | No | Credential type (used to special-case Anthropic OAuth tokens) |
value | string | Yes | Value to test |
{
"provider": "ANTHROPIC",
"type": "AI_CLI_TOKEN",
"value": "sk-ant-api03-xxxxx"
}
Response: 200 OK with { "valid": bool, "status": int, "error": string }.
Test Stored Credential
POST /api/v1/credentials/{credentialId}/test
Re-tests an already-stored credential by decrypting its value server-side and probing the provider. Records an audit event so the detail-sheet timeline reflects the manual check. (internal/api/credentials_test_endpoint.go:253)
Auth: OWNER, ADMIN, or MANAGER role (canRole(role, "update")). Rate-limited to 60 requests/minute per IP.
Response: 200 OK with { "valid": bool, "status": int, "error": string }.
| Status | Condition |
|---|
403 | Insufficient role |
404 | Credential not found |
Rotation & Audit
Rotate secrets without downtime via an overlap grace window, inspect the append-only audit trail, and manage rotation history.
Audit Timeline
GET /api/v1/credentials/{credentialId}/audit?limit=50
Returns the credential’s append-only audit timeline (USE, ROTATE, TEST, REVOKE, DETECTED, CREATED events). Backs the Audit tab in the detail Sheet. (internal/api/credential_audit.go:276)
Auth: OWNER, ADMIN, or MANAGER role (canRole(role, "update")). Audit reveals admin-action IPs, so VIEWER/MEMBER are blocked.
Query Parameters:
| Parameter | Type | Default | Description |
|---|
limit | integer | 50 | 1-500. Out-of-range values fall back to the default. |
Response: 200 OK — JSON array (newest first).
[
{
"id": "ca_01h9z7k0",
"event_type": "ROTATE",
"agent_id": null,
"ip_address": "10.0.4.21",
"metadata": { "rotation_id": "rot_abc", "grace_seconds": 86400, "rotated_by": "user_5" },
"occurred_at": "2026-05-14T09:12:44Z"
},
{
"id": "ca_01h9z6jp",
"event_type": "USE",
"agent_id": "agent_backend",
"ip_address": "10.0.4.21",
"metadata": null,
"occurred_at": "2026-05-14T09:11:02Z"
}
]
| Status | Condition |
|---|
403 | Insufficient role |
404 | Credential not found (or in another workspace) |
Rotate Credential
POST /api/v1/credentials/{credentialId}/rotate
Issues a new value and starts a configurable grace overlap window. The previous encrypted value is preserved on a new credential_rotations row for sidecar 401-fallback during the grace window, then scrubbed when status transitions to EXPIRED or CANCELLED. (internal/api/credential_rotation.go:77)
Rotation replaces the live secret. The previous value is retained only for the grace window (then scrubbed), and the old value is scrubbed immediately when the rotation is expired or cancelled.
Auth: OWNER or ADMIN role (canRole(role, "manage"))
Request Body:
| Field | Type | Required | Default | Description |
|---|
value | string | Yes | — | New secret value (will be encrypted with AES-256-GCM) |
grace_seconds | integer | No | 86400 | Grace window in seconds. Range 0..604800 (7 days). |
{
"value": "sk-ant-api03-newvalue",
"grace_seconds": 86400
}
Response: 200 OK
{
"id": "rot_01h9z7k0abc",
"credential_id": "cred_abc",
"grace_seconds": 86400,
"rotated_at": "2026-05-14T09:12:44Z",
"expires_at": "2026-05-15T09:12:44Z",
"rotated_by": "user_5",
"status": "ACTIVE",
"old_value_gone": false
}
| Status | Condition |
|---|
400 | Missing value, or grace_seconds outside 0..604800 |
401 | No authenticated user on the request |
403 | Insufficient role |
404 | Credential not found |
List Rotations
GET /api/v1/credentials/{credentialId}/rotations
Returns the rotation history for a credential, newest first. Powers the Settings tab in the detail Sheet. (internal/api/credential_rotation.go:209)
Auth: Session or CLI token + workspace membership
Response: 200 OK — JSON array.
[
{
"id": "rot_01h9z7k0abc",
"credential_id": "cred_abc",
"grace_seconds": 86400,
"rotated_at": "2026-05-14T09:12:44Z",
"expires_at": "2026-05-15T09:12:44Z",
"rotated_by": "user_5",
"status": "ACTIVE",
"old_value_gone": false
}
]
old_value_gone is true once status is EXPIRED or CANCELLED (the old encrypted value has been scrubbed from the row).
| Status | Condition |
|---|
404 | Credential not found |
Rotation Statuses
| Status | Description |
|---|
ACTIVE | Grace window open; sidecar may fall back to old_value on 401 |
EXPIRED | Grace window elapsed; old_value scrubbed by the hourly expiry worker |
CANCELLED | Operator ended grace early via DELETE /credential-rotations/{rotationId}; old_value scrubbed |
Cancel Rotation (End Grace Early)
DELETE /api/v1/credential-rotations/{rotationId}
Ends an ACTIVE grace overlap immediately, scrubbing the stored old value. Idempotent: already-terminal rotations return 200 with the existing status. (internal/api/credential_rotation.go:258)
Cancelling a rotation scrubs the retained old value at once, ending sidecar 401-fallback before the grace window would otherwise elapse.
Auth: OWNER or ADMIN role (canRole(role, "manage"))
Response: 200 OK
{ "status": "CANCELLED" }
For an already-terminal rotation:
{ "status": "EXPIRED", "message": "rotation already terminal" }
| Status | Condition |
|---|
403 | Insufficient role |
404 | Rotation not found, or its credential lives in another workspace |
Lookups & Assignment
Helper lookups and the cross-reference to per-agent credential assignment.
Default Environment Variable
GET /api/v1/credentials/default-env-var?provider={provider}
Returns the default environment variable name for a given provider (e.g., GH_TOKEN for GITHUB). The handler reads only provider; the type query parameter is not consulted (internal/api/credentials.go:280). Recognised providers: GITHUB → GH_TOKEN, GITLAB → GITLAB_TOKEN, VERCEL → VERCEL_TOKEN, AWS → AWS_ACCESS_KEY_ID, KUBERNETES → KUBECONFIG; any other provider returns an empty string.
Auth: Session or CLI token (no workspace context needed)
Response: 200 OK
{ "env_var": "GH_TOKEN" }
Agent Credential Assignment
See the Agents page for credential assignment endpoints:
| Endpoint | Method | Description |
|---|
GET /api/v1/agents/{agentId}/credentials | GET | List assigned credentials |
POST /api/v1/agents/{agentId}/credentials | POST | Assign credential to agent |
DELETE /api/v1/agents/{agentId}/credentials/{assignmentId} | DELETE | Remove assignment |
Reference
Enums and value sets used across the credential endpoints above.
Credential Types
The closed type enum (internal/api/credentials_types.go, enforced by both Create and Update):
| Type | Description |
|---|
AI_CLI_TOKEN | LLM API key (or OAuth bearer) for agent CLI adapters |
API_KEY | External service API key |
CLI_TOKEN | Generic CLI tool token |
SECRET | Arbitrary secret value |
OAUTH2 | OAuth 2.0 token (managed via OAuth flow) |
USERPASS | Username + password pair (username cleartext, password encrypted; requires username) |
SSH_KEY | PEM-encoded private key (validated to begin with -----BEGIN ... PRIVATE KEY-----) |
CERTIFICATE | PEM-encoded certificate (validated to begin with -----BEGIN CERTIFICATE-----) |
GENERIC_SECRET | Opaque value, no shape validation |
Providers
provider is a free-form string. The values below are the ones with a
live validation probe in the Test / Test Stored endpoints
(internal/api/credentials_test_endpoint.go); any other provider value
(including NONE) is accepted and stored, but Test returns
valid: true with "No validation available for this provider".
| Provider | Validation probe |
|---|
ANTHROPIC | GET api.anthropic.com/v1/models (AI_CLI_TOKEN / sk-ant-oat tokens accepted without probing) |
OPENAI | GET api.openai.com/v1/models |
GOOGLE | GET generativelanguage.googleapis.com/v1/models |
CURSOR | GET api2.cursor.sh/v0/me |
FACTORY | GET app.factory.ai/api/cli/auth/whoami |
GITHUB | GET api.github.com/user |
GITLAB | GET gitlab.com/api/v4/user |
VERCEL | GET api.vercel.com/v2/user |
NONE / other | No probe (Generic / custom) |
Statuses
| Status | Description |
|---|
ACTIVE | Ready to use |
PENDING | Awaiting OAuth completion |
RATE_LIMITED | Temporarily rate-limited |
EXPIRED | Needs renewal |
REVOKED | Manually revoked |
ERROR | Validation/connection test failed |