> ## Documentation Index
> Fetch the complete documentation index at: https://learn.actionist.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Credentials and API keys

> Your client's secrets are permanently out of your reach. Here is how that is enforced, and how to hand a connection back to the client without friction.

<div className="mxp-hero xc-border">
  <div className="exp-eyebrow mxp-rise">THE ONE FIXED RULE</div>

  <h2 className="mxp-hero-title mxp-rise" style={{animationDelay:'0.06s'}}>
    A boundary you never have to think about. <span style={{background:'linear-gradient(90deg,#24A4FF,#9000FF,#E3008E)',WebkitBackgroundClip:'text',backgroundClip:'text',color:'transparent'}}>It is built in, not agreed to.</span>
  </h2>

  <p className="mxp-hero-dek mxp-rise" style={{animationDelay:'0.13s'}}>
    Every other permission on a client's workspace is a toggle they choose to flip. Credentials are not on that list, and never will be. This page shows how that limit is enforced end to end, and how to keep a client onboarding moving the moment you hit it.
  </p>
</div>

<div className="w-full py-10">
  <div className="actionist-tight flex flex-col gap-2 mb-6">
    <span className="actionist-section-eyebrow">In the Manage Reseller panel</span>
    <h2 className="text-2xl md:text-3xl font-semibold tracking-tight text-gray-900 dark:text-gray-50">What the client sees.</h2>

    <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl">
      A client managing your access sees eight permission toggles and one card that is not a toggle at all.
    </p>
  </div>

  <div className="xrs-hard-limit-row" style={{marginTop:0}}>
    <div>
      <div style={{fontWeight:600, fontSize:13}}>Credentials & API keys</div>
      <div style={{fontSize:12, opacity:0.7}}>Permanently restricted. No toggle, no exception.</div>
    </div>

    <span className="xrs-hard-limit-badge">HARD LIMIT</span>
  </div>

  <Danger>
    Permanently restricted. No toggle, no exception. Your reseller can never see API keys, OAuth tokens, or service-account credentials, regardless of what's enabled above.
  </Danger>

  <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl" style={{marginTop:'18px'}}>
    The panel banner puts the same idea in one line: "Your reseller can only access the scopes enabled below · Credentials are always off-limits." That is not a summary of the toggles below it, it is a separate statement about a separate thing.
  </p>

  <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl" style={{marginTop:'12px'}}>
    That separateness is structural, not just written copy. The client's nine delegated scopes are `prompts_and_instructions`, `tools_and_integrations`, `skills`, `agent_memories`, `files`, `calendar_and_schedules`, `chat_history`, `audit_logs`, and `billing_summary`. Credentials is not among them. It lives in its own hard-restrictions list, where the only recorded value for `restricted` is the literal `true`. There is no representable state in which credentials are unrestricted, because the type system never defined one.
  </p>

  <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl" style={{marginTop:'12px'}}>
    Practically, this means a client can grant you every one of the eight toggles, including Files and Chat History, and the credentials line does not move. There is no combination of grants that adds up to visibility on a secret. Nothing you do inside a delegated session, and no setting a client flips, changes which card carries the HARD LIMIT badge.
  </p>

  <Note>
    The hard-restriction card is served from the API with a hardcoded fallback in the client, so if the network call to fetch it ever fails, the client still sees the same restricted message. Today both paths render identically, there is no version of this card that reads as unrestricted.
  </Note>
</div>

<hr className="xc-divider" />

