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.
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
publications | integer | Active publications. |
niches | integer | Niches 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
slug | string | The niche identifier, used in the next route. |
name | string | The niche name in the requested language. |
description | string | What kind of material the publications in this niche accept. |
publicationCount | integer | How many active publications the niche has. |
averageDa | integer | Average 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
slug | string | The niche identifier. |
name | string | The niche name. |
publicationCount | integer | How many publications the niche has in total. |
averageDa | integer | The niche’s average authority. |
publications | object[] | 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
publicatii | integer | Active publications with a public price. |
retea | integer | How many of them are our own. |
autoritate | object | Minimum, maximum, median, mean and the distribution across bands. |
termene | object[] | How many publications deliver in how many days. |
preturi | object[] | 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
organization | object | `id` and `name`. |
scopes | string[] | The key’s scopes. |
aiPublishing | boolean | `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
page | int | Requested page, from 1. |
pageSize | int | Entries per page, at most 100. |
search | string | Searches name and domain. |
campaign | string | Only publications accepting the given type (see `campaign-types`). |
serviceType | string | `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[].id | uuid | The publication id. Used when requesting a publication. |
entries[].metrics | object | DR, DA, PA and backlink count. `null` where we have no reading. |
entries[].audit | object | null | `flagged` is the only field that changes a decision: a site Google flags as dangerous should not be bought. |
entries[].preturiCampanii | object | Publication price per accepted campaign type, in minor units. |
entries[].access | object | `OWN`, `CREDIT` (covered by a package), `MONEY` (paid from balance), `UNAVAILABLE`. When `packages` has more than one entry, publishing requires `packageTypeId`. |
entries[].masurat | object | What 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· required | string(3–300) | The article title. |
contentHtml· required | string(≤500 000) | The article body, as HTML. |
focusKeyword | string | The keyword to optimise for. **One only** — from a list, the first is kept. |
tags | string[] | At most three, however many are sent. A comma-separated list inside one element is also accepted. |
metaDescription | string(≤320) | The description shown in search results. |
slug | string | The proposed article URL slug. WordPress may change it on collision; what actually resulted is read back and kept on the publication. |
excerpt | string(≤1000) | The summary themes use in listings and on category pages. |
featuredImageAlt | string(≤300) | The featured image's alt text. Its absence is one of the findings the SEO analysis reports. |
seoTitle | string(≤200) | The search-result title, when it differs from the article title. |
featuredImageUrl | string | The featured image. Can be the URL returned by `POST /partner/media`. |
campaignId | uuid | The campaign the article belongs to. |
idempotencyKey | string(≤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)· required | file | The document. |
(al doilea fișier) | file | The featured image, for documents with none in the body. |
campaignId | uuid | The campaign the article belongs to. |
folderId | uuid | The 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[] | array | The articles created, with title and word count. |
images | object | How many images were imported, reused, converted, and why the rest were skipped. |
sanitized | object | What 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· required | string | The 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· required | string | The 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· required | array | The chosen documents, with `id`, `nume` and `mimeType` from the listing. |
imagineId | string | The id of an image in the same folder, used as the featured image. |
campaignId | uuid | The 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
cursor | uuid | The id of the last article seen. Absent on the first page. |
limit | int (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
redactedAt | datetime | null | When writing finished. `null` while queued. |
redactionFindings | array | What 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. |
suggestedCampaignType | string | null | What 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. |
classificationConfidence | int | null | Out of 100. Rules alone give high confidence; when a model was needed to separate two candidates it is lower — and it shows. |
classificationReason | string | null | The reason, written for a person. It can be argued with. |
version | int | Increases only when the title or content changes. Part of the publication idempotency key. |
status | string | `DRAFT`, `IN_REVIEW`, `APPROVED`, `SCHEDULED`, `PUBLISHING`, `PUBLISHED`, `FAILED`, `REJECTED`, `ARCHIVED`. |
createdViaApi | boolean | It 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· required | string(10–200) | One sentence, as you would tell a writer. The title comes from it. |
campaignType· required | string | One of the types from `campaign-types`. |
cuvinte· required | 500 | 700 | 1000 | 1500 | Target length. |
cuvantCheie | string | One only, one to four words. |
brand | string | The promoted brand. |
adresaPromovata | string | The URL the article points to. |
fapte | string[] (≤20) | One item per entry: prices, years in business, certifications, who can be quoted. |
campaignId | uuid | The campaign the delivered article joins. |
note | string(≤2000) | Anything else you want to tell us about the order. |
idempotencyKey | string(≤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
status | string | `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
score | int | 0–100, the percentage of checks passed. |
issues[] | array | `code`, `severity` and `message`. |
stats | object | Words, headings, images without alt text, internal and external links, keyword density, and the lengths of title, description and slug. |
outline | array | The 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
campaignType | string | The 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
curenta | object | The current version, so the first revision has something to compare against. |
revizii[] | array | At 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)· required | file | The image. |
folderId | uuid | The 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· required | uuid | The article to publish. |
siteIds· required | uuid[] | The publications chosen from the catalogue. |
campaignType· required | string | The campaign type. Must be among the site's `acceptedCampaigns`. |
packageTypeId | uuid | Which package pays, when several cover the site. |
licenseNumber | string(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. |
extrasBySite | object | Extras 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. |
scheduledFor | datetime | When 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
status | string | `DRAFT`, `AWAITING_APPROVAL`, `APPROVED`, `PUBLISHING`, `PUBLISHED`, `FAILED`, `REJECTED`, `CANCELLED`. |
cursor | uuid | The id of the last publication seen. Absent on the first page. |
limit | int (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
status | string | `DRAFT`, `ACTIVE`, `PAUSED`, `COMPLETED`, `ARCHIVED`. |
cursor | uuid | The id of the last campaign seen. Absent on the first page. |
limit | int (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· required | string(2–150) | Campaign name. |
campaignType | string | Binds 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. |
clientName | string(≤150) | The end client’s label. Free text: clients have no account on the platform. |
objective | string(≤500) | What the campaign is for. |
notes | string(≤2000) | Internal notes. |
startsAt | datetime | ISO 8601. Must precede `endsAt`. |
endsAt | datetime | ISO 8601. |
budgetCents | int | A tracked budget, in minor units. Not a separate wallet: the money stays in the organisation’s account. |
budgetCredits | int | Limit on credits spent in the campaign. |
allowedSiteIds | uuid[] (≤500) | Allowed sites. Empty means “any in the catalogue”. |
idempotencyKey | string(≤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
from | date | Range start. |
to | date | Range 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)) < 300Events
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.
Read next