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

# Secrets

> Credential management with tier-based access control (account, source, global).

There is **no `secrets` block in the definition**. Secrets are not declared — they are *named*, and read back by name through `ctx.secrets`. The names come from exactly three places:

| Where the name comes from | The name is |
| - | - |
| Your **activation form** field name | the exact field name you posted, verbatim |
| `webhooks[].signature.secretName` | whatever string you put there |
| `oauth.credentials.clientIdSecretName` / `clientSecretSecretName` | whatever string you put there |

## Activation form secrets

Every string field in your activation form becomes readable as a secret:

```tsx theme={null}
// Form submission payload
const data = {
  apiKey: 'user-entered-value', // String → becomes a secret
  merchantId: 'user-entered-value', // String → becomes a secret
  countryCode: 'US', // String → becomes a secret
  retryCount: 3, // Number → NOT a secret
  advanced: { nested: true }, // Object → NOT a secret
};

// Server-side, all three strings are readable
const apiKey = await ctx.secrets.get('apiKey');
const merchantId = await ctx.secrets.get('merchantId');
const countryCode = await ctx.secrets.get('countryCode');
```

**The field name is the secret name.** No prefixing, no snake\_casing, no transformation.

## Keep names in a constant

Renaming a form field renames the secret and breaks every handler reading it. Put the name in one module and import it on both sides:

```ts theme={null}
// src/config.ts — the single source of truth
export const ACME_API_KEY = 'acme_api_key';
export const ACME_MERCHANT_ID = 'acme_merchant_id';
```

In the form:

```tsx theme={null}
import { ACME_API_KEY, ACME_MERCHANT_ID } from './config';

const ActivationSchema = z.object({
  [ACME_API_KEY]: z.string().min(1),
  [ACME_MERCHANT_ID]: z.string().min(1),
});

// ...
<FormField name={ACME_API_KEY} … />
```

In handlers:

```ts theme={null}
import { ACME_API_KEY, ACME_MERCHANT_ID } from './config';

const apiKey = await ctx.secrets.get(ACME_API_KEY);
const merchantId = await ctx.secrets.get(ACME_MERCHANT_ID);
```

Now the two cannot drift.

## Credential tiers

The host resolves each name across its credential tiers — account, then source, then global — plus the values from your activation, and hands your handler the resolved set.

| Tier | Issued by | Writable from integration code |
| - | - | - |
| Account | Your activation form, or your own `secrets.set()` | Yes, via `secrets.set()` |
| Source | Admin | No |
| Global | Admin | No |

Where two tiers hold the same name, a value the **user** entered at activation wins over an admin-issued one; a value your own code wrote with `secrets.set()` does not, so a partner cannot shadow a credential an admin issues later. `ctx.secrets.set()` writes only to the account tier for this `{account, product}`.

## Reading secrets

```ts theme={null}
// Get a secret (string | null)
const apiKey = await ctx.secrets.get('acme_api_key');

// List all secret names (values withheld)
const names = await ctx.secrets.names();
```

Both are async. `get` returns `null` when no tier holds the name. `names()` returns names only, never values.

## Writing secrets

```ts theme={null}
// Write to account tier only — succeeds
await ctx.secrets.set('acme_access_token', token);

// A name that resolves only from the source or global tier — throws SecretAccessError
await ctx.secrets.set('platform_key', value);
```

`set()` throws `SecretAccessError` if the name resolves only from source or global tiers. This is intentional: **only account-tier values are writable**, so a partner cannot shadow admin-issued credentials.

## Common patterns

### Storing OAuth tokens

Persist tokens from `onTokens` with `secrets.set()` — they land in the account tier:

```ts theme={null}
// In oauth.onTokens(ctx, tokens)
await ctx.secrets.set('acme_access_token', tokens.accessToken);

if (tokens.refreshToken) {
  await ctx.secrets.set('acme_refresh_token', tokens.refreshToken);
}
```

### Webhook signature validation

Name the secret in your webhook declaration and the transport handles verification:

```ts theme={null}
const integration = defineIntegration({
  webhooks: [
    {
      id: 'payment-updated',
      signature: {
        header: 'x-acme-signature',
        secretName: 'acme_webhook_secret', // resolved like any other secret name
      },
      async handler(ctx, event) {
        // Signature already verified by the transport
        // Just use the payload
      },
    },
  ],
});
```

### Missing API key

Handle the case where the key is not yet configured:

```ts theme={null}
const apiKey = await ctx.secrets.get(ACME_API_KEY);

if (!apiKey) {
  ctx.logger.debug({}, 'API key not configured; skipping request.');
  return { status: 'skipped', detail: 'Not configured.' };
}

const response = await ctx.fetch('https://api.acme.example/data', {
  headers: { authorization: `Bearer ${apiKey}` },
});
```

## Troubleshooting

### SecretAccessError on `set()`

The message begins `Secret "<name>" resolves only from an admin-issued tier (source or global) and cannot be written from integration code.` The error carries `code: 'SECRET_ACCESS_DENIED'` and `secretName`.

An admin-issued credential can only be updated where it was issued. If your code needs to store its own value, write it under a name of its own.

### Missing secret at tool runtime

A tool reads a secret that was never configured at activation. This is expected — use `get()` and handle `null`:

```ts theme={null}
const apiKey = await ctx.secrets.get('acme_api_key');
if (!apiKey) {
  return { error: 'Integration not configured.' };
}
```

## Next steps

* [Activation form](/sdk/surfaces/activation-form) — declare form fields that become secrets
* [Webhooks](/sdk/surfaces/webhooks) — signature verification via secret names
* [OAuth](/sdk/surfaces/oauth) — token storage via `onTokens`


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.