<div className="w-full py-10 xc-rise">
  <div className="actionist-tight flex flex-col gap-2 mb-6">
    <span className="actionist-section-eyebrow">How it actually holds</span>
    <h2 className="text-2xl md:text-3xl font-semibold tracking-tight text-gray-900 dark:text-gray-50">Four layers, each one sufficient on its own.</h2>

    <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl">
      This is not one check that a bug could slip past. It is four independent walls, stacked from the screen down to the database row.
    </p>
  </div>

  <div className="mxp-pipe">
    <div className="mxp-pipe-node">
      <strong>Interface</strong>
      <span>In a delegated session the Credentials Vault panel does not render. It is absent, not greyed out. Separately, every app-connect control locks the moment any delegated session is active, no matter which scopes are granted.</span>
    </div>

    <div className="mxp-pipe-link" />

    <div className="mxp-pipe-node">
      <strong>Route</strong>
      <span>Every credentials endpoint rejects delegated access before any business logic runs. So do machine identities and pairing codes. The request never reaches code that could act on it.</span>
    </div>

    <div className="mxp-pipe-link d2" />

    <div className="mxp-pipe-node">
      <strong>Service</strong>
      <span>Listing credentials returns metadata with secrets redacted. There is no human reveal endpoint anywhere in the service. The only plaintext path requires a machine-identity bearer token, checked against exactly which credentials that identity can reach, and it writes an audit row every time.</span>
    </div>

    <div className="mxp-pipe-link d3" />

    <div className="mxp-pipe-node">
      <strong>Storage</strong>
      <span>Secrets are AES-256-GCM encrypted under a per-user key that is itself wrapped by Cloud KMS. That key is never stored in plaintext. The table the list endpoint reads from has no ciphertext column at all, so redaction is not a filter someone could forget, it is what the schema allows.</span>
    </div>
  </div>

  <div className="exp-surface-grid" style={{marginTop:'22px'}}>
    <div className="exp-surface-tile"><strong>No screen to click through.</strong> The Vault panel is missing entirely in a delegated session, so there's nothing to attempt.</div>
    <div className="exp-surface-tile"><strong>No door left unlocked.</strong> The rejection happens at the route, before any handler runs, for delegated sessions, machine identities, and pairing codes alike.</div>
    <div className="exp-surface-tile"><strong>No reveal button, ever.</strong> Even a client viewing their own vault never sees a stored secret. Plaintext only ever flows through an audited machine-identity path.</div>
    <div className="exp-surface-tile"><strong>Nothing to leak from storage.</strong> The metadata table has no ciphertext column, so a query against it cannot return a secret whatever the access level of the caller.</div>
  </div>

  <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl" style={{marginTop:'20px'}}>
    Notice what none of these four layers depend on: your intent, your good behaviour, or a permission a client remembered to leave off. Each one holds on its own, which is the point. A bug in the interface layer would still leave the route rejecting the request. A bug in the route would still leave the service returning redacted metadata. A bug in the service would still leave the storage layer with nothing readable to return.
  </p>
</div>

<div className="w-full py-10 xc-rise">
  <div className="actionist-tight flex flex-col gap-2 mb-6">
    <span className="actionist-section-eyebrow">Read this as an advantage</span>
    <h2 className="text-2xl md:text-3xl font-semibold tracking-tight text-gray-900 dark:text-gray-50">Why this is good for you.</h2>
  </div>

  <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl">
    A hard limit removes a whole category of liability from the reseller relationship. You cannot leak what you cannot see, and no breach on your end can ever expose a client's API keys or tokens, because they never passed through your hands in the first place. That also means the client never has to weigh trust against convenience when they decide how much to delegate to you. They can hand you real, working access to configure their agents, apps, and schedules without a second thought about the one thing that actually keeps their other accounts safe.
  </p>

  <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl" style={{marginTop:'12px'}}>
    It also simplifies your own pitch. When a prospective client asks how you'll be managing their workspace, "I can configure almost everything, and I am structurally unable to see your API keys or tokens" is a sentence you can say once and never have to revisit.
  </p>

  <Check>
    You don't need a security policy to back this up in a sales conversation. It's enforced in the product, on every delegated session, for every client, with no configuration on your end required.
  </Check>
</div>

