---
title: "Example: API endpoint management"
manual: "nr-vault"
version: "1.0"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-vault:usage-api-endpoint-example@1.0"
source: "Usage/ApiEndpointExample.rst"
rendered: "2026-09-18T07:37:50+00:00"
---

# Example: API endpoint management {#example-api-endpoint-management}

A common pattern is storing API endpoints with their credentials in a database
table. This example shows how to combine TCA vault fields with the HTTP client.

**Table of contents**

-   [Step 1: Define the TCA table](https://docs.typo3.org/permalink/netresearch/nr-vault:step-1-define-the-tca-table@1.0)
-   [Step 2: Create a DTO for type safety](https://docs.typo3.org/permalink/netresearch/nr-vault:step-2-create-a-dto-for-type-safety@1.0)
-   [Step 3: Create a service for authenticated requests](https://docs.typo3.org/permalink/netresearch/nr-vault:step-3-create-a-service-for-authenticated-requests@1.0)
-   [Step 4: Use the service](https://docs.typo3.org/permalink/netresearch/nr-vault:step-4-use-the-service@1.0)
-   [What happens under the hood](https://docs.typo3.org/permalink/netresearch/nr-vault:what-happens-under-the-hood@1.0)
-   [Security benefits](https://docs.typo3.org/permalink/netresearch/nr-vault:security-benefits@1.0)

## Step 1: Define the TCA table {#step-1-define-the-tca-table}

**EXT:my_extension/Configuration/TCA/tx_myext_apiendpoint.php**

```php
<?php

/*
 * Copyright (c) 2025-2026 Netresearch DTT GmbH
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

return [
    'ctrl' => [
        'title' => 'API Endpoints',
        'label' => 'name',
    ],
    'columns' => [
        'name' => [
            'label' => 'Name',
            'config' => ['type' => 'input', 'required' => true],
        ],
        'url' => [
            'label' => 'API Base URL',
            'config' => ['type' => 'input', 'required' => true],
        ],
        'token' => [
            'label' => 'API Token',
            'config' => [
                'type' => 'input',
                'renderType' => 'vaultSecret',
            ],
        ],
    ],
];

```

**Creating an API endpoint record (backend):**

1.  Go to **List** module and select your storage folder
1.  Click **\+ Create new record** and select **API Endpoint**
1.  Fill in the form:

    -   **Name:** `Stripe`
    -   **API Base URL:** `https://api.stripe.com/v1`
    -   **API Token:** Paste your actual API key (stored securely in vault)
1.  Click **Save**

The token field uses `renderType: 'vaultSecret'` which:

-   Shows a masked password field with a reveal button — and a copy button
    outside the hardened profile, which disables copying because the clipboard
    outlives the dialog
-   Automatically stores the secret in the vault on save
-   Stores only a UUID v7 reference in the database

**What gets stored in the database:**

**Database content (token is UUID, not the secret)**

```sql
SELECT uid, name, url, token FROM tx_myext_apiendpoint;
-- | uid | name   | url                       | token                                |
-- |-----|--------|---------------------------|--------------------------------------|
-- | 1   | Stripe | https://api.stripe.com/v1 | 01937b6e-4b6c-7abc-8def-0123456789ab |
```

## Step 2: Create a DTO for type safety {#step-2-create-a-dto-for-type-safety}

**EXT:my_extension/Classes/Domain/Dto/ApiEndpoint.php**

```php
<?php

/*
 * Copyright (c) 2025-2026 Netresearch DTT GmbH
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Dto;

final readonly class ApiEndpoint
{
    public function __construct(
        public int $uid,
        public string $name,
        public string $url,
        public string $token,  // Contains vault UUID, not the secret
    ) {}

    /**
     * @param array<string, mixed> $row
     */
    public static function fromDatabaseRow(array $row): self
    {
        return new self(
            uid: (int) $row['uid'],
            name: (string) $row['name'],
            url: (string) $row['url'],
            token: (string) $row['token'],
        );
    }
}

```

## Step 3: Create a service for authenticated requests {#step-3-create-a-service-for-authenticated-requests}

**EXT:my_extension/Classes/Service/ApiClientService.php**

```php
<?php

/*
 * Copyright (c) 2025-2026 Netresearch DTT GmbH
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use MyVendor\MyExtension\Domain\Dto\ApiEndpoint;
use Netresearch\NrVault\Http\SecretPlacement;
use Netresearch\NrVault\Service\VaultServiceInterface;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Http\RequestFactory;

final class ApiClientService
{
    public function __construct(
        private readonly VaultServiceInterface $vault,
        private readonly RequestFactory $requestFactory,
    ) {}

    /**
     * Call an API endpoint with vault-managed authentication.
     *
     * The token is retrieved from vault and injected at request time.
     * It never appears in this code and is wiped from memory immediately.
     */
    public function call(
        ApiEndpoint $endpoint,
        string $method,
        string $path,
        array $data = [],
    ): ResponseInterface {
        // Create PSR-7 request using TYPO3's RequestFactory
        $request = $this->requestFactory->createRequest(
            $method,
            rtrim($endpoint->url, '/') . '/' . ltrim($path, '/'),
        );

        if ($data !== [] && \in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
            $request = $request
                ->withHeader('Content-Type', 'application/json')
                ->withBody(
                    \GuzzleHttp\Psr7\Utils::streamFor(json_encode($data))
                );
        }

        // Send via VaultHttpClient - token never exposed to application
        return $this->vault->http()
            ->withAuthentication($endpoint->token, SecretPlacement::Bearer)
            ->withReason('API call to ' . $endpoint->name . ': ' . $path)
            ->sendRequest($request);
    }

    /**
     * Convenience method for GET requests.
     */
    public function get(ApiEndpoint $endpoint, string $path): array
    {
        $response = $this->call($endpoint, 'GET', $path);

        return json_decode(
            $response->getBody()->getContents(),
            true,
            512,
            JSON_THROW_ON_ERROR,
        );
    }
}

```

## Step 4: Use the service {#step-4-use-the-service}

**Example controller or command**

```php
<?php

/*
 * Copyright (c) 2025-2026 Netresearch DTT GmbH
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

use MyVendor\MyExtension\Domain\Dto\ApiEndpoint;
use MyVendor\MyExtension\Service\ApiClientService;

// Load endpoint from database
$row = $connection->select(['*'], 'tx_myext_apiendpoint', ['uid' => 1])
    ->fetchAssociative();
$endpoint = ApiEndpoint::fromDatabaseRow($row);

// Make authenticated API call
$customers = $this->apiClientService->get($endpoint, '/customers');

```

## What happens under the hood {#what-happens-under-the-hood}

1.  The `token` field contains a UUID v7 like `01937b6e-4b6c-...`
1.  `VaultHttpClient::sendRequest()` retrieves the actual token from vault
1.  Token is injected into the `Authorization: Bearer ...` header
1.  `sodium_memzero()` immediately wipes the token from memory
1.  The HTTP call is logged to the audit trail (without the secret)

## Security benefits {#security-benefits}

This pattern provides several security advantages:

-   ****No secret exposure****

    Application code never sees the actual API token. The DTO contains only the
    vault UUID, which is useless without vault access.

-   ****Memory safety****

    Secrets are cleared from memory immediately after injection using
    `sodium_memzero()`.

-   ****Audit trail****

    Every HTTP call is logged with the endpoint name, HTTP method, URL, and
    status code - but never the secret itself.

-   ****Separation of concerns****

    Credential management is handled by the vault. Application code focuses on
    business logic.

-   ****No CLI or file access required****

    Editors and admins can manage API endpoints entirely through the TYPO3
    backend. The vault secret field provides a secure password input with
    reveal — and copy, outside the hardened profile — with no command line
    needed.
