Skip to content

API documentation

The same operations as the interface, through an API key: catalogue, articles, publications, campaigns, reports, writing. Each route with its required permission, fields and a sample response. Same source as the OpenAPI file.

Authentication

The key goes in the `Authorization` header. It is issued from the integrations page and shown once — we keep only its fingerprint, exactly as with passwords.

curl --location 'https://app.comunicate.top/api/v1/partner/balance' \
  --header 'Authorization: Bearer bk_live_…'
Base URL
https://app.comunicate.top/api/v1
Rate limit
Per key, per minute — set on the key when you issue it. One runaway integration does not block the same account’s other keys. Exceeding it returns 429 with `Retry-After`, not 403: the one case where retrying makes sense.
AI routes
Writing, document import and metadata completion carry an extra limit: sixty per hour, per organisation, however many keys you hold. They consume the model quotas, which belong to the platform rather than to your account — and a single client exhausting them would stop generation for everyone.
Organisation
Comes from the key, not the URL. There is no `organizationId` to send, and none to get wrong.
Errors
A real HTTP status — 400, 401, 403, 404, 409 — and a body with `message`. On validation, `errors` says which field and why.

An error response

{
  "message": "Datele trimise nu sunt valide",
  "errors": [{ "path": "tags", "code": "invalid_type", "message": "Expected array, received string" }]
}

Permissions

Each key gets only the permissions it needs. An integration that reads the catalogue cannot publish, however hard it tries.

  • CATALOG_READReading the publication catalogue and the campaign types.
  • ARTICLES_READReading the organisation's articles.
  • ARTICLES_WRITECreating and editing articles, importing documents and Drive folders, requesting AI writing.
  • MEDIA_WRITEUploading images to the media library.
  • PUBLICATIONS_READReading publications and their status.
  • PUBLICATIONS_WRITERequesting publication — the only permission that spends credits or money.
  • CAMPAIGNS_READReading campaigns.
  • CAMPAIGNS_WRITECreating campaigns.
  • BALANCE_READReading the balance and owned packages.
  • REPORTS_READReading reports over a date range.
35 routes

Public catalogue

Four routes that work with no key at all: the network’s niches, one niche with sample publications, the catalogue figures and the market statistics. This is the same data the public site shows, served as JSON — useful for comparisons, for an assistant that wants to know what is in the network, or for anyone who wants to see the shape of the data before opening an account. There is deliberately no public route for the full publication list: the inventory is not downloadable in one request.

GET/public/statsCatalogue figuresno key

How many active publications the network has, and across how many niches. No key needed.

Example

curl 'https://app.comunicate.top/api/v1/public/stats'

Response fields

publicationsintegerActive publications.
nichesintegerNiches with publications.

Response

{ "publications": 3822, "niches": 21 }
GET/public/nichesThe network’s nichesno key

All niches, with the publication count and average authority of each. No key needed.

Parameters

locale'ro' | 'en'Language of names and descriptions. Defaults to `ro`.

Example

curl 'https://app.comunicate.top/api/v1/public/niches?locale=ro'

Response fields

slugstringThe niche identifier, used in the next route.
namestringThe niche name in the requested language.
descriptionstringWhat kind of material the publications in this niche accept.
publicationCountintegerHow many active publications the niche has.
averageDaintegerAverage domain authority (Moz DA).

Response

[
  {
    "slug": "stiri",
    "name": "Știri",
    "description": "Publicațiile de actualitate generală din rețea…",
    "publicationCount": 1299,
    "averageDa": 38
  }
]
GET/public/niches/{slug}One niche, with sample publicationsno key

The requested niche and a few of its publications, with authority, turnaround, price and how paid content is labelled. It is a sample, not the inventory: the full list is visible from an account.

Parameters

locale'ro' | 'en'Language of names and descriptions. Defaults to `ro`.

Example

curl 'https://app.comunicate.top/api/v1/public/niches/stiri?locale=ro'

Response fields

