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

Endpoint contract

Each endpoint identifies Auth, Request, Response, and Status. Unless noted, requests and responses are JSON, workspace routes require a session/CLI token plus membership, and secret values are never returned. The shared failures are 401 unauthenticated, 403 unauthorized, 400 invalid input, 404 missing or invisible resource, 409 uniqueness/lifecycle conflict, and 500 unexpected handler/database failure. Test and reveal routes may also return 429. Route gates and status writes are verified in internal/api/router_crews.go, internal/api/credentials_mutate.go, internal/api/credential_rotation.go, and internal/api/credential_fields.go. Auth: Use the role/capability written on the endpoint; read visibility is also enforced for crew-scoped credentials. Request: JSON body and query parameters are shown on the endpoint. Secret values may be sent only to create, rotate, test, or reveal operations. Response: Credential metadata, rotation records, bindings, and field metadata never contain the encrypted credential value; reveal is the sole explicit exception and is separately audited. Status: Success is 200 for reads/updates, 201 for creates, and 204 where a delete section says so. Invalid input is 400, authorization is 401 or 403, invisible resources are 404, conflicts are 409, and rate limits are 429.

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

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) Ordering is type ASC, created_at DESC, id ASC, so paging with limit/offset is stable across requests. Pass paginate=true to use the opt-in cursor form instead of the legacy array. That form defaults to limit=50, caps limit at 500, orders by created_at DESC, id DESC, and returns { "credentials": [...], "next_cursor": "...", "limit": 50 }; next_cursor is null on the last page. A malformed cursor or a non-positive limit in this mode returns 400. Response: 200 OK

Response Fields


Create Credential

Auth: MANAGER+ role, or a member with the credential.create capability for this workspace. Request Body:
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).

Get Credential

Returns credential metadata (never the encrypted value). Response: 200 OK — credential object with crew_ids, agent_names, and mcp_used.

Update Credential

Both methods behave as partial update. Auth: OWNER, ADMIN, or MANAGER role (canRole(role, "update") in internal/api/credentials_mutate.go:319) Updatable Fields: Note: The status field cannot be updated via this endpoint. Status changes are managed by the credential monitor and OAuth refresh worker.
Changing provider to or from OPENAI_COMPAT requires sending value in the same request, and returns 400 otherwise.For most providers the stored value is an opaque token. For a provider whose upstream comes from the credential it is a {"baseURL", "apiKey", "headers"} object that Crewship splits before delivery. Switching between the two re-interprets bytes that are already stored: one direction leaves a bare key under a loader that expects an object (the credential saves and then silently reaches no agent), the other leaves an object under a loader that expects a key — which would send the base URL and every custom header upstream in an Authorization header. Crewship refuses rather than guess a conversion.
Response: 200 OK — updated credential object.

Delete Credential

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

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

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)
Response: 200 OK with { "valid": bool, "status": int, "error": string }.

Test Stored Credential

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, "supported": bool }. supported reports whether a real upstream probe ran. Crewship maintains probes for ANTHROPIC, OPENAI, GOOGLE, CURSOR, FACTORY, GITHUB, GITLAB and VERCEL, plus any credential of type ENDPOINT_URL. Every other provider falls through to a no-op that answers valid: true — literally true, since nothing failed, but nothing was attempted either. Read supported before valid: supported: false means the credential was not checked, which is neither a pass nor a failure. valid retains its original meaning for existing clients.

Rotation & Audit

Rotate secrets without downtime via an overlap grace window, inspect the append-only audit trail, and manage rotation history.

Audit Timeline

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: Response: 200 OK — JSON array (newest first).

Rotate Credential

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, or any member holding the credential.rotate capability (layered gate requireRoleOrCapabilityOrForbid — lets an oncall MANAGER/MEMBER rotate a leaked token without blanket vault access) Request Body:
Response: 200 OK
The response is EXPIRED immediately when grace_seconds is 0; otherwise it starts as ACTIVE. cancelled_at is nullable and is set when a rotation is cancelled.

