---
question: Where do I put an integration API key?
kind: task
via_web: available
via_cli: available
via_python: available
via_api: available
via_config: available
ref_web: System › Data › Secrets
ref_cli: abi config validate --modules
ref_python: Secret.get
ref_api: PUT /api/admin/system/resources/secret/entry
ref_config: "{{ secret.X }}"
prereq: You need an in-engine module with a field for the key on its `Configuration` (see [Reference it from the module](#reference-it-from-the-module) below).
next:
  - /capabilities/build/integrations/use-in-an-agent
  - /capabilities/build/integrations/test-an-integration
  - /capabilities/build/secrets/add-a-secret
---

# Add credentials

An integration gets its API key from the module that owns it. Adding a credential takes two things:

1. **Store the value** in the engine secret service. Its default adapter is `dotenv`, which reads and writes `.env` at the project root. Each tab below stores it a different way.
2. **Reference it from the module** in `config.yaml`, as `{{ secret.MY_API_KEY }}` on a field of the module's `Configuration`. That part is the same for every tab: see [Reference it from the module](#reference-it-from-the-module).

The engine renders `{{ secret.X }}` when it loads `config.yaml`, so a new or changed value takes effect at the next engine start. While it renders, an environment variable of the same name wins over the secret service.

:::warning Two stores use the word secret
Workspace Settings › Secrets in Nexus (`POST /api/secrets`) stores rows in the workspace database. Those rows never fill `{{ secret.X }}` and modules cannot read them. Use the engine secret service, as on this page. See [Add a secret](/user-guide/build/secrets/add-a-secret).
:::

## Web UI

### Open the engine secrets

Open the **System** app from the sidebar, then the **Data** tab, then **Secrets**: "Credentials the engine reads from its secret adapters (.env, Naas)." The System app is for platform super admins only.

### Add the secret

Click **New secret**. Enter the name (`MY_API_KEY`: letters, digits, `_`, `.` and `-`, not starting with a digit) and the value, then save. It is written to every configured secret adapter, `.env` included.

![System app, Data tab, on Secrets: the New secret dialog](/abi/Screenshot_System_New_Secret.png)

*System › Data › Secrets › New secret. The value is saved as text and masked in the list.*

### Restart the engine

Restart the engine so `config.yaml` is rendered again with the new value.

## CLI

### Let the engine ask for it

Reference the key in `config.yaml` first ([below](#reference-it-from-the-module)), then load the configuration from a terminal:

```bash
abi config validate --modules
```

When `MY_API_KEY` is in neither the environment nor the secret service, it asks: "Secret 'MY_API_KEY' not found. Please enter the value for MY_API_KEY". The answer is saved through the secret service: with the `dotenv` adapter, into `.env`. Without a terminal, the load fails with "Secret 'MY_API_KEY' not found and no TTY available to prompt."

### Check the module configuration

The same command then builds each enabled module's `Configuration` and prints `✓ my_module`, or the validation error. The command is [`abi config validate`](/user-guide/reference/cli#abi-config-validate).

There is no `abi` command that writes a single secret. `abi secrets naas push-env-as-base64` pushes a whole env file to a Naas secret, for the `naas` adapter.

## Python

### Store the value

From a module that lists `Secret` in its dependencies:

```python title="__init__.py"
from naas_abi_core.module.Module import BaseModule, ModuleConfiguration, ModuleDependencies
from naas_abi_core.services.secret.Secret import Secret


class ABIModule(BaseModule):
    dependencies: ModuleDependencies = ModuleDependencies(modules=[], services=[Secret])
```

```python
secret = ABIModule.get_instance().engine.services.secret
secret.set("MY_API_KEY", "sk-...")
```

`set` writes every configured adapter. Without `Secret` in `dependencies`, the call raises "Module my_module does not have access to …".

### Read it at run time

```python
api_key = secret.get("MY_API_KEY")
```

`get` asks each adapter in order and returns the first value it finds, or `None`. Reading it this way skips `config.yaml`: a value written through the secret service (the System app, `set`) is seen without a restart. The `dotenv` adapter reads `.env` once, at start, so a hand edit of the file waits for the next start.

:::warning The environment and .env can disagree
The two read paths do not use the same order. Rendering `config.yaml` takes the environment variable first, then the secret service. The `dotenv` adapter's `get` takes the `.env` value first, and the environment variable only when `.env` has no such key. With `MY_API_KEY` set to different values in both places, `{{ secret.MY_API_KEY }}` and `secret.get("MY_API_KEY")` return different values. Keep one source, or set `environment_first: true` on the adapter so both prefer the environment.
:::

## REST API

### Write the secret

```bash
curl -s -X PUT "http://localhost:9879/api/admin/system/resources/secret/entry?id=MY_API_KEY" \
  -H "Authorization: Bearer $NEXUS_ACCESS_TOKEN" \
  --data-binary 'sk-...'
```

This is the call behind the System app's Secrets view. The body is the raw value. The caller must be a platform super admin, signed in to Nexus (see [Enable in a workspace](/user-guide/build/knowledge-graph/enable-ontology?via=api) for the sign-in call). Replacing an existing secret also needs `&confirm=MY_API_KEY`.

### Restart the engine

The value reaches `{{ secret.X }}` at the next engine start.

## Config

### Put the value in .env

```bash title=".env"
MY_API_KEY=sk-...
```

The `dotenv` adapter reads `.env` at the project root, once, when the engine starts: restart after an edit. The file must exist. A project without a `services.secret` block gets that adapter by default.

### Check the adapter block

```yaml title="config.yaml"
services:
  secret:
    secret_adapters:
      - adapter: "dotenv"
        config: {}
```

`config.path` names another file. Other adapters are `naas` and `base64`. The full block is in the [Configuration Reference](/user-guide/get-started/configuration#secret).

## Reference it from the module

Declare the field on the module's `Configuration`:

```python title="src/my_module/__init__.py"
class ABIModule(BaseModule):
    class Configuration(ModuleConfiguration):
        my_api_key: str
```

Set it in `config.yaml`:

```yaml title="config.yaml"
modules:
  - module: my_module
    enabled: true
    config:
      my_api_key: "{{ secret.MY_API_KEY }}"
```

A key under `config:` that the `Configuration` does not declare is rejected when the engine loads the module. Code reads the value as `ABIModule.get_instance().configuration.my_api_key`: see [Use in an agent](/user-guide/build/integrations/use-in-an-agent).