slugstringThe niche identifier.
namestringThe niche name.
publicationCountintegerHow many publications the niche has in total.
averageDaintegerThe niche’s average authority.
publicationsobject[]The sample: `domain`, `language`, `metricDa`, `metricPa`, `deliveryDays`, `priceCents`, `currency`, `marcaj`.

Response

{
  "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-piataMarket statisticsno key

The catalogue’s aggregate figures: how many publications, how many from our own network, the authority distribution, turnaround times and the price for each campaign type. The data behind the “What an advertorial costs” page.

Example

curl 'https://app.comunicate.top/api/v1/public/statistici-piata'

Response fields

publicatiiintegerActive publications with a public price.
reteaintegerHow many of them are our own.
autoritateobjectMinimum, maximum, median, mean and the distribution across bands.
termeneobject[]How many publications deliver in how many days.
preturiobject[]Per campaign type: price, minimum, maximum and how many publications accept it.

Response

{
  "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 }
  ]
}

Key and permissions

The first call of any integration: who the key is and what it may do. This is also where you see whether the organisation enabled "AI assistants may order and publish" — the switch that decides what an MCP assistant can do with the account’s money.

GET/partner/meWho am Iany key

The organisation behind the key, its permissions and the AI assistants switch. Requires no scope: any valid key may ask.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/me' \
  --header 'Authorization: Bearer bk_live_…'

Response fields

organizationobject`id` and `name`.
scopesstring[]The key’s scopes.
aiPublishingboolean`true` if AI assistants may order publications and writing (Integrations → AI assistants).

Response

{
  "organization": { "id": "e0b5…", "name": "Agenția Exemplu SRL" },
  "keyPrefix": "bk_live_a1b2",
  "scopes": ["CATALOG_READ", "ARTICLES_WRITE"],
  "aiPublishing": false
}

Catalogue

Which publications exist, what each campaign type costs, and whether your packages cover them.

GET/partner/catalogList publicationsCATALOG_READ

The paginated catalogue, with the same filters as the interface. Each entry also states how it can be paid for: from a package, from the balance, or not at all.

Parameters

pageintRequested page, from 1.
pageSizeintEntries per page, at most 100.
searchstringSearches name and domain.
campaignstringOnly publications accepting the given type (see `campaign-types`).
serviceTypestring`PUBLISH_ONLY`, `WRITE_AND_PUBLISH`, `HOMEPAGE_PLACEMENT` or `FACEBOOK_SHARE`. For the other three, only sites with an active offer of the requested type appear — plain publishing is the base service, any priced site has it.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/catalog?page=1&campaign=SEO' \
  --header 'Authorization: Bearer bk_live_…'

Response fields

entries[].iduuidThe publication id. Used when requesting a publication.
entries[].metricsobjectDR, DA, PA and backlink count. `null` where we have no reading.
entries[].auditobject | null`flagged` is the only field that changes a decision: a site Google flags as dangerous should not be bought.
entries[].preturiCampaniiobjectPublication price per accepted campaign type, in minor units.
entries[].accessobject`OWN`, `CREDIT` (covered by a package), `MONEY` (paid from balance), `UNAVAILABLE`. When `packages` has more than one entry, publishing requires `packageTypeId`.
entries[].masuratobjectWhat we measured from our own orders: average hours to going live, how many verified publications are still fine. `null` where we have not published enough.

Response