List 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. The same visibility filter as GET /credentials/{id} applies: MANAGER+ see every credential’s history; MEMBER/VIEWER only see WORKSPACE-scoped credentials or CREW-scoped ones belonging to a crew they are a member of. Response: 200 OK — JSON array. Each entry also includes nullable cancelled_at when a rotation was cancelled.
old_value_gone is true once status is EXPIRED or CANCELLED (the old encrypted value has been scrubbed from the row).

Rotation Statuses


Cancel Rotation (End Grace Early)

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, or any member holding the credential.rotate capability (same layered gate as rotate — ending a grace window early is part of the rotation lifecycle) Response: 200 OK
For an already-terminal rotation:

Bindings

A credential’s name is the account (github-acme). The slot is the environment variable the agent reads (GH_TOKEN). A binding connects them in one scope:
Two rules govern the whole surface:
  • One slot, one credential, per scope. Enforced by a unique index; a conflicting write is refused with 409, never applied as a replacement.
  • Resolution is agent > crew > workspace. The most specific scope that binds a slot wins, and only that one is delivered.
A credential with no binding is delivered under its own name — the pre-binding behaviour, unchanged.

List Bindings

Query parameters (all optional, all exact-match): scope, crew_id, agent_id, credential_id, slot. Auth: Session or CLI token, workspace context required. Response: 200 OK

Create Binding

Role: OWNER / ADMIN (manage).
Response: 201 Created with the binding.

Delete Binding

Role: OWNER / ADMIN (manage). Response: 204 No Content, or 404 if the binding is not in the caller’s workspace. The row is really deleted — it is the slot claim, so anything that merely hid it would keep the slot occupied and 409 every later write. Bindings also cascade with their scope: deleting a crew, an agent, or the credential itself removes the bindings that pointed at it.

Resolve Slots for an Agent

Reports the slot map the agent will actually boot with, after resolution. Read-only, and values are never included. Response: 200 OK
source, most specific first: agent_grant (an explicit per-agent assignment), agent_binding, crew_binding, workspace_binding, crew_link (no binding — delivered under the credential’s own name). The response may also contain a warnings array when a credential was renamed, normalised, or could not be assigned a deliverable slot; warnings never contain secret values.

Custom Fields

Custom fields extend a credential with named parts such as a non-secret region or an additional secret. Field keys are lower snake case, start with a letter, and are limited to 64 characters. Secret field values are encrypted and are never returned; non-secret values are returned as value.

List Custom Fields

Auth: authenticated workspace member with visibility of the credential. Response: 200 OK

Add Custom Field

Auth: MANAGER+ role.
is_secret defaults to true when omitted. value is required and is capped at 64 KiB. The response is 201 Created with the field object. A missing or invisible credential returns 404; invalid keys/values or a duplicate key returns 400.

Update Custom Field

PUT /api/v1/credentials/{credentialId}/fields/{fieldKey} Auth: Authenticated workspace member with the create capability (the same write tier as updating the credential). Credential visibility is enforced against the current workspace; an invisible credential is reported as 404. Request: Path parameters are credentialId and fieldKey. JSON body:
value is required and capped at 64 KiB. is_secret and ordinal are optional; omitted is_secret preserves the field’s current classification, and the path key cannot be renamed. Response: 200 OK with the updated field object. Secret field values are omitted (value is null); non-secret values are returned. Statuses: 400 invalid JSON, key, value, or ordinal; 401 unauthenticated; 403 insufficient role/capability; 404 credential or field not found; 500 unexpected database or encryption failure.

Delete Custom Field

DELETE /api/v1/credentials/{credentialId}/fields/{fieldKey} Auth: Authenticated workspace member with the create capability. Credential visibility is enforced against the current workspace. Request: No body. credentialId and fieldKey are required path parameters. Response: 200 OK with { "success": true }. Statuses: 401 unauthenticated; 403 insufficient role/capability; 404 credential or field not found; 500 unexpected database failure.

Lookups & Assignment

Helper lookups and the cross-reference to per-agent credential assignment.

Default Environment Variable

