diff --git a/src/ApiClient/BackendApiClient.php b/src/ApiClient/BackendApiClient.php
new file mode 100644
index 0000000..5fdaa6c
--- /dev/null
+++ b/src/ApiClient/BackendApiClient.php
@@ -0,0 +1,234 @@
+httpClient === null) {
+ $this->httpClient = new Client(
+ [
+ 'base_uri' => self::TWEAKWISE_BACKEND_API_BASE_URL
+ ]
+ );
+ }
+
+ return $this->httpClient;
+ }
+
+ /**
+ * @param string $path
+ * @param Store $store
+ * @return ResponseInterface
+ * @throws GuzzleException
+ * @throws LocalizedException
+ */
+ public function doRequest(
+ string $path,
+ Store $store
+ ): ResponseInterface {
+ try {
+ return $this->getHttpClient()
+ ->request(
+ 'GET',
+ $path,
+ [
+ 'headers' => [
+ 'TWN-InstanceKey' => $this->config->getGeneralAuthenticationKey($store),
+ 'TWN-Authentication' => $this->config->getBackendApiToken($store)
+ ]
+ ]
+ );
+ } catch (Exception $e) {
+ $this->logger->critical(
+ 'Tweakwise Backend API request failed',
+ [
+ 'path' => $path,
+ 'exception' => $e->getMessage()
+ ]
+ );
+ throw $e;
+ }
+ }
+
+ /**
+ * @param Store $store
+ * @return array
+ */
+ public function getAttributes(Store $store): array
+ {
+ $cacheKey = $this->getCacheKey('attributes', (int)$store->getId());
+ if ($this->cacheExists($cacheKey)) {
+ return $this->getFromCache($cacheKey);
+ }
+
+ $attributes = [];
+ try {
+ $response = $this->doRequest('attribute', $store);
+ $contents = $response->getBody()->getContents();
+ $result = $this->jsonSerializer->unserialize($contents);
+
+ if (!isset($result['Records'])) {
+ return [];
+ }
+
+ foreach ($result['Records'] as $record) {
+ $attributes[] = [
+ 'value' => $record['UrlName'],
+ 'label' => $record['Name'],
+ 'id' => $record['Id']
+ ];
+ }
+
+ $this->cache->save($this->jsonSerializer->serialize($attributes), $cacheKey, [], $this->getCacheLifetime());
+
+ return $attributes;
+ } catch (GuzzleException | Exception $e) {
+ $this->logger->critical(
+ 'Retrieving attributes from Tweakiwse Backend API failed',
+ [
+ 'exception' => $e->getMessage()
+ ]
+ );
+ return [];
+ }
+ }
+
+ /**
+ * @param string $attributeCode
+ * @param Store $store
+ * @return int|null
+ */
+ public function getAttributeIdByCode(string $attributeCode, Store $store): ?int
+ {
+ $attributes = $this->getAttributes($store);
+ $index = array_search($attributeCode, array_column($attributes, 'value'), true);
+
+ return $index !== false ? (int)$attributes[$index]['id'] : null;
+ }
+
+ /**
+ * @param int $attributeId
+ * @param Store $store
+ * @return array
+ */
+ public function getAttributeValues(int $attributeId, Store $store): array
+ {
+ $cacheKey = $this->getCacheKey('attribute_values', (int)$store->getId(), $attributeId);
+ if ($this->cacheExists($cacheKey)) {
+ return $this->getFromCache($cacheKey);
+ }
+
+ $attributeValues = [];
+ try {
+ $response = $this->doRequest(sprintf('attribute/%s/values', $attributeId), $store);
+ $contents = $response->getBody()->getContents();
+ $result = $this->jsonSerializer->unserialize($contents);
+
+ if (!isset($result['Records'])) {
+ return [];
+ }
+
+ foreach ($result['Records'] as $record) {
+ $attributeValues[] = [
+ 'value' => $record['Value'],
+ 'label' => $record['Value'],
+ ];
+ }
+
+ $this->cache->save($this->jsonSerializer->serialize($attributeValues), $cacheKey, [], $this->getCacheLifetime());
+
+ return $attributeValues;
+ } catch (GuzzleException | Exception $e) {
+ $this->logger->critical(
+ 'Retrieving attribute valus from Tweakiwse Backend API failed',
+ [
+ 'exception' => $e->getMessage()
+ ]
+ );
+ return [];
+ }
+ }
+
+ /**
+ * @param string $type
+ * @param int $storeId
+ * @param int|null $attributeId
+ * @return string
+ */
+ private function getCacheKey(string $type, int $storeId, ?int $attributeId = null): string
+ {
+ if ($attributeId) {
+ return sprintf('tweakwise_backend_api_result_%s_%s_%s', $type, $attributeId, $storeId);
+ }
+ return sprintf('tweakwise_backend_api_result_%s_%s', $type, $storeId);
+ }
+
+ /**
+ * @param string $cacheKey
+ * @return bool
+ */
+ private function cacheExists(string $cacheKey): bool
+ {
+ /** @phpstan-ignore-next-line */
+ return $this->cache->load($cacheKey) !== false;
+ }
+
+ /**
+ * @param string $cacheKey
+ * @return array
+ */
+ private function getFromCache(string $cacheKey): array
+ {
+ return (array)$this->jsonSerializer->unserialize($this->cache->load($cacheKey));
+ }
+
+ /**
+ * Protected function added so that cache lifetime is overwritable
+ * @return int
+ */
+ protected function getCacheLifetime(): int
+ {
+ return self::CACHE_LIFETIME;
+ }
+}
diff --git a/src/Controller/Adminhtml/Ajax/AbstractFacetController.php b/src/Controller/Adminhtml/Ajax/AbstractFacetController.php
new file mode 100644
index 0000000..79f6020
--- /dev/null
+++ b/src/Controller/Adminhtml/Ajax/AbstractFacetController.php
@@ -0,0 +1,39 @@
+config->getBackendApiToken($store);
+ }
+}
diff --git a/src/Controller/Adminhtml/Ajax/FacetAttributes.php b/src/Controller/Adminhtml/Ajax/FacetAttributes.php
new file mode 100644
index 0000000..903b178
--- /dev/null
+++ b/src/Controller/Adminhtml/Ajax/FacetAttributes.php
@@ -0,0 +1,145 @@
+resultJsonFactory->create();
+ $facetKey = $this->request->getParam('facet_key');
+ $otherAttributeOption = ['value' => self::OTHER_ATTRIBUTE_VALUE, 'label' => 'Other (text field)'];
+
+ if ($facetKey === self::OTHER_ATTRIBUTE_VALUE) {
+ return $result->setData([$otherAttributeOption]);
+ }
+
+ $attributeValues = [];
+ /** @var Store $store */
+ foreach ($this->storeManager->getStores() as $store) {
+ if ($this->isBackendApiEnabled($store)) {
+ $attributeValues = array_merge($this->executeBackendApiRequest($store, $facetKey), $attributeValues);
+ continue;
+ }
+
+ $attributeValues = array_merge($this->executeDefaultRequest((int)$store->getId(), $facetKey), $attributeValues);
+ }
+
+ $attributeValues[] = $otherAttributeOption;
+
+ $attributeValues = array_values(array_unique($attributeValues, SORT_REGULAR));
+
+ return $result->setData($attributeValues);
+ }
+
+ /**
+ * @param int $storeId
+ * @param string|null $facetKey
+ * @return array
+ * @throws Exception
+ */
+ private function executeDefaultRequest(int $storeId, ?string $facetKey): array
+ {
+ $facetAttributeRequest = $this->requestFactory->create();
+
+ $filterTemplate = $this->request->getParam('filter_template');
+ if ($filterTemplate) {
+ $facetAttributeRequest->setParameter('tn_ft', $filterTemplate);
+ }
+
+ if ($facetKey && $facetAttributeRequest instanceof FacetAttributeRequest) {
+ $facetAttributeRequest->addFacetKey($facetKey);
+ }
+
+ $categoryId = $this->helper->getTweakwiseId(
+ $storeId,
+ (int) $this->request->getParam('category_id')
+ );
+ if ($categoryId) {
+ $facetAttributeRequest->setParameter('tn_cid', $categoryId);
+ }
+
+ /** @var FacetAttributesResponse $response */
+ $response = $this->client->request($facetAttributeRequest);
+
+ if (!$response->getAttributes()) {
+ return [];
+ }
+
+ $attributes = [];
+ foreach ($response->getAttributes() as $attribute) {
+ $attributes[] = [
+ 'value' => $attribute['title'],
+ 'label' => $attribute['title']
+ ];
+ }
+
+ return $attributes;
+ }
+
+ /**
+ * @param Store $store
+ * @param string|null $facetKey
+ * @return array
+ */
+ private function executeBackendApiRequest(Store $store, ?string $facetKey): array
+ {
+ if (!$facetKey) {
+ return [];
+ }
+
+ $attributeId = $this->backendApiClient->getAttributeIdByCode($facetKey, $store);
+ if (!$attributeId) {
+ return [];
+ }
+
+ return $this->backendApiClient->getAttributeValues($attributeId, $store);
+ }
+}
diff --git a/src/Controller/Adminhtml/Ajax/Facets.php b/src/Controller/Adminhtml/Ajax/Facets.php
new file mode 100644
index 0000000..45feecf
--- /dev/null
+++ b/src/Controller/Adminhtml/Ajax/Facets.php
@@ -0,0 +1,117 @@
+resultJsonFactory->create();
+
+ $facets = [];
+ /** @var Store $store */
+ foreach ($this->storeManager->getStores() as $store) {
+ if ($this->isBackendApiEnabled($store)) {
+ $facets = array_merge($this->executeBackendApiRequest($store), $facets);
+ continue;
+ }
+
+ $facets = array_merge($this->executeDefaultRequest((int)$store->getId()), $facets);
+ }
+
+ $facets[] = ['value' => self::OTHER_ATTRIBUTE_VALUE, 'label' => 'Other (text field)'];
+
+ $facets = array_values(array_unique($facets, SORT_REGULAR));
+
+ return $result->setData($facets);
+ }
+
+ /**
+ * @param int $storeId
+ * @return array
+ * @throws Exception
+ */
+ private function executeDefaultRequest(int $storeId): array
+ {
+ $facetRequest = $this->requestFactory->create();
+
+ $filterTemplate = $this->request->getParam('filter_template');
+ if ($filterTemplate) {
+ $facetRequest->setParameter('tn_ft', $filterTemplate);
+ }
+
+ $categoryId = $this->helper->getTweakwiseId(
+ $storeId,
+ (int) $this->request->getParam('category_id')
+ );
+ if ($categoryId) {
+ $facetRequest->setParameter('tn_cid', $categoryId);
+ }
+
+ /** @var FacetResponse $response */
+ $response = $this->client->request($facetRequest);
+
+ $facets = [];
+ foreach ($response->getFacets() as $facet) {
+ $facets[] = [
+ 'value' => $facet->getFacetSettings()->getUrlKey(),
+ 'label' => $facet->getFacetSettings()->getTitle()
+ ];
+ }
+
+ return $facets;
+ }
+
+ /**
+ * @param Store $store
+ * @return array
+ */
+ private function executeBackendApiRequest(Store $store): array
+ {
+ return $this->backendApiClient->getAttributes($store);
+ }
+}
diff --git a/src/Model/Client/Request/FacetAttributeRequest.php b/src/Model/Client/Request/FacetAttributeRequest.php
new file mode 100644
index 0000000..7b05be0
--- /dev/null
+++ b/src/Model/Client/Request/FacetAttributeRequest.php
@@ -0,0 +1,33 @@
+setPath(sprintf('%s/%s/attributes', $this->path, $facetKey));
+ }
+}
diff --git a/src/Model/Client/Request/FacetRequest.php b/src/Model/Client/Request/FacetRequest.php
new file mode 100644
index 0000000..4b3e49d
--- /dev/null
+++ b/src/Model/Client/Request/FacetRequest.php
@@ -0,0 +1,24 @@
+data['attributes'] ?? [];
+ }
+
+ /**
+ * @param array $attributesData
+ * @return $this
+ */
+ public function setAttributes(array $attributesData): FacetAttributesResponse
+ {
+ $attributesData = $this->normalizeArray($attributesData, 'attributes');
+
+ foreach ($attributesData as $attributeData) {
+ $attribute = $this->attributeTypeFactory->create()->setData($attributeData);
+
+ $attributes = $attribute->getValue('attribute');
+
+ if (isset($attributes[0])) {
+ $this->data['attributes'] = $attribute->getValue('attribute');
+ } else {
+ // Only one result
+ $this->data['attributes'][] = $attribute->getValue('attribute');
+ }
+
+ break;
+ }
+
+ return $this;
+ }
+}
diff --git a/src/Model/Client/Response/FacetResponse.php b/src/Model/Client/Response/FacetResponse.php
new file mode 100644
index 0000000..680069b
--- /dev/null
+++ b/src/Model/Client/Response/FacetResponse.php
@@ -0,0 +1,63 @@
+data['facets'] ?? [];
+ }
+
+ /**
+ * @param array $facetsData
+ * @return $this
+ */
+ public function setFacets(array $facetsData): FacetResponse
+ {
+ $facetsData = $this->normalizeArray($facetsData, 'facet');
+
+ $facets = [];
+ foreach ($facetsData as $facetData) {
+ $facet = $this->facetTypeFactory->create()->setData($facetData);
+
+ // Remove tree, link and slider facets
+ if (
+ $facet->getFacetSettings()->getSelectionType() !== 'checkbox' &&
+ $facet->getFacetSettings()->getSelectionType() !== 'color'
+ ) {
+ continue;
+ }
+
+ $facets[] = $facet;
+ }
+
+ $this->data['facets'] = $facets;
+ return $this;
+ }
+}
diff --git a/src/Model/Config.php b/src/Model/Config.php
new file mode 100644
index 0000000..0773053
--- /dev/null
+++ b/src/Model/Config.php
@@ -0,0 +1,24 @@
+getStoreConfig(self::BACKEND_API_TOKEN_PATH, $store);
+ }
+}
diff --git a/src/Plugin/Model/LandingPagePlugin.php b/src/Plugin/Model/LandingPagePlugin.php
new file mode 100644
index 0000000..995183a
--- /dev/null
+++ b/src/Plugin/Model/LandingPagePlugin.php
@@ -0,0 +1,31 @@
+ $filterAttribute) {
+ if ($filterAttribute['attribute'] !== AbstractFacetController::OTHER_ATTRIBUTE_VALUE) {
+ continue;
+ }
+
+ $result[$key]['attribute'] = $filterAttribute['attribute_other'];
+ $result[$key]['value'] = $filterAttribute['attribute_value_other'];
+ }
+
+ return $result;
+ }
+}
diff --git a/src/etc/adminhtml/di.xml b/src/etc/adminhtml/di.xml
new file mode 100644
index 0000000..c90cbb2
--- /dev/null
+++ b/src/etc/adminhtml/di.xml
@@ -0,0 +1,35 @@
+
+