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 are401 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
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
MANAGER+ role, or a member with the credential.create capability
for this workspace.
Request Body:
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
200 OK — credential object with crew_ids, agent_names, and mcp_used.
Update Credential
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.
Response: 200 OK — updated credential object.
Delete Credential
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
internal/api/credentials_test_endpoint.go:229)
200 OK with { "valid": bool, "status": int, "error": string }.
Test Stored Credential
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
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
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)
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:
200 OK
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
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)
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)
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
Bindings
A credential’sname is the account (github-acme). The slot is the environment variable the agent reads (GH_TOKEN). A binding connects them in one scope:
- 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.
name — the pre-binding behaviour, unchanged.
List Bindings
scope, crew_id, agent_id, credential_id, slot.
Auth: Session or CLI token, workspace context required.
Response: 200 OK
Create Binding
OWNER / ADMIN (manage).
Response:
201 Created with the binding.
Delete Binding
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
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 asvalue.
List Custom Fields
200 OK
Add Custom Field
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
GH_TOKEN for GITHUB), and whether that provider can be probed. 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.
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. 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
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:
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 ininternal/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 asensitivity. 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
200 OK
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
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
200 OK
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.