# Credential Sources

Credential sources store the secret material that later gets projected into outbound auth flows.

Sources are created once per team and then referenced by `sourceRef` from sandbox or template network policy.

The API surface is:

| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/api/v1/credential-sources` | List sources |
| `POST` | `/api/v1/credential-sources` | Create a source |
| `GET` | `/api/v1/credential-sources/{name}` | Get source metadata |
| `PUT` | `/api/v1/credential-sources/{name}` | Replace a source |
| `DELETE` | `/api/v1/credential-sources/{name}` | Delete a source |

`POST` creates a new source and returns `409 Conflict` when the name already exists. `PUT` rotates an existing source and returns `404 Not Found` when the name does not exist.

<Callout variant="info">
Source specs are write-only. Read APIs return metadata such as `name`, `resolverKind`, `currentVersion`, and timestamps, but not the raw secret values.
</Callout>

<Callout variant="info">
CLI examples assume you already ran `s0 auth login` and selected a current team with `s0 team use <team-id>`.
</Callout>

## Resolver Kinds

| Resolver kind | Key spec fields | Typical use |
|--------------|-----------------|-------------|
| `static_headers` | `spec.staticHeaders.values` | Bearer tokens and header fragments |
| `static_tls_client_certificate` | `spec.staticTLSClientCertificate.certificatePem`, `privateKeyPem`, optional `caPem` | mTLS client authentication |
| `static_username_password` | `spec.staticUsernamePassword.username`, `password` | Username/password based outbound auth |
| `static_ssh_private_key` | `spec.staticSSHPrivateKey.privateKeyPem`, optional `passphrase` | SSH transparent proxy upstream authentication |

Resolver kind is immutable for a source name. Rotate the secret material within the same resolver kind. To change from one resolver kind to another, create a new source and update the affected bindings to reference that new source.

## Storage Backends

Credential source storage is selected by the platform, not by the credential source API caller.

Sandbox0 supports encrypted PostgreSQL storage and Vault-compatible storage backends. API users keep sending the same write-only `spec` in both modes. For self-hosted backend configuration, see [Self-hosted Configuration](/docs/sandbox/self-hosted/configuration#credential-source-storage).

## Create A Source

<Endpoint method="POST">
/api/v1/credential-sources
</Endpoint>

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `source, err := client.CreateCredentialSource(ctx, apispec.CredentialSourceWriteRequest{
    Name: "github-source",
    ResolverKind: apispec.CredentialSourceResolverKindStaticHeaders,
    Spec: apispec.CredentialSourceWriteSpec{
        StaticHeaders: apispec.NewOptStaticHeadersSourceSpec(apispec.StaticHeadersSourceSpec{
            Values: apispec.NewOptStaticHeadersSourceSpecValues(
                apispec.StaticHeadersSourceSpecValues{
                    "token": os.Getenv("GITHUB_TOKEN"),
                },
            ),
        }),
    },
})
if err != nil {
    log.Fatal(err)
}
fmt.Println(source.Name)`
    },
    {
      label: "Python",
      language: "python",
      code: `import os
from sandbox0.apispec.models.credential_source_write_request import CredentialSourceWriteRequest

source = client.create_credential_source(
    CredentialSourceWriteRequest.from_dict({
        "name": "github-source",
        "resolverKind": "static_headers",
        "spec": {
            "staticHeaders": {
                "values": {
                    "token": os.environ["GITHUB_TOKEN"],
                },
            },
        },
    })
)
print(source.name)`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `const source = await client.credentialSources.create({
    name: 'github-source',
    resolverKind: 'static_headers',
    spec: {
        staticHeaders: {
            values: {
                token: process.env.GITHUB_TOKEN!,
            },
        },
    },
});
console.log(source.name);`
    },
    {
      label: "CLI",
      language: "bash",
      code: `cat <<'EOF' > github-source.yaml
name: github-source
resolverKind: static_headers
spec:
    staticHeaders:
        values:
            token: ghp_example
EOF

s0 credential create -f github-source.yaml`
    }
  ]}
/>

## List Sources

<Endpoint method="GET">
/api/v1/credential-sources
</Endpoint>

Read APIs return metadata only. Use list to discover source names, resolver kinds, status, and current versions.

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `sources, err := client.ListCredentialSources(ctx)
if err != nil {
    log.Fatal(err)
}
for _, source := range sources {
    fmt.Printf("- %s (%s) version=%d\\n", source.Name, source.ResolverKind, source.CurrentVersion.Or(0))
}`
    },
    {
      label: "Python",
      language: "python",
      code: `sources = client.list_credential_sources()
for source in sources:
    print(f"- {source.name} ({source.resolver_kind}) version={source.current_version}")`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `const sources = await client.credentialSources.list();
