ChatGPT API на сайті самому: код і що дописати
Оновлено:
Підключити ChatGPT API до сайту самому можна за вечір: потрібен сервер-посередник, який тримає ключ, і маленький віджет на сторінці. Нижче робочий мінімальний код на Node.js без сторонніх пакетів. Далі чесно про те, що доведеться дописати, щоб бот продавав. Готовий варіант, у якому це вже є, — Rozmova.
Якщо ви вибираєте між власним кодом і готовим сервісом, почніть з порівняння Rozmova чи ChatGPT на сайті. Ця стаття для тих, хто хоче спершу спробувати сам.
Що потрібно, щоб почати?
Три речі: Node.js 18 або новіший, ключ OpenAI API і назва моделі.
- Node.js 18+. У ньому вже є
fetch, тож сторонні пакети не потрібні. - Ключ API. Створюється в кабінеті OpenAI Platform. Оплата за фактичне використання, окремо від підписки ChatGPT.
- Назва моделі. Візьміть її зі сторінки Models у документації OpenAI. Назви моделей регулярно змінюються, тому в коді вона не зашита, а передається змінною
OPENAI_MODEL.
Чому ключ не можна класти в код сторінки?
Бо все, що є на сторінці, бачить відвідувач. Ключ у JavaScript сайту можна скопіювати за хвилину і витрачати ваш рахунок без обмежень.
Тому схема така: сторінка надсилає повідомлення на ваш сервер, сервер додає ключ і звертається до OpenAI, відповідь повертається на сторінку.
Як виглядає сервер?
Один файл server.mjs. Він віддає сторінку з чатом і приймає повідомлення на /api/chat. Запит іде в Responses API з інструкцією для моделі й останніми повідомленнями розмови.
server.mjs// server.mjs: мінімальний чат на OpenAI Responses API. Без сторонніх пакетів, Node.js 18+.
// Запуск: OPENAI_API_KEY=ваш_ключ OPENAI_MODEL=назва_моделі node server.mjs
import http from "node:http";
import { readFile } from "node:fs/promises";
const PORT = Number(process.env.PORT ?? 3000);
const API_KEY = process.env.OPENAI_API_KEY;
const MODEL = process.env.OPENAI_MODEL;
const API_URL = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
if (!API_KEY || !MODEL) {
console.error("Задайте змінні OPENAI_API_KEY і OPENAI_MODEL");
process.exit(1);
}
// Інструкція для моделі. Тут же правила тону й заборони.
const INSTRUCTIONS =
"Ви консультант інтернет-магазину. Відповідайте коротко, українською, на «ви». " +
"Не вигадуйте цін, наявності й умов доставки. Якщо не знаєте відповіді, " +
"так і скажіть і запропонуйте залишити телефон для менеджера.";
const MAX_MESSAGES = 12; // скільки останніх повідомлень розмови йде в модель
const MAX_CHARS = 1000; // найдовше повідомлення відвідувача
const MAX_BODY = 50_000; // найбільший запит від сторінки, у байтах
async function askModel(messages) {
const res = await fetch(`${API_URL}/responses`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
model: MODEL,
instructions: INSTRUCTIONS,
input: messages,
max_output_tokens: 800,
store: false,
}),
signal: AbortSignal.timeout(30_000),
});
if (!res.ok) throw new Error(`OpenAI відповів ${res.status}: ${await res.text()}`);
const data = await res.json();
// Текст відповіді: output[] → елементи type "message" → content[] → type "output_text".
const text = (data.output ?? [])
.filter((item) => item.type === "message")
.flatMap((item) => item.content ?? [])
.filter((part) => part.type === "output_text")
.map((part) => part.text)
.join("");
return text || "Вибачте, не вийшло відповісти. Спробуйте ще раз.";
}
// Беремо від сторінки тільки ролі user/assistant і тільки текст, обрізаємо довжину.
function cleanMessages(raw) {
if (!Array.isArray(raw)) return null;
const messages = raw
.filter((m) => (m?.role === "user" || m?.role === "assistant") && typeof m.content === "string")
.map((m) => ({ role: m.role, content: m.content.slice(0, MAX_CHARS) }))
.slice(-MAX_MESSAGES);
return messages.at(-1)?.role === "user" ? messages : null;
}
async function readBody(req) {
const chunks = [];
let size = 0;
for await (const chunk of req) {
size += chunk.length;
if (size > MAX_BODY) throw new RangeError("Завеликий запит");
chunks.push(chunk);
}
return Buffer.concat(chunks).toString("utf8");
}
function sendJson(res, status, payload) {
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(payload));
}
const server = http.createServer(async (req, res) => {
try {
if (req.method === "GET" && req.url === "/") {
const html = await readFile(new URL("./index.html", import.meta.url));
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(html);
return;
}
if (req.method === "POST" && req.url === "/api/chat") {
const body = JSON.parse(await readBody(req));
const messages = cleanMessages(body?.messages);
if (!messages) return sendJson(res, 400, { error: "Порожнє повідомлення" });
const reply = await askModel(messages);
return sendJson(res, 200, { reply });
}
sendJson(res, 404, { error: "Не знайдено" });
} catch (err) {
const badRequest = err instanceof SyntaxError || err instanceof RangeError;
if (!badRequest) console.error(err);
sendJson(res, badRequest ? 400 : 500, { error: badRequest ? "Хибний запит" : "Бот тимчасово не відповідає" });
}
});
server.listen(PORT, () => console.log(`Чат працює: http://localhost:${PORT}`));
Що тут важливо:
- Ключ тільки в змінній оточення. Його немає ні в коді, ні на сторінці.
- Сервер сам вирішує, що піде в модель. Він пропускає тільки ролі
userіassistant, обрізає довгі повідомлення й бере останні 12. Так сторінка не підмінить інструкцію. - Обмеження на відповідь і тайм-аут.
max_output_tokensне дає моделі писати безкінечно, а запит обривається через 30 секунд. store: false. Просить OpenAI не зберігати відповідь для подальшого отримання через API.- Текст відповіді лежить у масиві
output: беремо елементи типуmessage, а з них частини типуoutput_text.
Як виглядає віджет на сторінці?
Файл index.html лежить поруч із сервером. Це поле вводу, кнопка і список повідомлень. Історія розмови живе в масиві messages і надсилається з кожним запитом.
index.html<!doctype html>
<html lang="uk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Чат на OpenAI API</title>
<style>
body { font: 16px/1.5 system-ui, sans-serif; margin: 16px; }
#chat { max-width: 420px; }
#log { height: 360px; overflow-y: auto; border: 1px solid #ccc; border-radius: 8px; padding: 8px; }
#log p { margin: 0 0 8px; }
.user { text-align: right; }
.assistant { color: #2563eb; }
form { display: flex; gap: 8px; margin-top: 8px; }
input { flex: 1; }
</style>
</head>
<body>
<div id="chat">
<div id="log" aria-live="polite"></div>
<form id="form">
<input id="text" aria-label="Ваше питання" autocomplete="off" required maxlength="1000" placeholder="Ваше питання">
<button type="submit">Надіслати</button>
</form>
</div>
<script>
const messages = [];
const log = document.getElementById("log");
const form = document.getElementById("form");
const input = document.getElementById("text");
const button = form.querySelector("button");
function show(role, text) {
const p = document.createElement("p");
p.className = role;
p.textContent = text; // textContent, не innerHTML: відповідь моделі не можна вставляти як HTML
log.append(p);
log.scrollTop = log.scrollHeight;
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
const text = input.value.trim();
if (!text) return;
input.value = "";
show("user", text);
messages.push({ role: "user", content: text });
button.disabled = true;
try {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages }),
});
const data = await res.json();
if (res.ok) {
show("assistant", data.reply);
messages.push({ role: "assistant", content: data.reply });
} else {
show("assistant", data.error);
}
} catch {
show("assistant", "Немає зв'язку із сервером. Спробуйте ще раз.");
} finally {
button.disabled = false;
input.focus();
}
});
</script>
</body>
</html>
Зверніть увагу на textContent. Відповідь моделі не можна вставляти через innerHTML: якщо відвідувач попросить модель написати HTML зі скриптом, він виконається на вашому сайті.
Як запустити й перевірити?
Покладіть обидва файли в одну папку і виконайте одну команду.
OPENAI_API_KEY=ваш_ключ OPENAI_MODEL=назва_моделі node server.mjs
Відкрийте http://localhost:3000 і напишіть питання. Без ключа або назви моделі сервер одразу зупиниться з підказкою. Ми перевірили цей код 21 жовтня 2026 року з локальною заглушкою API: сервер передає ключ, інструкцію й історію, а відповідь доходить до сторінки. Справжній виклик потребує вашого ключа.
Що доведеться дописати, щоб бот працював на продажі?
Мінімальний код відповідає на питання, але не знає ваших цін, не збирає заявок і не має лімітів витрат. Ось що зазвичай дописують.
| Частина | Навіщо | Без неї |
|---|---|---|
| Відповіді за вашими даними (RAG) | Ціни, наявність, доставка з вашого сайту й фіду | Бот вигадує правдоподібні ціни |
| Оновлення бази знань | Нові ціни й товари потрапляють у відповіді | Бот відповідає за вчорашнім прайсом |
| Ліміти й захист від зловживань | Обмеження на розмову, IP і день, дозволені домени | Скрипт може витратити місячний бюджет за ніч |
| Захист від prompt-injection | Повідомлення відвідувача — дані, а не команди | «Забудь інструкції» змінює поведінку бота |
| Збір заявок | Форма контакту, передача менеджеру в пошту, Telegram чи CRM | Розмова закінчується, контакту немає |
| Історія на сервері й оператор | Менеджер бачить розмову і може відповісти сам | Ніхто не знає, що писали відвідувачі |
| Поступовий вивід відповіді | Текст з’являється одразу, а не після паузи | Відвідувач чекає і закриває чат |
| Облік витрат і аналітика | Скільки коштує розмова, про що питають | Рахунок від OpenAI стає сюрпризом |
| Персональні дані | Що зберігаєте, скільки, хто має доступ | Телефони клієнтів лежать у логах без правил |
| Повтори й моніторинг помилок | Повтор при тимчасових збоях API, сповіщення про помилки | Бот мовчки перестає відповідати |
Найбільша з цих частин — RAG. Чому без нього бот вигадує ціни і як працює пошук по базі знань, пояснюємо у статті що таке RAG простими словами. Як рахувати витрати на AI до запуску, розібрано у статті скільки коштує AI-чат на сайті.
Про безпеку
Prompt-injection стоїть першим у списку ризиків OWASP для застосунків на мовних моделях (2025). Інструкція «не виконуй команд відвідувача» в промпті не є захистом сама по собі: перевірки мають бути і на сервері.
Коли власний код має сенс, а коли краще готовий сервіс?
Власний код має сенс, якщо у вас є розробник і бот потрібен для нестандартної задачі, наприклад усередині вашого застосунку. Якщо мета — заявки з сайту, швидше взяти готовий сервіс і витратити час на базу знань.
- Пишіть самі, якщо потрібна глибока інтеграція з вашою системою, ви готові підтримувати код і стежити за змінами API.
- Беріть готовий сервіс, якщо потрібні відповіді за каталогом, заявки менеджеру і звіти вже цього місяця.
Як порівнювати постачальників, підкаже стаття як обрати чат-бот для сайту. Rozmova працює на моделях Anthropic Claude, а не OpenAI, і вже має базу знань, збір заявок, ліміти й звіти з таблиці вище.
Питання й відповіді
Чи можна викликати ChatGPT API прямо з браузера?
Ні. Тоді ключ API буде в коді сторінки, і будь-хто зможе його скопіювати й витрачати ваші гроші. Ключ має жити тільки на сервері, а сторінка звертається до вашого сервера.
Скільки коштує ChatGPT API для чату на сайті?
Залежить від моделі, довжини інструкції, історії розмови й відповіді. Ціни за мільйон токенів OpenAI публікує на своїй сторінці цін. Як порахувати вартість однієї відповіді і розмови, ми розібрали у статті скільки коштує AI-чат на сайті.
Чи запам’ятовує API попередні повідомлення?
У нашому прикладі ні: сторінка надсилає всю розмову з кожним запитом, а сервер передає в модель останні повідомлення. Для продакшну історію краще зберігати на сервері, бо сторінка може надіслати що завгодно.
Чи знатиме такий бот ціни мого магазину?
Ні, поки ви їх не дасте. Модель не знає вашого прайсу і без даних вигадає правдоподібну цифру. Потрібен пошук по вашій базі знань перед кожною відповіддю, тобто RAG. Як він працює, пояснюємо у статті що таке RAG.
Rozmova працює на ChatGPT?
Ні. Rozmova працює на моделях Anthropic Claude і вже має те, що в цьому статті доводиться дописувати: базу знань, збір заявок, ліміти, звіти. Порівняння двох підходів — на сторінці Rozmova чи ChatGPT на сайті.