openapi: 3.0.3
info:
  title: PlaCla API
  version: "1.0"
  description: >
    REST API PlaCly pro blogery a inzerenty. Ceny v celých Kč (integer),
    obsah česky. Chyby mají jednotný tvar {error: {code, message, support_code}}.
  contact:
    email: info@placla.cz
servers:
  - url: https://api.placla.cz/v1
security:
  - bearerAuth: []

paths:
  /me:
    get:
      summary: Identita a kredit majitele klíče
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Me" }

  /blogs:
    get:
      summary: Vlastní blogy (role B)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Blog" }
    post:
      summary: Založení blogu (bloger; čeká na schválení redakcí)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, url, description]
              properties:
                name: { type: string, maxLength: 255 }
                url: { type: string, description: "URL blogu (sociální profily API nepodporuje)" }
                description: { type: string, description: "min. 250 znaků" }
                monthly_visitors: { type: integer }
      responses:
        "200": { description: "OK — {id, approved: false}" }
        "400": { $ref: "#/components/responses/Chyba" }

  /blogs/{id}/metrics:
    get:
      summary: Metriky a verdikt blogu (PlaClaRank)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Metrics" } } } }
        "404": { $ref: "#/components/responses/Chyba" }

  /blogs/{id}/demands:
    get:
      summary: Poptávky dostupné pro blog (vč. VIP)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/DemandForBlog" }
        "422": { $ref: "#/components/responses/Chyba" }

  /blogs/{id}/services:
    get:
      summary: Extra služby blogu
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK }
    post:
      summary: Nová extra služba (max 3 aktivní)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ServiceInput" }
      responses:
        "201": { description: Vytvořeno }
        "422": { $ref: "#/components/responses/Chyba" }

  /blogs/{id}/services/{sid}:
    patch:
      summary: Úprava extra služby
      parameters: [ { $ref: "#/components/parameters/id" }, { $ref: "#/components/parameters/sid" } ]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ServiceInput" }
      responses:
        "200": { description: OK }
    delete:
      summary: Archivace (s ?permanent=true trvalé smazání archivované)
      parameters:
        - { $ref: "#/components/parameters/id" }
        - { $ref: "#/components/parameters/sid" }
        - name: permanent
          in: query
          schema: { type: boolean }
      responses:
        "200": { description: OK }

  /blogs/{id}/services/{sid}/restore:
    post:
      summary: Obnova archivované služby
      parameters: [ { $ref: "#/components/parameters/id" }, { $ref: "#/components/parameters/sid" } ]
      responses:
        "200": { description: OK }
        "409": { $ref: "#/components/responses/Chyba" }

  /blogs/{id}:
    patch:
      summary: Úprava blogu
      parameters: [ { $ref: "#/components/parameters/id" } ]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                description: { type: string }
                note: { type: string }
                min_price: { type: integer }
                monthly_visitors: { type: integer }
                writing_service: { type: integer, enum: [0, 1, 2] }
                writing_price_per_ns: { type: integer }
      responses:
        "200": { description: OK }
        "422": { $ref: "#/components/responses/Chyba" }

  /blogs/{id}/offers:
    post:
      summary: Podání nabídky na poptávku (role B, scope write)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [demand_id]
              properties:
                demand_id: { type: integer }
                reward: { type: integer, description: "Požadovaná odměna v Kč = vyšší nabídka" }
                note: { type: string, description: "Nápad na článek" }
      responses:
        "201":
          description: Nabídka vytvořena
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: integer }
                  state: { type: string, enum: [nabidnuto, nabidnuto_vyssi] }
                  demand_id: { type: integer }
                  blog_id: { type: integer }
                  price: { type: integer }
                  reward: { type: integer }
        "409": { $ref: "#/components/responses/Chyba" }
        "422": { $ref: "#/components/responses/Chyba" }

  /blogs/{id}/demands/{did}/decline:
    post:
      summary: Odmítnutí poptávky blogem
      parameters: [ { $ref: "#/components/parameters/id" }, { $ref: "#/components/parameters/did" } ]
      responses:
        "200": { description: OK }
        "409": { $ref: "#/components/responses/Chyba" }

  /blogs/{id}/demands/decline:
    post:
      summary: Odmítnutí všech dostupných poptávek blogu
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK }

  /demands:
    get:
      summary: Vlastní poptávky s počty (role I)
      responses:
        "200": { description: OK }
    post:
      summary: Založení poptávky (inzerent; vzniká rovnou aktivní, vyžaduje kredit >= cena)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, url, description]
              properties:
                kind:
                  type: string
                  enum: [standard, vip, publication, writing]
                  default: standard
                  description: "publication = dodáte hotový text; writing = napíše redakce (cena se neposílá)"
                name: { type: string, maxLength: 255 }
                url: { type: string, description: "cílový web (http:// se doplní)" }
                price: { type: integer, description: "cena za článek (Kč); minima dle druhu, u writing se ignoruje" }
                description: { type: string, description: "zadání" }
                keywords: { type: string, maxLength: 255, description: "povinné kromě kind: publication" }
                expectation: { type: string, description: "standard/vip: text, nebo 1=SEO, 2=zmínky, 3=konverze, 0=jiné" }
                ns: { type: number, default: 1, description: "rozsah v normostranách (u publication vždy 1)" }
                links_mode: { type: integer }
                keyword_urls:
                  type: array
                  description: "páry klíčové slovo + URL; ukládají se jen u links_mode 1 a 5"
                  items:
                    type: object
                    required: [keyword]
                    properties:
                      keyword: { type: string }
                      url: { type: string }
      responses:
        "200": { description: "OK — {id, kind, state: aktivni, vip, price, name}" }
        "400": { $ref: "#/components/responses/Chyba" }
        "422": { $ref: "#/components/responses/Chyba" }

  /demands/{id}:
    get:
      summary: Detail poptávky
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK }

  /demands/{id}/offers:
    get:
      summary: Nabídky blogů k poptávce (metriky, verdikt)
      parameters:
        - { $ref: "#/components/parameters/id" }
        - name: state
          in: query
          schema: { type: string, enum: [odlozene] }
      responses:
        "200": { description: OK }

  /deals:
    get:
      summary: >
        Obchody (strana podle role klíče). Bez state jen stavy viditelné na webu;
        proplacené inzerentovi 2 roky od proplacení (výpis s demand_id bez limitu).
      parameters:
        - name: state
          in: query
          schema:
            type: string
            enum: [nabidnuto, nabidnuto_vyssi, akceptovano, ke_schvaleni, proplaceno,
                   odmitnuto, zruseno, arbitraz, arbitraz_neproplaceno, storno, proplaceno_text]
        - name: blog_id
          in: query
          schema: { type: integer }
        - name: demand_id
          in: query
          schema: { type: integer }
        - name: zmeneno_od
          in: query
          description: jen obchody změněné od daného času (přírůstková synchronizace)
          schema: { type: string, format: date-time }
        - name: strana
          in: query
          schema: { type: integer, default: 1 }
        - name: na_stranu
          in: query
          schema: { type: integer, default: 50, maximum: 200 }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Deal" }
                  strana: { type: integer }
                  na_stranu: { type: integer }
                  celkem: { type: integer }

  /deals/{id}:
    get:
      summary: Detail obchodu včetně plného zadání poptávky (i po akceptaci)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200":
          description: OK — Deal s plným objektem demand (keywords, keyword_urls, links_mode, description, expectation, ns, publication, attachment)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Deal" }

  /deals/{id}/text:
    get:
      summary: Text napsaný redakcí (bloger/redaktor; až po schválení, vyžaduje-li ho služba)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK }
        "409": { $ref: "#/components/responses/Chyba" }

  /deals/{id}/article:
    post:
      summary: Odevzdání publikovaného článku (role B, scope write)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
                extra_urls:
                  type: object
                  additionalProperties:
                    type: array
                    items: { type: string, format: uri }
      responses:
        "200": { description: "Odevzdáno, state = ke_schvaleni" }
        "409": { $ref: "#/components/responses/Chyba" }
        "422": { $ref: "#/components/responses/Chyba" }

  /deals/{id}/writing:
    post:
      summary: Objednání napsání textu redakcí (blokuje kredit blogera)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK }
        "409": { $ref: "#/components/responses/Chyba" }
    delete:
      summary: Zrušení napsání (vratka)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK }
        "409": { $ref: "#/components/responses/Chyba" }

  /payouts:
    get:
      summary: Čekající odměny (pending) + arbitráže (arbitration) + vyplacené po měsících (role B)
      parameters:
        - name: rok
          in: query
          schema: { type: integer }
      responses:
        "200": { description: OK }

  /offers/{id}/accept:
    post:
      summary: Akceptace nabídky (inzerent; blokuje kredit, podpora Idempotency-Key)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                price: { type: integer, description: "nižší cena; jen u blogerů s lower_price_allowed, min. 75 % nabídnuté ceny" }
                services: { type: array, items: { type: integer }, description: "ID extra služeb" }
                keywords:
                  type: array
                  items:
                    type: object
                    required: [keyword]
                    properties:
                      keyword: { type: string }
                      url: { type: string }
                note: { type: string, description: "poznámka blogerovi" }
                article_text: { type: string, description: "povinné u poptávky s publication: true" }
      responses:
        "200": { description: "OK — {id, state, price, credit_remaining}" }
        "409": { $ref: "#/components/responses/Chyba" }
        "422": { $ref: "#/components/responses/Chyba" }

  /offers/{id}/decline:
    post:
      summary: Odmítnutí nabídky (inzerent)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                rating: { type: integer }
                reason: { type: string }
      responses:
        "200": { description: OK }
        "409": { $ref: "#/components/responses/Chyba" }

  /offers/{id}/defer:
    post:
      summary: Odložení nabídky stranou (inzerent)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK }

  /offers/{id}/restore:
    post:
      summary: Vrácení odložené nabídky (inzerent)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK }

  /offers/{id}/block:
    post:
      summary: Blokace blogu přes nabídku (inzerent; smaže jeho otevřené nabídky)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK }
        "409": { $ref: "#/components/responses/Chyba" }

  /deals/{id}/payout:
    post:
      summary: Proplacení článku (inzerent; nevratné, Idempotency-Key doporučen)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                ratings: { type: array, items: { type: integer }, minItems: 4, maxItems: 4 }
      responses:
        "200": { description: "OK — {id, state, price, blogger_reward}" }
        "409": { $ref: "#/components/responses/Chyba" }

  /deals/payout:
    post:
      summary: Hromadné proplacení (inzerent)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ids: { type: array, items: { type: integer } }
                all: { type: boolean, description: "true = vše ke schválení (stav 2)" }
      responses:
        "200": { description: "OK — {paid: [...], errors: [...]}" }

  /deals/{id}/cancel:
    post:
      summary: Storno akceptovaného obchodu (inzerent; vratka blokace)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string }
      responses:
        "200": { description: OK }
        "409": { $ref: "#/components/responses/Chyba" }

  /deals/{id}/claim:
    post:
      summary: Připomínka/reklamace článku (inzerent; bloger dostane webhook claim.created)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string }
      responses:
        "200": { description: OK }
        "409": { $ref: "#/components/responses/Chyba" }

  /deals/{id}/rating:
    post:
      summary: Hodnocení proplaceného obchodu 1-5, jen jednou (inzerent)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rating]
              properties:
                rating: { type: integer, minimum: 1, maximum: 5 }
      responses:
        "200": { description: OK }
        "409": { $ref: "#/components/responses/Chyba" }

  /demands/{id}/activate:
    post:
      summary: Aktivace poptávky (inzerent; vyžaduje kredit >= cena)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK }
        "422": { $ref: "#/components/responses/Chyba" }

  /demands/{id}/deactivate:
    post:
      summary: Deaktivace poptávky (inzerent)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      responses:
        "200": { description: OK }

  /webhook:
    get:
      summary: Nastavení webhooku klíče
      responses:
        "200": { description: OK }
    put:
      summary: Nastavení URL webhooku (vrací jednorázově secret)
      description: >-
        URL musí být HTTPS. Webhook je vázaný na API klíč — zneplatněním klíče zaniká.
        Události: demand.created, deal.accepted, deal.paid, claim.created,
        message.created, webhook.test (typ v poli event; neznámé názvy ignorujte).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri, description: "https:// adresa" }
      responses:
        "200": { description: "OK; secret jen v této odpovědi" }
    delete:
      summary: Vypnutí webhooku
      responses:
        "200": { description: OK }

  /credit/transactions:
    get:
      summary: Pokladní deník účtu (obě role; jako web posledních 100 záznamů)
      responses:
        "200": { description: OK }

  /invoices:
    get:
      summary: Vystavené faktury + nezaplacené proformy (odkazy, platební údaje, QR)
      responses:
        "200": { description: "OK — {invoices: [...], pending_proformas: [...]}" }

  /credit/top-up:
    post:
      summary: Dobití kreditu bankovním převodem (vystaví proformu; kartou jen na webu)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount: { type: integer, description: "částka bez DPH (Kč)" }
                allow_duplicate: { type: boolean, description: "vystavit i při existující nezaplacené proformě na stejnou částku" }
      responses:
        "200": { description: "OK — {vs, amount, amount_with_vat, bank_account, proforma_url, qr_url}" }
        "409": { $ref: "#/components/responses/Chyba" }
        "422": { $ref: "#/components/responses/Chyba" }

  /logs:
    get:
      summary: Historie účtu - události s částkami (obě role; jako web posledních 100 záznamů)
      responses:
        "200": { description: OK }

  /spend:
    get:
      summary: Útrata inzerenta po měsících (?rok=)
      responses:
        "200": { description: "OK — {spend_monthly: [{month, articles, amount}]}" }

  /deals/{id}/messages:
    post:
      summary: Odeslání zprávy k běžícímu obchodu (obě role; bloger až po akceptaci — 409 nabidka_ceka)
      parameters: [ { $ref: "#/components/parameters/id" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string }
      responses:
        "200": { description: OK }
        "409": { $ref: "#/components/responses/Chyba" }
    get:
      summary: Komunikace k obchodu (?oznacit_prectene=1 označí jako přečtené)
      parameters:
        - $ref: "#/components/parameters/id"
        - name: oznacit_prectene
          in: query
          schema: { type: string, enum: ["1"] }
      responses:
        "200": { description: OK }

  /deals/{id}/claims:
    post:
      summary: >
        Příspěvek do vlákna reklamace (obě role). Bloger u stavů
        ke_schvaleni/arbitráž nebo otevřeného problému; inzerent tamtéž -
        a u článku proplaceného před méně než 2 lety otevírá „problém". arbitration=true (jen
        ke_schvaleni, min. 50 znaků) předá spor arbitrovi. Obsah prochází
        serverovou ochranou - nález vrací 422 podezrely_obsah, vědomé odeslání
        beze změny = zopakovat s presto_odeslat=true. /deals/{id}/claim je alias.
      parameters: [ { $ref: "#/components/parameters/id" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string, maxLength: 10000 }
                arbitration: { type: boolean, default: false }
                presto_odeslat: { type: boolean, default: false }
      responses:
        "200":
          description: Vloženo
          content:
            application/json:
              schema:
                type: object
                properties:
                  deal_id: { type: integer }
                  claim_created: { type: boolean }
                  problem_opened: { type: boolean }
                  arbitration: { type: boolean }
        "422": { description: podezrely_obsah - ochrana komunikace }
    get:
      summary: Připomínky/reklamace k obchodu (?oznacit_prectene=1 označí jako přečtené)
      parameters:
        - $ref: "#/components/parameters/id"
        - name: oznacit_prectene
          in: query
          schema: { type: string, enum: ["1"] }
      responses:
        "200": { description: OK }

  /webhook/deliveries:
    get:
      summary: Posledních 50 doručení webhooku (ladění)
      responses:
        "200": { description: OK }

  /webhook/test:
    post:
      summary: Okamžité testovací doručení webhook.test (synchronně, vrací odpověď serveru)
      responses:
        "200": { description: OK }
        "409": { $ref: "#/components/responses/Chyba" }

  /webhook/rotate-secret:
    post:
      summary: Nový secret bez změny URL (vrací se jen jednou)
      responses:
        "200": { description: OK }
        "409": { $ref: "#/components/responses/Chyba" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API klíč z nastavení účtu na sluzba.placla.cz
  parameters:
    id:
      name: id
      in: path
      required: true
      schema: { type: integer }
    sid:
      name: sid
      in: path
      required: true
      schema: { type: integer }
    did:
      name: did
      in: path
      required: true
      schema: { type: integer }
  responses:
    Chyba:
      description: Chyba v jednotném tvaru
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: object
                properties:
                  code: { type: string }
                  message: { type: string }
                  support_code: { type: string, example: PC-4A5CAE }
  schemas:
    Me:
      type: object
      properties:
        id: { type: integer }
        login: { type: string }
        role: { type: string, enum: [bloger, inzerent] }
        scopes: { type: array, items: { type: string } }
        credit:
          type: object
          properties:
            available: { type: integer }
            blocked: { type: integer }
    Blog:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        url: { type: string }
        type: { type: string, enum: [web, social] }
        approved: { type: boolean }
        min_price: { type: integer }
        monthly_visitors: { type: integer }
        monthly_visitors_updated: { type: string }
        writing_service: { type: integer }
        writing_price_per_ns: { type: integer }
        da: { type: integer }
        metrics_updated: { type: string }
        published_count: { type: integer }
    Metrics:
      type: object
      properties:
        blog_id: { type: integer }
        score: { type: integer, description: "PlaClaRank 0-100" }
        verdict: { type: string, enum: [gold, silver, bronze, fail] }
        verdict_text: { type: string }
        da: { type: integer }
        pa: { type: integer }
        spam: { type: integer }
        signals:
          type: array
          items:
            type: object
            properties:
              status: { type: string, enum: [ok, warn, bad] }
              name: { type: string }
              description: { type: string }
        updated: { type: string }
    DemandForBlog:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        url: { type: string }
        vip: { type: boolean }
        type: { type: string, enum: [web, social] }
        keywords: { type: string }
        ns: { type: number }
        publication: { type: boolean }
        expectation: { type: string, nullable: true }
        reward: { type: integer, description: "Odměna blogera při základní ceně" }
    Deal:
      type: object
      properties:
        id: { type: integer }
        state: { type: string }
        blog:
          type: object
          properties: { id: { type: integer }, name: { type: string } }
        demand:
          type: object
          properties: { id: { type: integer }, name: { type: string } }
        web_url: { type: string, format: uri, description: "odkaz na detail obchodu na webu PlaCly (podle role klíče)" }
        changed_at: { type: string, format: date-time, description: "čas poslední změny (u obchodů starších než srpen 2026 čas nasazení pole)" }
        price: { type: integer }
        ns: { type: number }
        express_hours: { type: integer }
        article_url: { type: string, nullable: true }
        reason: { type: string, nullable: true }
        rating: { type: integer, nullable: true }
        claim:
          type: object
          properties: { open: { type: boolean }, count: { type: integer } }
        unread_messages: { type: integer }
        writing:
          type: object
          properties:
            ordered: { type: boolean }
            text_state: { type: string, nullable: true }
            editor_price: { type: integer, nullable: true }
            approved_by_advertiser: { type: boolean }
        services: { type: array, items: { type: object } }
        reward: { type: integer, description: "Jen pro blogera" }
    ServiceInput:
      type: object
      required: [name, price]
      properties:
        name: { type: string }
        price: { type: integer }
        extra_ns: { type: integer, minimum: 0, maximum: 5 }
        express_hours: { type: integer, enum: [0, 3, 6, 12, 24, 48] }
        urls: { type: array, items: { type: string, format: uri } }
