Documentația API
Aceleași operațiuni ca în interfață, prin cheie de API: catalog, articole, publicări, campanii, rapoarte, redactare. Fiecare rută cu permisiunea cerută, câmpurile și un exemplu de răspuns. Aceeași sursă ca fișierul OpenAPI.
Autentificare
Cheia se trimite în antetul `Authorization`. Se emite din pagina de integrări și se vede o singură dată — noi păstrăm doar amprenta ei, exact ca la parole.
curl --location 'https://app.comunicate.top/api/v1/partner/balance' \
--header 'Authorization: Bearer bk_live_…'- Adresa de bază
https://app.comunicate.top/api/v1- Limita de cereri
- Per cheie, pe minut — scrisă pe cheie când o emiți. O integrare scăpată de sub control nu blochează celelalte chei ale aceluiași cont. Depășirea întoarce 429 cu `Retry-After`, nu 403: singurul caz în care reîncercarea are sens.
- Rutele cu AI
- Scrierea, importul de documente și completarea metadatelor au o limită în plus: șaizeci pe oră, pe organizație, indiferent câte chei ai. Consumă din cotele modelelor, care sunt ale platformei, nu ale contului tău — iar un singur client care le-ar epuiza ar opri generarea pentru toți.
- Organizația
- Vine din cheie, nu din adresă. Nu există niciun `organizationId` de trimis, și nici de greșit.
- Erorile
- Cod HTTP real — 400, 401, 403, 404, 409 — și un corp cu `message`. La validare, `errors` spune care câmp și de ce.
Un răspuns de eroare
{
"message": "Datele trimise nu sunt valide",
"errors": [{ "path": "tags", "code": "invalid_type", "message": "Expected array, received string" }]
}Permisiuni
Fiecare cheie primește doar permisiunile de care are nevoie. O integrare care citește catalogul n-are cum să publice, oricât ar încerca.
CATALOG_READCitirea catalogului de publicații și a tipurilor de campanie.ARTICLES_READCitirea articolelor organizației.ARTICLES_WRITECrearea și modificarea articolelor, importul de documente și din Drive, cererea de redactare.MEDIA_WRITEÎncărcarea de imagini în galerie.PUBLICATIONS_READCitirea publicărilor și a stării lor.PUBLICATIONS_WRITECererea de publicare — singura permisiune care cheltuie credite sau bani.CAMPAIGNS_READCitirea campaniilor.CAMPAIGNS_WRITECrearea campaniilor.BALANCE_READCitirea soldului și a pachetelor deținute.REPORTS_READCitirea rapoartelor pe interval.
Catalogul public
Patru rute care merg fără nicio cheie: nișele rețelei, o nișă cu publicații de probă, cifrele catalogului și statistica pieței. Sunt aceleași date pe care le arată site-ul public, servite ca JSON — bune pentru comparații, pentru un asistent care vrea să știe ce e în rețea, sau pentru cine vrea să vadă cum arată datele înainte să-și facă cont. Nu există rută publică pentru lista întreagă de publicații, deliberat: inventarul nu se descarcă dintr-o cerere.
GET/public/statsCifrele cataloguluifără cheie
Câte publicații active are rețeaua și pe câte nișe. Fără cheie.
Exemplu
curl 'https://app.comunicate.top/api/v1/public/stats'Câmpuri din răspuns
publications | integer | Publicații active. |
niches | integer | Nișe cu publicații. |
Răspuns
{ "publications": 3822, "niches": 21 }GET/public/nichesNișele rețeleifără cheie
Toate nișele, cu numărul de publicații și autoritatea medie a fiecăreia. Fără cheie.
Parametri
locale | 'ro' | 'en' | Limba numelor și descrierilor. Implicit `ro`. |
Exemplu
curl 'https://app.comunicate.top/api/v1/public/niches?locale=ro'Câmpuri din răspuns
slug | string | Identificatorul nișei, folosit în ruta următoare. |
name | string | Numele nișei în limba cerută. |
description | string | Ce fel de materiale primesc publicațiile din nișă. |
publicationCount | integer | Câte publicații active are nișa. |
averageDa | integer | Autoritatea de domeniu medie (Moz DA). |
Răspuns
[
{
"slug": "stiri",
"name": "Știri",
"description": "Publicațiile de actualitate generală din rețea…",
"publicationCount": 1299,
"averageDa": 38
}
]GET/public/niches/{slug}O nișă, cu publicații de probăfără cheie
Nișa cerută și câteva publicații din ea, cu autoritate, termen, preț și felul marcajului publicitar. E o mostră, nu inventarul: lista întreagă se vede din cont.
Parametri
locale | 'ro' | 'en' | Limba numelor și descrierilor. Implicit `ro`. |
Exemplu
curl 'https://app.comunicate.top/api/v1/public/niches/stiri?locale=ro'Câmpuri din răspuns
slug | string | Identificatorul nișei. |
name | string | Numele nișei. |
publicationCount | integer | Câte publicații are nișa în total. |
averageDa | integer | Autoritatea medie a nișei. |
publications | object[] | Mostra: `domain`, `language`, `metricDa`, `metricPa`, `deliveryDays`, `priceCents`, `currency`, `marcaj`. |
Răspuns
{
"slug": "stiri",
"name": "Știri",
"publicationCount": 1299,
"averageDa": 38,
"publications": [
{
"domain": "corrierefiorentino.corriere.it",
"language": "it",
"metricDa": 92,
"metricPa": 62,
"deliveryDays": 2,
"priceCents": 1296200,
"currency": "RON",
"marcaj": "PROPRIU"
}
]
}GET/public/statistici-piataStatistica piețeifără cheie
Cifrele agregate ale catalogului: câte publicații, câte din rețeaua proprie, distribuția autorității, termenele și prețul pe fiecare tip de campanie. Datele din spatele paginii „Cât costă un advertorial".
Exemplu
curl 'https://app.comunicate.top/api/v1/public/statistici-piata'Câmpuri din răspuns
publicatii | integer | Publicații active cu preț public. |
retea | integer | Câte dintre ele sunt ale noastre. |
autoritate | object | Minim, maxim, mediană, medie și distribuția pe intervale. |
termene | object[] | Câte publicații livrează în câte zile. |
preturi | object[] | Pe fiecare tip de campanie: preț, minim, maxim și câte publicații îl acceptă. |
Răspuns
{
"publicatii": 3822,
"retea": 109,
"autoritate": { "minim": 1, "maxim": 92, "mediana": 35, "medie": 34 },
"termene": [{ "zile": 2, "cate": 3822 }],
"preturi": [
{ "tip": "PRESS_RELEASE", "lei": 35, "minim": 35, "maxim": 14868, "publicatii": 1737 }
]
}Cheia și permisiunile
Prima cerere a oricărei integrări: cine e cheia și ce are voie. Tot aici se vede dacă organizația a pornit „asistenții AI pot comanda și publica" — comutatorul care decide ce poate face un asistent MCP cu banii contului.
GET/partner/meCine suntorice cheie
Organizația din spatele cheii, permisiunile ei și comutatorul pentru asistenți AI. Nu cere nicio permisiune: orice cheie validă poate întreba.
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/me' \
--header 'Authorization: Bearer bk_live_…'Câmpuri din răspuns
organization | object | `id` și `name`. |
scopes | string[] | Permisiunile cheii. |
aiPublishing | boolean | `true` dacă asistenții AI pot comanda publicări și redactare (Integrări → Asistenți AI). |
Răspuns
{
"organization": { "id": "e0b5…", "name": "Agenția Exemplu SRL" },
"keyPrefix": "bk_live_a1b2",
"scopes": ["CATALOG_READ", "ARTICLES_WRITE"],
"aiPublishing": false
}Catalog
Ce publicații există, cât costă pe fiecare tip de campanie și dacă le poți plăti din pachetele pe care le ai.
GET/partner/catalogLista publicațiilorCATALOG_READ
Catalogul paginat, cu aceleași filtre ca în interfață. Fiecare intrare spune și cum se poate plăti: din pachet, din sold, sau deloc.
Parametri
page | int | Pagina cerută, de la 1. |
pageSize | int | Câte intrări pe pagină, cel mult 100. |
search | string | Caută în nume și domeniu. |
campaign | string | Doar publicațiile care acceptă tipul dat (vezi `campaign-types`). |
serviceType | string | `PUBLISH_ONLY`, `WRITE_AND_PUBLISH`, `HOMEPAGE_PLACEMENT` sau `FACEBOOK_SHARE`. Pentru celelalte trei, doar site-urile cu o ofertă activă de tipul cerut apar în listă — publicarea simplă e serviciul de bază, îl are orice site cu preț. |
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/catalog?page=1&campaign=SEO' \
--header 'Authorization: Bearer bk_live_…'Câmpuri din răspuns
entries[].id | uuid | Id-ul publicației. Cu el se cere publicarea. |
entries[].metrics | object | DR, DA, PA și numărul de backlinkuri. `null` unde nu avem citire. |
entries[].audit | object | null | `flagged` e singurul câmp care schimbă o decizie: un site marcat de Google ca periculos n-ar trebui cumpărat. |
entries[].preturiCampanii | object | Prețul publicării, pe fiecare tip de campanie acceptat, în bani. |
entries[].access | object | `OWN` (site propriu), `CREDIT` (acoperit de un pachet), `MONEY` (se plătește din sold), `UNAVAILABLE`. Când `packages` are mai multe intrări, publicarea cere `packageTypeId`. |
entries[].masurat | object | Ce am măsurat noi din propriile comenzi: media orelor până la apariție, câte publicări verificate mai sunt în regulă. `null` unde n-am publicat destul. |
Răspuns
{
"entries": [
{
"id": "3f1c…",
"name": "Exemplu.ro",
"url": "https://exemplu.ro",
"language": "ro",
"niches": ["Auto"],
"metrics": { "dr": 34, "da": 41, "pa": 38, "backlinks": 12045 },
"audit": { "flagged": false, "threatTypes": [], "perfMobile": 62, "crux": "AVERAGE" },
"acceptedCampaigns": ["SEO", "BRAND_AWARENESS"],
"preturiCampanii": { "SEO": 18000, "BRAND_AWARENESS": 15000 },
"deliveryDays": 2,
"automat": true,
"linkPolicy": "DOFOLLOW",
"maxLinks": 2,
"access": { "kind": "MONEY", "packages": [], "priceCents": 18000, "currency": "RON", "affordable": true }
}
],
"page": 1,
"pageSize": 25,
"total": 184
}GET/partner/catalog/{siteId}O publicațieCATALOG_READ
Aceeași formă ca o intrare din listă, plus detaliile care nu încap în listare: condițiile pe fiecare tip de campanie, temele acceptate, ofertele active — redactare, plasare pe homepage, distribuire pe Facebook. Lipsa unei oferte dintr-un tip înseamnă că site-ul nu-l vinde.
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/catalog/3f1c…' \
--header 'Authorization: Bearer bk_live_…'Răspuns
{
"id": "3f1c…",
"name": "Exemplu.ro",
"acceptedTopics": "Fără politică, fără jocuri de noroc.",
"offers": [
{ "serviceType": "WRITE_AND_PUBLISH", "words": 700, "priceCents": 25000, "currency": "RON" },
{ "serviceType": "HOMEPAGE_PLACEMENT", "words": null, "priceCents": 5000, "currency": "RON" }
]
}GET/partner/campaign-typesTipurile de campanieCATALOG_READ
Tipul nu e o etichetă, ci un set de condiții pe care articolul le trece sau nu la trimitere. Verificarea lor înainte scutește un lanț de respingeri, una câte una.
Ce tipuri acceptă un site anume e altă întrebare, și stă în catalog, la `acceptedCampaigns`.
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/campaign-types' \
--header 'Authorization: Bearer bk_live_…'Răspuns
[
{
"id": "SEO",
"rezumat": "Advertorial optimizat: articolul susține un cuvânt-cheie și trimite către pagina promovată.",
"obligatoriu": [
"Cel puțin un link către site-ul promovat",
"Cuvânt-cheie declarat, prezent în text",
"Minimum 300 de cuvinte"
],
"interzis": ["Ancore fără conținut („click aici")"]
},
{
"id": "CASINO",
"rezumat": "Jocuri de noroc: se publică doar cu marcajele cerute de lege, pe site-urile care acceptă.",
"obligatoriu": ["Marcajul 18+", "Avertismentul de joc responsabil", "Licența ONJN a operatorului"],
"interzis": ["Promisiuni de câștig"]
}
]Articole
Un articol intră în platformă pe patru căi: scris de tine în HTML, importat dintr-un document, luat dintr-un folder Drive, sau scris de platformă de la un brief. Toate produc același lucru — o ciornă care se poate publica.
POST/partner/articlesCreează un articol din HTMLARTICLES_WRITE
Calea directă, când ai deja textul. Marcajul trece prin aceeași sanitizare ca importul: scripturile, stilurile inline și elementele care nu-și au locul într-un articol se elimină, iar ce s-a scos apare în răspuns.
Câmpurile de optimizare se normalizează, nu se resping: dacă trimiți cinci cuvinte-cheie despărțite prin virgulă, se păstrează primul; dacă trimiți zece etichete, se păstrează primele trei. Un articol susține un singur termen, iar douăzeci de etichete înseamnă pagini de arhivă cu un singur text.
Corpul cererii
title· obligatoriu | string(3–300) | Titlul articolului. |
contentHtml· obligatoriu | string(≤500 000) | Corpul articolului, în HTML. |
focusKeyword | string | Cuvântul-cheie pentru care se optimizează. **Unul singur** — dintr-o listă se păstrează primul. |
tags | string[] | Cel mult trei, oricâte s-ar trimite. Se acceptă și lista despărțită prin virgulă într-un singur element. |
metaDescription | string(≤320) | Descrierea din rezultatele căutării. |
slug | string | Adresa propusă a articolului. WordPress o poate schimba la coliziune; ce a ieșit efectiv se citește înapoi și se păstrează pe publicare. |
excerpt | string(≤1000) | Rezumatul folosit de teme în listări și pe pagina de categorie. |
featuredImageAlt | string(≤300) | Textul alternativ al imaginii reprezentative. Lipsa lui e una dintre constatările pe care le raportează analiza SEO. |
seoTitle | string(≤200) | Titlul din rezultatele căutării, când diferă de cel al articolului. |
featuredImageUrl | string | Imaginea reprezentativă. Poate fi adresa întoarsă de `POST /partner/media`. |
campaignId | uuid | Campania în care intră articolul. |
idempotencyKey | string(≤200) | Aleasă de tine — un UUID generat local e suficient. Trimisă din nou, cu aceeași organizație, întoarce articolul deja creat în loc să facă unul nou. Fără efect la `PATCH`. Recomandat pentru orice cod care poate retrimite cererea după un timeout. |
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/articles' \
--header 'Authorization: Bearer bk_live_…' \
--header 'Content-Type: application/json' \
--data '{
"title": "Ce verifici la o firmă de amenajări înainte să semnezi",
"contentHtml": "<p>Un contract de amenajare se semnează o dată…</p>",
"focusKeyword": "amenajări interioare",
"tags": ["amenajari", "interioare", "design"],
"idempotencyKey": "9f2c7e40-9b1e-4c2a-8b0e-2e6a1f9c3d10"
}'Răspuns
{
"id": "2668…",
"title": "Ce verifici la o firmă de amenajări înainte să semnezi",
"status": "DRAFT",
"focusKeyword": "amenajări interioare",
"tags": ["amenajari", "interioare", "design"],
"idempotencyKey": "9f2c7e40-9b1e-4c2a-8b0e-2e6a1f9c3d10",
"version": 1
}POST/partner/articles/importImportă un documentARTICLES_WRITE
Se acceptă `.docx`, `.doc`, `.odt`, `.rtf`, `.fodt`, `.html` și `.htm`. Formatele vechi trec prin LibreOffice, deci durează câteva secunde în plus.
**Ordinea fișierelor contează**: primul e documentul, al doilea — opțional — imaginea reprezentativă. Nu se poate distinge după numele câmpului, fiindcă fiecare își numește fișierele cum vrea.
Imaginile din corpul documentului se aduc în galerie și se rescriu în text; cele care nu se pot aduce apar în `images.skipReasons`, cu motivul. Metadatele — descriere, etichete — se completează în fundal, după import.
Formular (multipart/form-data)
(primul fișier)· obligatoriu | file | Documentul. |
(al doilea fișier) | file | Imaginea reprezentativă, pentru documentele care n-au niciuna în corp. |
campaignId | uuid | Campania în care intră articolul. |
folderId | uuid | Folderul din galerie în care ajung imaginile documentului. |
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/articles/import' \
--header 'Authorization: Bearer bk_live_…' \
--form 'document=@articol.docx' \
--form 'imagine=@coperta.jpg' \
--form 'campaignId=8b2e…'Câmpuri din răspuns
articles[] | array | Articolele create, cu titlu și număr de cuvinte. |
images | object | Câte imagini au intrat, câte s-au refolosit, câte au fost convertite și de ce au fost ignorate celelalte. |
sanitized | object | Ce a eliminat sanitizarea. Dacă articolul arată altfel decât documentul, aici scrie de ce. |
Răspuns
{
"articles": [{ "id": "2668…", "title": "Ce verifici la o firmă de amenajări", "wordCount": 712 }],
"images": { "imported": 3, "reused": 1, "skipped": 1, "converted": 1, "skipReasons": ["WMF nu poate fi convertit"] },
"sanitized": { "elements": ["script"], "attributes": ["style"], "links": 2, "relativeLinks": 1 },
"warnings": []
}GET/partner/articles/driveCe e într-un folder DriveARTICLES_WRITE
Folderul trebuie partajat „oricine cu linkul": platforma îl citește cu contul ei, clientul nu se loghează nicăieri.
Listarea e un pas separat de import dinadins. Un folder are adesea și ciorne, și versiuni vechi, și documentul bun — iar importul a tot ce se găsește produce zece articole din care se șterg nouă.
Parametri
link· obligatoriu | string | Linkul folderului, așa cum îl dă Google. |
Exemplu
curl --location --get 'https://app.comunicate.top/api/v1/partner/articles/drive' \
--data-urlencode 'link=https://drive.google.com/drive/folders/1AbC…' \
--header 'Authorization: Bearer bk_live_…'Răspuns
{
"folderId": "1AbC…",
"fisiere": [
{ "id": "1x…", "nume": "Advertorial final.docx", "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "fel": "document", "marimeOcteti": 84213 },
{ "id": "1y…", "nume": "coperta.jpg", "mimeType": "image/jpeg", "fel": "imagine", "miniatura": "https://…" }
]
}POST/partner/articles/driveImportă din folderul DriveARTICLES_WRITE
Cel mult cincizeci de documente odată. Un document stricat nu oprește restul: rezultatul are o intrare per fișier, cu numărul de articole sau cu eroarea lui.
Imaginea aleasă se descarcă o singură dată și se folosește pentru toate documentele care n-au niciuna în corp.
Corpul cererii
link· obligatoriu | string | Același link de folder ca la listare. Fiecare fișier cerut se verifică împotriva lui: descărcarea se face cu contul platformei, care vede și folderele altor clienți, deci ruta nu acceptă identificatori Drive oarecare. |
fisiere· obligatoriu | array | Documentele alese, cu `id`, `nume` și `mimeType` din listare. |
imagineId | string | Id-ul unei imagini din același folder, folosită ca reprezentativă. |
campaignId | uuid | Campania în care intră articolele. |
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/articles/drive' \
--header 'Authorization: Bearer bk_live_…' \
--header 'Content-Type: application/json' \
--data '{
"link": "https://drive.google.com/drive/folders/1AbC…",
"fisiere": [{ "id": "1x…", "nume": "Advertorial final.docx", "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }],
"imagineId": "1y…"
}'Răspuns
{
"rezultate": [
{ "nume": "Advertorial final.docx", "articole": 1 },
{ "nume": "Ciorna veche.doc", "eroare": "Fișierul nu e un document valid sau e protejat cu parolă" }
]
}PATCH/partner/articles/{articleId}Modifică un articolARTICLES_WRITE
Toate câmpurile de la creare, toate opționale. Se trimit doar cele care se schimbă. Aceleași normalizări: un cuvânt-cheie, cel mult trei etichete.
`version` crește **doar când se schimbă titlul sau conținutul**, nu la orice modificare. Ea intră în cheia de idempotență a publicării: un articol corectat și retrimis e o publicare nouă, pe când o etichetă schimbată nu produce alt articol pe site.
Un articol aflat în curs de publicare nu se poate modifica: se întoarce `409`. Worker-ul lucrează pe conținutul citit la începutul procesului, iar o modificare acum ar pune pe site altceva decât ce e în platformă.
Exemplu
curl --location --request PATCH 'https://app.comunicate.top/api/v1/partner/articles/2668…' \
--header 'Authorization: Bearer bk_live_…' \
--header 'Content-Type: application/json' \
--data '{ "metaDescription": "Ce verifici înainte să semnezi contractul." }'Răspuns
{
"id": "2668…",
"metaDescription": "Ce verifici înainte să semnezi contractul.",
"version": 2
}GET/partner/articlesLista articolelorARTICLES_READ
Articolele organizației, cel mai recent modificat primul. Paginată pe cursor: `nextCursor` din răspuns se retrimite ca `cursor` pentru pagina următoare, `null` la ultima.
Parametri
cursor | uuid | Id-ul ultimului articol văzut. Lipsă la prima pagină. |
limit | int (implicit 50, cel mult 200) | Câte articole pe pagină. |
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/articles?limit=50' \
--header 'Authorization: Bearer bk_live_…'Răspuns
{
"items": [
{ "id": "2668…", "title": "Ce verifici la o firmă de amenajări", "status": "DRAFT", "version": 2 }
],
"nextCursor": "9a11…"
}GET/partner/articles/{articleId}Un articolARTICLES_READ
Articolul întreg, cu textul lui. Aici se vede și dacă o ciornă cerută prin `redactare` s-a terminat: `redactedAt` completat înseamnă gata, iar `redactionFindings` spune ce a rămas nerezolvat.
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/articles/e056…' \
--header 'Authorization: Bearer bk_live_…'Câmpuri din răspuns
redactedAt | datetime | null | Când s-a terminat de scris. `null` cât timp e la coadă. |
redactionFindings | array | Ce n-a ieșit bine, cu `cod` și `mesaj`. Un articol cu constatări blocante se poate trimite, dar are șanse mari să fie respins de publisher. |
suggestedCampaignType | string | null | Ce fel de articol pare a fi, după ce l-am citit. Separat dinadins de tipul comandat: un articol clasificat „SEO" dar cumpărat ca mențiune de brand e o întrebare de pus înainte de plată, nu o corectură făcută pe la spate. |
classificationConfidence | int | null | Din 100. Regulile singure dau siguranță mare; când a fost nevoie de model ca să despartă doi candidați, e mai mică — și se vede. |
classificationReason | string | null | Motivul, scris pentru un om. Se poate contrazice. |
version | int | Crește doar la schimbarea titlului sau a conținutului. Intră în cheia de idempotență a publicării. |
status | string | `DRAFT`, `IN_REVIEW`, `APPROVED`, `SCHEDULED`, `PUBLISHING`, `PUBLISHED`, `FAILED`, `REJECTED`, `ARCHIVED`. |
createdViaApi | boolean | A intrat printr-o cheie, nu din interfață. Tot ce creezi prin API apare în cont ca orice altceva — la Articole, la Campanii, la Publicări — cu semnul ăsta lângă, ca să se vadă ce a făcut integrarea. |
Răspuns
{
"id": "e056…",
"title": "Cum alegi un service auto autorizat pentru garanția mașinii",
"contentHtml": "<h2>…</h2>",
"redactedAt": "2026-09-03T14:22:10.000Z",
"redactionFindings": [
{ "cod": "cifre-nesustinute", "mesaj": "Textul dă cifre care nu se află printre datele primite: 250, 400.", "blocant": true }
]
}Comenzi de redactare
Prin API nu se pornește nicio generare. Trimiți o comandă, o vedem, o scriem sau o refuzăm cu motiv, iar tu afli rezultatul pe aceeași cale. Motivul e simplu: o cheie rulează într-un script și poate cere o mie de articole peste noapte, din cote care sunt ale platformei, comune tuturor clienților. Iar un text scris de model, livrat automat și nevăzut de nimeni de la noi, pleacă spre publisheri sub numele nostru.
POST/partner/redactareComandă un articolARTICLES_WRITE
Răspunsul e comanda, cu `status: "NOUA"`. Când e gata, aceeași comandă are `status: "LIVRATA"` și `articleId` completat.
**Se plătește din sold, nu din credite.** Un credit e „o publicare inclusă" dintr-un pachet, nu ore de scris. Prețul se rezervă în clipa în care intră comanda și se consumă la livrare; un refuz, o anulare sau o scriere eșuată la noi îl eliberează integral. Fără sold, comanda e respinsă cu `400` și nu se creează deloc — mai bine un refuz la intrare decât un articol scris pe datorie.
`fapte` e singurul câmp măsurat care schimbă calitatea textului. Cu datele clientului în față, articolul le folosește cu cifrele lor exacte; fără ele rămâne general — corect, dar mai puțin util. Regula e strictă: **doar ce apare acolo are voie să apară în articol ca cifră sau ca nume**.
Corpul cererii
tema· obligatoriu | string(10–200) | O frază, cum ai spune-o unui redactor. Din ea iese și titlul. |
campaignType· obligatoriu | string | Unul dintre tipurile din `campaign-types`. |
cuvinte· obligatoriu | 500 | 700 | 1000 | 1500 | Lungimea țintă. |
cuvantCheie | string | Unul singur, una–patru cuvinte. |
brand | string | Brandul promovat. |
adresaPromovata | string | Adresa către care duce articolul. |
fapte | string[] (≤20) | Câte un lucru pe rând: prețuri, ani de activitate, certificări, cine poate fi citat. |
campaignId | uuid | Campania în care intră articolul livrat. |
note | string(≤2000) | Orice altceva vrei să ne spui despre comandă. |
idempotencyKey | string(≤200) | Aleasă de tine — un UUID generat local e suficient. Trimisă din nou, cu exact aceleași date, întoarce comanda deja creată în loc să facă una nouă. Recomandat pentru orice cod care poate retrimite cererea după un timeout: fără cheie, o retrimitere creează o a doua comandă și o a doua rezervare de bani. |
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/redactare' \
--header 'Authorization: Bearer bk_live_…' \
--header 'Content-Type: application/json' \
--data '{
"tema": "Cum alegi un service auto autorizat pentru garanția mașinii",
"campaignType": "SEO",
"cuvinte": 700,
"cuvantCheie": "service auto autorizat",
"brand": "Service Exemplu",
"fapte": ["Service-ul lucrează din 2009.", "Revizia de bază costă 350 de lei."],
"idempotencyKey": "9f2c7e40-9b1e-4c2a-8b0e-2e6a1f9c3d10"
}'Răspuns
{
"id": "7c1f…",
"status": "NOUA",
"tema": "Cum alegi un service auto autorizat pentru garanția mașinii",
"campaignType": "SEO",
"cuvinte": 700,
"priceCents": 3900,
"currency": "RON",
"chargedCents": null,
"articleId": null,
"rejectionReason": null,
"idempotencyKey": "9f2c7e40-9b1e-4c2a-8b0e-2e6a1f9c3d10",
"createdAt": "2026-09-03T12:00:00.000Z",
"deliveredAt": null
}GET/partner/redactare/{orderId}Starea unei comenziARTICLES_READ
Stările: `NOUA` (a intrat), `IN_LUCRU` (cineva de la noi a luat-o), `LIVRATA` (`articleId` completat, `chargedCents` spune cât s-a încasat), `REFUZATA` (cu `rejectionReason`), `ANULATA`, `ESUATA` (scrierea n-a reușit la noi). În ultimele trei, suma rezervată se întoarce în sold.
`articleId` se întoarce **numai la livrare**. Cât timp se scrie, ciorna e a noastră: dacă modelul a scos ceva slab, se rescrie sau se refuză, fără ca cineva să fi apucat s-o trimită mai departe.
Un webhook pe `redaction.delivered` scutește interogarea în buclă — între cerere și articol trece o revizie făcută de un om, iar durata ei nu se poate prezice.
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/redactare/7c1f…' \
--header 'Authorization: Bearer bk_live_…'Răspuns
{
"id": "7c1f…",
"status": "LIVRATA",
"articleId": "e056…",
"priceCents": 3900,
"chargedCents": 3900,
"deliveredAt": "2026-09-03T15:20:00.000Z"
}GET/partner/redactareComenzile taleARTICLES_READ
`status` filtrează. Cele mai noi primele.
Parametri
status | string | `NOUA`, `IN_LUCRU`, `LIVRATA`, `REFUZATA`, `ANULATA`. |
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/redactare?status=LIVRATA' \
--header 'Authorization: Bearer bk_live_…'Răspuns
[{ "id": "7c1f…", "status": "LIVRATA", "articleId": "e056…" }]POST/partner/redactare/{orderId}/anuleazaRenunță la o comandăARTICLES_WRITE
Numai cât timp e `NOUA`. Odată ce cineva de la noi a luat-o, munca e făcută sau în curs, iar anularea nu mai e a ta: se întoarce `409`, iar închiderea rămâne un refuz cu motiv, din partea noastră.
Exemplu
curl --location --request POST 'https://app.comunicate.top/api/v1/partner/redactare/7c1f…/anuleaza' \
--header 'Authorization: Bearer bk_live_…'Răspuns
{ "id": "7c1f…", "status": "ANULATA" }Verificări
Ce se poate spune despre un articol fără să cheme niciun model: analiza SEO și potrivirea cu tipul de campanie. Amândouă sunt pe reguli, deci nu costă, nu sunt limitate și dau același răspuns de fiecare dată. Ele **raportează**; nu corectează nimic. Completarea rămâne a ta, printr-un `PATCH`.
GET/partner/articles/{articleId}/seoAnaliza SEOARTICLES_READ
Scor, constatări, statistici și structura de titluri. Scorul e procentul de verificări trecute, nu o promisiune de poziționare.
Ce contează în răspuns e `issues`: fiecare are un `code`, o severitate și un mesaj care spune ce lipsește. Ce se repară — titlu prea lung, descriere lipsă, imagini fără text alternativ — se scrie cu `PATCH /partner/articles/{id}`.
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/articles/2668…/seo' \
--header 'Authorization: Bearer bk_live_…'Câmpuri din răspuns
score | int | 0–100, procentul verificărilor trecute. |
issues[] | array | `code`, `severity` și `message`. |
stats | object | Cuvinte, titluri, imagini fără text alternativ, linkuri interne și externe, densitatea cuvântului-cheie, lungimile titlului, descrierii și slugului. |
outline | array | Structura de titluri, cu nivelul fiecăruia. |
Răspuns
{
"score": 78,
"issues": [
{ "code": "meta-description-missing", "severity": "warning", "message": "Articolul n-are descriere meta.", "autoFixable": false }
],
"stats": { "wordCount": 712, "headingCount": 5, "imageCount": 2, "imagesWithoutAlt": 1, "internalLinks": 0, "externalLinks": 2, "keywordCount": 6, "keywordDensity": 0.84, "titleLength": 74, "metaLength": 0, "slugLength": 0 },
"outline": [{ "level": 2, "text": "Devizul, pe articole" }]
}GET/partner/articles/{articleId}/potrivireSe potrivește cu tipul de campanie?ARTICLES_READ
Altă întrebare decât analiza SEO: aceea spune cât de bine e scris textul, asta spune dacă e articolul **comandat**. Un text impecabil SEO e un refuz sigur într-o campanie de mențiune de brand, fiindcă poartă linkuri.
Verificarea oprește oricum comanda la trimitere. Fără ruta asta, singura cale de a afla era să încerci — articol cu articol, din erori. Răspunsul include și `cerinte`, ca mesajul către client să nu fie doar „nu se potrivește".
Când articolul e într-o campanie legată de un tip, verdictul vine oricum pe articol, în `potrivireCampanie`; asta e pentru un tip pe care încă nu l-ai ales.
Parametri
campaignType | string | Tipul verificat. Implicit `SEO`. |
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/articles/2668…/potrivire?campaignType=BRAND_MENTION' \
--header 'Authorization: Bearer bk_live_…'Răspuns
{
"potrivit": false,
"constatari": [
{ "severitate": "BLOCANT", "mesaj": "Articolul are 2 linkuri externe; o mențiune de brand nu duce niciun link." }
],
"cerinte": {
"rezumat": "Mențiune: brandul apare în text, fără niciun link.",
"obligatoriu": ["Numele brandului în text", "Minimum 200 de cuvinte"],
"interzis": ["Orice link extern"]
}
}GET/partner/articles/{articleId}/revisionsVersiunile anterioareARTICLES_READ
Textul întreg al fiecărei versiuni înlocuite. Se scrie o revizie la fiecare schimbare de titlu sau conținut — adică exact când crește `version`.
Există pentru momentul în care publisherul spune că articolul de pe site nu mai seamănă cu ce a acceptat: răspunsul cere textul întreg, nu o mostră din jurnal.
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/articles/2668…/revisions' \
--header 'Authorization: Bearer bk_live_…'Câmpuri din răspuns
curenta | object | Versiunea de acum, ca să existe cu ce compara prima revizie. |
revizii[] | array | Cel mult cincizeci, de la cea mai recentă. `changedBy` e gol când schimbarea a venit printr-o cheie de API. |
Răspuns
{
"curenta": { "version": 3, "title": "Titlul de acum", "contentHtml": "<p>…</p>" },
"revizii": [
{ "id": "aa11…", "version": 2, "title": "Titlul dinainte", "contentHtml": "<p>…</p>", "changedBy": null, "createdAt": "2026-09-01T10:00:00.000Z" }
]
}DELETE/partner/articles/{articleId}Șterge un articolARTICLES_WRITE
Răspunde `204` la reușită. Refuzat cu `409` dacă articolul are publicări: istoricul unei publicări trebuie să poată arăta ce s-a trimis, iar un articol publicat se scoate din listă, nu din bază.
Exemplu
curl --location --request DELETE 'https://app.comunicate.top/api/v1/partner/articles/2668…' \
--header 'Authorization: Bearer bk_live_…'Răspuns
(fără corp — 204 No Content)Galerie
POST/partner/mediaÎncarcă o imagineMEDIA_WRITE
JPEG, PNG, GIF, WebP sau AVIF. Tipul se determină din conținut, nu din extensie sau din antet: un fișier deghizat e respins.
Aceeași imagine încărcată de două ori nu se dublează — se recunoaște după amprentă și se întoarce cea existentă. Adresa din răspuns se poate pune direct în `featuredImageUrl` la un articol.
Formular (multipart/form-data)
(fișier)· obligatoriu | file | Imaginea. |
folderId | uuid | Folderul din galerie. |
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/media' \
--header 'Authorization: Bearer bk_live_…' \
--form 'file=@coperta.jpg'Răspuns
{
"id": "4e4a…",
"filename": "coperta.jpg",
"url": "/api/v1/organizations/adb4…/media/4e4a…/content",
"mimeType": "image/jpeg",
"sizeBytes": 84213,
"width": 1200,
"height": 630
}Publicări
O publicare leagă un articol de o publicație. E singurul loc din API care cheltuie credite sau bani.
POST/partner/publicationsCere publicareaPUBLICATIONS_WRITE
Un articol, una sau mai multe publicații.
**Ambiguitatea de plată nu se ghicește.** Când mai multe pachete deținute acoperă același site, cererea se respinge cu `409` și lista opțiunilor; integrarea alege și reia cu `packageTypeId`. A ghici ar însemna să consumăm din pachetul greșit, iar asta nu se poate întoarce.
Corpul cererii
articleId· obligatoriu | uuid | Articolul de publicat. |
siteIds· obligatoriu | uuid[] | Publicațiile alese din catalog. |
campaignType· obligatoriu | string | Tipul campaniei. Trebuie să fie printre `acceptedCampaigns` ale site-ului. |
packageTypeId | uuid | Din ce pachet se plătește, când mai multe acoperă site-ul. |
licenseNumber | string(3–100) | Licența ONJN a operatorului. Obligatorie la `campaignType: "CASINO"` — publicitatea la jocuri de noroc trebuie s-o afișeze, iar platforma o scrie singură pe articol înainte de trimitere. |
extrasBySite | object | Extrase cerute per site — `"HOMEPAGE_PLACEMENT"`, `"FACEBOOK_SHARE"`, cel mult amândouă. Cheia e `siteId`, valoarea o listă de tipuri; fiecare cerut trebuie să aibă o ofertă activă pe site (vezi `GET /partner/catalog`), altfel cererea se respinge. |
scheduledFor | datetime | Când să apară, dacă nu imediat. |
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/publications' \
--header 'Authorization: Bearer bk_live_…' \
--header 'Content-Type: application/json' \
--data '{
"articleId": "2668…",
"siteIds": ["3f1c…"],
"campaignType": "SEO",
"extrasBySite": { "3f1c…": ["HOMEPAGE_PLACEMENT"] }
}'Răspuns
{
"publications": [
{
"id": "9a3b…",
"siteId": "3f1c…",
"status": "AWAITING_APPROVAL",
"amountCents": 18000,
"paymentSource": "MONEY",
"extras": [
{ "type": "HOMEPAGE_PLACEMENT", "priceCents": 5000, "currency": "RON", "chargedCents": 5000 }
]
}
]
}GET/partner/publicationsLista publicărilorPUBLICATIONS_READ
Toate publicările organizației. `status` filtrează. Paginată pe cursor: `nextCursor` din răspuns se retrimite ca `cursor` pentru pagina următoare, `null` la ultima.
Pentru urmărirea stării, webhookurile sunt de preferat: `publication.published` vine cu adresa articolului în clipa în care apare, iar interogarea în buclă consumă din limita de cereri fără să afle nimic în plus.
Parametri
status | string | `DRAFT`, `AWAITING_APPROVAL`, `APPROVED`, `PUBLISHING`, `PUBLISHED`, `FAILED`, `REJECTED`, `CANCELLED`. |
cursor | uuid | Id-ul ultimei publicări văzute. Lipsă la prima pagină. |
limit | int (implicit 50, cel mult 200) | Câte publicări pe pagină. |
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/publications?status=PUBLISHED&limit=50' \
--header 'Authorization: Bearer bk_live_…'Răspuns
{
"items": [
{ "id": "9a3b…", "status": "PUBLISHED", "publishedUrl": "https://exemplu.ro/articol", "publishedAt": "2026-09-01T08:14:00.000Z" }
],
"nextCursor": null
}GET/partner/publications/{publicationId}O publicarePUBLICATIONS_READ
Publicarea întreagă, cu istoricul ei de evenimente și cu motivul, când a eșuat sau a fost respinsă.
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/publications/9a3b…' \
--header 'Authorization: Bearer bk_live_…'Răspuns
{
"id": "9a3b…",
"status": "FAILED",
"lastError": "Site-ul a răspuns 401 la autentificare",
"attempts": 3,
"publishedUrl": null
}Campanii
Gruparea articolelor și a publicărilor pe client sau pe proiect. Echivalentul „proiectelor" din alte platforme.
GET/partner/campaignsLista campaniilorCAMPAIGNS_READ
`status` filtrează. Paginată pe cursor: `nextCursor` din răspuns se retrimite ca `cursor` pentru pagina următoare, `null` la ultima.
Parametri
status | string | `DRAFT`, `ACTIVE`, `PAUSED`, `COMPLETED`, `ARCHIVED`. |
cursor | uuid | Id-ul ultimei campanii văzute. Lipsă la prima pagină. |
limit | int (implicit 50, cel mult 200) | Câte campanii pe pagină. |
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/campaigns?limit=50' \
--header 'Authorization: Bearer bk_live_…'Răspuns
{
"items": [{ "id": "8b2e…", "name": "Client Exemplu — Q3", "status": "ACTIVE" }],
"nextCursor": null
}POST/partner/campaignsCreează o campanieCAMPAIGNS_WRITE
Corpul e strict: un câmp pe care nu-l cunoaște e respins cu `400`, nu ignorat în tăcere. Mai bine o eroare la prima încercare decât o setare care pare aplicată și nu e.
Corpul cererii
name· obligatoriu | string(2–150) | Numele campaniei. |
campaignType | string | Leagă campania de un tip. Fiecare articol care intră în ea e verificat automat împotriva cerințelor tipului, iar verdictul apare pe articol în `potrivireCampanie`. Nu blochează scrierea — refuzul rămâne la comandă, unde e și plata. |
clientName | string(≤150) | Eticheta clientului final. E text liber: clienții n-au cont în platformă. |
objective | string(≤500) | Ce urmărește campania. |
notes | string(≤2000) | Însemnări interne. |
startsAt | datetime | ISO 8601. Trebuie să fie înaintea lui `endsAt`. |
endsAt | datetime | ISO 8601. |
budgetCents | int | Buget de urmărit, în bani. Nu e un portofel separat: banii stau în contul organizației. |
budgetCredits | int | Limită de credite consumate în campanie. |
allowedSiteIds | uuid[] (≤500) | Site-urile permise. Gol înseamnă „oricare din catalog". |
idempotencyKey | string(≤200) | Aleasă de tine — un UUID generat local e suficient. Trimisă din nou, cu aceeași organizație, întoarce campania deja creată în loc să facă una nouă. Recomandat pentru orice cod care poate retrimite cererea după un timeout. |
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/campaigns' \
--header 'Authorization: Bearer bk_live_…' \
--header 'Content-Type: application/json' \
--data '{
"name": "Client Exemplu — Q3",
"clientName": "Exemplu SRL",
"campaignType": "SEO",
"idempotencyKey": "9f2c7e40-9b1e-4c2a-8b0e-2e6a1f9c3d10"
}'Răspuns
{
"id": "8b2e…",
"name": "Client Exemplu — Q3",
"clientName": "Exemplu SRL",
"campaignType": "SEO",
"status": "ACTIVE",
"idempotencyKey": "9f2c7e40-9b1e-4c2a-8b0e-2e6a1f9c3d10",
"articleCount": 0
}GET/partner/campaigns/{campaignId}O campanieCAMPAIGNS_READ
Campania, cu articolele și publicările ei.
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/campaigns/8b2e…' \
--header 'Authorization: Bearer bk_live_…'Răspuns
{ "id": "8b2e…", "name": "Client Exemplu — Q3", "articles": 12, "publications": 34 }PATCH/partner/campaigns/{campaignId}Modifică o campanieCAMPAIGNS_WRITE
Toate câmpurile de la creare, toate opționale. Se trimit doar cele care se schimbă.
Cel mai des folosit pentru `campaignType`: leagă o campanie deja creată de un tip, sau o scoate cu `null`. Fiecare articol din campanie e reverificat automat împotriva noului tip — vezi `potrivireCampanie` pe articol.
Exemplu
curl --location --request PATCH 'https://app.comunicate.top/api/v1/partner/campaigns/8b2e…' \
--header 'Authorization: Bearer bk_live_…' \
--header 'Content-Type: application/json' \
--data '{ "campaignType": "BRAND_AWARENESS" }'Răspuns
{ "id": "8b2e…", "name": "Client Exemplu — Q3", "campaignType": "BRAND_AWARENESS", "articleCount": 12 }DELETE/partner/campaigns/{campaignId}Șterge o campanieCAMPAIGNS_WRITE
Răspunde `204` la reușită. Refuzată cu `409` dacă e activă și are articole — aceeași regulă ca în interfață: o campanie cu istoric se arhivează (`PATCH` cu `status: "ARCHIVED"`), nu se șterge.
Exemplu
curl --location --request DELETE 'https://app.comunicate.top/api/v1/partner/campaigns/8b2e…' \
--header 'Authorization: Bearer bk_live_…'Răspuns
(fără corp — 204 No Content)Sold și rapoarte
GET/partner/balanceSoldul disponibilBALANCE_READ
Pachetele deținute, cu creditul rămas pe fiecare, și soldul în bani. Se verifică înainte de a cere publicarea — altfel integrarea află din eroare, după ce a construit deja articolul.
`available` scade rezervările: o publicare în curs a blocat deja creditul, chiar dacă nu l-a consumat.
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/balance' \
--header 'Authorization: Bearer bk_live_…'Răspuns
{
"credits": [
{ "packageTypeId": "c1…", "packageName": "Pachet Regional 20", "available": 14, "total": 20 }
],
"money": { "availableCents": 125000, "currency": "RON" }
}GET/partner/reports/summaryRezumatul unei perioadeREPORTS_READ
Implicit ultimele 30 de zile. O integrare care nu trimite interval primește ceva util, nu tot istoricul.
Parametri
from | date | Începutul intervalului. |
to | date | Sfârșitul intervalului. |
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/reports/summary?from=2026-08-01&to=2026-08-31' \
--header 'Authorization: Bearer bk_live_…'Răspuns
{
"from": "2026-08-01T00:00:00.000Z",
"to": "2026-08-31T00:00:00.000Z",
"publications": 42,
"published": 39,
"failed": 3,
"distinctSites": 21,
"creditsUsed": 18,
"moneySpentCents": 372000,
"successRate": 0.93
}GET/partner/reports/publicationsPublicările din intervalREPORTS_READ
Rând cu rând, pentru raportarea către client.
Exemplu
curl --location 'https://app.comunicate.top/api/v1/partner/reports/publications?from=2026-08-01' \
--header 'Authorization: Bearer bk_live_…'Răspuns
[
{ "publishedAt": "2026-08-14T09:02:00.000Z", "article": "Ce verifici la o firmă de amenajări", "site": "exemplu.ro", "url": "https://exemplu.ro/articol", "campaign": "Client Exemplu — Q3" }
]Webhookuri
Interogarea în buclă a stării unei publicări consumă din limita ta de cereri și află, de cele mai multe ori, că nu s-a schimbat nimic. Un webhook ajunge în clipa în care articolul apare, cu adresa lui. Capetele se configurează din pagina de integrări, nu prin API — cine își schimbă destinația evenimentelor trebuie să fie autentificat ca persoană.
Reîncercări
Un endpoint care nu răspunde cu 2xx (sau nu răspunde deloc) e reîncercat automat, cu pauze din ce în ce mai mari: 30 de secunde, apoi 1, 2, 4, 8, 16, 32 de minute — șapte încercări în total. Dacă tot eșuează, livrarea rămâne vizibilă în pagina de integrări, cu răspunsul primit și motivul, și se poate reîncerca manual de acolo — dacă și reîncercarea manuală eșuează, intră din nou în rotația automată.
Cum se verifică semnătura
Fiecare livrare are `X-Birou-Signature: t=<timp>,v1=<hmac>`. HMAC-SHA256 se calculează peste `<timp>.<corpul brut>`, cu secretul capătului — nu doar peste corp: fără marca de timp, o livrare interceptată ar putea fi retrimisă oricând cu aceeași semnătură validă. Verifică și că `t` e recent.
const [t, v1] = header.split(',').map((p) => p.split('=')[1]);
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
// comparație în timp constant, apoi: Math.abs(Date.now() / 1000 - Number(t)) < 300Evenimente
article.createdUn articol a fost creat, prin orice cale.article.generatedO ciornă cerută din interfață s-a terminat de scris. Comenzile trimise prin API folosesc `redaction.delivered`, fiindcă între cerere și articol trece și o revizie făcută de un om.article.failedScrierea unei ciorne a eșuat.publication.submittedPublicarea a fost trimisă către publisher.publication.publishedArticolul a apărut pe site. Aici vine adresa lui.publication.failedPublicarea a eșuat sau a fost respinsă.indexing.updatedStarea de indexare a unui articol publicat s-a schimbat.order.acceptedPublisherul a acceptat comanda.redaction.deliveredO comandă de redactare a fost livrată. Vine cu `orderId` și `articleId` — de aici se citește articolul.redaction.rejectedO comandă de redactare a fost refuzată. Vine cu motivul.
Mai departe