<div className="w-full py-10 xc-rise">
  <div className="actionist-tight flex flex-col gap-2 mb-6">
    <span className="actionist-section-eyebrow">What to actually do</span>
    <h2 className="text-2xl md:text-3xl font-semibold tracking-tight text-gray-900 dark:text-gray-50">The handoff.</h2>

    <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl">
      There is no workaround for a locked control, and you shouldn't want one. Here is the four-step move that keeps setup on track anyway.
    </p>
  </div>

  <Steps>
    <Step title="Recognise the lock">
      A control that needs the client's login or secret renders disabled, with a lock icon in front of it. This is deliberate, not a bug or a missing permission on your side.

      <div className="xrs-lock-ctl">
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />

          <path d="M7 11V7a5 5 0 0 1 10 0v4" />
        </svg>

        Connect
      </div>

      <div className="xrs-lock-tip">
        <strong>Client action required</strong>
        This app requires the client's credentials or authorization to complete installation. Ask the client to install or connect this app directly.
      </div>
    </Step>

    <Step title="Tell the client precisely which app and which action">
      Don't say "finish the setup." Say the app by name and the exact step: "Open Slack in the Apps section and click Connect," not "connect your tools." A vague ask sends a client hunting through a workspace they may not know well yet.
    </Step>

    <Step title="Let them connect it themselves">
      The client signs in to their own Actionist account and installs or connects the app from the Apps section. This is the only path. No reseller scope, present or future, changes that, because the guard checks whether a delegated session is active at all, not which scopes it carries.
    </Step>

    <Step title="Verify and carry on">
      Once the client confirms the app is connected, resume in your own delegated session. The control you handed off unlocks the moment the underlying connection exists, and everything else you were configuring around it stays exactly where you left it.
    </Step>
  </Steps>

  <Tip>
    Batch these handoffs where you can. If a client onboarding needs three different app connections, list all three for the client in one message rather than pinging them each time you hit the next lock. It reads as a plan, not a series of interruptions.
  </Tip>
</div>

<hr className="xc-divider" />

<div className="w-full py-10 xc-rise">
  <div className="actionist-tight flex flex-col gap-2 mb-6">
    <span className="actionist-section-eyebrow">The actual boundary</span>
    <h2 className="text-2xl md:text-3xl font-semibold tracking-tight text-gray-900 dark:text-gray-50">What you can still do.</h2>

    <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl">
      The hard limit is narrower than it sounds. Most of setting up a client's tooling never touches a secret at all.
    </p>
  </div>

  <div className="xrs-two">
    <div className="xrs-two-col can">
      <div className="xrs-two-tag">You can</div>
      <div className="xrs-two-item">Manage the Apps section, including tools, MCP servers, and app connections that don't require credentials, whenever `tools_and_integrations` is granted.</div>
      <div className="xrs-two-item">Browse, search, and read app details, subscriptions, and existing connections, even without that scope.</div>
      <div className="xrs-two-item">Reference an already-existing credential when wiring up a new binding, such as pointing a new agent at a connection the client set up earlier, without ever seeing or handling the plaintext.</div>
      <div className="xrs-two-item">Configure everything downstream of a connection once it exists: agents, skills, schedules, and the rest of the scopes the client has enabled.</div>
    </div>

    <div className="xrs-two-col cant">
      <div className="xrs-two-tag">Always the client</div>
      <div className="xrs-two-item">Installing credential-based apps.</div>
      <div className="xrs-two-item">Connecting OAuth apps.</div>
      <div className="xrs-two-item">Disabling or modifying anything tied to API keys, OAuth tokens, or other secrets, regardless of this setting.</div>
      <div className="xrs-two-item">Viewing, copying, or exporting a stored secret in any form, at any permission level.</div>
    </div>
  </div>

  <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl" style={{marginTop:'18px'}}>
    In the product's own words, this is what `tools_and_integrations` grants and what it deliberately withholds:
  </p>

  <div className="xc-quote">
    <p>"Manage the Apps section, including tools, MCP servers, and app connections that don't require credentials. Installing credential-based apps, connecting OAuth apps, and disabling or modifying anything tied to API keys, OAuth tokens, or other secrets is always off-limits, regardless of this setting."</p>
    <span>tools\_and\_integrations scope description</span>
  </div>

  <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl" style={{marginTop:'14px'}}>
    MCP servers sit under the same `tools_and_integrations` grant as apps. For exactly what you can configure on an MCP server connection versus what waits on the client, treat the same credential boundary described here as the baseline, and check <a href="/Folders/mcp">the MCP reference</a> for the rest of that surface.
  </p>

  <AccordionGroup>
    <Accordion title="Can I just ask the client to paste me their key instead?" icon="circle-question-mark">
      No. Even if a client wanted to hand you a key directly outside the product, nothing in Actionist is built to accept it from you. Every credential a client's workspace uses has to be entered by the client, inside their own session, through the Credentials Vault or an app's own connect flow.
    </Accordion>

    <Accordion title="Does this change once I'm a certified reseller with every scope granted?" icon="circle-question-mark">
      No. Certification and scope grants govern the eight toggles. Credentials sit outside that system entirely, so no combination of certification status and granted scopes ever includes them.
    </Accordion>
  </AccordionGroup>
