Skip to main content
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

MethodEndpointPurpose
GET/api/v1/credentialsList credentials with assignment info
POST/api/v1/credentialsCreate 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/testValidate a value without storing it
POST/api/v1/credentials/{credentialId}/testRe-test a stored credential
GET/api/v1/credentials/{credentialId}/auditRead the credential audit timeline
POST/api/v1/credentials/{credentialId}/rotateRotate with grace overlap
GET/api/v1/credentials/{credentialId}/rotationsList rotation history
DELETE/api/v1/credential-rotations/{rotationId}End a grace window early
GET/api/v1/credentials/default-env-varDefault 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)
ParameterTypeDefaultDescription
workspace_idstringRequired workspace context.
limitinteger100Page size. Capped at 500; values <= 0 fall back to the default.
offsetinteger0Row 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

FieldTypeDescription
idstringCredential ID
namestringDisplay name
descriptionstring?Description
typestringCredential type
providerstringProvider identifier
statusstringCurrent status
scopestringWORKSPACE or CREW
crew_idstring?Legacy single crew ID
crew_idsstring[]All associated crew IDs
account_labelstring?Account label
account_emailstring?Account email
token_expires_atstring?Token expiry timestamp
last_checked_atstring?Last health check timestamp
last_errorstring?Last error message
_count_agent_credentialsintegerNumber of agent assignments
agent_namesstring[]Names of assigned agents
mcp_usedbooleanWhether used by MCP bindings
usernamestring?USERPASS cleartext identifier (null for all other types)
last_used_atstring?Latest USE event recorded by the audit timeline
last_used_ipsstring[]Ring-buffer (max 5) of recent caller IPs
tagsstring[]Free-form tag labels (always non-null in JSON)
created_by_actor_typestring?Attribution (v98): user, agent, or system
created_by_actor_idstring?Attribution (v98): the actor id, null for system rows
provisioned_for_servicestring?<crew>/<service> tag for Crewship-managed (AUTO_MANAGED) rows; null otherwise
created_atstringISO 8601 timestamp
updated_atstringISO 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:
FieldTypeRequiredDefaultDescription
namestringYesDisplay name (1-255 characters)
valuestringConditionalSecret value (required unless type is OAUTH2, or pending = true)
descriptionstringNonullDescription
typestringNo"SECRET"Credential type
providerstringNo"NONE"Provider identifier
scopestringNo"WORKSPACE""WORKSPACE" or "CREW"
crew_idstringNonullLegacy single crew ID
crew_idsstring[]No[]Associated crew IDs (auto-sets scope to CREW)
tagsstring[]No[]Free-form tag labels
account_labelstringNonullAccount label
account_emailstringNonullAccount email
usernamestringNonullCleartext identifier half of USERPASS credentials (required when type is USERPASS)
token_expires_atstringNonullToken expiry timestamp
security_levelintegerNo1Security level (1-3)
refresh_tokenstringNonullOAuth refresh token
oauth_client_idstringNonullOAuth client ID (OAUTH2 type only)
oauth_client_secretstringNonullOAuth client secret
oauth_auth_urlstringNonullOAuth authorization URL
oauth_token_urlstringNonullOAuth token URL
oauth_scopesstringNonullOAuth scopes (space-separated)
pendingbooleanNofalseCreate 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).
StatusCondition
400Missing name, missing value (non-OAuth2), invalid crew_id, invalid type/USERPASS without username
403Insufficient role (requires MANAGER, ADMIN, or OWNER)
409Credential 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.
StatusCondition
404Credential 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:
FieldTypeDescription
namestringDisplay name
descriptionstringDescription
valuestringNew secret value (re-encrypted; resets status to ACTIVE and records an inline-rotate audit event)
typestringCredential type
providerstringProvider identifier
scopestringWORKSPACE or CREW
crew_idstringLegacy single crew ID
crew_idsstring[]Associated crew IDs (replaces existing)
tagsstring[]Free-form tag labels (empty array / null clears the column)
account_labelstringAccount label
account_emailstringAccount email
usernamestringUSERPASS cleartext identifier
token_expires_atstringToken expiry timestamp
security_levelintegerSecurity 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.
StatusCondition
400No fields to update, invalid crew_id
403Insufficient role
404Credential 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
{
  "success": true
}
StatusCondition
403Insufficient role
404Credential 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)
FieldTypeRequiredDescription
providerstringNoProvider to test against (drives which API is probed)
typestringNoCredential type (used to special-case Anthropic OAuth tokens)
valuestringYesValue 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 }.
StatusCondition
403Insufficient role
404Credential 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:
ParameterTypeDefaultDescription
limitinteger501-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"
  }
]
StatusCondition
403Insufficient role
404Credential 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:
FieldTypeRequiredDefaultDescription
valuestringYesNew secret value (will be encrypted with AES-256-GCM)
grace_secondsintegerNo86400Grace 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
}
StatusCondition
400Missing value, or grace_seconds outside 0..604800
401No authenticated user on the request
403Insufficient role
404Credential 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).
StatusCondition
404Credential not found

Rotation Statuses

StatusDescription
ACTIVEGrace window open; sidecar may fall back to old_value on 401
EXPIREDGrace window elapsed; old_value scrubbed by the hourly expiry worker
CANCELLEDOperator 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" }
StatusCondition
403Insufficient role
404Rotation 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: GITHUBGH_TOKEN, GITLABGITLAB_TOKEN, VERCELVERCEL_TOKEN, AWSAWS_ACCESS_KEY_ID, KUBERNETESKUBECONFIG; 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:
EndpointMethodDescription
GET /api/v1/agents/{agentId}/credentialsGETList assigned credentials
POST /api/v1/agents/{agentId}/credentialsPOSTAssign credential to agent
DELETE /api/v1/agents/{agentId}/credentials/{assignmentId}DELETERemove 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):
TypeDescription
AI_CLI_TOKENLLM API key (or OAuth bearer) for agent CLI adapters
API_KEYExternal service API key
CLI_TOKENGeneric CLI tool token
SECRETArbitrary secret value
OAUTH2OAuth 2.0 token (managed via OAuth flow)
USERPASSUsername + password pair (username cleartext, password encrypted; requires username)
SSH_KEYPEM-encoded private key (validated to begin with -----BEGIN ... PRIVATE KEY-----)
CERTIFICATEPEM-encoded certificate (validated to begin with -----BEGIN CERTIFICATE-----)
GENERIC_SECRETOpaque 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".
ProviderValidation probe
ANTHROPICGET api.anthropic.com/v1/models (AI_CLI_TOKEN / sk-ant-oat tokens accepted without probing)
OPENAIGET api.openai.com/v1/models
GOOGLEGET generativelanguage.googleapis.com/v1/models
CURSORGET api2.cursor.sh/v0/me
FACTORYGET app.factory.ai/api/cli/auth/whoami
GITHUBGET api.github.com/user
GITLABGET gitlab.com/api/v4/user
VERCELGET api.vercel.com/v2/user
NONE / otherNo probe (Generic / custom)

Statuses

StatusDescription
ACTIVEReady to use
PENDINGAwaiting OAuth completion
RATE_LIMITEDTemporarily rate-limited
EXPIREDNeeds renewal
REVOKEDManually revoked
ERRORValidation/connection test failed