{
  "openapi": "3.1.0",
  "info": {
    "title": "BlackSheepMail API",
    "version": "1.0.0",
    "description": "Email-automatizációs SaaS HTTP-API-ja: ügyfél-, csoport-, sablon- és kampánykezelés, webshop-kapcsolatok, valamint egy gépi (AI-agent) képesség-pillanatkép. Ez a dokumentum CSAK az agent-releváns, fő végpontokat írja le (nem admin/belső/fizetési-webhook végpontokat) — a forrás a tényleges router/controller/dispatch kód (routing/ARouter.php, Controller/Controller.php, php/Adat_feldolg.php és a traits/ alatti handler trait-ek). ChatGPT custom GPT (Actions) importhoz NE ezt a fájlt használd, hanem a GPT-re szabott openapi.gpt.json variánst (abszolút servers-URL, ≤300 karakteres leírások, csak a Bearer-végpontok).",
    "contact": {
      "name": "BlackSheepMail"
    }
  },
  "servers": [
    {
      "url": "/",
      "description": "Relatív gyökér — a tényleges abszolút URL futásidőben az App::url() (APP_URL env) alapján alakul ki."
    }
  ],
  "components": {
    "securitySchemes": {
      "sessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "PHPSESSID",
        "description": "Session-alapú bejelentkezés. A nem-bejelentkezett GET /agent/* kérés 401 JSON-t kap (nem HTML-redirectet); a többi nem-bejelentkezett GET HTML-redirectet kap login-ra."
      },
      "csrfToken": {
        "type": "apiKey",
        "in": "header",
        "name": "X-CSRF-TOKEN",
        "description": "Minden mutáló (POST/PUT/PATCH/DELETE) kéréshez kötelező, a szerver-oldali session-tokennel kell egyeznie (hash_equals). Küldhető body-mezőként (csrf_token) is a header helyett. A routing/ARouter.php checkCsrf()-je központilag, minden POST-on ellenőrzi — kivéve a külső (signature-hitelesített) webhook/OAuth-callback végpontokat, amelyek NEM részei ennek a dokumentumnak."
      },
      "apiKulcs": {
        "type": "http",
        "scheme": "bearer",
        "description": "Per-user REST API-kulcs (Fázis E-1, scope-mátrix: CONN-1/CONN-1b) az Authorization: Bearer <token> fejlécben. A token alakja 'bsm_' + 64 hex karakter (összesen 68). A user a settings-felületen generálja/forgatja/vonja vissza (egy aktív kulcs / user). MINDEN kulcsnak van egy `scope` mezője: 'esemeny' (DEFAULT — csak a POST /api/esemeny hívható) | 'olvasas' (a POST /api/esemeny MELLETT az 5 olvasó GET-végpont is hívható — ügyfél/szekvencia/kampány/sablon lekérdezés) | 'teljes' (olvasás MELLETT a kampány-indítás 2 végpontja is hívható: POST /api/kampany/elokeszit + POST /api/kampany/inditas). Az esemény-küldés a scope-mátrix MINIMUMa: mindhárom scope-pal működik. A KAMPÁNY-INDÍTÁS KÉTLÉPÉSES, CHAT-ALAPÚ MEGERŐSÍTÉST IGÉNYEL: az AI-kliens ELŐBB a POST /api/kampany/elokeszit-et hívja (ez validál, összefoglalót ad, NEM indít semmit), majd a kapott összefoglalót MEGMUTATJA a felhasználónak a beszélgetésben és MEGKÉRDEZI, el akarja-e indítani — CSAK a felhasználó IGEN válaszára hívja a POST /api/kampany/inditas-t a kapott tokennel. Egy elszigetelt/véletlen toolhívás emiatt sosem indíthat kampányt (token nélkül az inditas 400-at ad). A hitelesítés: formátum-ellenőrzés → SHA-256 hash-lookup (csak 'aktiv' kulcs) → {user_id, scope}. Forrás: service/ApiKulcsService.php::hitelesit()."
      }
    },
    "schemas": {
      "AltalanosValasz": {
        "type": "object",
        "description": "A legtöbb JSON-végpont közös válasz-burka (Adat_feldolg::respond() és a WebshopService/Controller mintája).",
        "properties": {
          "success": { "type": "boolean" },
          "code": { "type": "integer", "description": "Gépi HTTP-szerű állapotkód (a tényleges HTTP-státusszal egyezően); némely régi (legacy) válaszban hiányzik, csak a HTTP-státusz hordozza." },
          "message": { "type": "string" },
          "error": { "type": "string", "description": "Gépi hibakód (pl. LIMIT_TULLEPVE, FUNKCIO_NEM_ELERHETO) — csak kapu-blokk esetén jelenik meg, a frontend ebből dönt 'Csomag bővítése' modal megjelenítéséről." },
          "data": {}
        },
        "required": ["success"]
      },
      "HibaValasz": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "const": false },
          "message": { "type": "string" },
          "code": { "type": "integer" },
          "error": { "type": "string" }
        },
        "required": ["success"]
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Nincs bejelentkezve.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/HibaValasz" },
            "example": { "success": false, "message": "Nincs bejelentkezve" }
          }
        }
      },
      "CsrfFailed": {
        "description": "Hiányzó vagy érvénytelen CSRF-token.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/HibaValasz" },
            "example": { "success": false, "message": "CSRF failed" }
          }
        }
      },
      "ValidationError": {
        "description": "Validációs hiba (pl. hiányzó/hibás mező).",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/HibaValasz" }
          }
        }
      },
      "Forbidden": {
        "description": "A művelet a fiók csomagjában/jogosultságában nem elérhető (entitlement-kapu blokkolta — lásd Hozzaferes).",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/HibaValasz" },
            "example": { "success": false, "message": "Ez a funkció a csomagodban nem érhető el.", "code": 403, "error": "FUNKCIO_NEM_ELERHETO" }
          }
        }
      },
      "Conflict": {
        "description": "Ütközés (pl. már létező, egyedi kulcsra ütköző rekord).",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/HibaValasz" }
          }
        }
      }
    }
  },
  "paths": {
    "/api/esemeny": {
      "post": {
        "summary": "Publikus esemény-push (szekvencia-trigger a user saját rendszeréből)",
        "description": "Forrás: Controller::api_esemeny_post() (routing/ARouter.php: publicPostRoutes + CSRF-kivétel; ALLOWED_METHODS: 'api_esemeny_post'). KÜLSŐ, NEM session/CSRF-védett hívás — Bearer-TOKENNEL hitelesít (apiKulcs security scheme, ApiKulcsService::hitelesit). A user a SAJÁT rendszeréből küld egy egyedi eseményt, ami a MEGLÉVŐ aszinkron feldolgozón át (WebshopFeldolgozoService) 'egyedi_esemeny' szekvencia-triggert indíthat. A kérés-kiszolgálás SZÁNDÉKOSAN könnyű (idempotens Webshop_esemenyek-insert), gyors 200 (Shopify-SLA elv); a tényleges beiratkoztatás a háttér-cron dolga. Rate-limit IP-alapú; a body-cap 64 KB.",
        "operationId": "apiEsemeny",
        "tags": ["Agent"],
        "security": [{ "apiKulcs": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["esemeny", "email"],
                "properties": {
                  "esemeny": { "type": "string", "maxLength": 80, "pattern": "^[A-Za-z0-9_.-]+$", "description": "Az egyedi esemény neve — ehhez illeszkedik a szekvencia 'egyedi_esemeny' trigger-node config.esemeny_nev-je." },
                  "email": { "type": "string", "format": "email", "description": "Az ügyfél email-je (FILTER_VALIDATE_EMAIL). Az ügyfelet email-egyezésből oldja fel a feldolgozó (find-only — nem hoz létre új ügyfelet)." },
                  "nev": { "type": "string", "maxLength": 255, "description": "Opcionális ügyfélnév." },
                  "esemeny_azonosito": { "type": "string", "maxLength": 255, "description": "Opcionális idempotencia-kulcs — ha megadva, ugyanaz az azonosító kétszer beküldve NEM duplikál (Webshop_esemenyek UNIQUE). Ha hiányzik, a szerver generál egyet." },
                  "adatok": { "type": "object", "description": "Opcionális kulcs=>érték map a sablon-tokenekhez ({{adat.<kulcs>}}). Korlátok: max 50 kulcs, kulcs-regex ^[A-Za-z0-9_]{1,40}$, érték max 500 karakter (a SzekvenciaEnrollService::adatTokenek() szabálya)." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Az esemény rögzítve (idempotens: duplikátum esetén is 200).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "const": true },
                    "uj": { "type": "boolean", "description": "true, ha TÉNYLEGESEN új sor jött létre; false, ha ez egy már ismert esemény (idempotens duplikátum)." }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Érvénytelen JSON vagy hiányzó/hibás mező (esemeny/email/adatok).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } }
          },
          "401": {
            "description": "Érvénytelen vagy hiányzó API-kulcs (Bearer token).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } }
          },
          "413": {
            "description": "A kérés törzse túl nagy (> 64 KB).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } }
          },
          "429": {
            "description": "Túl sok kérés (IP-alapú rate-limit) — a retry_after mezőben a visszaszámláló.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } }
          }
        }
      }
    },
    "/api/tranzakcios": {
      "post": {
        "summary": "Tranzakciós email küldése (sablonos, EGY címzett)",
        "description": "Forrás: Controller::api_tranzakcios_post() → TranzakciosKuldesService::kuld() (routing/ARouter.php: publicPostRoutes + CSRF-kivétel; ALLOWED_METHODS: 'api_tranzakcios_post'). KÜLSŐ, NEM session/CSRF-védett hívás — Bearer-TOKENNEL hitelesít (apiKulcs security scheme, ApiKulcsService::hitelesit), a MINIMÁLIS scope **'teljes'** (Security-audit H-1, 2026-07-29: a küldés SZÁNDÉKOSAN nem tartozik az 'iras' scope-ba — annak szerződése kifejezetten NEM-KÜLDŐ írás, hogy egy AI-asszisztensnek adott kulcs ne levelezhessen a névjegyzékkel; 'esemeny'/'olvasas'/'iras' kulcs 403-at kap). Hívásonként PONTOSAN EGY címzett kap egy SABLONOS levelet (jelszó-emlékeztető, rendelés-visszaigazolás, számla-értesítő) — ez NEM tömeges/marketing-küldés. FIZETŐS CSOMAG KELL HOZZÁ: a 'tranzakcios_kuldes' capability a 2-es (Standard), 3-as (AI), 4-es (Pro) és 5-ös (Enterprise) csomagon él, a Free-n NEM (403, error=FUNKCIO_NEM_ELERHETO). ⚠️ KÜLÖN KVÓTA: a fogyasztás a 'tranzakcios' flow-metrikát terheli, ami a kampány-küldés 'kuldes' kvótájától TELJESEN FÜGGETLEN (havi periódus; Free 0 · Standard 1000 · AI 10000 · Pro 50000 · Enterprise 250000) — a tranzakciós levelek tehát NEM fogyasztják a kampány-keretet, és fordítva sem. A havi kvóta MELLETT a választott csatorna NAPI kerete is érvényes (429, error=NAPI_LIMIT_TULLEPVE). ⚠️ HÁROM SZÁNDÉKOS ELTÉRÉS a marketing-küldéstől (Ádám döntése — termék-döntés, nem hiba): (a) a tranzakciós küldés NEM követi a leiratkozási listát (nincs 'kihagyando_cimek'/suppression-szűrés) — egy jelszó-emlékeztetőnek akkor is ki kell mennie, ha a címzett a MARKETINGRŐL leiratkozott (iparági norma: MailerSend/Mandrill/Brevo); (b) a lábléc leiratkozó-link ÉS megnyitás-pixel NÉLKÜL megy (EmailKuldoSeged::lablecTranzakcios), ezért ezekre a levelekre megnyitási statisztika NEM keletkezik; (c) nincs kattintás-követő link-átírás sem — egy jelszó-visszaállító linket a saját redirectünkön átvezetni kifejezetten káros. A visszaélés (marketing átcsempészése ezen az úton) ellen a fék nem technikai tiltás, hanem: szűkebb külön kvóta + hívásonként egy címzett + fizetős-csomag kapu + teljes naplózás (Email_kuldesi_naplo.forras = 'tranzakcios'). A küldés SZINKRON: a válasz naplo_id-je a keletkezett Email_kuldesi_naplo sor azonosítója.",
        "operationId": "apiTranzakcios",
        "tags": ["Agent"],
        "security": [{ "apiKulcs": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email"],
                "description": "A sablonra a 'sablon_id' VAGY a 'sablon_nev' mezővel hivatkozz — legalább az EGYIK kötelező (ha egyik sincs: 400). Ha MINDKETTŐT megadod, a 'sablon_id' élvez elsőbbséget (TranzakciosKuldesService::sablonFeloldas — nem hiba, de kerülendő).",
                "properties": {
                  "sablon_id": { "type": "integer", "minimum": 1, "description": "A user SAJÁT sablonjának azonosítója (a GET /api/sablonok eredményéből). A feloldás SablonService::egy($user_id, $id)-vel, user-szűrten történik, így idegen user sablonjára hivatkozva 404 a válasz (IDOR-védett)." },
                  "sablon_nev": { "type": "string", "maxLength": 255, "description": "A user SAJÁT sablonjának PONTOS neve — a sablon_id alternatívája (kényelmi feloldás névből, a saját sablonlistán trim-elt, teljes egyezéssel; azonos nevű sablonok esetén az elsőt találja meg). Nem létező név esetén 404." },
                  "email": { "type": "string", "format": "email", "maxLength": 255, "description": "A címzett email-címe (FILTER_VALIDATE_EMAIL, max 255 karakter). PONTOSAN EGY címzett hívásonként — nincs tömb/vesszős lista. A cím NEM kell hogy meglévő ügyfél legyen." },
                  "valtozok": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Opcionális kulcs=>érték map a sablon helyettesítő-tokenjeihez. MINDKÉT token-forma cserélődik: [kulcs] és {{kulcs}}. Korlátok (TranzakciosKuldesService::valtozokTisztitasa): max 30 kulcs, kulcs-regex ^[A-Za-z0-9_]{1,40}$, érték max 500 karakter — a szabálytalan kulcsok/nem-skalár értékek NÉMÁN kimaradnak. Minden behelyettesített érték htmlspecialchars-olt (XSS-védelem), és a csere EGYETLEN menetben történik, így egy behelyettesített érték nem válhat maga is token-forrássá. A [email]/{{email}} token beépítetten a címzett címét kapja, de saját 'email' kulcs felülírhatja." },
                  "csatorna": { "type": "string", "enum": ["gmail_api", "smtp", "ses"], "description": "Opcionális küldési csatorna. Ha elhagyod, a user ténylegesen bekötött csatornája dönt (Hozzaferes::kuldesiCsatorna); ha nincs bekötött csatorna, a válasz 409. Ismeretlen érték esetén BESZÉDES 400 (szándékosan nincs néma fallback). A választott csatorna a csomag-képességéhez is kötött (smtp_standard / smtp_ses)." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A levél kiment — a 'naplo_id' a keletkezett Email_kuldesi_naplo sor azonosítója (forras='tranzakcios'), amivel a küldés utólag visszakereshető.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "const": true },
                    "naplo_id": { "type": "integer", "description": "Az Email_kuldesi_naplo sor azonosítója." }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Érvénytelen JSON, hiányzó/hibás 'email', hiányzó sablon-hivatkozás (sem sablon_id, sem sablon_nev), nem-objektum 'valtozok', vagy ismeretlen 'csatorna' érték.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } }
          },
          "401": {
            "description": "Érvénytelen vagy hiányzó API-kulcs (Bearer token). A hibás kísérlet abuse-védelmi számlálót növel.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } }
          },
          "403": {
            "description": "Jogosultság-hiány: (a) a kulcs scope-ja nem 'teljes' (a küldés SZÁNDÉKOSAN nem tartozik az 'iras' scope-ba — az kifejezetten nem-küldő írás); VAGY (b) a csomag nem tartalmazza a 'tranzakcios_kuldes' képességet (Free) — ekkor a válasz 'error' mezője FUNKCIO_NEM_ELERHETO; VAGY (c) a választott csatorna a csomagban nem engedélyezett.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } }
          },
          "404": {
            "description": "A megadott sablon nem található (nem létező id/név, vagy nem a hívó userhez tartozik).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } }
          },
          "409": {
            "description": "A felhasználónak nincs bekötött küldési csatornája (Google-fiók / SMTP / Amazon SES) — előbb a Beállításokban kell összekötni.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } }
          },
          "413": {
            "description": "A kérés törzse túl nagy (> 64 KB).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } }
          },
          "429": {
            "description": "Kvóta/ütem-korlát: (a) IP-alapú rate-limit (a retry_after mezőben a visszaszámláló); (b) a havi 'tranzakcios' kvóta elfogyott — error=LIMIT_TULLEPVE, a kampány-kvótától FÜGGETLEN keret; (c) a csatorna napi kerete elfogyott — error=NAPI_LIMIT_TULLEPVE. A (b)/(c) esetben a válasz 'naplo_id'-t is hordozhat (a napló-sor 'hibas' állapotban keletkezett).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } }
          },
          "500": {
            "description": "Váratlan küldési hiba (a napló-sor 'hibas' állapotba kerül, a lefoglalt kvóta visszaíródik).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } }
          }
        }
      }
    },
    "/api/ugyfelek": {
      "get": {
        "summary": "Ügyfél-keresés/lista (AI-connector olvasó végpont)",
        "description": "Forrás: Controller::api_ugyfelek() → UgyfelService::kereses() → Ugyfelek::kereses_lapozott(). Bearer-hitelesített, scope='olvasas' VAGY 'teljes' KÖTELEZŐ (scope='esemeny' kulcs 403-at kap). User-szűrt (a Bearer-kulcs user_id-je), read-only, lapozható. Rate-limit: közös 'api_olvasas' kulcson (IP-alapú).",
        "operationId": "apiUgyfelek",
        "tags": ["Agent"],
        "security": [{ "apiKulcs": [] }],
        "parameters": [
          { "name": "q", "in": "query", "required": false, "schema": { "type": "string", "maxLength": 100 }, "description": "Opcionális keresőszó — LIKE-illeszkedés vezetéknévre/keresztnévre/becenévre/e-mailre." },
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }, "description": "Max 100." },
          { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 0, "default": 0 } }
        ],
        "responses": {
          "200": {
            "description": "Sikeres lekérés.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "const": true },
                    "code": { "type": "integer", "const": 200 },
                    "limit": { "type": "integer" },
                    "offset": { "type": "integer" },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "ugyfel_id": { "type": "integer" },
                          "vezetek_nev": { "type": "string" },
                          "kereszt_nev": { "type": "string" },
                          "bece_nev": {},
                          "email": { "type": "string" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Érvénytelen vagy hiányzó API-kulcs.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "403": { "description": "A kulcs scope-ja 'esemeny' — olvasási jog (scope='olvasas') szükséges.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "429": { "description": "Túl sok kérés (rate-limit).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } }
        }
      }
    },
    "/api/szekvenciak": {
      "get": {
        "summary": "A user szekvenciáinak listája (AI-connector olvasó végpont)",
        "description": "Forrás: Controller::api_szekvenciak() → SzekvenciaService::lista(). Bearer-hitelesített, scope='olvasas' VAGY 'teljes' KÖTELEZŐ. User-szűrt, read-only. Igényli az 'email_szekvencia' csomag-képességet (403, ha a csomag nem tartalmazza).",
        "operationId": "apiSzekvenciak",
        "tags": ["Agent"],
        "security": [{ "apiKulcs": [] }],
        "responses": {
          "200": {
            "description": "Sikeres lekérés.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "const": true },
                    "code": { "type": "integer", "const": 200 },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "integer" },
                          "nev": { "type": "string" },
                          "allapot": { "type": "string" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Érvénytelen vagy hiányzó API-kulcs.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "403": { "description": "A kulcs scope-ja 'esemeny' — olvasási jog szükséges.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "429": { "description": "Túl sok kérés (rate-limit).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } }
        }
      }
    },
    "/api/szekvencia-statisztika": {
      "get": {
        "summary": "Egy szekvencia monitorozási statisztikája (AI-connector olvasó végpont)",
        "description": "Forrás: Controller::api_szekvencia_statisztika() → SzekvenciaService::statisztika($id). Bearer-hitelesített, scope='olvasas' VAGY 'teljes' KÖTELEZŐ. IDOR-védett (a szekvencia a Bearer-kulcs user_id-jének sajátja kell legyen — 404 különben). Igényli az 'email_szekvencia' csomag-képességet (403, ha a csomag nem tartalmazza).",
        "operationId": "apiSzekvenciaStatisztika",
        "tags": ["Agent"],
        "security": [{ "apiKulcs": [] }],
        "parameters": [
          { "name": "id", "in": "query", "required": true, "schema": { "type": "integer" }, "description": "A szekvencia azonosítója." }
        ],
        "responses": {
          "200": {
            "description": "Sikeres lekérés.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "const": true },
                    "code": { "type": "integer", "const": 200 },
                    "osszesito": {
                      "type": "object",
                      "properties": {
                        "beiratkozott": { "type": "integer" },
                        "aktiv": { "type": "integer" },
                        "kesz": { "type": "integer" },
                        "leallt": { "type": "integer" },
                        "cel_elerve": { "type": "integer" }
                      }
                    },
                    "nodeok": { "type": "object", "description": "node_id => {itt_all, kuldott, megnyitott, kattintott}." }
                  }
                }
              }
            }
          },
          "400": { "description": "Hiányzó vagy érvénytelen 'id' paraméter.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "401": { "description": "Érvénytelen vagy hiányzó API-kulcs.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "403": { "description": "A kulcs scope-ja 'esemeny' — olvasási jog szükséges.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "404": { "description": "A szekvencia nem található (vagy nem a kulcs usere).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "429": { "description": "Túl sok kérés (rate-limit).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } }
        }
      }
    },
    "/api/kampanyok": {
      "get": {
        "summary": "Kampány-lista (AI-connector olvasó végpont)",
        "description": "Forrás: Controller::api_kampanyok() → KampanyService::listaz(). Bearer-hitelesített, scope='olvasas' VAGY 'teljes' KÖTELEZŐ. User-szűrt, read-only, lapozható (PHP-oldali array_slice a meglévő, nem lapozott listaz()-eredményen).",
        "operationId": "apiKampanyok",
        "tags": ["Agent"],
        "security": [{ "apiKulcs": [] }],
        "parameters": [
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 } },
          { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 0, "default": 0 } }
        ],
        "responses": {
          "200": {
            "description": "Sikeres lekérés.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "const": true },
                    "code": { "type": "integer", "const": 200 },
                    "limit": { "type": "integer" },
                    "offset": { "type": "integer" },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "integer" },
                          "nev": { "type": "string" },
                          "allapot": {},
                          "csatorna": { "type": "string" },
                          "idopont": {}
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Érvénytelen vagy hiányzó API-kulcs.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "403": { "description": "A kulcs scope-ja 'esemeny' — olvasási jog szükséges.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "429": { "description": "Túl sok kérés (rate-limit).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } }
        }
      }
    },
    "/api/sablonok": {
      "get": {
        "summary": "Sablon-lista (AI-connector olvasó végpont)",
        "description": "Forrás: Controller::api_sablonok() → SablonService::listaz(). Bearer-hitelesített, scope='olvasas' VAGY 'teljes' KÖTELEZŐ. User-szűrt, read-only.",
        "operationId": "apiSablonok",
        "tags": ["Agent"],
        "security": [{ "apiKulcs": [] }],
        "responses": {
          "200": {
            "description": "Sikeres lekérés.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "const": true },
                    "code": { "type": "integer", "const": 200 },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "integer" },
                          "nev": { "type": "string" },
                          "targy": { "type": "string" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Érvénytelen vagy hiányzó API-kulcs.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "403": { "description": "A kulcs scope-ja 'esemeny' — olvasási jog szükséges.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "429": { "description": "Túl sok kérés (rate-limit).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } }
        }
      }
    },
    "/api/kampany/elokeszit": {
      "post": {
        "summary": "Kampány-indítás ELŐKÉSZÍTÉSE (CONN-1b, 1. lépés) — összefoglaló + megerősítés-token, NEM indít",
        "description": "Forrás: Controller::api_kampany_elokeszit_post() → ApiKampanyService::elokeszit(). Bearer-hitelesített, scope='teljes' KÖTELEZŐ. A KÉTLÉPÉSES chat-megerősítési folyamat 1. lépése: NÉV→id feloldja a sablont és a csoportot (USER-SZŰRT — más felhasználó sablonja/csoportja sosem található), ellenőrzi hogy van-e küldési csatorna (Google-fiók vagy SMTP), és egy emberi nyelvű összefoglalót (`osszefoglalo.szoveg`) + egy rövid élettartamú (~10 perc), egyszer-használatos megerősítés-tokent ad vissza. EZ A HÍVÁS NEM INDÍT KAMPÁNYT, nem foglal küldési kvótát, nem hoz létre kampány-rekordot — csak a tokent és a hozzá tartozó adatot tárolja. Az AI-kliensnek KÖTELEZŐ ezután megmutatnia az `osszefoglalo.szoveg`-et a felhasználónak a beszélgetésben és megkérdeznie, el akarja-e indítani a kampányt — csak IGEN válasz esetén szabad meghívnia a POST /api/kampany/inditas-t a kapott tokennel.",
        "operationId": "apiKampanyElokeszit",
        "tags": ["Agent"],
        "security": [{ "apiKulcs": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kampany_nev": { "type": "string", "maxLength": 255, "description": "A leendő kampány neve." },
                  "sablon_nev": { "type": "string", "description": "A felhasználó SAJÁT sablonjának neve (pontos egyezés, NÉV→id feloldás)." },
                  "csoport_nev": { "type": "string", "description": "A felhasználó SAJÁT csoportjának neve (pontos egyezés, NÉV→id feloldás)." },
                  "idopont": { "type": "string", "nullable": true, "description": "Kiküldési időpont 'Y-m-d H:i:s' alakban. Hiányzó/null esetén AZONNALI kiküldés." }
                },
                "required": ["kampany_nev", "sablon_nev", "csoport_nev"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sikeres előkészítés — az összefoglalót MUTASD MEG a felhasználónak, és csak a jóváhagyására hívd az inditas-t.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "const": true },
                    "token": { "type": "string", "description": "Egyszer-használatos, ~10 percig érvényes megerősítés-token — add tovább a POST /api/kampany/inditas hívásban." },
                    "osszefoglalo": {
                      "type": "object",
                      "properties": {
                        "kampany_nev": { "type": "string" },
                        "sablon_nev": { "type": "string" },
                        "csoport_nev": { "type": "string" },
                        "cimzett_szam": { "type": "integer", "description": "A célcsoport becsült (aktuális) címzett-száma." },
                        "csatorna": { "type": "string", "description": "'gmail_api' vagy 'smtp' — a ténylegesen bekötött küldési csatorna." },
                        "idopont": { "type": "string", "nullable": true, "description": "null = azonnali kiküldés." },
                        "szoveg": { "type": "string", "description": "Emberi nyelvű összefoglaló — EZT MUTASD MEG a felhasználónak, és kérdezd meg, el akarja-e indítani." }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Hiányzó/érvénytelen mező vagy időpont-formátum.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "401": { "description": "Érvénytelen vagy hiányzó API-kulcs.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "403": { "description": "A kulcs scope-ja nem 'teljes' — kampány-indítási jog szükséges.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "404": { "description": "A megadott nevű sablon vagy csoport nem található (a kulcs userénél).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "409": { "description": "Nincs bekötött küldési csatorna (Google-fiók/SMTP).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "429": { "description": "Túl sok kérés (rate-limit).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } }
        }
      }
    },
    "/api/kampany/inditas": {
      "post": {
        "summary": "Kampány-indítás VÉGREHAJTÁSA (CONN-1b, 2. lépés) — a token birtokában TÉNYLEGESEN elindít",
        "description": "Forrás: Controller::api_kampany_inditas_post() → ApiKampanyService::inditas(). Bearer-hitelesített, scope='teljes' KÖTELEZŐ. A KÉTLÉPÉSES chat-megerősítési folyamat 2. lépése — KIZÁRÓLAG a POST /api/kampany/elokeszit válaszában kapott token birtokában hívható, ÉS csak azután, hogy az AI-kliens megmutatta az összefoglalót a felhasználónak és a felhasználó megerősítette (IGEN). A token EGYSZER-HASZNÁLATOS (atomi adatbázis-művelettel védett a dupla-beváltás/replay ellen — párhuzamos/ismételt hívás 409-et kap), ~10 percig érvényes, és a kibocsátó Bearer-kulcs userére kötött (más felhasználó tokenje nem található). A tényleges indítás a MEGLÉVŐ, kapus KampanyService::letrehoz() úton történik — a küldési csatorna, a küldési kvóta és a tulajdonjog ott ÚJRA érvényesül. Minden sikeres indítás naplózva (audit-nyom).",
        "operationId": "apiKampanyInditas",
        "tags": ["Agent"],
        "security": [{ "apiKulcs": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": { "type": "string", "description": "A POST /api/kampany/elokeszit válaszában kapott megerősítés-token." }
                },
                "required": ["token"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A kampány sikeresen elindult.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "const": true },
                    "kampany_id": { "type": "integer" },
                    "message": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": { "description": "Hiányzó, érvénytelen vagy lejárt token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "401": { "description": "Érvénytelen vagy hiányzó API-kulcs.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "403": { "description": "A kulcs scope-ja nem 'teljes' — kampány-indítási jog szükséges.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "409": { "description": "A token már fel lett használva (dupla-beváltás), VAGY a kampány címzettjei nem férnek bele a jelenlegi küldési kvótába (ez utóbbi esetben az appban, a Kampányok oldalon vihető végig a listaszűkítés/várakozás döntés).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } },
          "429": { "description": "Túl sok kérés (rate-limit).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } }
        }
      }
    },
    "/agent/capabilities": {
      "get": {
        "summary": "Gépi (AI-agent) képesség- és limit-pillanatkép",
        "description": "Forrás: Controller/Controller.php agent_capabilities() (routing/ARouter.php ALLOWED_METHODS: 'agent_capabilities'). Read-only, nincs mellékhatás. A jelenlegi bejelentkezett fiók csomagját, képességeit, effektív limiteit, használatát, birtokolt add-onjait és a teljes csomag/add-on katalógust adja vissza (Hozzaferes::frontendSnapshot() az egyetlen igazságforrás).",
        "operationId": "agentCapabilities",
        "tags": ["Agent"],
        "security": [{ "sessionCookie": [] }],
        "responses": {
          "200": {
            "description": "Sikeres lekérés.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "const": true },
                    "plan": {
                      "type": "object",
                      "properties": {
                        "id": {},
                        "kod": { "type": "string" },
                        "nev": { "type": "string" },
                        "admin": { "type": "boolean" },
                        "expires_at": {}
                      }
                    },
                    "capabilities": { "type": "object", "description": "A snapshot 'kepessegek' mezője — kép.-kulcs => bool (a tényleges alak a Hozzaferes::frontendSnapshot()-ból; nem bontottuk ki tovább, hogy ne találjunk ki mezőneveket)." },
                    "limits": { "type": "object", "description": "Effektív (add-on bónuszokkal növelt) limitek metrika => szám." },
                    "usage": { "type": "object", "description": "A snapshot 'hasznalat' mezője — metrika => felhasznált mennyiség." },
                    "addons": { "type": "object", "description": "A felhasználó birtokolt add-onjai (részletesen)." },
                    "catalog": {
                      "type": "object",
                      "properties": {
                        "plans": { "type": "array", "items": {} },
                        "addons": { "type": "array", "items": {} }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/letrehozas": {
      "get": {
        "summary": "Ügyfelek nézet (HTML)",
        "description": "Forrás: Controller::letrehozas() — a view/ugyfel.php HTML-oldalt adja vissza (nem gépi JSON). Az agent számára elsősorban a POST /letrehozas (source-alapú JSON-API) releváns.",
        "operationId": "ugyfelekNezet",
        "tags": ["Ugyfel"],
        "security": [{ "sessionCookie": [] }],
        "responses": {
          "200": { "description": "HTML.", "content": { "text/html": { "schema": { "type": "string" } } } }
        }
      },
      "post": {
        "summary": "Ügyfél-műveletek (létrehozás / listázás / egy lekérése / módosítás)",
        "description": "Forrás: Controller::letrehozas_post() → Adat_feldolg::melyik_oldal_kuldi() switch ($_POST['source']). A 'source' mező dönti el a konkrét műveletet (lásd a request body description-jét). Validáció: traits/UgyfelHandlerTrait.php. Security-pontosítás: a route a routerben publikus POST (a regisztrációs 'source' session nélkül is elérhető), de MINDEN ügyfél-művelet handler-szinten session-user_id-hoz kötött — session nélkül az ügyfél-források hibát adnak; az itt jelölt sessionCookie+csrfToken az ügyfél-műveletekre vonatkozik.",
        "operationId": "ugyfelMuvelet",
        "tags": ["Ugyfel"],
        "security": [{ "sessionCookie": [], "csrfToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": ["source"],
                "properties": {
                  "source": {
                    "type": "string",
                    "enum": ["letrehozas", "letrehozas_ugyfel", "letrehozas_egy_ugyfel__lekeres", "egy_ugyfel_modositas"],
                    "description": "'letrehozas' = új ügyfél létrehozása + lista visszaadása; 'letrehozas_ugyfel' = csak listázás; 'letrehozas_egy_ugyfel__lekeres' = egy ügyfél lekérése (ugyfel_id-val); 'egy_ugyfel_modositas' = egy ügyfél módosítása."
                  },
                  "teljes_nev": { "type": "string", "description": "Kötelező 'letrehozas' és 'egy_ugyfel_modositas' esetén — a backend szétszedi vezetéknév/keresztnév-re (magyar sorrend)." },
                  "becenev": { "type": "string", "description": "Kötelező; '###' sentinel = nincs becenév (NULL-lá válik). 3-20 karakter, Unicode betű/szám/_." },
                  "email": { "type": "string", "format": "email" },
                  "szuletesi_ev": { "type": "string", "description": "Formátum: YYYY-MM-DD. Üres vagy '1000-13-32' sentinel = NULL." },
                  "foglalkozas": { "type": "string", "description": "'###' sentinel = NULL. Csak betű+szóköz." },
                  "nevnap": { "type": "string", "description": "Formátum: MM-DD. Üres vagy '13-32' sentinel = nincs névnap." },
                  "ugyfel_id": { "type": "integer", "description": "Kötelező 'letrehozas_egy_ugyfel__lekeres' és 'egy_ugyfel_modositas' esetén." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sikeres művelet — a konkrét 'source' szerint listát vagy egy rekordot ad vissza data-ban.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AltalanosValasz" } } }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/CsrfFailed" }
        }
      }
    },
    "/sablon": {
      "get": {
        "summary": "Sablonok nézet (HTML)",
        "description": "Forrás: Controller::sablon() — view/sablon.php HTML. Az agent számára a POST /sablon a releváns JSON-API.",
        "operationId": "sablonNezet",
        "tags": ["Sablon"],
        "security": [{ "sessionCookie": [] }],
        "responses": { "200": { "description": "HTML.", "content": { "text/html": { "schema": { "type": "string" } } } } }
      },
      "post": {
        "summary": "Sablon-műveletek (létrehozás / listázás / egy lekérése / módosítás / vizuális szerkesztő)",
        "description": "Forrás: Controller::sablon_post() → Adat_feldolg switch ($_POST['source']). Validáció: traits/SablonHandlerTrait.php. A 'sablon_vizualis' / 'egy_sablon_vizualis_modositas' ágakhoz a fiók csomagjának tartalmaznia kell a 'vizualis_szerkeszto' képességet (entitlement-kapu, lásd Hozzaferes) — különben 403 FUNKCIO_NEM_ELERHETO.",
        "operationId": "sablonMuvelet",
        "tags": ["Sablon"],
        "security": [{ "sessionCookie": [], "csrfToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": ["source"],
                "properties": {
                  "source": {
                    "type": "string",
                    "enum": ["Sablon", "Sablon_ki", "egy_sablon_ki", "egy_sablon_modositas", "sablon_vizualis", "egy_sablon_vizualis_modositas"],
                    "description": "'Sablon' = új (sima szöveges) sablon létrehozása; 'Sablon_ki' = listázás; 'egy_sablon_ki' = egy sablon lekérése; 'egy_sablon_modositas' = sima sablon módosítása; 'sablon_vizualis' = új vizuális (HTML, drag-and-drop szerkesztő) sablon létrehozása; 'egy_sablon_vizualis_modositas' = vizuális sablon módosítása."
                  },
                  "nev": { "type": "string", "description": "Kötelező. Max 150 karakter, vezérlő karakter nélkül." },
                  "targy": { "type": "string", "description": "Kötelező (email tárgy). Max 255 karakter." },
                  "tartalom": {
                    "type": "string",
                    "description": "Kötelező. A sima ('Sablon'/'egy_sablon_modositas') ágon NEM tartalmazhat HTML taget (max 50 000 karakter); a vizuális ('sablon_vizualis'/'egy_sablon_vizualis_modositas') ágon a HTML a VÁRT formátum (sanitizálva, max 3 000 000 karakter a beágyazott képek miatt)."
                  },
                  "sablon_id": { "type": "integer", "description": "Kötelező 'egy_sablon_ki', 'egy_sablon_modositas', 'egy_sablon_vizualis_modositas' esetén." }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Sikeres művelet.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AltalanosValasz" } } } },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/csoport": {
      "get": {
        "summary": "Csoportok nézet (HTML)",
        "description": "Forrás: Controller::csoport() — view/csoport.php HTML. Az agent számára a POST /csoport a releváns JSON-API.",
        "operationId": "csoportNezet",
        "tags": ["Csoport"],
        "security": [{ "sessionCookie": [] }],
        "responses": { "200": { "description": "HTML.", "content": { "text/html": { "schema": { "type": "string" } } } } }
      },
      "post": {
        "summary": "Csoport-műveletek (létrehozás / listázás / egy lekérése / módosítás — tagok hozzáadása/eltávolítása)",
        "description": "Forrás: Controller::csoport_post() → Adat_feldolg switch ($_POST['source']). Validáció: traits/CsoportHandlerTrait.php.",
        "operationId": "csoportMuvelet",
        "tags": ["Csoport"],
        "security": [{ "sessionCookie": [], "csrfToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": ["source"],
                "properties": {
                  "source": {
                    "type": "string",
                    "enum": ["Csoport", "csoport_ki", "egy_csoport_ki", "egy_csoport_modosit"],
                    "description": "'Csoport' = új csoport létrehozása (local_list ügyfél-id-kból); 'csoport_ki' = listázás; 'egy_csoport_ki' = egy csoport lekérése; 'egy_csoport_modosit' = csoport módosítása (név + tagok hozzáadása/törlése)."
                  },
                  "csoport_nev": { "type": "string", "description": "Kötelező 'Csoport' és 'egy_csoport_modosit' esetén. 3-50 karakter, '<', '>', 'script' nélkül." },
                  "local_list": { "type": "string", "description": "'Csoport' esetén kötelező: JSON-tömb stringként, elemei {\"id\": <ügyfél_id>} alakban (üres tömb is megengedett)." },
                  "csoport_id": { "type": "integer", "description": "Kötelező 'egy_csoport_ki' és 'egy_csoport_modosit' esetén." },
                  "torlendo_csoport_ugyfelek": { "type": "string", "description": "'egy_csoport_modosit' esetén opcionális: JSON-tömb stringként, [{\"id\": <ügyfél_id>}, ...] — a csoportból eltávolítandó ügyfelek." },
                  "csoport_ugyfelek_hozzaadas": { "type": "string", "description": "'egy_csoport_modosit' esetén opcionális: JSON-tömb stringként, [{\"id\": <ügyfél_id>}, ...] — a csoporthoz hozzáadandó ügyfelek. Ugyanaz az id nem szerepelhet egyszerre a törlendő és a hozzáadandó listában." }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Sikeres művelet.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AltalanosValasz" } } } },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/CsrfFailed" }
        }
      }
    },
    "/kampany": {
      "get": {
        "summary": "Kampányok nézet (HTML)",
        "description": "Forrás: Controller::kampany() — view/kampany.php HTML. Az agent számára a POST /kampany a releváns JSON-API.",
        "operationId": "kampanyNezet",
        "tags": ["Kampany"],
        "security": [{ "sessionCookie": [] }],
        "responses": { "200": { "description": "HTML.", "content": { "text/html": { "schema": { "type": "string" } } } } }
      },
      "post": {
        "summary": "Kampány-műveletek (létrehozás / listázás / egy lekérése / módosítás / próba-email)",
        "description": "Forrás: Controller::kampany_post() → Adat_feldolg switch ($_POST['source']). Validáció: traits/KampanyHandlerTrait.php. A 'kuldo_csatorna'='smtp' választáshoz a fiók csomagjának tartalmaznia kell az 'smtp_standard' képességet (entitlement-kapu) ÉS beállított SMTP-t — különben a kapu blokkolja (403, error mező a frontend 'Csomag bővítése' modaljához).",
        "operationId": "kampanyMuvelet",
        "tags": ["Kampany"],
        "security": [{ "sessionCookie": [], "csrfToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": ["source"],
                "properties": {
                  "source": {
                    "type": "string",
                    "enum": ["kampany", "kampany_ki", "egy_kampany_ki", "egy_kampany_modosit", "kampany_proba"],
                    "description": "'kampany' = új kampány létrehozása/ütemezése; 'kampany_ki' = listázás; 'egy_kampany_ki' = egy kampány lekérése; 'egy_kampany_modosit' = kampány módosítása; 'kampany_proba' = próba-email küldése a saját fiók email-címére (rate-limit: 1/perc/user)."
                  },
                  "csoport_ids": { "type": "string", "description": "Kötelező 'kampany'/'egy_kampany_modosit' esetén: JSON-tömb stringként (pl. '[3,7]') vagy sima tömb — a célzott csoportok id-jai. Legalább 1, max 1000 elem." },
                  "sablon_id": { "type": "integer", "description": "Kötelező — a kiküldendő email-sablon azonosítója." },
                  "kampany_nev": { "type": "string", "description": "Kötelező. Max 255 karakter." },
                  "kuldo_csatorna": { "type": "string", "enum": ["gmail_api", "smtp"], "default": "gmail_api", "description": "Küldési csatorna; ismeretlen érték esetén 'gmail_api'-ra esik vissza." },
                  "schedule": { "type": "string", "enum": ["now", "later", "once", "daily", "weekly", "monthly"], "default": "now", "description": "'now' esetén a backend adja a küldési időt; egyébként a kuldes_ideje mező kötelező." },
                  "kuldes_ideje": { "type": "string", "description": "Formátum: 'Y-m-d H:i:s' (Europe/Budapest). Kötelező, ha schedule != 'now'; jövőbeli időpontnak kell lennie." },
                  "kampany_id": { "type": "integer", "description": "Kötelező 'egy_kampany_ki' és 'egy_kampany_modosit' esetén." },
                  "kizart_ugyfel_idk": { "type": "string", "description": "'kampany' esetén opcionális (a kampány-limit figyelmeztetés UI második körös döntéséhez): JSON-tömb a kizárandó ügyfél-id-kból. 'egy_kampany_modosit' esetén is opcionális (2026-07-17): a kampányhoz mentett kizárt címzettek TELJES cseréje — hiányzó/üres érték a meglévő kizárásokat is törli." },
                  "varakozas": { "type": "boolean", "description": "'kampany' esetén opcionális, a figyelmeztetés-döntési folyamat része." },
                  "figyelmeztetes_dontes": { "type": "boolean", "description": "'kampany' esetén opcionális — true, ha a user a limit-figyelmeztetés UTÁN véglegesíti a küldést." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sikeres művelet. MEGJEGYZÉS: 'kampany' esetén a válasz HARMADIK ága is lehet — nem hiba, nem végleges siker —, amikor a kampány-limit FIGYELMEZTETÉST ad vissza ('figyelmeztetes':true, payloadban pl. cimzett_szam/utolso_x_cimzett); ekkor a frontend a kizart_ugyfel_idk/varakozas/figyelmeztetes_dontes mezőkkel küldi a második, döntés-utáni POST-ot. Ez a payload alak a tényleges kódból (KampanyService::letrehoz) NEM teljesen feltérképezett — JELÖLVE, nem tippeltünk éleset.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AltalanosValasz" } } }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "description": "Rate-limit (csak 'kampany_proba' esetén — 1 próba/perc/user).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } }
        }
      }
    },
    "/Delete_modify_record": {
      "post": {
        "summary": "Rekord (soft) törlése — ügyfél / csoport / sablon / kampány",
        "description": "Forrás: routing/ARouter.php ALLOWED_METHODS 'delete_modify_record_post' → Controller::Delete_modify_record_post() → Adat_feldolg switch source='torles' → traits/DeleteHandlerTrait.php::torles_utani_kiiras() → php/Delete_modify_record.php::handle_delete(). Soft delete (torles_ideje mezőt állítja), IDOR-biztos (id + a bejelentkezett user_id egyezésére szűr). A path írásmódja ('/Delete_modify_record', nagy kezdőbetűkkel) a tényleges frontend hívásból igazolva (utils/js/Delet_records.js: MS.post('/Delete_modify_record', ...)) — a router a path→method leképzést case-insensitive végzi, így kisbetűs írásmód is működne, de a kliens ezt a casing-et használja.",
        "operationId": "rekordTorles",
        "tags": ["Torles"],
        "security": [{ "sessionCookie": [], "csrfToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": ["source", "record_id", "oldal_id"],
                "properties": {
                  "source": { "type": "string", "const": "torles", "description": "Kötelező — az Adat_feldolg::melyik_oldal_kuldi() dispatch-kulcsa, ezen a végponton mindig 'torles'." },
                  "record_id": { "type": "integer", "description": "A törlendő rekord azonosítója (pozitív egész)." },
                  "oldal_id": {
                    "type": "string",
                    "enum": ["ugyfel", "csoport", "sablon", "kampany"],
                    "description": "Az entitás típusa — kizárólag ez a 4 érték engedélyezett (php/Delete_modify_record.php::$allowedEntities whitelist)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sikeres törlés — a válasz data-ja az érintett entitás FRISSÍTETT listája (a torles_utani_kiiras() az oldal_id szerint újra lekéri és visszaadja a listát).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AltalanosValasz" } } }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "description": "A rekord nem található, vagy már törölve van, vagy nem a hívó usere.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } }
        }
      }
    },
    "/webshop/kapcsolatok": {
      "get": {
        "summary": "A bejelentkezett felhasználó webshop-kapcsolatainak listája",
        "description": "Forrás: Controller::webshop_kapcsolatok() → WebshopService::kapcsolat_lista($uid) → Webshop_kapcsolatok::lista_user_szerint(). Csak a saját (nem törölt) kapcsolatokat adja vissza.",
        "operationId": "webshopKapcsolatokLista",
        "tags": ["Webshop"],
        "security": [{ "sessionCookie": [] }],
        "responses": {
          "200": {
            "description": "Sikeres lekérés.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "const": true },
                    "code": { "type": "integer", "const": 200 },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "integer" },
                          "platform": { "type": "string", "enum": ["shopify", "woocommerce", "unas", "shoprenter", "generic"] },
                          "bolt_azonosito": { "type": "string" },
                          "allapot": { "type": "string", "enum": ["fuggo", "aktiv", "hiba", "torolt"], "description": "A kapcsolat állapota (a 2026-06-23_01 migráció ENUM-ja; default: 'fuggo')." },
                          "kosar_sablon_id": {},
                          "kosar_csatorna": { "type": "string", "enum": ["gmail_api", "smtp"] },
                          "cel_csoport_id": {},
                          "cel_csoport_auto": { "type": "boolean" },
                          "utolso_szinkron": {},
                          "letrehozva": { "type": "string" },
                          "modositva": { "type": "string" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/webshop/kapcsolat/letrehozas": {
      "post": {
        "summary": "Új (manuális, kulcs-megadásos) webshop-kapcsolat létrehozása",
        "description": "Forrás: Controller::webshop_kapcsolat_letrehozas_post() → WebshopService::kapcsolat_letrehozas(). CSAK a manuális platformokhoz (generic/unas/shoprenter) — a woocommerce/shopify platformra ez 400-at ad ('Ehhez a platformhoz OAuth-összekötést használj'), azokhoz a /webshop/oauth/{platform}/start végpont való (NEM része ennek a dokumentumnak — a feladat hatóköre csak a 3 explicit webshop-kapcsolat végpontra szól). Entitlement-kapu: 'webshop_integracio' képesség szükséges.",
        "operationId": "webshopKapcsolatLetrehozas",
        "tags": ["Webshop"],
        "security": [{ "sessionCookie": [], "csrfToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": ["platform", "bolt_azonosito"],
                "properties": {
                  "platform": { "type": "string", "enum": ["generic", "unas", "shoprenter"], "description": "woocommerce/shopify esetén 400 — azokhoz OAuth-flow való." },
                  "bolt_azonosito": { "type": "string", "maxLength": 255, "description": "A bolt egyedi azonosítója (platform+bolt_azonosito együtt egyedi — ütközés esetén 409)." },
                  "api_kulcs": { "type": "string", "maxLength": 4096, "description": "Opcionális — titkosítva tárolva (KulcsTitkosito)." },
                  "webhook_secret": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "Opcionális. Ha megadva (unas/shoprenter eset, ahol a platform generálja a secretet a saját oldalán), ezt tároljuk titkosítva, és a válaszban NEM adjuk vissza plaintextben. Ha üres/hiányzik (generic eset), a szerver generál egy random secretet és AZT a válaszban egyszer plaintextben visszaadja."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sikeres létrehozás.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "const": true },
                    "code": { "type": "integer", "const": 200 },
                    "id": { "type": "integer" },
                    "webhook_url": { "type": "string", "description": "A platform felé beállítandó webhook-cél URL." },
                    "webhook_secret": { "type": "string", "description": "CSAK akkor jelenik meg, ha a kliens NEM adott meg saját webhook_secret-et (generic eset) — egyszeri, plaintext megjelenítés." }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/webshop/kapcsolat/torles": {
      "post": {
        "summary": "Webshop-kapcsolat törlése (soft)",
        "description": "Forrás: Controller::webshop_kapcsolat_torles_post() → WebshopService::kapcsolat_torles($id, $uid) → Webshop_kapcsolatok::torles(). IDOR-biztos (id + a bejelentkezett user_id egyezésére szűr).",
        "operationId": "webshopKapcsolatTorles",
        "tags": ["Webshop"],
        "security": [{ "sessionCookie": [], "csrfToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": ["id"],
                "properties": {
                  "id": { "type": "integer", "description": "A törlendő webshop-kapcsolat azonosítója." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sikeres törlés.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "const": true },
                    "code": { "type": "integer", "const": 200 }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "description": "A kapcsolat nem található (vagy nem a hívó usere).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HibaValasz" } } } }
        }
      }
    }
  }
}
