Skip to content

All posts

Technical Foundations

API keys for retrieval integrations: one key for each application

A practical guide to the API keys a retrieval integration needs in foundation4. It covers creating a key, granting permissions, classification allow-lists, expiry and rotation, and the errors you will see.

A row of closed teal metal lockers, each with its own lock

Every request to foundation4 carries an API key. The key identifies the caller, and the permissions on the key decide what the caller can do. This guide walks through setting up keys for a typical retrieval integration: one service that keeps a pipeline current, and one application that searches it. It uses the requests and responses from the Manage API keys and permissions guide, so you can follow along against your own deployment.

The pattern throughout is simple. Each application gets its own key, with the permissions its job requires. The docs recommend a dedicated key for each integration, because a leaked key then affects one integration, and you can replace one key without touching the others.

What a key is

An API key has two parts: an identifier in UUID format and a secret. Every request sends both, in the x-api-key and x-api-key-secret headers.

foundation4 generates the secret when it creates the key. It returns the secret once, in that response, and never again. It stores an Argon2id hash of the secret, computed with a random salt. No endpoint returns the secret or the hash. This matters for operations: a lost secret cannot be recovered or regenerated. You replace the key instead, as described in the rotation section below.

Because the secret travels in a header on every request, the docs state that every client path to foundation4 uses TLS. TLS terminates at the ingress controller or the Gateway in front of the API server.

The master key stays with administrators

Each deployment has one master key. It is created at installation and holds read, write and execute permission on every object type, with access to every classification. Its job is administration: you use it to create the scoped keys that applications use.

The master key never goes into application configuration. There is also an operational reason to keep it out of circulation. The API server and the workers authenticate with the master key at startup, so the master key must stay active and unexpired for the deployment to start. If an application leaked it, you could not simply switch it off.

You can also create a separate administration key with the permissions that key management needs: write permission on the api-keys object type, plus every permission and classification that it will grant to other keys.

Permissions are three bits

A permission is a sum of three bits:

Permission Value Grants
Read 4 Listing and retrieving objects, and reading documents
Write 2 Creating, changing and deleting objects
Execute 1 Using objects, such as adding documents and searching

Read and execute together is 5. All three is 7.

A permission applies at one of two scopes. On an object type, such as pipelines or text-splitters, it covers every object of that type, including objects created later. On an individual object, it covers that one object, identified by its ID. A key's permission on an object combines both scopes. Permissions on a pipeline also govern the documents and fragments in that pipeline.

For application keys, the docs grant permissions on individual objects. A search key for one pipeline then has no permission on any other pipeline.

What each kind of key needs

The Access control page lists the permissions each operation requires. These are the ones a retrieval integration uses:

Operation Required permissions
Add a document or a new version Read and execute on the pipeline, execute on the text splitter that processes the document, and the document's classification in the allow-list
Expire a document Write and execute on the pipeline, and the document's classification in the allow-list
Delete a document Read, write and execute on the pipeline, and the document's classification in the allow-list
Search a pipeline Read and execute on the pipeline

From that table, two typical keys follow.

An ingestion key for a service that keeps a pipeline current holds 7 on the pipeline, so that it can add, expire and delete documents. It also holds 1 on the pipeline's default text splitter, because adding a document requires execute on the splitter that processes it. If your documents name a different splitter in text_splitter_id, the key needs execute on that splitter as well.

A search key for an application that only searches holds 5 on the pipeline. It needs nothing on the text splitter, and it cannot change or delete the pipeline, because it holds no write permission.

Creating a key

Creating a key takes two requests. The first creates the key itself:

curl -X POST "$FOUNDATION4_URL/api-keys" \
  -H "x-api-key: $FOUNDATION4_API_KEY" \
  -H "x-api-key-secret: $FOUNDATION4_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "support-indexer",
    "description": "Indexing service for the support knowledge base",
    "expiration": "2027-09-30T00:00:00Z"
  }'

The response has status 201. The secret field holds a generated 48-character string, shown here as a placeholder:

{
  "id": "59971347-4b50-4edd-b6f6-12184d2af420",
  "created_at": "2026-09-30T10:00:00.615180Z",
  "updated_at": "2026-09-30T10:00:00.615180Z",
  "name": "support-indexer",
  "description": "Indexing service for the support knowledge base",
  "secret": "change-me",
  "active": true,
  "expiration": "2027-09-30T00:00:00Z"
}

Copy the secret into the application's secret store now. This is the only response that contains it.

The name must be unique across the deployment. The expiration time is optional, but setting one gives you a date by which the key is replaced. A key without an expiration time does not expire.

Granting permissions and the allow-list

The second request grants permissions on individual objects. For the ingestion key, that is 7 on the pipeline and 1 on its default text splitter. You can read the splitter's identifier from the default_text_splitter_id field of GET /pipelines/{id}.

