diff --git a/src/Authorization/AuthorizationService.php b/src/Authorization/AuthorizationService.php
deleted file mode 100644
index 5486600877c8456811197da5b46c0421185a7a45..0000000000000000000000000000000000000000
--- a/src/Authorization/AuthorizationService.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Dbp\Relay\CoreBundle\Authorization;
-
-/**
- * @deprecated use AbstractAuthorizationService instead
- */
-abstract class AuthorizationService extends AbstractAuthorizationService
-{
-}
diff --git a/src/Http/ApiConnection.php b/src/Http/ApiConnection.php
index 1b22c4f4f08c7262d3ebe7d8fdb05c9c55f38d5b..2bf5fbd1a28aa81733daa124406fb33d235aa7ae 100644
--- a/src/Http/ApiConnection.php
+++ b/src/Http/ApiConnection.php
@@ -80,7 +80,7 @@ class ApiConnection
     /**
      * @throws ConnectionException
      */
-    public function get(string $uri, array $options): ResponseInterface
+    public function get(string $uri, array $query): ResponseInterface
     {
         $requestOptions = [
             Connection::REQUEST_OPTION_HEADERS => [
@@ -88,7 +88,21 @@ class ApiConnection
             ],
         ];
 
-        return $this->getApiConnection()->get($uri, $options, $requestOptions);
+        return $this->getConnection()->get($uri, $query, $requestOptions);
+    }
+
+    /**
+     * @throws ConnectionException
+     */
+    public function postJSON(string $uri, array $parameters): ResponseInterface
+    {
+        $requestOptions = [
+            Connection::REQUEST_OPTION_HEADERS => [
+                'Authorization' => 'Bearer '.$this->getAccessToken(),
+            ],
+        ];
+
+        return $this->getConnection()->postJSON($uri, $parameters, $requestOptions);
     }
 
     /**
@@ -123,7 +137,7 @@ class ApiConnection
         return $this->accessToken;
     }
 
-    private function getApiConnection(): Connection
+    private function getConnection(): Connection
     {
         if ($this->connection === null) {
             $connection = new Connection($this->config[self::API_URL_CONFIG_PARAMETER]);
diff --git a/src/Http/ConnectionException.php b/src/Http/ConnectionException.php
index 3c3ee6fc1a115cb47090d54eca3ce43ee0452f4a..5ba57a6f1de2b44c49620605cfac706c5a5dcb18 100644
--- a/src/Http/ConnectionException.php
+++ b/src/Http/ConnectionException.php
@@ -11,6 +11,7 @@ class ConnectionException extends \RuntimeException
 {
     public const REQUEST_EXCEPTION = 1;
     public const JSON_EXCEPTION = 2;
+    public const INVALID_DATA_EXCEPTION = 3;
 
     /** @var RequestInterface|null */
     private $request;