{
  "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}One publicationCATALOG_READ

The same shape as a list entry, plus what does not fit in a listing: per-campaign terms, accepted topics, active offers — writing, homepage placement, Facebook sharing. A missing offer for a type means the site does not sell it.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/catalog/3f1c…' \
  --header 'Authorization: Bearer bk_live_…'

Response

{
  "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-typesCampaign typesCATALOG_READ

A type is not a label but a set of conditions the article passes or fails on submission. Checking them up front saves a chain of one-by-one rejections.

Which types a given site accepts is a different question, answered by `acceptedCampaigns` in the catalogue.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/campaign-types' \
  --header 'Authorization: Bearer bk_live_…'

Response

[
  {
    "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"]
  }
]

Articles

An article enters the platform four ways: written by you as HTML, imported from a document, taken from a Drive folder, or written by the platform from a brief. All produce the same thing — a draft that can be published.

POST/partner/articlesCreate an article from HTMLARTICLES_WRITE

The direct route, when you already have the text. The markup goes through the same sanitiser as imports: scripts, inline styles and elements that do not belong in an article are removed, and what was removed appears in the response.

Optimisation fields are normalised, not rejected: five comma-separated keywords keep the first; ten tags keep the first three. An article supports one term, and twenty tags mean archive pages with a single text on them.

Request body

title· requiredstring(3–300)The article title.
contentHtml· requiredstring(≤500 000)The article body, as HTML.
focusKeywordstringThe keyword to optimise for. **One only** — from a list, the first is kept.
tagsstring[]At most three, however many are sent. A comma-separated list inside one element is also accepted.
metaDescriptionstring(≤320)The description shown in search results.
slugstringThe proposed article URL slug. WordPress may change it on collision; what actually resulted is read back and kept on the publication.
excerptstring(≤1000)The summary themes use in listings and on category pages.
featuredImageAltstring(≤300)The featured image's alt text. Its absence is one of the findings the SEO analysis reports.
seoTitlestring(≤200)The search-result title, when it differs from the article title.
featuredImageUrlstringThe featured image. Can be the URL returned by `POST /partner/media`.
campaignIduuidThe campaign the article belongs to.
idempotencyKeystring(≤200)Chosen by you — a locally generated UUID is enough. Sent again, for the same organisation, it returns the article already created instead of making a new one. No effect on `PATCH`. Recommended for any code that might resend the request after a timeout.

Example

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"
  }'

Response