curl -X POST "$FOUNDATION4_URL/api-keys/$INDEXER_KEY_ID/permissions" \
  -H "x-api-key: $FOUNDATION4_API_KEY" \
  -H "x-api-key-secret: $FOUNDATION4_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "permissions": [
      {"object_type": "pipelines", "object_id": "'"$PIPELINE_ID"'", "permission": 7},
      {"object_type": "text-splitters", "object_id": "'"$TEXT_SPLITTER_ID"'", "permission": 1}
    ],
    "classifications": ["public", "internal"]
  }'

The response has status 200 and an empty body. foundation4 applies all entries in one transaction. If any entry is refused, no entry changes.

The same endpoint changes and removes permissions. An entry replaces the key's permission on the same object, and an entry with permission set to 0 removes it. Objects not named in the request stay as they were. A key can grant only what it holds itself, so an administration key cannot hand out more than it has.

What the allow-list governs

The classifications list is the key's classification allow-list for the pipeline. It is easy to misread, so it is worth being exact about its scope.

The allow-list governs writes to documents. Adding, expiring and deleting a document requires the document's classification in the allow-list. Expiring or deleting every document in a pipeline requires every classification of the pipeline. The allow-list also governs classification management: reading or changing a pipeline's classifications requires *, which permits every classification of the pipeline, including ones added later.

The allow-list does not govern search. A search request names the reader's classifications itself, in its classification field, and results are scoped to those classifications and any they inherit. Your application chooses those classifications for each end user, because your application holds the user's identity and entitlements. A search key therefore does not limit which classifications a search can name. That responsibility sits with the application, as described in Classifications.

Two rules follow from the request format. The list applies to every pipeline entry in the request. A request that names a pipeline but omits the list removes that pipeline's allow-list, so the key can then use no classification of it. Always send the complete intended list, and grant pipelines that need different lists in separate requests.

Checking a key

GET /api-keys/{id}/permissions lists every entry on a key. The allow-list appears on pipeline entries, and classifications is null on other object types:

[
  {
    "object_type": "pipelines",
    "object_id": "c4a7e2d1-6b3f-4f8e-a2d9-7e1b5c3f9a06",
    "permission": 7,
    "classifications": ["public", "internal"]
  },
  {
    "object_type": "text-splitters",
    "object_id": "019252e9-b4a0-7713-9a69-d701b4f4a2d1",
    "permission": 1,
    "classifications": null
  }
]

The entries come in no fixed order. From the application side, POST /login with the new key's headers confirms that the key authenticates and returns status 201. Its permissions object lists permissions on object types only, so it is empty for a key that holds permissions on individual objects. To check one object, GET /permissions/{object_type}/{object_id} returns the calling key's combined permission as a bare number, such as 7.

Expiry, deactivation and rotation

A key authenticates only before its expiration time. After that, every request returns HTTP 401. PATCH /api-keys/{id} changes the expiration, and "expiration": null removes it. GET /api-keys?order_by=expiration lists keys with expired ones first and keys without an expiration last, so you can see which keys to replace next.

The secret cannot be changed, so rotation means replacing the key. The guide's procedure keeps the old key working until the application runs on the new one:

  1. Create a replacement key with a new name and a new expiration time.
  2. Read the old key's entries with GET /api-keys/{id}/permissions, and grant the same entries and allow-lists to the replacement.
  3. Switch the application's configuration to the replacement, and confirm it with POST /login.
  4. Deactivate the old key with PATCH /api-keys/{id} and {"active": false}. Confirm that POST /login with the old key now returns 401.
  5. Once the application has run on the replacement without errors, delete the old key with DELETE /api-keys/{id}. Deleting a key also deletes its permission entries.

Deactivation and deletion take effect on the next request. Until you delete it, a deactivated key can be restored with {"active": true}, which gives you a way back if something was missed. A key cannot delete itself.

Errors you will see

These are the errors a team most often meets while setting up keys:

Status Message Usual cause
401 Invalid API Key or Secret The key does not exist, the secret does not match, or the key is inactive or expired
403 Unauthorized: access level <level> required for Api Key with id <id> The administration key grants something it does not hold, or lacks write permission on api-keys
404 Pipeline not found A search names a pipeline the key cannot read and execute
404 Classification not found A document's classification is not in the key's allow-list
409 Conflict unique error Another key already has the name
422 Plain text starting Failed to deserialize the JSON body into the target type: An unknown object_type, a permission above 7, or a missing field

Summary

Give each application its own key. Keep the master key for administration. Grant an ingestion key 7 on its pipeline and 1 on its text splitter, with the classifications it writes in the allow-list. Grant a search key 5 on its pipeline, and have the application name each reader's classifications in every search. Set an expiration on every key, and rotate by replacement.

The full procedure is in Manage API keys and permissions. To talk through how this fits your environment, request a briefing.

See foundation4 inside your perimeter.

Tell us about your environment and we will walk you through how foundation4 fits.

Request a briefing