for (const source of sources) {
    console.log('- ' + source.name + ' (' + source.resolverKind + ') version=' + source.currentVersion);
}`
    },
    {
      label: "CLI",
      language: "bash",
      code: `s0 credential list`
    }
  ]}
/>

## Get Source Metadata

<Endpoint method="GET">
/api/v1/credential-sources/{'{name}'}
</Endpoint>

Use get when you know the source name and want the current metadata record without listing everything.

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `source, err := client.GetCredentialSource(ctx, "github-source")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("%s version=%d status=%s\\n", source.Name, source.CurrentVersion.Or(0), source.Status.Or(""))`
    },
    {
      label: "Python",
      language: "python",
      code: `source = client.get_credential_source("github-source")
print(source.name, source.current_version, source.status)`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `const source = await client.credentialSources.get('github-source');
console.log(source.name, source.currentVersion, source.status);`
    },
    {
      label: "CLI",
      language: "bash",
      code: `s0 credential get github-source`
    }
  ]}
/>

## Update Or Rotate A Source

Use `PUT /api/v1/credential-sources/{name}` to replace the source contents while keeping the same source name and resolver kind. Existing bindings continue to point at that source name.

Sources are reusable. Rotate the source once, then keep bindings and credential rules stable by continuing to reference the same `sourceRef`. Already-claimed sandboxes pick up the new source version through realtime ctld network-cache invalidation. Nomad claims and paused updates lock source rows while publishing current bindings; active updates pin source identity while pending and resolve the current version again only after the exact node acknowledgement. Therefore a concurrent rotation either precedes publication or advances the newly published binding, rather than leaving it on an obsolete version. If invalidation delivery is interrupted by a restart or database notification outage, the binding's resolved-material cache TTL remains the fallback consistency bound. The default TTL is 5 minutes, or the binding's `cachePolicy.ttl` when set.

<Endpoint method="PUT">
/api/v1/credential-sources/{'{name}'}
</Endpoint>

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `source, err := client.UpdateCredentialSource(ctx, "github-source", apispec.CredentialSourceWriteRequest{
    ResolverKind: apispec.CredentialSourceResolverKindStaticHeaders,
    Spec: apispec.CredentialSourceWriteSpec{
        StaticHeaders: apispec.NewOptStaticHeadersSourceSpec(apispec.StaticHeadersSourceSpec{
            Values: apispec.NewOptStaticHeadersSourceSpecValues(
                apispec.StaticHeadersSourceSpecValues{
                    "token": os.Getenv("ROTATED_GITHUB_TOKEN"),
                },
            ),
        }),
    },
})
if err != nil {
    log.Fatal(err)
}
fmt.Println(source.Name, source.CurrentVersion.Or(0))`
    },
    {
      label: "Python",
      language: "python",
      code: `import os
from sandbox0.apispec.models.credential_source_write_request import CredentialSourceWriteRequest

source = client.update_credential_source(
    "github-source",
    CredentialSourceWriteRequest.from_dict({
        "name": "github-source",
        "resolverKind": "static_headers",
        "spec": {
            "staticHeaders": {
                "values": {
                    "token": os.environ["ROTATED_GITHUB_TOKEN"],
                },
            },
        },
    })
)
print(source.name, source.current_version)`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `const source = await client.credentialSources.update('github-source', {
    resolverKind: 'static_headers',
    spec: {
        staticHeaders: {
            values: {
                token: process.env.ROTATED_GITHUB_TOKEN!,
            },
        },
    },
});
console.log(source.name, source.currentVersion);`
    },
    {
      label: "CLI",
      language: "bash",
      code: `cat <<'EOF' > github-source-rotated.yaml
name: github-source
resolverKind: static_headers
spec:
    staticHeaders:
        values:
            token: ghp_rotated_example
EOF

s0 credential update github-source -f github-source-rotated.yaml`
    }
  ]}
/>

## Delete A Source

<Endpoint method="DELETE">
/api/v1/credential-sources/{'{name}'}
</Endpoint>

Delete only succeeds after no current sandbox binding, pending Nomad network mutation, or template credential binding still references the source. Canceling a pending Nomad update releases its source pins; deleting a sandbox releases current bindings only after physical runtime cleanup reaches the durable claim-cleanup boundary.

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `_, err := client.DeleteCredentialSource(ctx, "github-source")
if err != nil {
    log.Fatal(err)
}
fmt.Println("deleted github-source")`
    },
    {
      label: "Python",
      language: "python",
      code: `client.delete_credential_source("github-source")
print("deleted github-source")`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `await client.credentialSources.delete('github-source');
console.log('deleted github-source');`
    },
    {
      label: "CLI",
      language: "bash",
      code: `s0 credential delete github-source`
    }
  ]}
/>

## Next Steps

<CardGroup>
  <Card title="Egress Auth" href="/docs/sandbox/credential/egress-auth" cta="Continue">
    Inject destination-scoped outbound credentials through network policy.
  </Card>

  <Card title="Overview" href="/docs/sandbox/template" cta="Continue">
    Define reusable sandbox environments before creating or scaling sandboxes.
  </Card>
</CardGroup>
