# Krealo Publisher: complete API and MCP reference > Plain-text reference for AI agents and developers. Generated from the code that serves the API, so it matches production. Summary and links: https://publisher.krealomedia.com/llms.txt. Live manifest (always current, includes rate limits per plan): https://publisher.krealomedia.com/api/docs Krealo Publisher is a multi-tenant social media and blog management platform by Krealo Media. This file covers two ways to work with it programmatically: 1. The REST API (https://publisher.krealomedia.com/api), authenticated with an API token. 2. The MCP server (https://publisher.krealomedia.com/api/mcp), for AI clients such as Claude and ChatGPT. Never put a real token in a prompt, a chat or a repository: use the placeholders below and keep real values in a credential store. ## MCP (Model Context Protocol) Any MCP client can use the Publisher as a tool server. The tools are thin wrappers over the REST API: same validation, same per-company isolation, same rate limit. ### Endpoint - URL: `https://publisher.krealomedia.com/api/mcp` - Transport: Streamable HTTP, JSON-RPC 2.0 over POST. Stateless: no session, no SSE stream (GET returns 405). - Protocol versions: 2025-11-25, 2025-06-18, 2025-03-26. - Methods: initialize, ping, tools/list, tools/call. JSON-RPC batches are accepted, up to 10 messages per request. - Auth header: `Authorization: Bearer ` (`X-Api-Token: ` also works). - A request without a valid token gets 401 with a `WWW-Authenticate` header that points to the OAuth metadata. ### Authentication, option A: OAuth 2.1 (interactive clients: Claude, ChatGPT, Muse, ...) The client discovers everything by itself from the endpoint: 1. `GET https://publisher.krealomedia.com/.well-known/oauth-protected-resource` (also served under `/api/mcp`) names the authorization server. 2. `GET https://publisher.krealomedia.com/.well-known/oauth-authorization-server` lists the endpoints: - authorization: `https://publisher.krealomedia.com/oauth/authorize` (consent page, see below) - token: `https://publisher.krealomedia.com/api/mcp/oauth/token` (grants: authorization_code, refresh_token) - dynamic client registration (RFC 7591): `POST https://publisher.krealomedia.com/api/mcp/oauth/register` 3. Public client only (`token_endpoint_auth_method: none`). PKCE is mandatory, method S256 only. The optional `resource` parameter (RFC 8707) must be `https://publisher.krealomedia.com/api/mcp`. 4. Tokens: the access token lasts 60 minutes; the refresh token lasts 90 days and is ROTATED on every refresh (the old one stops working; reusing a spent authorization code revokes the token issued with it). Authorization codes live 5 minutes and work once. 5. Each user can keep up to 20 live connections; revoke them in Settings > API. Redirect URI allow-list. The `redirect_uri` must match EXACTLY one of these, or be a loopback address (`http://localhost`, `http://127.0.0.1` or `http://[::1]` with any port, for desktop and CLI clients): - https://claude.ai/api/mcp/auth_callback - https://claude.com/api/mcp/auth_callback - https://agent.meta.ai/api/hatch/oauth/callback - https://chatgpt.com/connector_platform_oauth_redirect - https://bonsommeil.ca/api/admin/publisher/callback - https://chat.krealomedia.com/publisher/callback Registration with any other redirect_uri is rejected (400 invalid_redirect_uri). A new web callback has to be requested from Krealo Media and added to the allow-list in code; it cannot be self-registered. Consent page: the user signs in to the Publisher and approves on `https://publisher.krealomedia.com/oauth/authorize`. It is shown in English by default; pass `ui_locales=fr` or `ui_locales=es` (space-separated list, first supported one wins) to get French or Spanish. The page names the application that is asking for access. ### Authentication, option B: connector keys (server-to-server) For clients that cannot complete the browser flow. A signed-in user creates a key in Settings > API (connector keys; it is shown once). Send it as `Authorization: Bearer `. It expires after 90 days by default (1 to 365), cannot be refreshed, up to 10 live keys per user, and is revoked in the same list. A connector key only works on `https://publisher.krealomedia.com/api/mcp`: it is rejected (401 WRONG_AUDIENCE) on the REST API. Store it in the app's credential store, never paste it in a chat. ### Scope and permissions The only advertised scope is `tasks`. It is a historical name and it is NOT a restriction: the server never checks the scope per tool. A token (OAuth or connector key) gives access to EVERY tool listed below, with exactly the permissions of the user who approved it: that user's companies, nothing else (403 COMPANY_FORBIDDEN otherwise), and the account needs an active plan with API access (402 SUBSCRIPTION_REQUIRED otherwise). Read-only restriction per tool is not available. Treat any connector token as a full-access credential for that user. ### Limits and errors - Every tools/call counts against the same per-plan rate limit as the REST API (per token). Live numbers per plan: `GET https://publisher.krealomedia.com/api/docs`, field `rateLimits`. AI generation (create_post / create_blog with auto:true, generate_blog_from_idea, generate_post_from_idea) has a separate, smaller bucket. - Batch: at most 10 JSON-RPC messages per request; messages in a batch run one after another. - Tool errors come back as a normal result with `isError: true` and a text starting with `ERROR `. Do not retry 401, 402, 403 or 400 blindly. 429 RATE_LIMITED includes "Retry in N s": wait. A 202 from create_blog means accepted, not done: poll get_blog_job, do not repeat the call. - Protocol-level errors use JSON-RPC codes: -32600 invalid request, -32601 unknown method, -32602 unknown tool. - There is no hard delete anywhere: delete/archive tools set archived:true and can be undone (restore_task). - Not exposed over MCP on purpose: creating or revoking tokens, sending direct messages to people (Messenger/Instagram), rewriting an already-published blog, and publishing right now (a post scheduled less than 10 minutes ahead is rejected: publish-now needs human approval in /approvals). Scheduled posts can be edited only while they are still scheduled; retry_failed_post requeues the SAME post and never creates a new one. ### Tools (63) Generated from the server's tool definitions (the one-line summaries are the server's own descriptions, which are in Spanish). [read] only reads, [write] creates or changes data, [destructive] archives or removes something, or registers an external forwarding target (flagged destructiveHint; archiving is reversible, nothing is hard-deleted). A trailing * marks a required argument. Call tools/list for the full JSON Schema of each one. - `list_tasks` [read] (companyId, status, assignee, limit, includeArchived): Lista tareas del tablero del Publisher que tu cuenta puede ver, las más nuevas primero. - `get_task` [read] (taskId*): Una tarea con todos sus campos y sus comentarios. - `create_task` [write] (companyId*, title*, description, status, priority, assignees, dueDate, tagIds): Crea una tarea en el tablero. - `update_task` [write] (taskId*, status, priority, assignees, dueDate, title, description, tagIds, comment): Cambia campos de una tarea y, si mandas comment, deja además un comentario. - `comment_task` [write] (taskId*, text*, mentions): Añade un comentario a una tarea. - `list_companies` [read] (active, limit): Compañías que tu cuenta puede ver (id, name, active, logo CON fondo, logoTransparent SIN fondo…). - `list_tags` [read] (no arguments): Etiquetas de tareas de tu workspace (id, name, color). - `create_tag` [write] (name*, color): Crea una etiqueta en tu workspace. - `list_blog_ideas` [read] (companyId*, status, limit): Cola de ideas que alimenta al autoblog de una compañía. - `add_blog_ideas` [write] (companyId*, ideas, titles): Añade ideas (títulos) a la cola del autoblog de una compañía: de ahí salen los blogs que se generan. - `update_blog_idea` [write] (ideaId*, title, keyword, notes, position, restore): Cambia el título, la keyword o las notas de una idea PENDIENTE y/o la mueve en la cola (position: 1 = la próxima en generarse). - `reorder_blog_ideas` [write] (companyId*, ids*): Fija el orden de generación del autoblog: pasa los ids de las ideas pendientes en el orden deseado; las que no pongas siguen detrás, en su orden. - `delete_blog_idea` [destructive] (ideaId*): Saca una idea de la cola del autoblog. - `generate_blog_from_idea` [write] (companyId*, ideaId*, dryRun): Genera un blog COMPLETO con IA a partir de UNA idea de la cola (ideaId obligatorio: sin idea no se genera). - `get_blog` [read] (blogId*): Un blog con su cuerpo (HTML) y sus campos SEO (meta título, meta descripción, keyword, score), estado, fecha, destinos y traducciones. - `update_blog` [write] (blogId*, title, content, metaTitle, metaDescription, slug, focusKeyword, excerpt, tags, language, coverImageUrl, status, scheduledAt): Cambia título, cuerpo (content, HTML), campos SEO (metaTitle, metaDescription, slug, focusKeyword), excerpt, tags, idioma, portada o estado (draft | pending_approval | scheduled) de un blog que AÚN NO está publicado. - `get_post` [read] (postId*): Un post social con el texto de cada red, media, estado por red, fechas y, si falló, el motivo por red (failures). - `list_scheduled_posts` [read] (companyId, type, from, to, limit): Posts (y blogs) programados que aún no han salido, los más próximos primero, con editable:true si todavía se pueden modificar (update_scheduled_post). - `update_scheduled_post` [write] (postId*, caption, captions, title, mediaUrls, mediaType, scheduledAt, platforms, accountIds, status): Cambia un post que SIGUE programado: texto (caption, o captions por red), título, media (mediaUrls), fecha (scheduledAt) y redes (platforms/accountIds). - `list_failed_posts` [read] (companyId, days, from, to, limit): Posts que fallaron al publicar (failed, failed_final y partial), con el MOTIVO por red (failures[].error), su categoría, si el error es reintentable y si ya puede haber salido en la red (possiblyAlreadyPublished). - `retry_failed_post` [write] (postId*, force): Reintenta publicar un post que falló: reencola las redes fallidas del MISMO post (mismo id; NUNCA crea un post nuevo ni lo duplica). - `generate_post_from_idea` [write] (companyId*, idea*, platforms, accountIds, withImage): Genera un post social COMPLETO con IA a partir de una idea (idea obligatoria: de qué trata, para quién, qué se busca). - `list_activity` [read] (companyId*, from, to, category, limit): El feed de Activity del Publisher de una compañía: quién hizo qué y cuándo (posts, blogs, ideas, cambios), lo más reciente primero. - `get_calendar` [read] (companyId*, from, to, types): El calendario de una compañía por rango de fechas, como lo muestra /scheduler: posts, blogs y eventos agrupados por día (series recurrentes expandidas; días en UTC). - `get_company_stats` [read] (companyId*, period, from, to): El dashboard de una compañía en un periodo: posts por estado y por red (publicados, programados, fallidos, pendientes de aprobación), blogs, engagement por red (likes, comentarios, compartidos, vistas, alcance… SUMA d... - `set_company_logo` [write] (companyId*, logoUrl, logoTransparentUrl): Fija los DOS logos de una compañía: logoUrl = el logo CON fondo (se usa dentro de avatares y cuadros) y logoTransparentUrl = el logo SIN fondo (PNG/WebP con transparencia; va sobre la superficie de la app). - `archive_task` [destructive] (taskId*): Archiva una tarea (DELETE /tasks/{id}): desaparece del tablero pero NO se borra; restore_task la recupera. - `restore_task` [write] (taskId*): Saca una tarea del archivo (PATCH /tasks/{id} con archived:false). - `add_task_attachments` [write] (taskId*, attachments, driveLinks): Añade ficheros (attachments) y/o enlaces (driveLinks) a una tarea. - `create_post` [write] (companyId*, caption, title, platforms, accountIds, mediaUrls, mediaType, status, scheduledAt, locale, author, auto, prompt, withImage): Crea un post para las redes de una compañía. - `list_posts` [read] (companyId, status, platform, from, to, limit): Posts sociales (no blogs), los más nuevos primero, con su estado por red y el motivo si una red falló. - `create_blog` [write] (companyId*, auto, dryRun, ideaId, allowExistingUpcoming, title, content, status, scheduledAt, language, coverImageUrl, tags, author, metaTitle, metaDescription, slug, excerpt, blogHandle): Crea un artículo de blog de una compañía. - `list_blogs` [read] (companyId, status, from, to, limit): Artículos de blog, los más nuevos primero, con su estado y URL pública si ya se publicaron. - `get_blog_job` [read] (jobId*): Estado de una generación lanzada con create_blog: pending | running | done (con id y url del blog) | skipped (reason) | failed (error). - `list_blog_jobs` [read] (companyId, status, includeDryRun, limit): Generaciones de blog encoladas (sin el contenido: pide get_blog_job del que interese). - `create_event` [write] (title*, startAt*, endAt, companyId, note, location, allDay, attendees, attachments, recurrence): Crea un evento en el calendario compartido del workspace (/scheduler). - `list_events` [read] (companyId, from, to, limit): Eventos del calendario ordenados por inicio. - `update_event` [write] (eventId*, title, startAt, endAt, note, location, allDay, attendees, attachments, recurrence, archived): Cambia campos de un evento. - `delete_event` [destructive] (eventId*): Quita un evento del calendario. - `add_music` [write] (sourceUrl*, name, companyId): Encola la extracción del audio de un enlace de YouTube a la biblioteca de música del workspace. - `list_music` [read] (limit): Pistas de la biblioteca de música del workspace (nombre, url, duración). - `list_integrations` [read] (companyId*): Cuentas conectadas de una compañía (redes, blog, tienda, GA4…) con su salud y si son utilizables, más las fuentes de catálogo (catalogSources): Shopify/WooCommerce y «Products (manual)» cuando la compañía tiene produc... - `list_meta_destinations` [read] (companyId*): URLs a las que el Publisher reenvía lo que entra por Messenger e Instagram de una compañía. - `add_meta_destination` [destructive] (companyId*, url*, description, rotateSecret): Registra una URL https a la que reenviar cada mensaje entrante de Messenger/Instagram de la compañía, firmado con HMAC-SHA256. - `remove_meta_destination` [destructive] (destinationId*): Deja de reenviar a ese destino al momento: lo archiva y borra su secreto. - `store_sales` [read] (companyId*, period, from, to): Ventas de la tienda Shopify/WooCommerce de una compañía en un periodo, leídas de la tienda (no estimadas), con el día cortado en la zona horaria de la tienda. - `store_inventory` [read] (companyId*, search, lowStockThreshold, limit): Catálogo e inventario de la compañía: productos activos, agotados y con poco stock, o la búsqueda de un producto por nombre/SKU/GTIN (search). - `store_orders` [read] (companyId*, email, number): Pedidos de UN cliente de la tienda, por su correo (email) o número de pedido (number). - `get_analytics` [read] (companyId*, period, from, to, property): Informe GA4 de la web de una compañía (tráfico del sitio; para posts, engagement por red y blogs usa get_company_stats): usuarios, sesiones, vistas, conversiones, canales, páginas y eventos. - `upload_stats` [write] (companyId*, platform*, posts*, sourceUrl, capturedAt, collectedBy, accountId, source): Sube métricas ya leídas de publicaciones de redes sin API utilizable (X, YouTube, Pinterest, TikTok); se ven en /statistics. - `sync_catalog` [write] (products*, companyId, source, company, sentAt, stores, policies, dry_run, archive_missing, complete, default_status): Sincroniza el catálogo ENTERO de una compañía sin tienda conectada (p. - `list_products` [read] (companyId*, status, q, limit, offset): Productos manuales de una compañía (su catálogo propio, no el de Shopify), los más recientes primero, con título por idioma, precio desde, nº de variantes y estado. - `get_product` [read] (companyId*, productId*): Un producto manual completo: textos por idioma, media, variantes (precio, GTIN, disponibilidad), atributos y las últimas entradas de auditoría. - `upsert_product` [write] (companyId*, productId, externalId, id, status, kind, brand, condition, currency, title, tagline, description, url, media, googleProductCategory, productType, itemGroupId, attributes, variants): Crea o actualiza UN producto manual. - `archive_product` [destructive] (companyId*, productId*): Archiva un producto manual (status "archived"). - `merchant_preview` [read] (companyId*, productId): Muestra las ofertas que saldrían a Google Merchant con los productos ACTIVOS de la compañía (una por variante × idioma del feed): título compuesto, link, imagen, precio, GTIN, disponibilidad; y los avisos de validació... - `list_services` [read] (companyId*, status, q, category, limit, offset): Servicios de una compañía (lo que presta: limpieza dental, alquiler de cajas, reparación…), los más recientes primero, con nombre por idioma, precio (modelo + importe), duración, dónde se ofrece y estado. - `get_service` [read] (companyId*, serviceId*): Un servicio completo: textos por idioma, enlaces de página/reserva/contacto, precio, duración, dónde se presta, imágenes, preguntas frecuentes por idioma y las últimas entradas de auditoría. - `upsert_service` [write] (companyId*, serviceId, externalId, id, status, category, name, tagline, description, url, bookingUrl, contactUrl, price, durationMinutes, offered, media, faqs): Crea o actualiza UN servicio. - `archive_service` [destructive] (companyId*, serviceId*): Archiva un servicio (status "archived"): deja de salir en get_catalog. - `sync_services` [write] (services*, companyId, source, company, sentAt, stores, dry_run, archive_missing, complete, default_status): Bulk upsert de los servicios de una compañía con el payload que manda su web: { source, company, sentAt, services[], stores[] }. - `get_catalog` [read] (companyId*, language, limit): LO QUE OFRECE una compañía, en una sola llamada: sus productos manuales activos Y sus servicios activos (nombre, descripción, precio, enlaces, dónde se presta, preguntas frecuentes) más sus lugares. - `get_api_docs` [read] (no arguments): El contrato vivo de la API del Publisher: reglas, límites de uso por plan, códigos de error y cada endpoint con sus campos. ## REST API reference Only endpoints that exist are listed. `GET https://publisher.krealomedia.com/api/docs` additionally lists routes that are not detailed here (for example tags, statistics upload and Meta message destinations). ### Start here — auth and tokens One REST API for agents. Every response is flat and starts with "ok". Nothing is ever hard-deleted: deleting means archiving. Lists tell you when they were truncated — if "truncated" is true, do NOT conclude something does not exist. Base URL: https://publisher.krealomedia.com/api Send ONE of the three headers below. The API is included in the Pro and Business plans (trials too) — without one you get 402, not 403. Headers: - `X-Api-Token: kagent_…`: Recommended. Create yours in Settings > API with the “Create token” button. This is the one to paste into an agent. - `X-Personal-Token: kpat_…`: Your personal token, if you already have one. Same access, different lifecycle (one per person). - `Authorization: Bearer `: The normal Publisher session. Required — and only accepted — for POST /api/tokens/create. - `Content-Type: application/json`: Required on POST / PATCH / PUT. Common errors: - 401 NO_CREDENTIALS: No auth header at all. - 401 BAD_TOKEN_FORMAT: Token does not start with kagent_ / kpat_. - 401 INVALID_TOKEN · REVOKED_TOKEN · ORPHAN_TOKEN: Unknown, revoked, or no longer resolving to a user. - 401 INVALID_SESSION: The Bearer is not a valid Firebase ID token, or expired. - 402 SUBSCRIPTION_REQUIRED: Valid identity, but the account has no active Pro or Business plan. Upgrade in Settings → Billing. - 403 COMPANY_FORBIDDEN: The company exists but you cannot see it. THIS is the permission error you will hit most. - 400 MISSING_FIELD · INVALID_FIELD: The response names the offending field in "field". - 404 COMPANY_NOT_FOUND · TASK_NOT_FOUND · EVENT_NOT_FOUND: The id does not exist. #### GET /api/docs A live manifest of the API. No token needed, on purpose: an agent must be able to discover the API before it has credentials. Good for “what exists”, not for types. Parameters: - none Request: ``` curl https://publisher.krealomedia.com/api/docs ``` Response (200): ``` { "ok": true, "name": "Krealo Publisher — API de agentes", "base": "https://publisher.krealomedia.com/api", "auth": { "headers": ["X-Api-Token: kagent_…", "…"], "access": "…" }, "rules": ["Aislamiento por compañía…", "Nada de borrado duro…", "…"], "endpoints": [ { "method": "GET", "path": "/api/tasks", "query": "…" } ] } ``` Errors: None: this endpoint is public on purpose and exposes no client data. #### POST /api/tokens/create Mints a kagent_ token. SESSION ONLY — a token can never mint another token, so revoking a leaked one actually works. The cleartext value is returned exactly once. Parameters: - `name` (string, body, optional): Label to tell tokens apart. Max 60 chars, default "agente". Request: ``` curl -X POST \ -H "Authorization: Bearer $FIREBASE_ID_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"agente-posteador"}' \ https://publisher.krealomedia.com/api/tokens/create ``` Response (201): ``` { "ok": true, "id": "tok_123", "token": "kagent_…", "last4": "85dc", "name": "agente-posteador", "createdAt": "2026-08-06T12:00:00.000Z", "warning": "Guárdalo AHORA: sólo se muestra esta vez…", "usage": "Mándalo en el header \"X-Api-Token\" de cada petición." } ``` Errors: 403 {"ok":false,"code":"SESSION_REQUIRED"} if you send X-Api-Token instead of a session · 409 "TOO_MANY_TOKENS" once you have 10 active ones (revoke one first). #### GET /api/tokens Your tokens. Never the value — only the last 4, plus when each was last used, which is how you spot the one nobody uses any more. Parameters: - none Request: ``` curl -H "X-Api-Token: $KREALO_API_TOKEN" \ "https://publisher.krealomedia.com/api/tokens" ``` Response (200): ``` { "ok": true, "items": [ { "id": "tok_123", "name": "agente-posteador", "last4": "85dc", "active": true, "createdAt": "…", "lastUsedAt": "…", "revokedAt": null } ], "total": 1, "returned": 1, "truncated": false, "limit": 200 } ``` Errors: 401 {"ok":false,"code":"NO_CREDENTIALS"} with no token · 401 "INVALID_TOKEN" if unknown · 401 "REVOKED_TOKEN" if revoked · 402 "SUBSCRIPTION_REQUIRED" if your account has no active Pro or Business plan (a trial counts). #### POST /api/tokens/revoke Kills a token immediately. The record stays for the audit trail with active:false — nothing is hard-deleted. Parameters: - `tokenId` (string, body, required): The id from /api/tokens (not the token itself). Max 200 chars. Request: ``` curl -X POST \ -H "X-Api-Token: $KREALO_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"tokenId":"tok_123"}' \ https://publisher.krealomedia.com/api/tokens/revoke ``` Response (200): ``` { "ok": true, "id": "tok_123", "active": false } ``` Errors: 404 "TOKEN_NOT_FOUND" · 403 {"ok":false,"code":"TOKEN_FORBIDDEN","error":"Ese token es de otro usuario."} — only the owner or the superadmin can revoke it. #### GET /api/companies Every company you can see. Call this FIRST and cache the ids: almost every other endpoint wants a companyId, and guessing is the top cause of 403/404. Parameters: - `includeArchived` (boolean, query, optional): Only the literal string "true" counts. Archived companies are hidden by default. - `limit` (number, query, optional): Default 50, max 200. Above 200 it is clamped, not rejected. Request: ``` curl -H "X-Api-Token: $KREALO_API_TOKEN" \ "https://publisher.krealomedia.com/api/companies" ``` Response (200): ``` { "ok": true, "items": [ { "id": "abc123", "name": "Acme Store", "logo": "https://…", "ownerId": "…", "workspaceOwnerId": "…", "website": "https://example.com", "timezone": "America/Toronto", "primaryLanguage": "fr", "archived": false } ], "total": 46, "returned": 46, "truncated": false, "limit": 50 } ``` Errors: 401 {"ok":false,"code":"NO_CREDENTIALS"} with no token · 401 "INVALID_TOKEN" if unknown · 401 "REVOKED_TOKEN" if revoked · 402 "SUBSCRIPTION_REQUIRED" if your account has no active Pro or Business plan (a trial counts). ### Tasks The task a POST creates is byte-for-byte the one the board creates: same fields, same defaults. Taking a task means PATCHing it to in_progress BEFORE working, and to done when it is finished. #### POST /api/tasks/create Creates a task. Also answers at POST /api/tasks. The response carries publisherUrl — the deep link that opens it on the board. Parameters: - `title` (string, body, required): Max 200 chars. - `companyId` (string, body, required): From GET /api/companies. Max 200 chars. Access is checked against it — there is no way to name a company you cannot already see. - `status` (enum, body, optional): not_started (default) | in_progress | done. Anything else is a 400 INVALID_FIELD. - `priority` (enum, body, optional): alta | media | baja. English is accepted too (high/medium/low, urgent→alta) and normalised to the Spanish value the board filters on. - `assignees` (string | string[] (emails), body, optional): Max 30. Each MUST already exist in the Publisher, or the whole call is 400 ASSIGNEE_NOT_FOUND. - `tagIds` (string | string[], body, optional): Tag IDS, not names. Max 20, validated against task_tags — an invented tag breaks the board filter, so it is a 400. - `description` (string, body, optional): Max 20000 chars. - `attachments` (object[], body, optional): {url, name?, contentType?, size?}. Max 25. url must be http(s) or it is a 400. - `driveLinks` (object[], body, optional): {url, label?}. Max 25, http(s) only. Alias: links. - `dueDate` (string (ISO 8601), body, optional): Also startDate, endDate, publishDate. CAREFUL: an unparseable date is silently stored as null here — it does NOT 400 (PATCH does). - `parentTaskId` (string, body, optional): Makes it a subtask. Max 200 chars. Request: ``` curl -X POST \ -H "X-Api-Token: $KREALO_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"title":"Revisar el blog de agosto","companyId":"abc123","assignees":["user@example.com"],"priority":"alta"}' \ https://publisher.krealomedia.com/api/tasks/create ``` Response (201): ``` { "ok": true, "id": "task_123", "task": { "id": "task_123", "title": "Revisar el blog de agosto", "status": "not_started", "priority": "alta", "companyId": "abc123", "companyName": "Acme Store", "assignees": ["user@example.com"], "assigneeNames": ["Keneth Walters"], "tagIds": [], "attachments": [], "driveLinks": [], "dueDate": null, "archived": false, "createdAt": "…", "url": "…" }, "publisherUrl": "https://publisher.krealomedia.com/tasks?task=task_123" } ``` Errors: 403 {"ok":false,"code":"COMPANY_FORBIDDEN","error":"No tienes acceso a la compañía …","field":"companyId"} — you see exactly what you see on the web. 404 "COMPANY_NOT_FOUND" if the id does not exist. Plus the token errors (401 / 402). #### GET /api/tasks Finds tasks. Tasks with no company are never returned. If "truncated" is true the answer is partial — raise limit or narrow the filters instead of concluding something is missing. Parameters: - `companyId` (string, query, optional): From GET /api/companies. Max 200 chars. Access is checked against it — there is no way to name a company you cannot already see. - `status` (string (comma-separated), query, optional): e.g. in_progress,done. Max 10 values. - `assignee` (string (email), query, optional): Lowercased; must appear exactly in the task assignees. - `includeArchived` (boolean, query, optional): Only the literal "true". - `limit` (number, query, optional): Default 50, max 200. Above 200 it is clamped, not rejected. Request: ``` curl -H "X-Api-Token: $KREALO_API_TOKEN" \ "https://publisher.krealomedia.com/api/tasks?status=in_progress&limit=20" ``` Response (200): ``` { "ok": true, "items": [ { "id": "task_123", "title": "…", "status": "in_progress", "…": "…" } ], "total": 43, "returned": 20, "truncated": true, "limit": 20, "note": "Se devuelven 20 de 43. La lista está TRUNCADA: no concluyas que algo no existe…" } ``` Errors: 403 {"ok":false,"code":"COMPANY_FORBIDDEN","error":"No tienes acceso a la compañía …","field":"companyId"} — you see exactly what you see on the web. 404 "COMPANY_NOT_FOUND" if the id does not exist. Plus the token errors (401 / 402). #### GET /api/tasks/{id} One task plus its comments (oldest first, archived ones filtered out, capped at 200). Parameters: - none Request: ``` curl -H "X-Api-Token: $KREALO_API_TOKEN" \ "https://publisher.krealomedia.com/api/tasks/task_123" ``` Response (200): ``` { "ok": true, "task": { "id": "task_123", "title": "…", "status": "in_progress", "…": "…" }, "comments": { "items": [ { "id": "c1", "text": "…", "authorName": "…", "authorEmail": "…", "mentions": [], "parentId": null, "createdAt": "…", "editedAt": null } ], "total": 1, "returned": 1, "truncated": false, "limit": 200 } } ``` Errors: 404 "TASK_NOT_FOUND" (hint: GET /api/tasks lists the ones you can see) · 403 "TASK_FORBIDDEN" if it belongs to a company you cannot access · 403 "TASK_WITHOUT_COMPANY". Plus the token errors. #### PATCH /api/tasks/{id} Updates only the fields you send. Also answers to POST and PUT. Moving to in_progress stamps startedAt; moving to done stamps completedAt and endDate. Parameters: - `status` (enum, body, optional): not_started | in_progress | done. - `title` (string, body, optional): Max 200. Empty is a 400 — use archived to retire a task. - `description` (string, body, optional): Max 20000. Empty clears it. - `priority` (enum, body, optional): Same values as create. Empty clears it. - `assignees` (string[] (emails), body, optional): Replaces the list. Same existence check as create. - `tagIds` (string[], body, optional): Replaces the list. Max 20. - `dueDate` (string (ISO) | null, body, optional): Also startDate, endDate, publishDate. Here an invalid date IS a 400 INVALID_FIELD; send null to clear. - `attachments` (object[], body, optional): APPENDED, never replaced. Same for driveLinks. - `companyId` (string, body, optional): Moves the task. You need access to the destination company too. - `comment` (string, body, optional): Max 5000. Adds a comment in the same call; pair it with mentions. - `archived` (boolean, body, optional): true archives, false brings it back. Request: ``` curl -X PATCH \ -H "X-Api-Token: $KREALO_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"status":"in_progress","comment":"Lo tomo yo"}' \ https://publisher.krealomedia.com/api/tasks/task_123 ``` Response (200): ``` { "ok": true, "id": "task_123", "changed": ["status", "comment"], "commentId": "c2", "task": { "id": "task_123", "status": "in_progress", "…": "…" } } ``` Errors: Task errors (404 TASK_NOT_FOUND / 403 TASK_FORBIDDEN) · 400 {"ok":false,"code":"EMPTY_PATCH","error":"No mandaste nada que cambiar."} with a hint listing every accepted field. #### POST /api/tasks/{id}/comments Adds a comment. Mentioned people get a real notification on the board — the same one a human comment produces. Parameters: - `text` (string, body, required): Max 5000 chars. - `mentions` (string[] (emails), body, optional): Max 30. The author is never notified of their own comment. Request: ``` curl -X POST \ -H "X-Api-Token: $KREALO_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"text":"Desplegado y verificado","mentions":["user@example.com"]}' \ https://publisher.krealomedia.com/api/tasks/task_123/comments ``` Response (201): ``` { "ok": true, "commentId": "c2" } ``` Errors: 404 "TASK_NOT_FOUND" (hint: GET /api/tasks lists the ones you can see) · 403 "TASK_FORBIDDEN" if it belongs to a company you cannot access · 403 "TASK_WITHOUT_COMPANY". Plus the token errors. #### POST /api/tasks/{id}/attachments Adds files or links to an existing task. Always appends — it can never wipe what was already there. Parameters: - `attachments` (object[], body, optional): {url, name?}. Max 25, http(s) only. Alias: files. - `driveLinks` (object[], body, optional): {url, label?}. Max 25. At least one of the two lists must be non-empty. Request: ``` curl -X POST \ -H "X-Api-Token: $KREALO_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"driveLinks":[{"url":"https://drive.google.com/…","label":"Brief"}]}' \ https://publisher.krealomedia.com/api/tasks/task_123/attachments ``` Response (200): ``` { "ok": true, "id": "task_123", "added": { "attachments": 0, "driveLinks": 1 }, "task": { "id": "task_123", "…": "…" } } ``` Errors: 404 "TASK_NOT_FOUND" (hint: GET /api/tasks lists the ones you can see) · 403 "TASK_FORBIDDEN" if it belongs to a company you cannot access · 403 "TASK_WITHOUT_COMPANY". Plus the token errors. #### DELETE /api/tasks/{id} Archives the task. This API never hard-deletes anything: to bring it back, PATCH with {"archived": false}. Parameters: - none Request: ``` curl -X DELETE -H "X-Api-Token: $KREALO_API_TOKEN" \ https://publisher.krealomedia.com/api/tasks/task_123 ``` Response (200): ``` { "ok": true, "id": "task_123", "archived": true, "note": "La tarea se ARCHIVÓ (archived:true). Esta API nunca borra en duro…" } ``` Errors: 404 "TASK_NOT_FOUND" (hint: GET /api/tasks lists the ones you can see) · 403 "TASK_FORBIDDEN" if it belongs to a company you cannot access · 403 "TASK_WITHOUT_COMPANY". Plus the token errors. ### Posts and blogs Creating never publishes. A post is born as pending_approval unless you say otherwise, and a human approves it in the Publisher before anything reaches a network. #### POST /api/posts Creates a social post. Give it either platforms or accountIds — accountIds wins if both are present. It refuses rather than guessing when the company has nothing connected. Parameters: - `companyId` (string, body, required): From GET /api/companies. Max 200 chars. Access is checked against it — there is no way to name a company you cannot already see. - `caption` (string, body, required): The post copy. Max 20000 chars. Alias: text. - `platforms` (string[], body, optional): One of platforms or accountIds is required. Valid: facebook, instagram, tiktok, linkedin, google_business, pinterest, youtube, twitter. Max 20. Alias: networks. - `accountIds` (string[], body, optional): Exact connected accounts. Takes precedence over platforms. An unknown id is a 400 that lists the available ones. - `status` (enum, body, optional): draft | pending_approval (default) | scheduled. - `scheduledAt` (string (ISO 8601), body, optional): REQUIRED when status is "scheduled". An unparseable value is a 400. - `mediaUrls` (string[], body, optional): Max 10, all must be http(s). Alias: media. - `title` (string, body, optional): Max 200. Defaults to the first 80 chars of the caption. Request: ``` curl -X POST \ -H "X-Api-Token: $KREALO_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"companyId":"abc123","caption":"Ya disponible.","platforms":["facebook"],"status":"scheduled","scheduledAt":"2026-08-10T15:00:00.000Z"}' \ https://publisher.krealomedia.com/api/posts ``` Response (201): ``` { "ok": true, "id": "post_789", "post": { "id": "post_789", "title": "Ya disponible.", "caption": "Ya disponible.", "status": "scheduled", "companyId": "abc123", "platforms": [ { "platform": "facebook", "accountId": "acc_1", "publishStatus": "queued", "publishedAt": null, "externalUrl": null, "errorMessage": null } ], "mediaUrls": [], "scheduledAt": "2026-08-10T15:00:00.000Z", "publisherUrl": "https://publisher.krealomedia.com/scheduler" }, "note": "…" } ``` Errors: Company errors (403 COMPANY_FORBIDDEN / 404) · 409 "NO_CONNECTED_ACCOUNTS" if the company has none · 409 "PLATFORM_NOT_CONNECTED" (lists what IS connected) · 400 "ACCOUNT_NOT_FOUND" (lists the available ids) · 501 "AUTO_MODE_NOT_WIRED" if you send auto:true. #### GET /api/posts Lists social posts (blogs excluded). Without companyId only the first 10 companies are scanned and the total is marked approximate — always pass companyId if you care about completeness. Parameters: - `companyId` (string, query, optional): From GET /api/companies. Max 200 chars. Access is checked against it — there is no way to name a company you cannot already see. - `status` (string (comma-separated), query, optional): Max 10 values. - `from / to` (string, query, optional): Compared as TEXT against createdAt, so send full ISO for predictable results. - `limit` (number, query, optional): Default 50, max 200. Above 200 it is clamped, not rejected. Request: ``` curl -H "X-Api-Token: $KREALO_API_TOKEN" \ "https://publisher.krealomedia.com/api/posts?companyId=abc123&status=published&limit=10" ``` Response (200): ``` { "ok": true, "items": [ { "id": "post_1", "status": "published", "…": "…" } ], "total": 132, "returned": 10, "truncated": true, "limit": 10 } ``` Errors: 403 {"ok":false,"code":"COMPANY_FORBIDDEN","error":"No tienes acceso a la compañía …","field":"companyId"} — you see exactly what you see on the web. 404 "COMPANY_NOT_FOUND" if the id does not exist. Plus the token errors (401 / 402). #### POST /api/blogs Two modes. AUTO by default: the generator picks the keyword and writes it. Send content (or auto:false) to publish your own text instead. Parameters: - `companyId` (string, body, required): From GET /api/companies. Max 200 chars. Access is checked against it — there is no way to name a company you cannot already see. - `auto` (boolean, body, optional): Auto is the default. It turns off on its own as soon as you send content. - `title` (string, body, optional): REQUIRED in manual mode. Max 300 chars. - `content` (string (HTML), body, optional): REQUIRED in manual mode — and sending it is what selects manual mode. Max 200000 chars. Alias: body. - `status` (enum, body, optional): draft | pending_approval (default) | scheduled. - `scheduledAt` (string (ISO 8601), body, optional): Required when status is "scheduled". - `language` (string, body, optional): Defaults to the company primaryLanguage. Max 10 chars. - `ideaId` (string, body, optional): Auto mode only: aims the generator at one specific stored idea. - `metaTitle / metaDescription / slug / excerpt / tags / author / coverImageUrl` (string · string[], body, optional): Manual mode SEO fields. metaTitle defaults to title; tags max 20. Request: ``` curl -X POST \ -H "X-Api-Token: $KREALO_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"companyId":"abc123","auto":true}' \ https://publisher.krealomedia.com/api/blogs ``` Response (201): ``` { "ok": true, "id": "blog_1", "blog": { "id": "blog_1", "title": "…", "status": "pending_approval", "slug": "…", "language": "fr", "seoScore": 92, "…": "…" }, "generated": { "keyword": "…", "title": "…", "scheduledAt": "…", "languages": ["fr","en"], "translationStatus": "done", "imageStatus": "done", "seoScore": 92 } } ``` Errors: Company errors · 409 "NO_BLOG_ACCOUNT" if the company has no blog destination connected (blog, wordpress, wix, shopify) · 409 {"code":"BLOG_SKIPPED","skipped":true,"reason":"…"} when auto mode decides not to write · 503 "GENERATOR_UNAVAILABLE". #### GET /api/blogs Same filters as /api/posts, but returns only blog entries, with the SEO fields (slug, metaTitle, seoScore, translationStatus). Parameters: - `companyId` (string, query, optional): From GET /api/companies. Max 200 chars. Access is checked against it — there is no way to name a company you cannot already see. - `status` (string (comma-separated), query, optional): Max 10 values. - `limit` (number, query, optional): Default 50, max 200. Above 200 it is clamped, not rejected. Request: ``` curl -H "X-Api-Token: $KREALO_API_TOKEN" \ "https://publisher.krealomedia.com/api/blogs?companyId=abc123&limit=10" ``` Response (200): ``` { "ok": true, "items": [ { "id": "blog_1", "title": "…", "slug": "…", "language": "fr", "metaTitle": "…", "seoScore": 92, "…": "…" } ], "total": 8, "returned": 8, "truncated": false, "limit": 10 } ``` Errors: 403 {"ok":false,"code":"COMPANY_FORBIDDEN","error":"No tienes acceso a la compañía …","field":"companyId"} — you see exactly what you see on the web. 404 "COMPANY_NOT_FOUND" if the id does not exist. Plus the token errors (401 / 402). ### Events and music The shared calendar and the music library. Events can live without a company (an internal meeting); everything else is company-scoped. #### POST /api/events Creates a calendar event. companyId is optional here — an internal meeting belongs to nobody in particular. Parameters: - `title` (string, body, required): Max 200 chars. - `startAt` (string (ISO 8601), body, required): Aliases: date, start. Missing or unparseable is a 400 MISSING_FIELD. - `endAt` (string (ISO 8601), body, optional): Alias: end. Must be >= startAt or it is a 400 INVALID_FIELD. - `companyId` (string, body, optional): Optional. Validated if present. - `note` (string, body, optional): Max 5000. Alias: description. - `attendees` (string[] (emails), body, optional): Max 30. - `location` (string, body, optional): Max 300 chars. - `allDay` (boolean, body, optional): Strict true only. - `attachments` (object[], body, optional): {url, name, type}. Max 25. - `recurrence` (object, body, optional): Object only; anything else is stored as null. Request: ``` curl -X POST \ -H "X-Api-Token: $KREALO_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"title":"Revisión mensual Acme Store","startAt":"2026-08-12T14:00:00.000Z","companyId":"abc123","attendees":["user@example.com"]}' \ https://publisher.krealomedia.com/api/events ``` Response (201): ``` { "ok": true, "id": "ev_1", "event": { "id": "ev_1", "title": "Revisión mensual Acme Store", "startAt": "2026-08-12T14:00:00.000Z", "endAt": null, "allDay": false, "companyId": "abc123", "companyName": "Acme Store", "attendees": ["user@example.com"], "attachments": [], "recurrence": null, "archived": false, "url": "https://publisher.krealomedia.com/calendar" } } ``` Errors: 403 {"ok":false,"code":"COMPANY_FORBIDDEN","error":"No tienes acceso a la compañía …","field":"companyId"} — you see exactly what you see on the web. 404 "COMPANY_NOT_FOUND" if the id does not exist. Plus the token errors (401 / 402). #### GET /api/events Events in a date range, soonest first. Events without a company are included when you do not filter by one. Parameters: - `companyId` (string, query, optional): From GET /api/companies. Max 200 chars. Access is checked against it — there is no way to name a company you cannot already see. - `from / to` (string (ISO or YYYY-MM-DD), query, optional): A bare date is expanded to the whole day. An unparseable value is a 400 INVALID_FIELD naming from or to. - `limit` (number, query, optional): Default 50, max 200. Above 200 it is clamped, not rejected. Request: ``` curl -H "X-Api-Token: $KREALO_API_TOKEN" \ "https://publisher.krealomedia.com/api/events?from=2026-08-01&to=2026-08-31" ``` Response (200): ``` { "ok": true, "items": [ { "id": "ev_1", "title": "…", "startAt": "…", "…": "…" } ], "total": 3, "returned": 3, "truncated": false, "limit": 50 } ``` Errors: 403 {"ok":false,"code":"COMPANY_FORBIDDEN","error":"No tienes acceso a la compañía …","field":"companyId"} — you see exactly what you see on the web. 404 "COMPANY_NOT_FOUND" if the id does not exist. Plus the token errors (401 / 402). #### PATCH · PUT /api/events/{id} Updates only what you send. attachments are appended, never replaced. The endAt >= startAt check runs on the MERGED result, not just on what you sent. Parameters: - `title / note / location` (string, body, optional): Same limits as create. An empty title is a 400. - `startAt / endAt` (string (ISO) | null, body, optional): null clears endAt. An invalid date is a 400 INVALID_FIELD. - `attendees / attachments / recurrence / allDay / archived` (mixed, body, optional): Same shapes as create. archived:true retires the event. Request: ``` curl -X PATCH \ -H "X-Api-Token: $KREALO_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"startAt":"2026-08-13T14:00:00.000Z"}' \ https://publisher.krealomedia.com/api/events/ev_1 ``` Response (200): ``` { "ok": true, "id": "ev_1", "changed": ["startAt"], "event": { "…": "…" } } ``` Errors: 404 "EVENT_NOT_FOUND" · 403 "EVENT_FORBIDDEN" (isolated by company when it has one, otherwise by workspace) · 400 "EMPTY_PATCH" with a hint listing the accepted fields. #### DELETE /api/events/{id} Archives the event. Never a hard delete. Parameters: - none Request: ``` curl -X DELETE -H "X-Api-Token: $KREALO_API_TOKEN" \ https://publisher.krealomedia.com/api/events/ev_1 ``` Response (200): ``` { "ok": true, "id": "ev_1", "archived": true, "note": "Archivado (archived:true). Esta API nunca borra en duro." } ``` Errors: 404 "EVENT_NOT_FOUND" · 403 "EVENT_FORBIDDEN". #### POST /api/music Queues a YouTube extraction into the music library. It answers 202, not 201: a worker outside Cloud Functions does the actual work, so the track is not ready yet when you get the response. Parameters: - `sourceUrl` (string (URL), body, required): Must be youtube.com, youtu.be or music.youtube.com. Max 2000 chars. Aliases: youtubeUrl, url. - `name` (string, body, optional): Max 200. Defaults to the URL. - `companyId` (string, body, optional): Optional, validated if present. Request: ``` curl -X POST \ -H "X-Api-Token: $KREALO_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"sourceUrl":"https://youtu.be/xxxxxxxxxxx","name":"Intro agosto"}' \ https://publisher.krealomedia.com/api/music ``` Response (202): ``` { "ok": true, "id": "mus_1", "status": "pending", "note": "…" } ``` Errors: Company errors · 400 "INVALID_FIELD" for a non-YouTube URL · 501 "MUSIC_GENERATION_NOT_AVAILABLE" if you omit sourceUrl — there is no prompt-based music generation, only extraction. #### GET /api/music The music library. It is per workspace, not per company — there is no companyId filter because the library has no company. Parameters: - `limit` (number, query, optional): Default 50, max 200. Above 200 it is clamped, not rejected. Request: ``` curl -H "X-Api-Token: $KREALO_API_TOKEN" \ "https://publisher.krealomedia.com/api/music?limit=20" ``` Response (200): ``` { "ok": true, "items": [ { "id": "mus_1", "name": "Intro agosto", "url": "https://…", "durationSec": 128, "kind": "extract", "sourceUrl": "https://youtu.be/…", "createdAt": "…" } ], "total": 1, "returned": 1, "truncated": false, "limit": 20 } ``` Errors: 401 {"ok":false,"code":"NO_CREDENTIALS"} with no token · 401 "INVALID_TOKEN" if unknown · 401 "REVOKED_TOKEN" if revoked · 402 "SUBSCRIPTION_REQUIRED" if your account has no active Pro or Business plan (a trial counts). ### Company data (read-only) What the client has connected, and what it is producing: store sales, inventory, GA4. All read-only — none of these can change anything. #### GET /api/integrations What a company has connected, and what that unlocks. Check "capabilities" BEFORE calling store or analytics — it saves you a 409. Parameters: - `companyId` (string, query, required): From GET /api/companies. Max 200 chars. Access is checked against it — there is no way to name a company you cannot already see. Request: ``` curl -H "X-Api-Token: $KREALO_API_TOKEN" \ "https://publisher.krealomedia.com/api/integrations?companyId=abc123" ``` Response (200): ``` { "ok": true, "companyId": "abc123", "companyName": "Acme Store", "accounts": [ { "id": "acc_1", "platform": "facebook", "platformLabel": "Facebook", "accountName": "Acme Store", "status": "connected", "health": "ok", "usable": true } ], "capabilities": { "sales": "shopify", "inventory": "shopify", "analytics": true, "searchConsole": true }, "total": 1, "note": "…" } ``` Errors: 403 {"ok":false,"code":"COMPANY_FORBIDDEN","error":"No tienes acceso a la compañía …","field":"companyId"} — you see exactly what you see on the web. 404 "COMPANY_NOT_FOUND" if the id does not exist. Plus the token errors (401 / 402). #### GET /api/store/sales Shopify / WooCommerce sales for a period. Day boundaries are computed in the STORE timezone, not yours — that is why the response echoes the timezone it used. Parameters: - `companyId` (string, query, required): From GET /api/companies. Max 200 chars. Access is checked against it — there is no way to name a company you cannot already see. - `period` (string, query, optional): Default 7d. Canonical: hoy, ayer, 7d, 30d, mes, mes_pasado. English aliases (today, yesterday, week, month, last_month…) are accepted. - `from / to` (string (YYYY-MM-DD), query, optional): Override period. If either is present, from is required. Max span 92 days, else 400 BAD_PERIOD. Request: ``` curl -H "X-Api-Token: $KREALO_API_TOKEN" \ "https://publisher.krealomedia.com/api/store/sales?companyId=abc123&period=30d" ``` Response (200): ``` { "ok": true, "companyId": "abc123", "companyName": "Acme Store", "period": { "label": "30d", "from": "2026-07-07", "to": "2026-08-06", "days": 30, "timezone": "America/Toronto" }, "platform": "shopify", "store": { "…": "…" }, "currencies": [ { "currency": "CAD", "total": 12345.67, "orders": 42 } ], "ordersCounted": 42, "excluded": [ { "motivo": "cancelled", "n": 3 } ], "taxesIncludedInPrice": true, "warnings": [], "source": "shopify" } ``` Errors: Company errors · 409 "STORE_NOT_CONNECTED" if there is no store · 400 "BAD_PERIOD" (field: period) · 502 "STORE_READ_FAILED" if the store itself answered badly. #### GET /api/store/inventory Out-of-stock and low-stock products. A counter can come back null, which means “the platform could not count” — it does NOT mean zero. Parameters: - `companyId` (string, query, required): From GET /api/companies. Max 200 chars. Access is checked against it — there is no way to name a company you cannot already see. - `search` (string, query, optional): Filters by product name. Max 200 chars. - `lowStockThreshold` (number, query, optional): Default 5, clamped between 1 and 100. - `limit` (number, query, optional): Default 8 here — this one is items per list, NOT the usual 50/200. Request: ``` curl -H "X-Api-Token: $KREALO_API_TOKEN" \ "https://publisher.krealomedia.com/api/store/inventory?companyId=abc123&lowStockThreshold=5" ``` Response (200): ``` { "ok": true, "companyId": "abc123", "companyName": "Acme Store", "platform": "shopify", "store": { "…": "…" }, "counts": { "active": 320, "outOfStock": 4, "lowStock": 11, "lowStockThreshold": 5 }, "outOfStock": [ { "…": "…" } ], "lowStock": [ { "…": "…" } ], "search": "", "warnings": [], "note": "…" } ``` Errors: Company errors · 409 "STORE_NOT_CONNECTED" · 502 "STORE_READ_FAILED". #### GET /api/analytics GA4 for a period: totals, channels, top pages, events. If more than one property matches the company it refuses and asks you to pick one, rather than reporting the wrong site. Parameters: - `companyId` (string, query, required): From GET /api/companies. Max 200 chars. Access is checked against it — there is no way to name a company you cannot already see. - `period` (string, query, optional): Same values as /api/store/sales. Default 7d. - `from / to` (string (YYYY-MM-DD), query, optional): Override period. Max span 92 days. - `property` (string, query, optional): GA4 property selector. Needed when the company resolves to more than one. Request: ``` curl -H "X-Api-Token: $KREALO_API_TOKEN" \ "https://publisher.krealomedia.com/api/analytics?companyId=abc123&period=30d" ``` Response (200): ``` { "ok": true, "companyId": "abc123", "companyName": "Acme Store", "property": { "id": "properties/123", "name": "Acme Store", "timezone": "America/Toronto" }, "period": { "label": "30d", "from": "2026-07-07", "to": "2026-08-06", "days": 30 }, "totals": { "users": 4210, "sessions": 5602, "pageViews": 14903, "conversions": 87, "conversionsMetric": "conversions" }, "channels": [ { "…": "…" } ], "pages": [ { "…": "…" } ], "events": [ { "…": "…" } ], "warnings": [], "source": "ga4" } ``` Errors: Company errors · 409 "GA4_NOT_RESOLVED" (field: property) when no property matches or more than one does · 400 "BAD_PERIOD" · 502 "GA4_READ_FAILED".