# Gradius API v1 integration guide for LLM/code agents Gradius is an API for monetizing AI products with native markdown advertising. The API selects a relevant ad campaign, returns rendered ad content, click CTA data, and pricing metadata. This file is intended for code agents, IDE assistants, and integration bots. Use it as the source of truth when generating Gradius client code. Base URL: https://api.adlean.pro Auth header: `Auth: ` Protocol: HTTPS JSON Current public version: v1 ## Critical rules 1. Never expose `Auth` in frontend JavaScript, mobile apps, public widgets, localStorage, client logs, browser telemetry, or analytics events. 2. Always call Gradius from the publisher backend or from a trusted server-side proxy. 3. `chat_id` and `user_id` are required query parameters for every v1 endpoint. 4. `chat_id` must be stable inside one dialogue/session. Do not generate a new `chat_id` on every message. 5. `user_id` must be stable and non-empty. Do not pass an empty string. 6. Treat no-ad as a normal business response, not as an error. For every v1 ad endpoint, no-ad is an empty response array. 7. On no-ad, do not show fallback UI, placeholders, loading errors, or retry loops. 8. If the response contains an ad, render `content` exactly in the required surface for the endpoint. 9. Do not rewrite, hide, remove, or replace markdown links from `content`. 10. Do not manually build a CTA link into `content`: Gradius already returns markdown with a registered redirect link. 11. Preserve the ad after page refresh, chat switch, route change, or time passing. Ads must not disappear after being shown. 12. If the product supports light/dark theme switching, verify that ad text and links remain readable and clickable in every theme. 13. `timestamp`, `message_id`, and `user_type` are not v1 client parameters. 14. `lang` is a two-letter query parameter, default `ru`. 15. `model_version` is optional and should be sent only where supported: dialogue and generation endpoints. 16. Utility service endpoint does not accept `model_version`. If the publisher API key leaks due to unsafe publisher-side integration, the publisher may be financially responsible for abuse proportional to the caused damage. ## Endpoint selection Use the endpoint based on where the user will see the ad. ### Native ad inside a dialogue model response Endpoint: `POST /v1/native/dialogue_model/chat` Use when: - the service is a text chat, assistant, AI bot, or dialogue model; - the ad should be inserted into the assistant answer; - the UI can insert markdown from a `native-text-ad` placement by `content.insert_index`; - the product has a user -> assistant dialogue flow. Do not use when: - the service generates images, audio, video, files, or non-dialogue artifacts; - there is no assistant text answer; - the ad should be a standalone banner. ### Native ad for content generation models Endpoint: `POST /v1/native/generation_model/chat` Use when: - the service generates an image, audio, video, presentation, file, or other artifact from a text prompt; - the ad should be shown as a short markdown caption, side block, or banner near the generated result; - there is a user text prompt. Do not use when: - the model answers text questions in a chat format; use dialogue instead; - there is no user text question or prompt. ### Native ad for utility services Endpoint: `POST /v1/native/utility_service/chat` Use when: - the product performs a useful action such as downloading music, finding files, processing photos, recognizing images, converting documents, parsing data, or selecting content; - the service is a helper bot in Discord, Telegram groups, or similar group chats; - user text request and service text answer may both be absent or optional. Do not use when: - the ad must be inserted by `insert_index`; - the service is a free-form text generation product; use dialogue or generation depending on UI. ### Thinking-mode ad Endpoint: `POST /v1/thinking/dialogue_model/chat` Use only as an additional monetization layer for a dialogue model that already integrates `POST /v1/native/dialogue_model/chat`. Important: - Thinking-mode ads work only with native dialogue ads connected. - Send user requests to the thinking endpoint while the model is in thinking/loading state. - If thinking-mode ads are integrated, user requests must not also be sent to `POST /v1/native/dialogue_model/chat`. - The ad must remain visible after the final answer is generated, as a separate banner above or below the answer. Use when: - native dialogue ads are already integrated; - the model has a visible thinking/loading mode. Do not use when: - the model streams tokens immediately after the user question and has no thinking mode. ## Shared auth and request rules All requests must include: ```http Auth: Content-Type: application/json Accept: application/json ``` All v1 requests require: ```text chat_id= user_id= ``` Recommended common client behavior: - use a pooled HTTP client; - use timeouts; - retry only technical network failures if your backend policy allows it; - never retry no-ad responses; - log endpoint name, `chat_id`, `user_id` hash, ad/no-ad result, and HTTP status on the publisher side; - never log raw `Auth`; - avoid passing full user PII in `user_metadata`. ## Dialogue endpoint contract Path: ```text POST /v1/native/dialogue_model/chat ``` Query: ```text chat_id: string, required user_id: string, required role: "user" | "assistant", required lang: string, optional, default "ru" model_version: string | null, optional ``` Body: ```json { "text": "string", "user_metadata": {} } ``` Call pattern: 1. When the user sends a message, call the endpoint with `role=user`. 2. When the model has generated the answer, call the endpoint with `role=assistant`. 3. Only `role=assistant` can return an ad for insertion into the model answer. 4. If the response array contains a `native-text-ad` placement, insert `content.content` into the assistant answer at `content.insert_index`. Placement type: `native-text-ad` is a native text ad placement relevant to the dialogue context. Render its markdown `content.content` inside the assistant answer at `content.insert_index`. Success response: ```json [ { "type": "native-text-ad", "content": { "insert_index": 112, "content": "markdown advertising" }, "show_price": 1.2, "click_price": 45 } ] ``` No-ad response: ```json [] ``` UI requirements: - Required: insert markdown ad only from a `native-text-ad` placement and only by `content.insert_index`. - Required: leave the assistant answer unchanged when the response array is empty. - Required: do not hide/remove the ad after refresh, chat change, route change, or time passing. - Required: keep the ad readable in every supported theme. - Recommended: insert the ad in the same message as the assistant answer. - Recommended: optionally add shown ad content into the future model prompt so the model can answer follow-up questions about it. - Forbidden: hide, delete, fade out, or render the ad unreadably. ## Generation endpoint contract Path: ```text POST /v1/native/generation_model/chat ``` Query: ```text chat_id: string, required user_id: string, required lang: string, optional, default "ru" model_version: string | null, optional ``` Body: ```json { "text": "Text prompt used for artifact generation", "user_metadata": {} } ``` Response: ```json [ { "type": "native-text-ad", "content": "markdown advertising", "ad_context": { "campaign_id": 127, "url": "https://api.adlean.pro/r/c", "product_url": "https://gradius.pro", "similarity": 0.92, "intent_id": 12 }, "price": { "show_price": 1.2, "click_price": 45 }, "cta_text": "CTA text", "cta_link": "https://api.adlean.pro/r/c" } ] ``` UI requirements: - Required: show the ad as an accessible markdown banner/caption near the generated result. - Required: keep the link contrast and clickability. - Required: do not remove the ad after page refresh, chat/session change, or time passing. - Recommended: place the banner after the generated result or in a side zone. - Recommended for Telegram bots: add a button with `cta_text` and `cta_link` from the `native-text-ad` placement. - Forbidden: cover the generated artifact with the ad or hide the ad after it was shown. ## Utility endpoint contract Path: ```text POST /v1/native/utility_service/chat ``` Query: ```text chat_id: string, required user_id: string, required ``` Do not send `model_version` to this endpoint. Body: ```json { "user_metadata": {}, "user_text_request": "string | null", "model_text_answer": "string | null" } ``` Valid body variants: ```json { "user_metadata": {"tool": "music_downloader"}, "user_text_request": "Найди трек для travel-видео", "model_text_answer": "Нашел несколько треков без вокала" } ``` ```json { "user_metadata": {"tool": "photo_search"}, "user_text_request": "Найди похожие кроссовки по фото", "model_text_answer": null } ``` ```json { "user_metadata": {"tool": "file_converter"}, "user_text_request": null, "model_text_answer": "PDF успешно преобразован в DOCX" } ``` ```json { "user_metadata": {"tool": "group_helper_bot"}, "user_text_request": null, "model_text_answer": null } ``` Response format is the same as generation endpoint: an array of placements. Use the `native-text-ad` placement and render its `content`. UI requirements: - Required: show `content` from `native-text-ad` as a standalone markdown banner near the utility result. - Required: preserve the ad after refresh, chat switch, dialogue switch, or time passing. - Required: keep markdown links clickable and readable. - Recommended: if both request and answer exist, show the ad after the useful service answer. - Recommended for Telegram bots: add a button with `cta_text` and `cta_link` from the `native-text-ad` placement. - Forbidden: hide the CTA link or show a banner when the response array is empty. ## Thinking endpoint contract Path: ```text POST /v1/thinking/dialogue_model/chat ``` Query: ```text chat_id: string, required user_id: string, required role: "user" | "assistant", required lang: string, optional, default "ru" model_version: string | null, optional ``` Body: ```json { "text": "Current user question", "user_metadata": {} } ``` Expected use: - call with `role=user`; - show `content` from the returned `native-text-ad` placement during thinking/loading; - keep the banner after the final answer is ready; - use only together with native dialogue integration. UI requirements: - Required: ad is visible during thinking and remains visible after final answer generation. - Required: use only together with native dialogue API. - Recommended: render `cta_text` and `cta_link` from `native-text-ad` as a dedicated button/component. - Forbidden: remove/hide the ad after model answer, page refresh, chat switch, or time passing. ## Minimal cURL examples Dialogue user message: ```bash curl -X POST 'https://api.adlean.pro/v1/native/dialogue_model/chat' \ --url-query 'chat_id=chat_8f8f7d21' \ --url-query 'user_id=user_42' \ --url-query 'role=user' \ --url-query 'lang=ru' \ -H 'Auth: ' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ --data '{"text":"Как выбрать CRM?","user_metadata":{"platform":"web"}}' ``` Dialogue assistant answer: ```bash curl -X POST 'https://api.adlean.pro/v1/native/dialogue_model/chat' \ --url-query 'chat_id=chat_8f8f7d21' \ --url-query 'user_id=user_42' \ --url-query 'role=assistant' \ --url-query 'lang=ru' \ --url-query 'model_version=gpt-4.1-mini' \ -H 'Auth: ' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ --data '{"text":"Для старта подойдет CRM с простой аналитикой.","user_metadata":{"platform":"web"}}' ``` Generation model: ```bash curl -X POST 'https://api.adlean.pro/v1/native/generation_model/chat' \ --url-query 'chat_id=image_session_77' \ --url-query 'user_id=user_42' \ --url-query 'lang=ru' \ --url-query 'model_version=image-v3' \ -H 'Auth: ' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ --data '{"text":"Сгенерируй обложку для подкаста","user_metadata":{"surface":"result_caption"}}' ``` Utility service: ```bash curl -X POST 'https://api.adlean.pro/v1/native/utility_service/chat' \ --url-query 'chat_id=utility_session_7' \ --url-query 'user_id=user_42' \ -H 'Auth: ' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ --data '{"user_metadata":{"tool":"music_downloader"},"user_text_request":"Найди спокойный трек","model_text_answer":"Нашел несколько вариантов"}' ``` Thinking mode: ```bash curl -X POST 'https://api.adlean.pro/v1/thinking/dialogue_model/chat' \ --url-query 'chat_id=chat_8f8f7d21' \ --url-query 'user_id=user_42' \ --url-query 'role=user' \ --url-query 'lang=ru' \ --url-query 'model_version=gpt-4.1-mini' \ -H 'Auth: ' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ --data '{"text":"А какие CRM подойдут для малого отдела продаж?","user_metadata":{"platform":"web"}}' ``` ## Python async client example Use a shared `httpx.AsyncClient` in production. Do not create a new client for every request if the application handles high traffic. ```python import asyncio from typing import Any import httpx class GradiusClient: def __init__(self, api_key: str, base_url: str = "https://api.adlean.pro") -> None: self._client = httpx.AsyncClient( base_url=base_url, timeout=10, headers={ "Auth": api_key, "Accept": "application/json", "Content-Type": "application/json", }, ) async def close(self) -> None: await self._client.aclose() async def dialogue_message( self, *, chat_id: str, user_id: str, role: str, text: str, lang: str = "ru", model_version: str | None = None, user_metadata: dict[str, Any] | None = None, ) -> dict[str, Any]: params = { "chat_id": chat_id, "user_id": user_id, "role": role, "lang": lang, } if model_version: params["model_version"] = model_version response = await self._client.post( "/v1/native/dialogue_model/chat", params=params, json={"text": text, "user_metadata": user_metadata or {}}, ) response.raise_for_status() return response.json() async def generation_ad( self, *, chat_id: str, user_id: str, text: str, lang: str = "ru", model_version: str | None = None, user_metadata: dict[str, Any] | None = None, ) -> dict[str, Any]: params = {"chat_id": chat_id, "user_id": user_id, "lang": lang} if model_version: params["model_version"] = model_version response = await self._client.post( "/v1/native/generation_model/chat", params=params, json={"text": text, "user_metadata": user_metadata or {}}, ) response.raise_for_status() return response.json() async def utility_ad( self, *, chat_id: str, user_id: str, user_text_request: str | None = None, model_text_answer: str | None = None, user_metadata: dict[str, Any] | None = None, ) -> dict[str, Any]: response = await self._client.post( "/v1/native/utility_service/chat", params={"chat_id": chat_id, "user_id": user_id}, json={ "user_metadata": user_metadata or {}, "user_text_request": user_text_request, "model_text_answer": model_text_answer, }, ) response.raise_for_status() return response.json() def insert_native_ad(answer: str, ad_content: dict[str, Any]) -> str: insert_index = int(ad_content["insert_index"]) ad_text = str(ad_content["content"]) return f"{answer[:insert_index]} {ad_text} {answer[insert_index:]}" def native_text_ad(response: list[dict[str, Any]]) -> dict[str, Any] | None: return next( (item for item in response if item.get("type") == "native-text-ad"), None, ) async def main() -> None: client = GradiusClient(api_key="") try: result = await client.dialogue_message( chat_id="chat_8f8f7d21", user_id="user_42", role="assistant", text="Для старта подойдет CRM с простой аналитикой.", model_version="gpt-4.1-mini", ) placement = native_text_ad(result) if placement: final_answer = insert_native_ad("Ответ модели", placement["content"]) else: final_answer = "Ответ модели" print(final_answer) finally: await client.close() asyncio.run(main()) ``` ## JavaScript/TypeScript fetch example ```ts type GradiusResponse = { type: "native-text-ad" | string; content: string | {insert_index: number; content: string}; show_price?: number; click_price?: number; ad_context?: null | Record; price?: {show_price: number; click_price: number}; cta_text?: string | null; cta_link?: string | null; }[]; const BASE_URL = "https://api.adlean.pro"; async function postGradius( path: string, params: Record, body: unknown, ): Promise { const url = new URL(path, BASE_URL); for (const [key, value] of Object.entries(params)) { url.searchParams.set(key, value); } const response = await fetch(url, { method: "POST", headers: { Auth: process.env.GRADIUS_API_KEY ?? "", Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!response.ok) { throw new Error(`Gradius API error: ${response.status}`); } return response.json() as Promise; } function insertNativeAd(answer: string, content: {insert_index: number; content: string}): string { return [ answer.slice(0, content.insert_index), content.content, answer.slice(content.insert_index), ].join(" "); } function nativeTextAd(response: GradiusResponse) { return response.find((item) => item.type === "native-text-ad") ?? null; } const gradius = await postGradius( "/v1/native/dialogue_model/chat", { chat_id: "chat_8f8f7d21", user_id: "user_42", role: "assistant", lang: "ru", model_version: "gpt-4.1-mini", }, { text: "Для старта подойдет CRM с простой аналитикой.", user_metadata: {platform: "web"}, }, ); const modelAnswer = "Для старта подойдет CRM с простой аналитикой."; const placement = nativeTextAd(gradius); const finalAnswer = placement ? insertNativeAd(modelAnswer, placement.content) : modelAnswer; ``` ## Node.js with undici pool ```js import {Pool} from "undici"; const pool = new Pool("https://api.adlean.pro", { connections: 20, }); async function requestUtilityAd({apiKey, chatId, userId, userTextRequest, modelTextAnswer}) { const params = new URLSearchParams({ chat_id: chatId, user_id: userId, }); const response = await pool.request({ method: "POST", path: `/v1/native/utility_service/chat?${params.toString()}`, headers: { Auth: apiKey, Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ user_metadata: {tool: "group_helper_bot"}, user_text_request: userTextRequest ?? null, model_text_answer: modelTextAnswer ?? null, }), }); if (response.statusCode >= 400) { throw new Error(`Gradius API error: ${response.statusCode}`); } return response.body.json(); } ``` ## Go example ```go package gradius import ( "bytes" "context" "encoding/json" "fmt" "net/http" "net/url" "time" ) type Client struct { baseURL string apiKey string http *http.Client } func NewClient(apiKey string) *Client { return &Client{ baseURL: "https://api.adlean.pro", apiKey: apiKey, http: &http.Client{ Timeout: 10 * time.Second, }, } } func (c *Client) GenerationAd(ctx context.Context, chatID, userID, text string) (map[string]any, error) { params := url.Values{} params.Set("chat_id", chatID) params.Set("user_id", userID) params.Set("lang", "ru") payload := map[string]any{ "text": text, "user_metadata": map[string]any{ "surface": "result_caption", }, } body, err := json.Marshal(payload) if err != nil { return nil, err } requestURL := c.baseURL + "/v1/native/generation_model/chat?" + params.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Auth", c.apiKey) req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") resp, err := c.http.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= 400 { return nil, fmt.Errorf("gradius api error: %d", resp.StatusCode) } var result map[string]any if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } return result, nil } ``` ## PHP example ```php $chatId, "user_id" => $userId, "role" => "assistant", "lang" => "ru", "model_version" => "gpt-4.1-mini", ]); $payload = json_encode([ "text" => $answer, "user_metadata" => ["platform" => "web"], ], JSON_UNESCAPED_UNICODE); $ch = curl_init("https://api.adlean.pro/v1/native/dialogue_model/chat?$params"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Auth: $apiKey", "Accept: application/json", "Content-Type: application/json", ], CURLOPT_POSTFIELDS => $payload, CURLOPT_TIMEOUT => 10, ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status >= 400 || $body === false) { throw new RuntimeException("Gradius API error: $status"); } return json_decode($body, true); } ``` ## Telegram UI pattern When Gradius returns `cta_text` and `cta_link`, Telegram bots may render a dedicated button below the message. Keep the markdown content visible too unless product requirements explicitly define a button-only surface for that format. Python example with python-telegram-bot: ```python from telegram import InlineKeyboardButton, InlineKeyboardMarkup from telegram.constants import ParseMode async def send_ad_banner(bot, chat_id: int, gradius_response: list[dict]) -> None: placement = next( (item for item in gradius_response if item.get("type") == "native-text-ad"), None, ) if not placement: return content = placement["content"] cta_text = placement.get("cta_text") cta_link = placement.get("cta_link") reply_markup = None if cta_text and cta_link: reply_markup = InlineKeyboardMarkup([ [InlineKeyboardButton(text=cta_text, url=cta_link)] ]) await bot.send_message( chat_id=chat_id, text=content, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True, reply_markup=reply_markup, ) ``` ## Adding ad context to an OpenAI prompt For dialogue products, after showing a native ad in an assistant message, you may include the shown ad content in future model context. This lets the model answer user follow-up questions about the ad. Python: ```python from openai import OpenAI client = OpenAI() placement = next( (item for item in gradius_response if item.get("type") == "native-text-ad"), None, ) ad_text = placement["content"]["content"] if placement else None instructions = "Отвечай пользователю с учетом контекста диалога." if ad_text: instructions += ( "\n\nПользователь уже видел рекламную вставку ниже. " "Если он задаст вопрос о ней, отвечай по этому контексту:\n" f"{ad_text}" ) response = client.responses.create( model="gpt-4.1-mini", instructions=instructions, input=user_question, ) ``` JavaScript: ```js import OpenAI from "openai"; const client = new OpenAI(); const placement = gradiusResponse.find((item) => item.type === "native-text-ad") ?? null; const adText = placement?.content.content ?? null; let instructions = "Отвечай пользователю с учетом контекста диалога."; if (adText) { instructions += [ "", "Пользователь уже видел рекламную вставку ниже.", "Если он задаст вопрос о ней, отвечай по этому контексту:", adText, ].join("\n"); } const response = await client.responses.create({ model: "gpt-4.1-mini", instructions, input: userQuestion, }); ``` ## Error handling guidance HTTP errors: - `401 Unauthorized`: missing or invalid publisher API key. Treat as integration/config error. - 4xx other than 401: validate query/body contract. - 5xx or network timeout: technical error; decide whether to skip ad and preserve product UX. Business no-ad: - No-ad is not an HTTP error. No-ad is `[]` for every v1 ad endpoint. - Do not retry no-ad in a loop. - Do not show empty ad containers. Recommended resilient behavior: - On Gradius technical failure, continue core product response without ad. - Do not let ad API failures block the user’s primary AI result. - Keep observability on publisher backend, but scrub `Auth` and sensitive user text where needed. ## Acceptance checklist for code agents Before finalizing an integration, verify: - There is no `Auth` header in frontend/browser/mobile code. - Server-side client uses HTTPS base URL `https://api.adlean.pro`. - All v1 requests include `chat_id` and `user_id`. - `chat_id` is stable per session/dialogue. - `user_id` is stable and non-empty. - Dialogue endpoint sends both user and assistant messages with correct `role`. - Dialogue UI inserts ad only when the response array contains a `native-text-ad` placement. - Dialogue UI inserts `content.content` only at the placement `content.insert_index`. - Generation UI renders string `content` from `native-text-ad` and does not expect `insert_index`. - Utility UI does not send `model_version`. - Utility UI accepts `user_text_request` and `model_text_answer` as optional nullable fields. - Thinking endpoint is used only with native dialogue integration. - Thinking ads remain visible after final answer generation. - Markdown links remain clickable and are not rewritten. - Ads do not disappear after refresh, chat switch, route change, or time passing. - No-ad is handled as a normal business response. - API key leakage risk is documented for the publisher team. ## Do not generate code that does any of this - Calling Gradius directly from browser code with a real publisher key. - Saving the publisher key to localStorage/sessionStorage. - Passing `model_version` to `/v1/native/utility_service/chat`. - Creating a new `chat_id` for every message. - Omitting `user_id`. - Treating no-ad as an exception. - Replacing `cta_link` inside returned markdown. - Deleting the ad after a timer. - Showing thinking ads without dialogue native ads connected. - Sending the same user request to both thinking endpoint and native dialogue endpoint when thinking integration is active.