</div>

<hr className="xc-divider" />

<div className="w-full py-10 xc-rise">
  <div className="actionist-tight flex flex-col gap-2 mb-6">
    <span className="actionist-section-eyebrow">Under the hood</span>
    <h2 className="text-2xl md:text-3xl font-semibold tracking-tight text-gray-900 dark:text-gray-50">How secrets are stored.</h2>

    <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl">
      You will never need this to do your job, but it's worth knowing what actually backs the hard limit above.
    </p>
  </div>

  <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl">
    Every secret is encrypted with AES-256-GCM under a data encryption key generated per client. That key is itself wrapped by Google Cloud KMS rather than kept in the database in the open, so a compromise of the application database alone does not expose usable secrets. The metadata table the Credentials Vault reads from to render its list has no ciphertext column at all, encrypted bytes live only in a separate table, reachable only through a machine-identity reveal path that checks which credentials that specific identity can use and logs the attempt whether it succeeds or not.
  </p>

  <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl" style={{marginTop:'14px'}}>
    The vault stores four kinds of credential, and a stored credential can be bound to any of five things in a client's workspace. You'll recognise most of these by the app in front of them rather than the kind itself: a typical REST integration stores an `api_key`, an app that connects through a "Sign in with" flow stores a `token`, an API with an unusual auth header stores a `header_secret`, and a service-to-service or machine-to-machine integration stores `client_credentials`.
  </p>

  <div style={{marginTop:'18px'}}>
    | Credential kind      | What it is                                                                                                   |
    | -------------------- | ------------------------------------------------------------------------------------------------------------ |
    | `api_key`            | A single secret string sent as a header or query parameter, the most common shape for a REST API.            |
    | `token`              | A bearer token, often issued through an OAuth flow, that authorizes a connection or session.                 |
    | `header_secret`      | A named header value an agent needs to attach to outgoing requests, distinct from a plain API key.           |
    | `client_credentials` | A client ID and client secret pair, used for OAuth2 client-credentials or service-to-service authentication. |
  </div>

  <div style={{marginTop:'14px'}}>
    | Binding target       | What it connects to                                                                         |
    | -------------------- | ------------------------------------------------------------------------------------------- |
    | Agent                | The specific agent that reads this secret when it makes a tool call.                        |
    | Installed skill      | A skill from the marketplace that needs its own key to reach a third-party API.             |
    | Installed MCP server | An MCP server connection that authenticates with this credential.                           |
    | Connection           | A third-party account link, such as an OAuth connection to a platform.                      |
    | Runtime setting      | A workspace-level setting not tied to one specific agent, skill, MCP server, or connection. |
  </div>

  <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl" style={{marginTop:'16px'}}>
    The Credentials Vault itself is a collapsible panel inside Settings, not a tab. Its own rules hold no matter who is looking at it, client or reseller:
  </p>

  <div style={{marginTop:'10px'}}>
    * No reveal button for a stored value, ever.
    * No copy-to-clipboard of a secret value.
    * The eye toggle applies only to the field currently being typed into. It never fetches a value that was already saved.
    * Delete is blocked whenever active bindings exist, so nobody can accidentally remove a credential an agent, skill, MCP server, or connection still relies on.
    * OAuth-derived credentials show **Reconnect** instead of Rotate, because there is no user-holdable secret to paste back in.
  </div>

  <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl" style={{marginTop:'14px'}}>
    Every action on a credential, create, rotate, revoke, disable, enable, reveal, a batch reveal, refresh, validate, registering or revoking a machine identity, and the pairing-code exchange, is written to an audit log with an outcome of success, denied, or error. A reveal attempt is logged whether it succeeds or is denied.
  </p>

  <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl" style={{marginTop:'14px'}}>
    That audit trail is what makes the machine-identity reveal path safe to have at all. It exists so an agent, skill, or MCP server can actually use the secrets bound to it at runtime, and every one of those reads is checked against what that specific identity is allowed to reach, then logged, whether or not you or the client are anywhere in the loop at that moment.
  </p>
</div>

<hr className="xc-divider" />