Returns the default environment variable name for a given provider (e.g., GH_TOKEN for GITHUB), and whether that provider can be probed. Recognised providers: GITHUBGH_TOKEN, GITLABGITLAB_TOKEN, VERCELVERCEL_TOKEN, AWSAWS_ACCESS_KEY_ID, KUBERNETESKUBECONFIG; any other provider returns an empty string. The optional type parameter affects testable only: a credential of type ENDPOINT_URL is probeable whatever its provider, because the stored value is itself the target being dialled. env_var is derived from provider alone. Auth: Session or CLI token (no workspace context needed) Response: 200 OK
testable lets a client decide whether to offer a “Test” action without keeping its own list of probeable providers — the dashboard’s Add Credential form reads it for exactly that. The same flag rides on each credential in List and Get responses, so a detail view needs no extra call.

Agent Credential Assignment

See the Agents page for credential assignment endpoints:

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):

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".

Statuses

Revoking an LLM/provider token stops proxy injection within ~60s. A revoked credential is filtered out of every fresh delivery path immediately, and each agent’s sidecar also holds an in-memory snapshot of its provider tokens (loaded at container start). The sidecar reconciles that snapshot against the live set on a ~60s interval and drops any credential the server no longer lists (revoked or deleted), so a revoked provider key stops being injected into new outbound LLM requests within one interval — without waiting for the container to restart.File-based secrets are removed on revoke, too. File-based secrets written to /secrets/{agent-slug}/ (DB passwords, GH_TOKEN, SSH keys, certs) are materialized on disk at container start. On revoke (deleting a credential), the server reaches into every running crew container that mounted the credential as a file and removes the secret file(s) — exec’d inside the container as the agent UID (1001), the only principal that can unlink inside the agent’s 0700 secrets directory (the sidecar, UID 1002, cannot). This runs synchronously with the delete, so a successful revoke has already removed the files from live containers.
Removal is best-effort: a container that isn’t running is skipped (nothing to remove, and the revoked credential won’t be re-materialized on the next boot since it’s filtered from every delivery path). The .env hint-map keeps a now-inert reference to the removed file until the next container boot — harmless, since the file it points at is gone. If an exec fails transiently, recreate the crew container to force a clean rewrite.

Reveal

Reveal is the only endpoint in Crewship that returns a stored secret in plaintext. Every other read path omits values entirely.
Rotate instead, almost always. Most reasons to reveal a secret are really reasons to put a new secret somewhere. POST /credentials/{id}/rotate issues a new value, shows it once, and lets the old one drain through a grace window — no credential that is currently in use is ever exposed. Reveal exists for the cases where you genuinely need the value that is already deployed.
A reveal succeeds only when all of the following hold. Each is checked independently and each fails closed, so breaking one does not get you past the others.

Agents, API tokens and CI can never reveal

The endpoint requires an interactive, revocable user session. A CLI token (crewship_cli_… / crewship_admin_…), an X-Internal-Token, a sidecar request, and any future auth method that does not explicitly opt in are all refused with 403. This is a gate on the credential shape, not on identity. An agent’s request carries a real user’s id and role — that is how delegation works — so “who is this?” cannot separate a person from a container. “What did they present?” can. An agent that needs a secret gets it through the normal delivery paths (env var, /secrets mount) or asks a human via Keeper escalation; it never reads one back out of the vault. Because of this, crewship credential reveal asks for your password and signs you in interactively rather than using the stored CLI token.

Capability, not role

credentials:reveal is granted per person on the membership. It is deliberately absent from every capability bundle, including the admin bundle, so an OWNER or ADMIN whose membership has never been touched does not hold it. Being an OWNER is necessary for workspace-wide reveal and never sufficient. Recommended for corporate deployments: two people hold it, not the whole admin team.

Reveal Credential

Auth: Interactive session only + workspace membership. CLI tokens, internal tokens and sidecars are refused. Rate limit: Dedicated per-IP bucket (http.cred_reveal_per_min, default 3/min) — far tighter than the general API bucket, and unlike it, authenticated CLI callers are not exempt. Request Body:
Response: 200 OK with Cache-Control: no-store
journal_entry_id anchors the disclosure to the tamper-evident audit chain — quote it in an incident report and it can be verified. Errors:

The audit write is a precondition