{
  "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 a documentARTICLES_WRITE

Accepts `.docx`, `.doc`, `.odt`, `.rtf`, `.fodt`, `.html` and `.htm`. Older formats go through LibreOffice, so they take a few seconds longer.

**File order matters**: the first file is the document, the second — optional — is the featured image. They cannot be told apart by field name, because everyone names their files differently.

Images inside the document are pulled into the media library and rewritten in the text; those that could not be pulled appear in `images.skipReasons` with the reason. Metadata — description, tags — is filled in afterwards, in the background.

Form (multipart/form-data)

(primul fișier)· requiredfileThe document.
(al doilea fișier)fileThe featured image, for documents with none in the body.
campaignIduuidThe campaign the article belongs to.
folderIduuidThe media-library folder the document images land in.

Example

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…'

Response fields

articles[]arrayThe articles created, with title and word count.
imagesobjectHow many images were imported, reused, converted, and why the rest were skipped.
sanitizedobjectWhat the sanitiser removed. If the article looks different from the document, this says why.

Response

{
  "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/driveList a Drive folderARTICLES_WRITE

The folder must be shared "anyone with the link": the platform reads it with its own account, the client signs in nowhere.

Listing is deliberately a separate step from importing. A folder usually holds drafts, old versions and the good document — importing everything produces ten articles, nine of which get deleted.

Parameters

link· requiredstringThe folder link, as Google gives it.

Example

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_…'

Response

{
  "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 from the Drive folderARTICLES_WRITE

At most fifty documents at a time. A broken document does not stop the rest: the result has one entry per file, with either the article count or its error.

The chosen image is downloaded once and used for every document that has none in its body.

Request body

link· requiredstringThe same folder link used for the listing. Every requested file is checked against it: downloads use the platform account, which also sees other clients’ folders, so the route does not accept arbitrary Drive identifiers.
fisiere· requiredarrayThe chosen documents, with `id`, `nume` and `mimeType` from the listing.
imagineIdstringThe id of an image in the same folder, used as the featured image.
campaignIduuidThe campaign the articles belong to.

Example

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…"
  }'

Response

{
  "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}Update an articleARTICLES_WRITE

Every field from creation, all optional. Send only what changes. The same normalisations apply: one keyword, at most three tags.

`version` increases **only when the title or the content changes**, not on every edit. It is part of the publication idempotency key: a corrected, resubmitted article is a new publication, whereas a changed tag does not produce a different article on the site.

An article currently being published cannot be edited: you get `409`. The worker works from the content read at the start of the process, and an edit now would put something different on the site than what the platform holds.

Example

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." }'

Response

{
  "id": "2668…",
  "metaDescription": "Ce verifici înainte să semnezi contractul.",
  "version": 2
}
GET/partner/articlesList articlesARTICLES_READ

The organisation's articles, most recently updated first. Cursor-paginated: the response's `nextCursor` is sent back as `cursor` for the next page, `null` on the last one.

Parameters

cursoruuidThe id of the last article seen. Absent on the first page.
limitint (implicit 50, cel mult 200)How many articles per page.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/articles?limit=50' \
  --header 'Authorization: Bearer bk_live_…'

Response

{
  "items": [
    { "id": "2668…", "title": "Ce verifici la o firmă de amenajări", "status": "DRAFT", "version": 2 }
  ],
  "nextCursor": "9a11…"
}
GET/partner/articles/{articleId}One articleARTICLES_READ

The whole article, with its text. This is also where you see whether a draft requested through `redactare` has finished: `redactedAt` set means done, and `redactionFindings` says what remains unresolved.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/articles/e056…' \
  --header 'Authorization: Bearer bk_live_…'

Response fields

redactedAtdatetime | nullWhen writing finished. `null` while queued.
redactionFindingsarrayWhat did not come out right, with `cod` and `mesaj`. An article with blocking findings can be submitted, but is likely to be rejected by the publisher.
suggestedCampaignTypestring | nullWhat kind of article it appears to be, after we read it. Deliberately separate from the type ordered: an article classified "SEO" but bought as a brand mention is a question to raise before payment, not a silent correction.
classificationConfidenceint | nullOut of 100. Rules alone give high confidence; when a model was needed to separate two candidates it is lower — and it shows.
classificationReasonstring | nullThe reason, written for a person. It can be argued with.
versionintIncreases only when the title or content changes. Part of the publication idempotency key.
statusstring`DRAFT`, `IN_REVIEW`, `APPROVED`, `SCHEDULED`, `PUBLISHING`, `PUBLISHED`, `FAILED`, `REJECTED`, `ARCHIVED`.
createdViaApibooleanIt came in through a key, not the interface. Everything you create through the API appears in the account like anything else — under Articles, Campaigns, Publications — with this marker beside it, so what the integration did is visible.

Response

{
  "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 }
  ]
}

Writing orders

The API starts no generation. You place an order, we see it, we write it or decline it with a reason, and you learn the outcome the same way. The reason is simple: a key runs inside a script and can ask for a thousand articles overnight, out of quotas that belong to the platform and are shared by every client. And a model-written text delivered automatically, seen by nobody here, goes out to publishers under our name.

POST/partner/redactareOrder an articleARTICLES_WRITE

The response is the order, with `status: "NOUA"`. When it is done, the same order has `status: "LIVRATA"` and `articleId` filled in.

**It is paid from the money balance, not from credits.** A credit is “one publication included” from a package, not hours of writing. The price is reserved the moment the order arrives and consumed on delivery; a rejection, a cancellation or a writing failure on our side releases it in full. With no balance the order is refused with `400` and is not created at all — better a refusal at the door than an article written on credit.

`fapte` is the one measured field that changes the quality of the text. With the client's data in front of it, the article uses their exact figures; without it, it stays general — correct, but less useful. The rule is strict: **only what appears there may appear in the article as a figure or a name**.

Request body

tema· requiredstring(10–200)One sentence, as you would tell a writer. The title comes from it.
campaignType· requiredstringOne of the types from `campaign-types`.
cuvinte· required500 | 700 | 1000 | 1500Target length.
cuvantCheiestringOne only, one to four words.
brandstringThe promoted brand.
adresaPromovatastringThe URL the article points to.
faptestring[] (≤20)One item per entry: prices, years in business, certifications, who can be quoted.
campaignIduuidThe campaign the delivered article joins.
notestring(≤2000)Anything else you want to tell us about the order.
idempotencyKeystring(≤200)Chosen by you — a locally generated UUID is enough. Sent again with exactly the same data, it returns the order already created instead of making a new one. Recommended for any code that might resend the request after a timeout: without a key, a resend creates a second order and a second money reservation.

Example

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"
  }'

Response

{
  "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}Order statusARTICLES_READ

States: `NOUA` (received), `IN_LUCRU` (someone here picked it up), `LIVRATA` (`articleId` filled in, `chargedCents` says what was charged), `REFUZATA` (with `rejectionReason`), `ANULATA`, `ESUATA` (writing failed on our side). In the last three, the reserved amount returns to the balance.

`articleId` is returned **only on delivery**. While it is being written the draft is ours: if the model produced something weak, it gets rewritten or declined, without anyone having had the chance to send it on.

A `redaction.delivered` webhook saves you from polling — a human review sits between the request and the article, and its duration cannot be predicted.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/redactare/7c1f…' \
  --header 'Authorization: Bearer bk_live_…'

Response

{
  "id": "7c1f…",
  "status": "LIVRATA",
  "articleId": "e056…",
  "priceCents": 3900,
  "chargedCents": 3900,
  "deliveredAt": "2026-09-03T15:20:00.000Z"
}
GET/partner/redactareYour ordersARTICLES_READ

`status` filters. Most recent first.

Parameters

statusstring`NOUA`, `IN_LUCRU`, `LIVRATA`, `REFUZATA`, `ANULATA`.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/redactare?status=LIVRATA' \
  --header 'Authorization: Bearer bk_live_…'

Response

[{ "id": "7c1f…", "status": "LIVRATA", "articleId": "e056…" }]
POST/partner/redactare/{orderId}/anuleazaCancel an orderARTICLES_WRITE

Only while it is `NOUA`. Once someone here has picked it up the work is done or in progress and cancelling is no longer yours to do: you get `409`, and closing it stays a declined order with a reason, from our side.

Example

curl --location --request POST 'https://app.comunicate.top/api/v1/partner/redactare/7c1f…/anuleaza' \
  --header 'Authorization: Bearer bk_live_…'

Response

{ "id": "7c1f…", "status": "ANULATA" }

Checks

What can be said about an article without calling any model: the SEO analysis and the campaign-type fit. Both are rule-based, so they cost nothing, are not rate-limited, and give the same answer every time. They **report**; they fix nothing. Filling things in stays yours, through a `PATCH`.

GET/partner/articles/{articleId}/seoSEO analysisARTICLES_READ

Score, findings, statistics and the heading outline. The score is the percentage of checks passed, not a ranking promise.

What matters in the response is `issues`: each has a `code`, a severity and a message saying what is missing. Fixes — an over-long title, a missing description, images without alt text — are written with `PATCH /partner/articles/{id}`.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/articles/2668…/seo' \
  --header 'Authorization: Bearer bk_live_…'

Response fields

scoreint0–100, the percentage of checks passed.
issues[]array`code`, `severity` and `message`.
statsobjectWords, headings, images without alt text, internal and external links, keyword density, and the lengths of title, description and slug.
outlinearrayThe heading outline, with each level.

Response

{
  "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}/potrivireDoes it match the campaign type?ARTICLES_READ

A different question from the SEO analysis: that one says how well the text is written, this one says whether it is the article that was **ordered**. A flawless SEO text is a certain rejection in a brand-mention campaign, because it carries links.

The check stops the order at submission anyway. Without this route, the only way to find out was to try — article by article, from errors. The response also includes `cerinte`, so the message to the client is not merely "does not match".

When the article sits in a campaign bound to a type, the verdict arrives on the article anyway, in `potrivireCampanie`; this is for a type you have not chosen yet.

Parameters

campaignTypestringThe type checked. Defaults to `SEO`.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/articles/2668…/potrivire?campaignType=BRAND_MENTION' \
  --header 'Authorization: Bearer bk_live_…'

Response

{
  "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}/revisionsPrevious revisionsARTICLES_READ

The full text of every replaced version. A revision is written on each title or content change — exactly when `version` increases.

It exists for the moment a publisher says the article on the site no longer resembles what they accepted: answering that requires the whole text, not a sample from a log.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/articles/2668…/revisions' \
  --header 'Authorization: Bearer bk_live_…'

Response fields

curentaobjectThe current version, so the first revision has something to compare against.
revizii[]arrayAt most fifty, most recent first. `changedBy` is empty when the change came through an API key.

Response

{
  "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}Delete an articleARTICLES_WRITE

Responds `204` on success. Refused with `409` if the article has publications: a publication's history must be able to show what was sent, and a published article is removed from the list, not from the database.

Example

curl --location --request DELETE 'https://app.comunicate.top/api/v1/partner/articles/2668…' \
  --header 'Authorization: Bearer bk_live_…'

Response

(fără corp — 204 No Content)

Media

POST/partner/mediaUpload an imageMEDIA_WRITE

JPEG, PNG, GIF, WebP or AVIF. The type is detected from content, not from the extension or header: a disguised file is rejected.

The same image uploaded twice is not duplicated — it is recognised by checksum and the existing one is returned. The URL from the response can go straight into an article's `featuredImageUrl`.

Form (multipart/form-data)

(fișier)· requiredfileThe image.
folderIduuidThe media-library folder.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/media' \
  --header 'Authorization: Bearer bk_live_…' \
  --form 'file=@coperta.jpg'

Response

{
  "id": "4e4a…",
  "filename": "coperta.jpg",
  "url": "/api/v1/organizations/adb4…/media/4e4a…/content",
  "mimeType": "image/jpeg",
  "sizeBytes": 84213,
  "width": 1200,
  "height": 630
}

Publications

A publication ties an article to a publisher site. It is the only place in the API that spends credits or money.

POST/partner/publicationsRequest publicationPUBLICATIONS_WRITE

One article, one or more publications.

**Payment ambiguity is never guessed.** When several owned packages cover the same site, the request is rejected with `409` and the list of options; the integration picks one and retries with `packageTypeId`. Guessing would consume the wrong package, and that cannot be undone.

Request body

articleId· requireduuidThe article to publish.
siteIds· requireduuid[]The publications chosen from the catalogue.
campaignType· requiredstringThe campaign type. Must be among the site's `acceptedCampaigns`.
packageTypeIduuidWhich package pays, when several cover the site.
licenseNumberstring(3–100)The operator’s ONJN gambling licence. Required when `campaignType` is `"CASINO"` — gambling advertising must display it, and the platform writes it onto the article itself before sending it.
extrasBySiteobjectExtras requested per site — `"HOMEPAGE_PLACEMENT"`, `"FACEBOOK_SHARE"`, at most both. The key is `siteId`, the value a list of types; each one requested must have an active offer on that site (see `GET /partner/catalog`), otherwise the request is rejected.
scheduledFordatetimeWhen it should go live, if not immediately.

Example

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"] }
  }'

Response

{
  "publications": [
    {
      "id": "9a3b…",
      "siteId": "3f1c…",
      "status": "AWAITING_APPROVAL",
      "amountCents": 18000,
      "paymentSource": "MONEY",
      "extras": [
        { "type": "HOMEPAGE_PLACEMENT", "priceCents": 5000, "currency": "RON", "chargedCents": 5000 }
      ]
    }
  ]
}
GET/partner/publicationsList publicationsPUBLICATIONS_READ

All the organisation's publications. `status` filters. Cursor-paginated: the response's `nextCursor` is sent back as `cursor` for the next page, `null` on the last one.

For tracking state, webhooks are better: `publication.published` arrives with the article URL the moment it goes live, while polling burns your rate limit without learning anything extra.

Parameters

statusstring`DRAFT`, `AWAITING_APPROVAL`, `APPROVED`, `PUBLISHING`, `PUBLISHED`, `FAILED`, `REJECTED`, `CANCELLED`.
cursoruuidThe id of the last publication seen. Absent on the first page.
limitint (implicit 50, cel mult 200)How many publications per page.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/publications?status=PUBLISHED&limit=50' \
  --header 'Authorization: Bearer bk_live_…'

Response

{
  "items": [
    { "id": "9a3b…", "status": "PUBLISHED", "publishedUrl": "https://exemplu.ro/articol", "publishedAt": "2026-09-01T08:14:00.000Z" }
  ],
  "nextCursor": null
}
GET/partner/publications/{publicationId}One publicationPUBLICATIONS_READ

The whole publication, with its event history and the reason, when it failed or was rejected.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/publications/9a3b…' \
  --header 'Authorization: Bearer bk_live_…'

Response

{
  "id": "9a3b…",
  "status": "FAILED",
  "lastError": "Site-ul a răspuns 401 la autentificare",
  "attempts": 3,
  "publishedUrl": null
}

Campaigns

Grouping articles and publications by client or project. The equivalent of "projects" on other platforms.

GET/partner/campaignsList campaignsCAMPAIGNS_READ

`status` filters. Cursor-paginated: the response’s `nextCursor` is sent back as `cursor` for the next page, `null` on the last one.

Parameters

statusstring`DRAFT`, `ACTIVE`, `PAUSED`, `COMPLETED`, `ARCHIVED`.
cursoruuidThe id of the last campaign seen. Absent on the first page.
limitint (implicit 50, cel mult 200)How many campaigns per page.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/campaigns?limit=50' \
  --header 'Authorization: Bearer bk_live_…'

Response

{
  "items": [{ "id": "8b2e…", "name": "Client Exemplu — Q3", "status": "ACTIVE" }],
  "nextCursor": null
}
POST/partner/campaignsCreate a campaignCAMPAIGNS_WRITE

The body is strict: a field it does not know is rejected with `400`, not silently ignored. Better an error on the first attempt than a setting that looks applied and is not.

Request body

name· requiredstring(2–150)Campaign name.
campaignTypestringBinds the campaign to a type. Every article that joins it is checked automatically against the type’s requirements, and the verdict appears on the article in `potrivireCampanie`. It does not block writing — rejection stays at the order, where the payment is.
clientNamestring(≤150)The end client’s label. Free text: clients have no account on the platform.
objectivestring(≤500)What the campaign is for.
notesstring(≤2000)Internal notes.
startsAtdatetimeISO 8601. Must precede `endsAt`.
endsAtdatetimeISO 8601.
budgetCentsintA tracked budget, in minor units. Not a separate wallet: the money stays in the organisation’s account.
budgetCreditsintLimit on credits spent in the campaign.
allowedSiteIdsuuid[] (≤500)Allowed sites. Empty means “any in the catalogue”.
idempotencyKeystring(≤200)Chosen by you — a locally generated UUID is enough. Sent again, for the same organisation, it returns the campaign already created instead of making a new one. Recommended for any code that might resend the request after a timeout.

Example

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"
  }'

Response

{
  "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}One campaignCAMPAIGNS_READ

The campaign, with its articles and publications.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/campaigns/8b2e…' \
  --header 'Authorization: Bearer bk_live_…'

Response

{ "id": "8b2e…", "name": "Client Exemplu — Q3", "articles": 12, "publications": 34 }
PATCH/partner/campaigns/{campaignId}Update a campaignCAMPAIGNS_WRITE

Every field from creation, all optional. Send only what changes.

Most often used for `campaignType`: binds an already-created campaign to a type, or clears it with `null`. Every article in the campaign is automatically rechecked against the new type — see `potrivireCampanie` on the article.

Example

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" }'

Response

{ "id": "8b2e…", "name": "Client Exemplu — Q3", "campaignType": "BRAND_AWARENESS", "articleCount": 12 }
DELETE/partner/campaigns/{campaignId}Delete a campaignCAMPAIGNS_WRITE

Responds `204` on success. Refused with `409` if the campaign is active and has articles — the same rule as in the interface: a campaign with history gets archived (`PATCH` with `status: "ARCHIVED"`), not deleted.

Example

curl --location --request DELETE 'https://app.comunicate.top/api/v1/partner/campaigns/8b2e…' \
  --header 'Authorization: Bearer bk_live_…'

Response

(fără corp — 204 No Content)

Balance and reports

GET/partner/balanceAvailable balanceBALANCE_READ

The packages you own, with the credit left on each, and the money balance. Check it before requesting a publication — otherwise the integration finds out from an error, after it has already built the article.

`available` subtracts reservations: a publication in flight has already locked the credit, even if it has not consumed it.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/balance' \
  --header 'Authorization: Bearer bk_live_…'

Response

{
  "credits": [
    { "packageTypeId": "c1…", "packageName": "Pachet Regional 20", "available": 14, "total": 20 }
  ],
  "money": { "availableCents": 125000, "currency": "RON" }
}
GET/partner/reports/summaryPeriod summaryREPORTS_READ

Defaults to the last 30 days. An integration that sends no range gets something useful, not the entire history.

Parameters

fromdateRange start.
todateRange end.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/reports/summary?from=2026-08-01&to=2026-08-31' \
  --header 'Authorization: Bearer bk_live_…'

Response

{
  "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/publicationsPublications in rangeREPORTS_READ

Row by row, for reporting to the client.

Example

curl --location 'https://app.comunicate.top/api/v1/partner/reports/publications?from=2026-08-01' \
  --header 'Authorization: Bearer bk_live_…'

Response

[
  { "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" }
]

Webhooks

Polling a publication’s status burns your rate limit and, most of the time, learns that nothing changed. A webhook arrives the moment the article goes live, with its URL. Endpoints are configured from the integrations page, not through the API — whoever changes where events go must be signed in as a person.

Retries

An endpoint that doesn’t answer with 2xx (or doesn’t answer at all) is retried automatically, with growing pauses: 30 seconds, then 1, 2, 4, 8, 16, 32 minutes — seven attempts in total. If it still fails, the delivery stays visible on the integrations page, with the response received and the reason, and can be retried manually from there — if the manual retry fails too, it re-enters the automatic rotation.

Verifying the signature

Every delivery carries `X-Birou-Signature: t=<time>,v1=<hmac>`. HMAC-SHA256 is computed over `<time>.<raw body>` with the endpoint secret — not over the body alone: without the timestamp, an intercepted delivery could be replayed at any time with the same valid signature. Also check that `t` is recent.

const [t, v1] = header.split(',').map((p) => p.split('=')[1]);
const expected = crypto
  .createHmac('sha256', secret)
  .update(`${t}.${rawBody}`)
  .digest('hex');
// constant-time comparison, then: Math.abs(Date.now() / 1000 - Number(t)) < 300

Events

  • article.createdAn article was created, by any route.
  • article.generatedA draft requested from the interface has finished writing. Orders placed through the API use `redaction.delivered`, because a human review also sits between the request and the article.
  • article.failedWriting a draft failed.
  • publication.submittedThe publication was sent to the publisher.
  • publication.publishedThe article went live. This is where its URL arrives.
  • publication.failedThe publication failed or was rejected.
  • indexing.updatedThe indexing status of a published article changed.
  • order.acceptedThe publisher accepted the order.
  • redaction.deliveredA writing order was delivered. Carries `orderId` and `articleId` — the article is read from there.
  • redaction.rejectedA writing order was declined. Carries the reason.
Comunicate.top API docs: routes, permissions, examples