<div className="w-full py-10 xc-rise">
  <div className="actionist-tight flex flex-col gap-2 mb-6">
    <span className="actionist-section-eyebrow">When a secret changes</span>
    <h2 className="text-2xl md:text-3xl font-semibold tracking-tight text-gray-900 dark:text-gray-50">Rotation and revocation.</h2>

    <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl">
      These are client actions, not yours, but the client will ask you what happens next, so it's worth knowing.
    </p>
  </div>

  <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl">
    Rotating a credential creates a brand new secret version. It does not touch any binding, so every agent, skill, MCP server, or connection already pointed at that credential simply picks up the new version the next time it resolves the secret. Nothing needs to be rewired.
  </p>

  <Note>
    Rotation is write-only. Once a new version is created there is no way to read the old value back, or the new one, for that matter. If a client asks you to "check what the rotated key is," the honest answer is that nobody can, by design.
  </Note>

  <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl" style={{marginTop:'14px'}}>
    Revoking is different. It leaves every binding in place, pointed at a credential that no longer works. The agent, skill, MCP server, or connection that depended on it doesn't fail silently or fall back to a stale value, it returns a clear error the next time it tries to use the dead credential.
  </p>

  <Warning>
    Revoking a credential breaks every bound consumer until the client replaces it or re-binds a working one. If you're mid-setup and a client revokes something you're relying on, expect the affected agent or connection to start erroring immediately, not gradually.
  </Warning>

  <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl" style={{marginTop:'14px'}}>
    An OAuth-derived credential doesn't get revoked in quite the same way. When it expires, it flips to a "needs re-auth" state instead, and the client reconnects the same way they connected it the first time.
  </p>

  <p className="text-base text-gray-600 dark:text-gray-400 max-w-3xl" style={{marginTop:'14px'}}>
    None of these three actions, rotate, revoke, or reconnect, are things you trigger from inside a delegated session. What you can do is recognise the symptom when it shows up: an agent or connection that was working suddenly returning an authorization error is very often a credential the client just rotated, revoked, or that quietly expired. Flag it to the client rather than assuming something you configured is broken.
  </p>
</div>

<hr className="xc-divider" />

<div className="w-full py-10">
  <div className="actionist-cta-panel xc-border">
    <div className="relative z-10 flex flex-col items-center gap-5 max-w-2xl mx-auto">
      <span className="actionist-eyebrow">The one fixed rule</span>

      <h2 className="text-2xl md:text-3xl font-semibold tracking-tight text-white leading-tight">
        Configure everything you can. Hand back only what you must.
      </h2>

      <p className="text-base md:text-lg text-white/85">
        The credential boundary is fixed so the rest of your access doesn't have to be small.
      </p>

      <div className="flex flex-wrap items-center justify-center gap-3 pt-2">
        <a className="actionist-cta-btn-primary" href="https://app.actionist.ai" target="_blank" rel="noreferrer">
          Open the Dashboard

          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M5 12h14" />

            <path d="m12 5 7 7-7 7" />
          </svg>
        </a>

        <a className="actionist-cta-btn-secondary" href="https://actionist.ai/partners/" target="_blank" rel="noreferrer">
          Signup as a Partner
        </a>
      </div>
    </div>
  </div>
</div>

<div className="w-full pt-2 pb-6">
  <p className="actionist-trust-line">
    Credentials are enforced at the interface, the route, the service, and the storage layer, all four independently.<br />
    No toggle grants it, no exception lifts it, and no support request changes that.
  </p>
</div>

<div className="w-full pb-16 xc-rise">
  <div className="actionist-tight flex flex-col gap-2 mb-6">
    <span className="actionist-section-eyebrow">Keep going</span>
    <h2 className="text-2xl md:text-3xl font-semibold tracking-tight text-gray-900 dark:text-gray-50">Next steps.</h2>
  </div>

  <Columns cols={4}>
    <Card title="Apps, tools and integrations" icon="plug" href="/resellers/apps">
      What you can set up yourself, and the connection steps only the client can finish.
    </Card>

    <Card title="Client permissions" icon="shield-check" href="/resellers/permissions">
      The eight toggles the client controls, and how this hard limit sits outside all of them.
    </Card>

    <Card title="Credentials Vault" icon="key" href="/Folders/credentials">
      The full feature reference for how Actionist stores and manages secrets.
    </Card>

    <Card title="Troubleshooting" icon="life-buoy" href="/resellers/troubleshooting">
      What a locked control or a denied request actually means, and whose problem it is.
    </Card>
  </Columns>
</div>
