diff --git a/app/main.py b/app/main.py index 2f0f720..1a583b5 100644 --- a/app/main.py +++ b/app/main.py @@ -1,9 +1,10 @@ import logging import os +from typing import Any -from fastapi import FastAPI, Header, HTTPException +from fastapi import Body, Depends, FastAPI, Header, HTTPException, Query, Request -from app.models import CreateCompanyRequest, CreateCompanyResponse +from app.models import CompanyData, CreateCompanyResponse from app.raynet_client import ( RaynetAuthError, RaynetClient, @@ -18,20 +19,90 @@ APP_NAME = os.getenv("APP_NAME", "raynet") APP_VERSION = os.getenv("APP_VERSION", "1.0.0") ROOT_PATH = os.getenv("ROOT_PATH", "") +# Popis se zobrazí nahoře ve Swaggeru (/docs) – přihlašovací údaje jsou +# společné pro celé připojení a posílají se v hlavičkách u KAŽDÉHO requestu. +API_DESCRIPTION = """ +Stateless proxy nad **RAYNET CRM API v2**. + +## Přihlášení (společné pro celé připojení) + +Každý endpoint vyžaduje tři hlavičky. Jsou stejné pro všechna volání: + +| Hlavička | Co to je | Kde to vzít | +|-------------------|--------------------|-------------| +| **`X-Api-Key`** | API klíč (secret) | V RAYNET CRM: **Nastavení → Klíč k API** (vygeneruj / zkopíruj). | +| **`X-Raynet-Email`** | Email uživatele | Přihlašovací email do RAYNET (tvoří dvojici s API klíčem pro Basic Auth). | +| **`X-Instance-Name`** | Název instance | Identifikátor tvé RAYNET instance (subdoména účtu). | + +`X-Api-Key` je **secret** – nikdy se neloguje ani nevrací z endpointů. + +## Endpointy + +- Typované zkratky pro firmy: `POST/GET/PUT/DELETE /company...` +- Generický průchod na celé API: `/api/{resource}` – podporuje libovolnou + RAYNET entitu (`company`, `person`, `lead`, `businessCase`, `activity`, + `product`, `offer`, `order`, `project`, ...). + +Plná dokumentace RAYNET: +""" + app = FastAPI( title=APP_NAME, version=APP_VERSION, root_path=ROOT_PATH, - description="Stateless proxy nad RAYNET CRM API v2.", + description=API_DESCRIPTION, ) -@app.get("/health") +# --------------------------------------------------------------------------- # +# Sdílená credentials dependency – 3 hlavičky pro celé připojení. +# --------------------------------------------------------------------------- # +def get_client( + x_api_key: str = Header( + ..., alias="X-Api-Key", description="RAYNET API klíč (secret)." + ), + x_raynet_email: str = Header( + ..., alias="X-Raynet-Email", description="Email uživatele RAYNET." + ), + x_instance_name: str = Header( + ..., alias="X-Instance-Name", description="Název RAYNET instance." + ), +): + """Vytvoří RaynetClient z hlaviček a po dokončení requestu uvolní session.""" + client = RaynetClient( + api_key=x_api_key, + email=x_raynet_email, + instance_name=x_instance_name, + ) + try: + yield client + finally: + client.close() + + +def _run(fn, *args, **kwargs) -> Any: + """Spustí volání klienta a převede výjimky konektoru na HTTP odpovědi.""" + try: + return fn(*args, **kwargs) + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + except RaynetAuthError as exc: + raise HTTPException(status_code=401, detail=exc.message) from exc + except RaynetValidationError as exc: + raise HTTPException(status_code=400, detail=exc.message) from exc + except RaynetError as exc: + raise HTTPException(status_code=502, detail=exc.message) from exc + + +# --------------------------------------------------------------------------- # +# Servisní endpointy +# --------------------------------------------------------------------------- # +@app.get("/health", tags=["service"]) def health(): return {"status": "ok"} -@app.get("/version") +@app.get("/version", tags=["service"]) def version(): return { "app": APP_NAME, @@ -41,35 +112,105 @@ def version(): } +# --------------------------------------------------------------------------- # +# Typované zkratky pro firmy (company) +# --------------------------------------------------------------------------- # @app.post("/company", response_model=CreateCompanyResponse, tags=["company"]) def create_company( - request: CreateCompanyRequest, - x_api_key: str = Header( - ..., - alias="X-Api-Key", - description="RAYNET API klíč (secret). Předává se výhradně hlavičkou.", - ), + company: CompanyData, + client: RaynetClient = Depends(get_client), ): - """Vytvoří firmu v RAYNET CRM. - - - **X-Api-Key** (hlavička, secret): API klíč z RAYNET (Nastavení > API). - - **email** / **instance_name** (tělo): identifikátory účtu a instance. - - **company** (tělo): data firmy dle RAYNET CompanyInsertDto. - """ - try: - with RaynetClient( - api_key=x_api_key, - email=request.email, - instance_name=request.instance_name, - ) as client: - # Posíláme jen vyplněná pole (žádné None), aby RAYNET nedostal prázdné hodnoty. - data = request.company.model_dump(exclude_none=True) - result = client.create_company(data) - except RaynetAuthError as exc: - raise HTTPException(status_code=401, detail=exc.message) from exc - except RaynetValidationError as exc: - raise HTTPException(status_code=400, detail=exc.message) from exc - except RaynetError as exc: - raise HTTPException(status_code=502, detail=exc.message) from exc - + """Vytvoří firmu v RAYNET CRM (validovaná data dle CompanyInsertDto).""" + data = company.model_dump(exclude_none=True) + result = _run(client.create_company, data) return CreateCompanyResponse(id=result.get("id"), success=True, raw=result) + + +@app.get("/company", tags=["company"]) +def list_companies( + client: RaynetClient = Depends(get_client), + offset: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=1000), + fulltext: str | None = Query(None, description="Fulltextové hledání"), +): + """Vrátí seznam firem s podporou stránkování a fulltextu.""" + params = {"offset": offset, "limit": limit} + if fulltext: + params["fulltext"] = fulltext + return _run(client.list_companies, **params) + + +@app.get("/company/{company_id}", tags=["company"]) +def get_company(company_id: int, client: RaynetClient = Depends(get_client)): + """Vrátí detail firmy.""" + return _run(client.get_company, company_id) + + +@app.put("/company/{company_id}", tags=["company"]) +def update_company( + company_id: int, + data: dict = Body(..., description="Pole firmy ke změně"), + client: RaynetClient = Depends(get_client), +): + """Upraví firmu.""" + return _run(client.update_company, company_id, data) + + +@app.delete("/company/{company_id}", tags=["company"]) +def delete_company(company_id: int, client: RaynetClient = Depends(get_client)): + """Smaže firmu.""" + return _run(client.delete_company, company_id) + + +# --------------------------------------------------------------------------- # +# Generický průchod na CELÉ RAYNET API +# resource = libovolná entita: company, person, lead, businessCase, activity, +# product, offer, order, project, ... +# --------------------------------------------------------------------------- # +@app.get("/api/{resource}", tags=["generic"]) +def api_list( + resource: str, + request: Request, + client: RaynetClient = Depends(get_client), +): + """Seznam záznamů entity. Všechny query parametry se předávají do RAYNET + (např. `offset`, `limit`, `fulltext`, `name`, ...).""" + params = dict(request.query_params) + return _run(client.list_records, resource, params or None) + + +@app.get("/api/{resource}/{record_id}", tags=["generic"]) +def api_detail( + resource: str, record_id: str, client: RaynetClient = Depends(get_client) +): + """Detail jednoho záznamu.""" + return _run(client.get_record, resource, record_id) + + +@app.post("/api/{resource}", tags=["generic"]) +def api_create( + resource: str, + data: dict = Body(..., description="Data nového záznamu"), + client: RaynetClient = Depends(get_client), +): + """Vytvoří záznam (mapuje se na RAYNET `PUT /{resource}/`).""" + return _run(client.create_record, resource, data) + + +@app.put("/api/{resource}/{record_id}", tags=["generic"]) +def api_update( + resource: str, + record_id: str, + data: dict = Body(..., description="Pole ke změně"), + client: RaynetClient = Depends(get_client), +): + """Upraví záznam (mapuje se na RAYNET `POST /{resource}/{id}/`).""" + return _run(client.update_record, resource, record_id, data) + + +@app.delete("/api/{resource}/{record_id}", tags=["generic"]) +def api_delete( + resource: str, record_id: str, client: RaynetClient = Depends(get_client) +): + """Smaže záznam.""" + return _run(client.delete_record, resource, record_id) diff --git a/app/models.py b/app/models.py index 012b3ff..de60463 100644 --- a/app/models.py +++ b/app/models.py @@ -99,18 +99,6 @@ class CompanyData(BaseModel): model_config = {"extra": "allow"} -class CreateCompanyRequest(BaseModel): - """Vstup endpointu /company. - - API klíč (secret) se předává hlavičkou ``X-Api-Key`` – NENÍ součástí tohoto - těla. Email a název instance jsou běžné identifikátory, proto jdou v těle. - """ - - email: str = Field(..., examples=["user@firma.cz"], description="Email uživatele RAYNET") - instance_name: str = Field(..., examples=["moje-instance"], description="Název RAYNET instance") - company: CompanyData - - class CreateCompanyResponse(BaseModel): id: Optional[int] = Field(default=None, description="ID nově vytvořené firmy") success: bool = True diff --git a/app/raynet_client.py b/app/raynet_client.py index e421580..aeddaba 100644 --- a/app/raynet_client.py +++ b/app/raynet_client.py @@ -115,41 +115,99 @@ class RaynetClient: ) # ------------------------------------------------------------------ # - # Veřejné metody + # Generické CRUD nad libovolnou RAYNET entitou (resource) + # + # RAYNET má napříč celým API jednotné konvence: + # list GET /{resource}/ + # detail GET /{resource}/{id}/ + # create PUT /{resource}/ + # update POST /{resource}/{id}/ + # delete DELETE /{resource}/{id}/ + # Tyto metody proto pokrývají celé RAYNET API (company, person, lead, + # businessCase, activity, product, offer, order, project, ...). + # ------------------------------------------------------------------ # + def list_records(self, resource: str, params: Optional[dict] = None) -> dict: + """Vrátí seznam záznamů dané entity. + + Args: + resource: název entity, např. ``"company"``, ``"person"``, ``"lead"``. + params: filtry / stránkování (``offset``, ``limit``, ``fulltext``, ...). + """ + return self._request("GET", f"/{self._res(resource)}/", params=params) + + def get_record(self, resource: str, record_id: int | str) -> dict: + """Vrátí detail jednoho záznamu.""" + return self._request("GET", f"/{self._res(resource)}/{record_id}/") + + def create_record(self, resource: str, data: dict) -> dict: + """Vytvoří záznam (RAYNET používá pro create metodu PUT).""" + if not isinstance(data, dict): + raise ValueError("Parametr 'data' musí být slovník (dict).") + return self._request("PUT", f"/{self._res(resource)}/", json=data) + + def update_record(self, resource: str, record_id: int | str, data: dict) -> dict: + """Upraví záznam (RAYNET používá pro update metodu POST).""" + if not isinstance(data, dict): + raise ValueError("Parametr 'data' musí být slovník (dict).") + return self._request("POST", f"/{self._res(resource)}/{record_id}/", json=data) + + def delete_record(self, resource: str, record_id: int | str) -> dict: + """Smaže záznam.""" + return self._request("DELETE", f"/{self._res(resource)}/{record_id}/") + + def call( + self, + method: str, + path: str, + params: Optional[dict] = None, + json: Optional[Any] = None, + ) -> dict: + """Univerzální volání pro vnořené zdroje a speciální akce. + + Příklady cest: ``/company/{id}/address/``, + ``/company/{id}/lock``, ``/company/{id}/merge/{sourceId}/``. + """ + return self._request(method.upper(), path, params=params, json=json) + + # ------------------------------------------------------------------ # + # Pojmenované zkratky pro nejčastější entity (tenké wrappery) # ------------------------------------------------------------------ # def create_company(self, data: dict) -> dict: """Vytvoří firmu (company) v RAYNET CRM. - RAYNET používá pro vytvoření záznamu HTTP metodu ``PUT`` na kolekci - ``/company/`` (nikoli POST). - - Args: - data: tělo požadavku dle RAYNET dokumentace. Povinná pole jsou - ``name``, ``rating``, ``state`` a ``role``. Příklad kompletní - struktury viz :data:`EXAMPLE_COMPANY` níže. - - Returns: - Parsovaná JSON odpověď serveru (obsahuje mj. ``id`` nové firmy). - - Raises: - ValueError: pokud ``data`` nejsou slovník nebo chybí povinná pole. - RaynetAuthError: při chybě autentizace (401/403). - RaynetValidationError: při zamítnutí dat serverem (4xx). - RaynetError: při ostatních chybách (síť, 5xx, nevalidní JSON). + Povinná pole: ``name``, ``rating``, ``state``, ``role``. + Příklad struktury viz :data:`EXAMPLE_COMPANY` níže. """ - if not isinstance(data, dict): - raise ValueError("Parametr 'data' musí být slovník (dict).") - missing = [f for f in ("name", "rating", "state", "role") if not data.get(f)] + missing = [f for f in ("name", "rating", "state", "role") if not (data or {}).get(f)] if missing: raise ValueError( "Firma musí mít vyplněná povinná pole: " + ", ".join(missing) + "." ) + return self.create_record("company", data) - return self._request("PUT", "/company/", json=data) + def list_companies(self, **params: Any) -> dict: + return self.list_records("company", params or None) + + def get_company(self, company_id: int | str) -> dict: + return self.get_record("company", company_id) + + def update_company(self, company_id: int | str, data: dict) -> dict: + return self.update_record("company", company_id, data) + + def delete_company(self, company_id: int | str) -> dict: + return self.delete_record("company", company_id) # ------------------------------------------------------------------ # # Interní HTTP vrstva # ------------------------------------------------------------------ # + @staticmethod + def _res(resource: str) -> str: + """Očistí název resource (bez lomítek), ať nejde sestavit divná URL.""" + cleaned = (resource or "").strip().strip("/") + if not cleaned: + raise ValueError("Název resource nesmí být prázdný.") + return cleaned + def _request(self, method: str, path: str, **kwargs: Any) -> dict: url = f"{self.base_url}/{path.lstrip('/')}" kwargs.setdefault("timeout", self.timeout) diff --git a/documentation/raynet-company.md b/documentation/raynet-company.md deleted file mode 100644 index 95f542f..0000000 --- a/documentation/raynet-company.md +++ /dev/null @@ -1,134 +0,0 @@ -# RAYNET konektor – vytvoření firmy - -Stateless proxy nad [RAYNET CRM API v2](https://app.raynetcrm.com/api/doc/index-en.html). - -## Autentizace RAYNET - -RAYNET API používá: - -- **HTTP Basic Auth**: username = email uživatele, password = **API klíč** -- hlavičku **`X-Instance-Name`** = název instance -- base URL: `https://app.raynet.cz/api/v2` - -V této službě se **API klíč (secret) předává hlavičkou `X-Api-Key`**. Email a název -instance jsou běžné identifikátory a předávají se v těle požadavku. - -> Secret se nikdy neloguje, necommituje ani nevrací z endpointů (viz AGENTS.md). - -## Endpoint - -```text -POST /company -``` - -Veřejně přes AppFactory reverse proxy: - -```text -POST https://services.csbot.cz/apps//company -``` - -### Hlavičky - -| Hlavička | Povinná | Popis | -|--------------|---------|-------------------------------| -| `X-Api-Key` | ano | RAYNET API klíč (secret) | - -### Tělo požadavku - -```json -{ - "email": "user@firma.cz", - "instance_name": "moje-instance", - "company": { - "name": "ACME s.r.o.", - "rating": "A", - "state": "A_POTENTIAL", - "role": "A_SUBSCRIBER", - "regNumber": "12345678", - "taxNumber": "CZ12345678", - "taxPayer": "YES", - "addresses": [ - { - "address": { - "name": "Sídlo klienta", - "street": "Francouzská 6167/5", - "city": "Ostrava", - "province": "Morava", - "zipCode": "708 00", - "country": "CZ" - }, - "contactInfo": { - "email": "info@acme.cz", - "tel1": "+420 553 401 520", - "tel1Type": "recepce", - "www": "www.acme.cz" - } - } - ], - "tags": ["import", "konektor"] - } -} -``` - -#### Povinná pole firmy - -| Pole | Typ | Hodnoty | -|----------|--------|---------------------------------------------------------------| -| `name` | string | název firmy | -| `rating` | enum | `A`, `B`, `C` | -| `state` | enum | `A_POTENTIAL`, `B_ACTUAL`, `C_DEFERRED`, `D_UNATTRACTIVE` | -| `role` | enum | `A_SUBSCRIBER`, `B_PARTNER`, `C_SUPPLIER`, `D_RIVAL` | - -Volitelná pole: `regNumber` (IČO), `taxNumber` (DIČ), `taxPayer` (`YES`/`NO`), -`bankAccount`, `notice`, `owner`, `category`, `legalForm`, `paymentTerm`, -`turnover`, `territory`, `addresses`, `tags`, `customFields` a další dle -RAYNET `CompanyInsertDto`. Neuvedená/`null` pole se do RAYNET neposílají. - -### Odpověď (200) - -```json -{ - "id": 123456, - "success": true, - "raw": { "...": "surová odpověď RAYNET" } -} -``` - -### Chybové stavy - -| HTTP | Kdy | -|------|------------------------------------------------------------| -| 400 | RAYNET odmítl data (nevalidní vstup) | -| 401 | chybný API klíč / email / instance (autentizace selhala) | -| 422 | nevalidní tělo požadavku (FastAPI validace) | -| 502 | RAYNET nedostupný / chyba serveru / timeout | - -## Použití konektoru přímo v Pythonu - -```python -from app.raynet_client import RaynetClient, RaynetError - -client = RaynetClient(api_key="...", email="user@firma.cz", instance_name="moje-instance") -try: - company = client.create_company({ - "name": "ACME s.r.o.", - "rating": "A", - "state": "A_POTENTIAL", - "role": "A_SUBSCRIBER", - }) - print(company["id"]) -except RaynetError as exc: - print("Chyba:", exc.message, exc.status_code) -``` - -`RaynetClient` drží `requests.Session` (Basic Auth + `X-Instance-Name`), -automaticky opakuje přechodné chyby (síť, 429, 5xx) a vyhazuje typované -výjimky `RaynetAuthError` / `RaynetValidationError` / `RaynetError`. - -## Ověření po nasazení - -```text -GET https://services.csbot.cz/apps//health -GET https://services.csbot.cz/apps//docs -POST https://services.csbot.cz/apps//company (Swagger „Try it out") -``` diff --git a/documentation/raynet-connector.md b/documentation/raynet-connector.md new file mode 100644 index 0000000..85bf8c5 --- /dev/null +++ b/documentation/raynet-connector.md @@ -0,0 +1,152 @@ +# RAYNET konektor + +Stateless proxy nad [RAYNET CRM API v2](https://app.raynetcrm.com/api/doc/index-en.html). +Pokrývá celé RAYNET API díky jednotným REST konvencím. + +## Přihlášení (společné pro celé připojení) + +RAYNET API používá HTTP Basic Auth (email + API klíč) a hlavičku s názvem +instance. V této službě se předává **třemi hlavičkami u každého requestu**: + +| Hlavička | Co to je | Kde to vzít | +|-----------------------|-------------------|-------------| +| `X-Api-Key` | API klíč (secret) | RAYNET CRM: **Nastavení → Klíč k API** | +| `X-Raynet-Email` | Email uživatele | Přihlašovací email do RAYNET | +| `X-Instance-Name` | Název instance | Identifikátor instance (subdoména účtu) | + +> `X-Api-Key` je secret – nikdy se neloguje, necommituje ani nevrací z endpointů +> (viz AGENTS.md). Tyto údaje jsou zobrazené i nahoře ve Swaggeru (`/docs`). + +Base URL RAYNET: `https://app.raynet.cz/api/v2`. + +## REST konvence RAYNET (platí pro celé API) + +| Operace | RAYNET metoda + cesta | +|---------|------------------------------| +| List | `GET /{resource}/` | +| Detail | `GET /{resource}/{id}/` | +| Create | `PUT /{resource}/` | +| Update | `POST /{resource}/{id}/` | +| Delete | `DELETE /{resource}/{id}/` | + +Příklady entit: `company`, `person`, `lead`, `businessCase`, `activity`, +`product`, `offer`, `order`, `project`, … + +## Endpointy služby + +Veřejně přes AppFactory reverse proxy: `https://services.csbot.cz/apps//...` + +### Typované zkratky pro firmy + +| Metoda | Cesta | Popis | +|--------|-------------------------|-------------------------------| +| POST | `/company` | Vytvoří firmu (validace) | +| GET | `/company` | Seznam firem (offset/limit/fulltext) | +| GET | `/company/{id}` | Detail firmy | +| PUT | `/company/{id}` | Úprava firmy | +| DELETE | `/company/{id}` | Smazání firmy | + +### Generický průchod na celé API + +| Metoda | Cesta | Mapuje se na RAYNET | +|--------|-----------------------------|-------------------------------| +| GET | `/api/{resource}` | `GET /{resource}/` (query params se předávají) | +| GET | `/api/{resource}/{id}` | `GET /{resource}/{id}/` | +| POST | `/api/{resource}` | `PUT /{resource}/` (create) | +| PUT | `/api/{resource}/{id}` | `POST /{resource}/{id}/` (update) | +| DELETE | `/api/{resource}/{id}` | `DELETE /{resource}/{id}/` | + +## Vytvoření firmy – povinná pole + +| Pole | Typ | Hodnoty | +|----------|--------|------------------------------------------------------------| +| `name` | string | název firmy | +| `rating` | enum | `A`, `B`, `C` | +| `state` | enum | `A_POTENTIAL`, `B_ACTUAL`, `C_DEFERRED`, `D_UNATTRACTIVE` | +| `role` | enum | `A_SUBSCRIBER`, `B_PARTNER`, `C_SUPPLIER`, `D_RIVAL` | + +Volitelná: `regNumber` (IČO), `taxNumber` (DIČ), `taxPayer` (`YES`/`NO`), +`bankAccount`, `notice`, `owner`, `category`, `legalForm`, `paymentTerm`, +`turnover`, `territory`, `addresses`, `tags`, `customFields` a další dle +RAYNET `CompanyInsertDto`. + +### Příklad těla `POST /company` + +```json +{ + "name": "ACME s.r.o.", + "rating": "A", + "state": "A_POTENTIAL", + "role": "A_SUBSCRIBER", + "regNumber": "12345678", + "taxNumber": "CZ12345678", + "taxPayer": "YES", + "addresses": [ + { + "address": { + "name": "Sídlo klienta", + "street": "Francouzská 6167/5", + "city": "Ostrava", + "province": "Morava", + "zipCode": "708 00", + "country": "CZ" + }, + "contactInfo": { + "email": "info@acme.cz", + "tel1": "+420 553 401 520", + "tel1Type": "recepce", + "www": "www.acme.cz" + } + } + ], + "tags": ["import", "konektor"] +} +``` + +## Chybové stavy + +| HTTP | Kdy | +|------|-----------------------------------------------------------| +| 400 | RAYNET odmítl data (nevalidní vstup) | +| 401 | chybný API klíč / email / instance (autentizace selhala) | +| 422 | nevalidní vstup (FastAPI validace nebo chybí povinné pole)| +| 502 | RAYNET nedostupný / chyba serveru / timeout | + +## Použití konektoru přímo v Pythonu + +```python +from app.raynet_client import RaynetClient, RaynetError + +client = RaynetClient(api_key="...", email="user@firma.cz", instance_name="moje-instance") +try: + # pojmenovaná zkratka + company = client.create_company({ + "name": "ACME s.r.o.", "rating": "A", + "state": "A_POTENTIAL", "role": "A_SUBSCRIBER", + }) + + # generické CRUD nad libovolnou entitou + leads = client.list_records("lead", {"limit": 50}) + deal = client.get_record("businessCase", 123) + client.update_record("person", 42, {"lastName": "Novák"}) + + # speciální akce / vnořené zdroje + client.call("POST", f"/company/{company['id']}/lock") +except RaynetError as exc: + print("Chyba:", exc.message, exc.status_code) +``` + +`RaynetClient` drží `requests.Session` (Basic Auth + `X-Instance-Name`), +automaticky opakuje přechodné chyby (síť, 429, 5xx) a vyhazuje typované +výjimky `RaynetAuthError` / `RaynetValidationError` / `RaynetError`. + +## Ověření po nasazení + +```text +GET https://services.csbot.cz/apps//health +GET https://services.csbot.cz/apps//docs +``` + +Ve Swagger UI ověř, že `Try it out` volá endpointy přes `/apps/` a že +jsou vyžadované hlavičky `X-Api-Key`, `X-Raynet-Email`, `X-Instance-Name`. +```