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.

Step 1: Define the TCA table 

EXT:my_extension/Configuration/TCA/tx_myext_apiendpoint.php
<?php

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',
            ],
        ],
    ],
];
Copied!

Creating an API endpoint record (backend):

  1. Go to List module and select your storage folder
  2. Click + Create new record and select API Endpoint
  3. 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)
  4. Click Save

The token field uses renderType: 'vaultSecret' which:

  • Shows a masked password field with reveal/copy buttons
  • 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)
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 |
Copied!

Step 2: Create a DTO for type safety 

EXT:my_extension/Classes/Domain/Dto/ApiEndpoint.php
<?php

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'],
        );
    }
}
Copied!

Step 3: Create a service for authenticated requests 

EXT:my_extension/Classes/Service/ApiClientService.php
<?php

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,
        );
    }
}
Copied!

Step 4: Use the service 

Example controller or command
<?php

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');
Copied!

What happens under the hood 

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

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 functionality - no command line needed.