The reveal is recorded in internal/journal, which is an HMAC hash-chain with signed compaction checkpoints, before the value is read out of the row. If that write fails, the request returns 500 and no value is returned. There is no “return it now, audit later” path. The flat credential_audit timeline also gets a REVEAL row so the disclosure appears in the credential’s detail Sheet next to every other event on it. That table has no hash chain — a row can be deleted without trace — which is exactly why it is not the authoritative record. What the journal payload contains: actor, credential id and name, classification, reason, actor role, client IP. What it never contains: the value, or any hash or fingerprint of it. A digest looks like a harmless identifier and is not — journal rows are readable by anyone with journal access and travel in every backup, and a digest of a short secret is crackable offline in seconds. Recording one would turn the audit log into a second copy of the vault.

Classification

Every credential carries a sensitivity. It is the first thing the reveal gate consults and the only one that no role can override. SEALED has no escape hatch by design. Break-glass for a sealed credential is rotation: mint a new value, let the old one drain through its grace window. An escape hatch that exists is an escape hatch that gets used. Existing credentials default to STANDARD, which is safe only because the workspace reveal switch defaults to off.
RESTRICTED behaves identically to STANDARD today. The four-eyes second-approver flow that will distinguish them is deferred; classifying now still pays off, because that gate will key off this field.

Set Classification

Auth: Session or CLI token + workspace membership. Raising requires MANAGER+; lowering requires OWNER or ADMIN. The asymmetry is deliberate. Raising only ever removes reach, so it needs no ceremony — and if tightening is annoying, people stop tightening. Lowering hands out a key that did not exist a moment earlier, so it is written to the audit chain first and fails closed if that write fails. Request Body:
Response: 200 OK
A no-op change (same value) returns 200 and is not journaled. Errors: 400 unknown class · 403 lowering without OWNER/ADMIN · 404 not in this workspace · 500 audit write failed (the classification is unchanged)

Workspace Reveal Policy

Reveal is off for every workspace until an OWNER turns it on, including freshly created ones. A default of “on” would make a new tenant’s security posture depend on someone remembering to lock it.

Read Reveal Policy

Auth: Session or CLI token + MANAGER or above. MEMBER and VIEWER get 403 — whether a tenant has reveal enabled is target-selection information. A MANAGER reads it because they need to know the rules they work under, but cannot change it. Response: 200 OK

Set Reveal Policy

Auth: Session or CLI token + OWNER only. Journaled as a precondition: if the chained write fails, the switch does not move. An attacker who can wedge the audit log must not be able to open a tenant’s reveal surface unobserved. Both directions are recorded. Request Body:
Response: 200 OK
Enabling the switch does not grant anyone reveal on its own — each person still needs credentials:reveal on their membership. Errors: 400 body is not {"enabled": bool} · 403 caller is not OWNER · 500 audit write failed (switch unchanged)

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):

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".

Statuses

Revoking an LLM/provider token stops proxy injection within ~60s. A revoked credential is filtered out of every fresh delivery path immediately, and each agent’s sidecar also holds an in-memory snapshot of its provider tokens (loaded at container start). The sidecar reconciles that snapshot against the live set on a ~60s interval and drops any credential the server no longer lists (revoked or deleted), so a revoked provider key stops being injected into new outbound LLM requests within one interval — without waiting for the container to restart.File-based secrets are removed on revoke, too. File-based secrets written to /secrets/{agent-slug}/ (DB passwords, GH_TOKEN, SSH keys, certs) are materialized on disk at container start. On revoke (deleting a credential), the server reaches into every running crew container that mounted the credential as a file and removes the secret file(s) — exec’d inside the container as the agent UID (1001), the only principal that can unlink inside the agent’s 0700 secrets directory (the sidecar, UID 1002, cannot). This runs synchronously with the delete, so a successful revoke has already removed the files from live containers.
Removal is best-effort: a container that isn’t running is skipped (nothing to remove, and the revoked credential won’t be re-materialized on the next boot since it’s filtered from every delivery path). The .env hint-map keeps a now-inert reference to the removed file until the next container boot — harmless, since the file it points at is gone. If an exec fails transiently, recreate the crew container to force a clean rewrite.