diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..2db6d45
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 ProductFlow B.V.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..44ed92f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,45 @@
+  [](https://git.d3data.de/D3Private/klicktipp-php-client/raw/branch/main/LICENSE)
+
+# klicktipp-php-client
+
+An unofficial client for the Klicktipp API.
+
+## Installation
+This project can easily be installed through Composer.
+
+```
+composer require d3/klicktipp-php-client
+```
+
+## Set-up connection
+Prepare the client for connecting to Klicktipp with your client key and secret key.
+
+```php
+$klicktipp = new \D3\KlicktippPhpClient\Klicktipp(
+ $clientkey,
+ $secretkey,
+ new \GuzzleHttp\Client(...) // optional
+);
+```
+
+## search a subscriber
+
+```php
+$subscriberId = $klicktipp->subscriber()->search('me@johndoe.net');
+$subscriber = $klicktipp->subscriber()->search($subscriberId);
+```
+
+## Supported endpoints (still being added)
+
+[API](https://www.klicktipp.com/de/support/wissensdatenbank/rest-application-programming-interface-api/)
+
+:white_check_mark: = Done, and tested
+:ballot_box_with_check: = Done, but not yet tested
+:x: = Not yet developed
+:heavy_exclamation_mark: = deprecated/not supported
+
+| Endpoint | Status |
+|--------------------------------------------------------------------------------------|-------------------------|
+| account | :ballot_box_with_check: |
+| subscriber | :ballot_box_with_check: |
+| tag | :white_check_mark: |
diff --git a/composer.json b/composer.json
index 1e3d347..0f3f30d 100644
--- a/composer.json
+++ b/composer.json
@@ -17,6 +17,8 @@
],
"require": {
"php": ">=8.0",
+ "composer/composer": "^2.7.1",
+ "doctrine/collections": "^1.8.0",
"guzzlehttp/guzzle": "~7.0",
"ext-json": "*"
},
diff --git a/src/Connection.php b/src/Connection.php
new file mode 100644
index 0000000..450e66b
--- /dev/null
+++ b/src/Connection.php
@@ -0,0 +1,173 @@
+
+ * @link https://www.oxidmodule.com
+ */
+
+declare(strict_types=1);
+
+namespace D3\KlicktippPhpClient;
+
+use Composer\InstalledVersions;
+use D3\KlicktippPhpClient\Exceptions\BaseException;
+use GuzzleHttp\Client;
+use GuzzleHttp\ClientInterface;
+use GuzzleHttp\Exception\GuzzleException;
+use GuzzleHttp\Exception\RequestException;
+use Psr\Http\Message\ResponseInterface;
+use RuntimeException;
+
+class Connection
+{
+ public const URL = 'https://api.klicktipp.com/';
+
+ public const USERAGENT = 'Klicktipp-php-client';
+
+ protected string $client_key;
+
+ protected string $secret_key;
+
+ protected ?string $cookie = null;
+
+ /**
+ * Contains the HTTP client (e.g. Guzzle)
+ */
+ private ClientInterface $client;
+
+ public function __construct(string $client_key, string $secret_key)
+ {
+ $this->client_key = $client_key;
+ $this->secret_key = $secret_key;
+ }
+
+ public function getClientKey(): string
+ {
+ return $this->client_key;
+ }
+
+ public function getSecretKey(): string
+ {
+ return $this->secret_key;
+ }
+
+ /**
+ * @param ClientInterface $client
+ */
+ public function setClient(ClientInterface $client): void
+ {
+ $this->client = $client;
+ }
+
+ /**
+ * @return ClientInterface
+ */
+ public function getClient(): ClientInterface
+ {
+ $this->client = $this->client ??
+ new Client([
+ 'base_uri' => self::URL,
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json',
+ 'User-Agent' => self::USERAGENT.'/'.InstalledVersions::getVersion('d3/klicktipp-php-client')
+ ]
+ ]);
+
+ return $this->client;
+ }
+
+ /**
+ * @param string $method
+ * @param string $uri
+ * @param array $options
+ * @return ResponseInterface
+ * @throws BaseException|GuzzleException
+ */
+ public function request(string $method, string $uri, array $options = []): ResponseInterface
+ {
+ try {
+ $options['query'] = $options['query'] ?? [];
+
+ if (! empty($options['body'])) {
+ $options['body'] = json_encode($options['body']);
+ }
+
+ $header = [
+ 'Cookie' => $this->cookie ?? ''
+ ];
+ $options['headers'] = $header;
+
+ return $this->getClient()->request($method, $uri, $options);
+ } catch (RequestException $e) {
+ if ($e->hasResponse()) {
+ $this->parseResponse($e->getResponse());
+ }
+
+ throw new BaseException(
+ $e->getResponse()->getBody(),
+ $e->getResponse()->getStatusCode(),
+ $e
+ );
+ }
+ }
+
+ /**
+ * @param string $method
+ * @param string $uri
+ * @param array $options
+ * @return array
+ * @throws BaseException
+ * @throws GuzzleException
+ */
+ public function requestAndParse(string $method, string $uri, array $options = []): array
+ {
+ return $this->parseResponse($this->request($method, $uri, $options));
+ }
+
+ /**
+ * @param ResponseInterface $response
+ * @return array Parsed JSON result
+ * @throws BaseException
+ */
+ public function parseResponse(ResponseInterface $response): array
+ {
+ try {
+ // Rewind the response (middlewares might have read it already)
+ $response->getBody()->rewind();
+
+ $response_body = $response->getBody()->getContents();
+ $result_array = json_decode($response_body, true);
+
+ if ($response->getStatusCode() === 204) {
+ return [];
+ }
+
+ if (! is_array($result_array)) {
+ throw new BaseException(
+ sprintf('%s: %s', $response->getStatusCode(), $response_body),
+ $response->getStatusCode()
+ );
+ }
+
+ return $result_array;
+ } catch (RuntimeException $e) {
+ throw new BaseException(
+ $e->getMessage(),
+ 0,
+ $e
+ );
+ }
+ }
+
+ public function setCookie(?string $cookie): void
+ {
+ $this->cookie = $cookie;
+ }
+}
diff --git a/src/Exceptions/BaseException.php b/src/Exceptions/BaseException.php
new file mode 100644
index 0000000..c249cec
--- /dev/null
+++ b/src/Exceptions/BaseException.php
@@ -0,0 +1,14 @@
+
+ * @link https://www.oxidmodule.com
+ */
+
+declare(strict_types=1);
+
+namespace D3\KlicktippPhpClient;
+
+use D3\KlicktippPhpClient\Exceptions\BaseException;
+use D3\KlicktippPhpClient\Resources\Account;
+use D3\KlicktippPhpClient\Resources\Subscriber;
+use D3\KlicktippPhpClient\Resources\Tag;
+use GuzzleHttp\ClientInterface;
+use GuzzleHttp\Exception\GuzzleException;
+
+class Klicktipp
+{
+ protected string $client_key;
+
+ protected string $secret_key;
+
+ protected ?Connection $connection = null;
+
+ /**
+ * @throws BaseException
+ * @throws GuzzleException
+ */
+ public function __construct(
+ string $client_key,
+ string $secret_key,
+ ClientInterface $client = null
+ ){
+ $this->client_key = $client_key;
+ $this->secret_key = $secret_key;
+
+ if ($client) {
+ $this->getConnection()->setClient($client);
+ }
+
+ $this->account()->login();
+ }
+
+ private function getConnection(): Connection
+ {
+ if (!$this->connection) {
+ $this->connection = new Connection($this->client_key, $this->secret_key);
+ }
+ return $this->connection;
+ }
+
+ public function account(): Account
+ {
+ return new Account($this->getConnection());
+ }
+
+ public function subscriber(): Subscriber
+ {
+ return new Subscriber($this->getConnection());
+ }
+
+ public function tag(): Tag
+ {
+ return new Tag($this->getConnection());
+ }
+}
diff --git a/src/Resources/Account.php b/src/Resources/Account.php
new file mode 100644
index 0000000..1317f2f
--- /dev/null
+++ b/src/Resources/Account.php
@@ -0,0 +1,51 @@
+connection->request(
+ 'POST',
+ 'account/login',
+ [
+ 'query' => $this->getQuery(),
+ 'form_params' => [
+ 'username' => $this->connection->getClientKey(),
+ 'password' => $this->connection->getSecretKey()
+ ]
+ ]
+ );
+
+ $this->connection->setCookie(
+ current($response->getHeader('set-cookie'))
+ );
+
+ return $this->connection->parseResponse($response);
+ }
+
+ /**
+ * @throws BaseException|GuzzleException
+ */
+ public function logout(): bool
+ {
+ $response = $this->connection->requestAndParse(
+ 'POST',
+ 'account/logout',
+ ['query' => $this->getQuery()]
+ );
+
+ if (current($response)) {
+ $this->connection->setCookie(null);
+ }
+
+ return (bool) current($response);
+ }
+}
diff --git a/src/Resources/Model.php b/src/Resources/Model.php
new file mode 100644
index 0000000..4f54016
--- /dev/null
+++ b/src/Resources/Model.php
@@ -0,0 +1,76 @@
+connection = $connection;
+ }
+
+ public function getLimit(): ?int
+ {
+ return $this->limit;
+ }
+
+ public function setLimit(int $limit): static
+ {
+ $this->limit = $limit;
+ return $this;
+ }
+
+ public function getOffset(): ?int
+ {
+ return $this->offset;
+ }
+
+ public function setOffset(int $offset): static
+ {
+ $this->offset = $offset;
+ return $this;
+ }
+
+ public function setFilter(string $column, $operation, $value = null): static
+ {
+ if (is_null($value)) {
+ $value = $operation;
+ $operation = 'eq';
+ }
+
+ $this->filters[$column][$operation] = $value;
+ return $this;
+ }
+
+ public function getQuery(): array
+ {
+ return array_filter(array_merge(
+ $this->query,
+ $this->filters,
+ [ 'limit' => $this->getLimit(), 'offset' => $this->getOffset() ]
+ ));
+ }
+
+ public function setQuery(string|array $key, mixed $value = null): static
+ {
+ if (is_array($key)) {
+ $this->query = $key;
+ } else {
+ $this->query[$key] = $value;
+ }
+
+ return $this;
+ }
+}