diff --git a/src/ProxyApi/AbstractProxyDataEventSubscriber.php b/src/ProxyApi/AbstractProxyDataEventSubscriber.php
new file mode 100644
index 0000000000000000000000000000000000000000..02128fd99b49f9d6354267b06ad3fe77a1e952ca
--- /dev/null
+++ b/src/ProxyApi/AbstractProxyDataEventSubscriber.php
@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Dbp\Relay\CoreBundle\ProxyApi;
+
+use Exception;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\Exception\BadRequestException;
+
+abstract class AbstractProxyDataEventSubscriber implements EventSubscriberInterface
+{
+    protected const NAMESPACE = '';
+
+    public static function getSubscribedEvents(): array
+    {
+        return [
+            ProxyApi::PROXY_DATA_EVENT_NAME.static::NAMESPACE => 'onProxyDataEvent',
+        ];
+    }
+
+    /**
+     * @throws BadRequestException
+     */
+    public function onProxyDataEvent(ProxyDataEvent $event): void
+    {
+        $event->setHandled();
+        $proxyData = $event->getProxyData();
+        $functionName = $proxyData->getFunctionName();
+        $arguments = $proxyData->getArguments();
+        $returnValue = null;
+
+        if ($this->isFunctionDefined($functionName) === false) {
+            throw new BadRequestException(sprintf('unknown function "%s" under namespace "%s"', $functionName, static::NAMESPACE));
+        } elseif ($this->areAllRequiredArgumentsDefined($functionName, $arguments) === false) {
+            throw new BadRequestException(sprintf('incomplete argument list for function "%s" under namespace "%s"', $functionName, static::NAMESPACE));
+        }
+
+        try {
+            $returnValue = $this->callFunction($functionName, $arguments);
+        } catch (Exception $exception) {
+            $proxyData->setErrorsFromException($exception);
+        }
+
+        $proxyData->setData($returnValue);
+    }
+
+    abstract protected function isFunctionDefined(string $functionName): bool;
+
+    abstract protected function areAllRequiredArgumentsDefined(string $functionName, array $arguments): bool;
+
+    abstract protected function callFunction(string $functionName, array $arguments);
+}
diff --git a/src/ProxyApi/ApiConnection.php b/src/ProxyApi/ApiConnection.php
new file mode 100644
index 0000000000000000000000000000000000000000..9b0e137fe363676259c839f53c96cc1636cd7d22
--- /dev/null
+++ b/src/ProxyApi/ApiConnection.php
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Dbp\Relay\CoreBundle\ProxyApi;
+
+use Dbp\Relay\CoreBundle\Helpers\Tools;
+use Dbp\Relay\CoreBundle\Http\ApiConnection as BaseApiConnection;
+use Dbp\Relay\CoreBundle\Http\ConnectionException as BaseConnectionException;
+
+class ApiConnection extends BaseApiConnection
+{
+    private const NAMESPACE_PARAMETER_NAME = 'namespace';
+    private const FUNCTION_NAME_PARAMETER_NAME = 'functionName';
+    private const ARGUMENTS_PARAMETER_NAME = 'arguments';
+
+    private const PROXY_DATA_URI = 'proxy/proxydata';
+
+    /**
+     * @throws ConnectionException
+     */
+    public function callFunction(string $namespace, string $functionName, array $arguments = [])
+    {
+        $parameters = [
+            self::NAMESPACE_PARAMETER_NAME => $namespace,
+            self::FUNCTION_NAME_PARAMETER_NAME => $functionName,
+            self::ARGUMENTS_PARAMETER_NAME => $arguments,
+        ];
+
+        $responseBody = (string) $this->postJSON(self::PROXY_DATA_URI, $parameters)->getBody();
+
+        try {
+            $proxyData = Tools::decodeJSON($responseBody, true);
+        } catch (\JsonException $exception) {
+            throw new ConnectionException('failed to JSON decode API response: '.$exception->getMessage(), BaseConnectionException::JSON_EXCEPTION);
+        }
+
+        try {
+            $errors = $proxyData[ProxyApi::PROXY_DATA_ERRORS_KEY];
+            $returnValue = $proxyData[ProxyApi::PROXY_DATA_RETURN_VALUE_KEY];
+        } catch (\Exception $exception) {
+            throw new ConnectionException('API returned invalid ProxyData object', BaseConnectionException::INVALID_DATA_EXCEPTION);
+        }
+
+        if (!empty($errors)) {
+            $topLevelError = $errors[0];
+            throw new ConnectionException(sprintf('call to API function "%s" under namespace "%s" resulted in an error: %s (code: %s)', $functionName, $namespace, $topLevelError['message'] ?? 'message not available', $topLevelError['code'] ?? 'code not available'), ConnectionException::API_ERROR);
+        }
+
+        return $returnValue;
+    }
+}
diff --git a/src/ProxyApi/ConnectionException.php b/src/ProxyApi/ConnectionException.php
new file mode 100644
index 0000000000000000000000000000000000000000..f552ff94d9b50a939a2e31f8c2724c0106769a8a
--- /dev/null
+++ b/src/ProxyApi/ConnectionException.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Dbp\Relay\CoreBundle\ProxyApi;
+
+class ConnectionException extends \Dbp\Relay\CoreBundle\Http\ConnectionException
+{
+    public const API_ERROR = 101;
+}
diff --git a/src/ProxyApi/ProxyApi.php b/src/ProxyApi/ProxyApi.php
new file mode 100644
index 0000000000000000000000000000000000000000..1e4000f136a5412b4f8e99c017e58fa5bdb6263f
--- /dev/null
+++ b/src/ProxyApi/ProxyApi.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Dbp\Relay\CoreBundle\ProxyApi;
+
+class ProxyApi
+{
+    public const PROXY_DATA_EVENT_NAME = '';
+
+    public const PROXY_DATA_RETURN_VALUE_KEY = 'data';
+    public const PROXY_DATA_ERRORS_KEY = 'errors';
+}
diff --git a/src/ProxyApi/ProxyDataEvent.php b/src/ProxyApi/ProxyDataEvent.php
new file mode 100644
index 0000000000000000000000000000000000000000..8db1e2594faf18c00a4fffc4eb5a4e1c5e5d8b58
--- /dev/null
+++ b/src/ProxyApi/ProxyDataEvent.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Dbp\Relay\CoreBundle\ProxyApi;
+
+use Symfony\Contracts\EventDispatcher\Event;
+
+class ProxyDataEvent extends Event
+{
+    public const NAME = 'dbp.relay.proxy_bundle.proxy_data';
+
+    /** @var ProxyDataInterface */
+    private $proxyData;
+
+    /** @var bool */
+    private $wasHandled;
+
+    public function __construct(ProxyDataInterface $proxyData)
+    {
+        $this->proxyData = $proxyData;
+        $this->wasHandled = false;
+    }
+
+    public function getProxyData(): ProxyDataInterface
+    {
+        return $this->proxyData;
+    }
+
+    /**
+     * Indicate, that the event was handled, e.g. there was an event subscriber for the requested proxy data namespace.
+     */
+    public function setHandled(): void
+    {
+        $this->wasHandled = true;
+    }
+
+    /**
+     * True, if the event was handled, e.g. there was an event subscriber for the requested proxy data namespace, false otherwise.
+     */
+    public function wasHandled(): bool
+    {
+        return $this->wasHandled;
+    }
+}
diff --git a/src/ProxyApi/ProxyDataEventSubscriber.php b/src/ProxyApi/ProxyDataEventSubscriber.php
new file mode 100644
index 0000000000000000000000000000000000000000..d2c739843d73b6ba8c5a63a2aaeb6107d5fdb31a
--- /dev/null
+++ b/src/ProxyApi/ProxyDataEventSubscriber.php
@@ -0,0 +1,84 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Dbp\Relay\CoreBundle\ProxyApi;
+
+use Dbp\Relay\CoreBundle\Authorization\AuthorizationDataProviderProvider;
+use Dbp\Relay\CoreBundle\Helpers\Tools;
+use Exception;
+
+class ProxyDataEventSubscriber extends AbstractProxyDataEventSubscriber
+{
+    protected const NAMESPACE = 'core';
+
+    public const GET_AVAILABLE_ATTRIBUTES_FUNCTION_NAME = 'getAvailableAttributes';
+    public const GET_USER_ATTRIBUTES_FUNCTION_NAME = 'getUserAttributes';
+
+    public const USER_ID_PARAMETER_NAME = 'userId';
+
+    /**
+     * @var AuthorizationDataProviderProvider
+     */
+    private $provider;
+
+    public function __construct(AuthorizationDataProviderProvider $provider)
+    {
+        $this->provider = $provider;
+    }
+
+    protected function isFunctionDefined(string $functionName): bool
+    {
+        return
+            $functionName === self::GET_AVAILABLE_ATTRIBUTES_FUNCTION_NAME ||
+            $functionName === self::GET_USER_ATTRIBUTES_FUNCTION_NAME;
+    }
+
+    protected function areAllRequiredArgumentsDefined(string $functionName, array $arguments): bool
+    {
+        return
+            $functionName !== self::GET_USER_ATTRIBUTES_FUNCTION_NAME ||
+            !Tools::isNullOrEmpty($arguments[self::USER_ID_PARAMETER_NAME] ?? null);
+    }
+
+    /**
+     * @throws Exception
+     */
+    protected function callFunction(string $functionName, array $arguments): ?array
+    {
+        $returnValue = null;
+
+        switch ($functionName) {
+            case self::GET_AVAILABLE_ATTRIBUTES_FUNCTION_NAME:
+                $returnValue = $this->getAvailableAttributes();
+                break;
+            case self::GET_USER_ATTRIBUTES_FUNCTION_NAME:
+                $returnValue = $this->getUserAttributes($arguments[self::USER_ID_PARAMETER_NAME]);
+                break;
+        }
+
+        return $returnValue;
+    }
+
+    private function getAvailableAttributes(): array
+    {
+        $availableAttributes = [];
+
+        foreach ($this->provider->getAuthorizationDataProviders() as $provider) {
+            $availableAttributes = array_merge($availableAttributes, $provider->getAvailableAttributes());
+        }
+
+        return $availableAttributes;
+    }
+
+    private function getUserAttributes(string $userId): array
+    {
+        $userAttributes = [];
+
+        foreach ($this->provider->getAuthorizationDataProviders() as $provider) {
+            $userAttributes = array_merge($userAttributes, $provider->getUserAttributes($userId));
+        }
+
+        return $userAttributes;
+    }
+}
diff --git a/src/ProxyApi/ProxyDataInterface.php b/src/ProxyApi/ProxyDataInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..3c5d6e613deb0b11518d4f1bf81d8ced48f4175c
--- /dev/null
+++ b/src/ProxyApi/ProxyDataInterface.php
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Dbp\Relay\CoreBundle\ProxyApi;
+
+interface ProxyDataInterface
+{
+    public function getArguments(): array;
+
+    public function getFunctionName(): ?string;
+
+    public function setData($data);
+
+    public function setErrorsFromException(\Exception $exception): void;
+}