1. Зачем нужен ACP и почему это не просто «ещё один REST API»
Если смотреть цинично, ACP выглядит как набор обычных HTTP‑эндпоинтов и JSON‑структур: какой‑то /checkout_sessions, какие‑то webhooks, какие‑то токены. Легко подумать: «Окей, это ещё один кастомный API от очередной платформы». Но идея ACP глубже.
ACP задуман как открытый протокол взаимодействия между тремя участниками: платформой ИИ (например, ChatGPT), вашим commerce‑backend’ом и платёжным провайдером. Его цель — стандартизировать, как описывать продукты и цены, как ИИ объявляет о намерении пользователя купить, как создаётся сессия checkout, как исполняется платёж и как все участники узнают итоговый статус.
Ключевая мысль: один и тот же backend мерчанта, реализующий ACP, потенциально может работать не только с ChatGPT, но и с другими LLM‑платформами, которые поддержат этот стандарт. То есть вы не пишете «специальный API для ChatGPT», вы реализуете протокол commerce‑интеграции следующего поколения.
Instant Checkout в ChatGPT — это первая крупная реализация стандарта ACP. ChatGPT соблюдает этот протокол, вызывая ваши ACP‑эндпоинты и показывая пользователю красивый UI, но сами правила игры описаны в спецификациях ACP, а не зашиты «магией GPT» куда‑то внутрь чёрного ящика.
2. Три кита ACP: Product Feed, Agentic Checkout, Delegated Payment
У ACP есть три основные спецификации, которые мы будем постоянно упоминать:
| Спецификация | За что отвечает | Где это проявляется в GiftGenius |
|---|---|---|
| Product Feed Spec | Формат и поля товарного фида (SKU, цены, наличие, ссылки, флаги). | JSON/CSV фид с подарками, который индексирует OpenAI. |
| Agentic Checkout | REST‑контракт для checkout_session: создание, обновление, финал. | Наш ACP backend: эндпоинты /checkout_sessions и webhooks. |
| Delegated Payment | Как платёжные данные передаются мерчанту в виде делегированного токена. | Работа со Stripe Shared Payment Token при завершении платежа. |
Product Feed мы уже разобрали в предыдущих лекциях. Теперь нас интересуют два последних блока: Agentic Checkout и Delegated Payment.
Важно разделять три уровня:
- Стандарт (SPEC). Официальные документы описывают, какие поля и эндпоинты должны быть, какие статусы легальны и какие гарантии вы обязуетесь давать.
- Архитектурный паттерн (ARCH). Например, решение хранить SKU и заказы в отдельных таблицах, завести сервис‑обёртку вокруг ACP или использовать очередь для webhooks. Это хорошие практики, но не часть стандарта.
- Конкретная реализация (пример GiftGenius). Это наш учебный проект: структура наших таблиц, точные имена типов в TypeScript, как мы логируем заказы и т.п. Всё это пример, а не нормативный документ.
Мы будем постоянно подчеркивать, где заканчивается SPEC и начинается ваша архитектура — чтобы не вышло «я увидел в лекции поле persona_tags и решил, что это часть официальной спеки».
3. Checkout session изнутри: структура и статусы
Центральный объект Agentic Checkout Spec — это checkout_session на вашем бэкенде. Логически это состояние покупки: какие товары, на какую сумму, с какими вариантами доставки и в каком статусе сейчас находится попытка оплаты.
Спека описывает обязательные поля checkout_session примерно так (формулировки упрощены и частично сокращены по сравнению с оригиналом):
- id — строковый идентификатор сессии, который вы генерируете и возвращаете. ChatGPT будет использовать его во всех последующих вызовах.
- buyer — информация о покупателе: имя, email, телефон, иногда адрес. В реальной спека этот объект структурирован, чтобы PSP и ваши системы могли его надёжно использовать.
- status — строковый enum, отражающий текущее состояние покупки. Базовые статусы:
- not_ready_for_payment — ещё нельзя платить (например, не выбран вариант доставки или не пересчитаны налоги).
- ready_for_payment — всё готово, можно запрашивать платёжный токен и списывать деньги.
- completed — платёж прошёл успешно, заказ создан.
- canceled — покупка отменена (по инициативе пользователя или по ошибке).
- currency — код валюты в формате ISO 4217 в нижнем регистре ("usd", "eur" и т.п.).
- line_items — список позиций в корзине, каждая со своим SKU, количеством и вычисленной стоимостью.
- fulfillment_address — адрес доставки (если релевантно).
- fulfillment_options и fulfillment_option_id — возможные варианты доставки (или исполнения) и текущий выбранный вариант.
- totals — агрегированные суммы: стоимость товаров, налоги, доставка, итоговая сумма.
- order — объект, описывающий заказ, который будет создан после успешного завершения сессии.
- messages — список пользовательских сообщений, которые ChatGPT может показать покупателю: например, предупреждения или ошибки.
- links — список ссылок, например, на политику возвратов, Privacy Policy и Terms of Service.
Нам не обязательно реализовывать в демо все поля, но важно понимать идею: checkout_session — это «история и текущее состояние одной попытки покупки», и ChatGPT ожидает видеть в нём всё, что нужно для корректного UX.
Чтобы было проще, давайте введём в нашем учебном коде упрощённый тип:
// Упрощённая модель checkout_session для GiftGenius (не полная SPEC)
type GGCheckoutStatus = 'not_ready_for_payment' | 'ready_for_payment' | 'completed' | 'canceled';
type GGLineItem = { skuId: string; quantity: number; total: number };
type GGCheckoutSession = {
id: string;
status: GGCheckoutStatus;
currency: 'usd';
lineItems: GGLineItem[];
grandTotal: number;
};
Эта модель заведомо проще официальной, но отлично подходит для практики: учиться держать в голове статусы и переходы, не тоня в сотне полей.
4. Жизненный цикл checkout_session
Спецификация Agentic Checkout описывает несколько операций над checkout_session. В упрощённом виде жизненный цикл выглядит так:
- Создание сессии: POST /checkout_sessions.
- Обновление сессии: POST /checkout_sessions/{id}.
- Завершение сессии (complete): POST /checkout_sessions/{id}/complete.
- (Иногда) Отмена: отдельный cancel‑эндпоинт или перевод в canceled через обновление.
Глядя со стороны стейтов, можно нарисовать такую диаграмму:
stateDiagram-v2
[*] --> not_ready_for_payment
not_ready_for_payment --> ready_for_payment: расчёт доставки/налогов
выбор опций
ready_for_payment --> completed: успешный POST /complete
ready_for_payment --> canceled: отмена пользователем или ошибка
not_ready_for_payment --> canceled: ошибка, несовместимые данные
Создание checkout_session обычно начинает её в состоянии not_ready_for_payment или сразу ready_for_payment, если всё, что нужно для оплаты, уже известно (например, цифровой товар без доставки и налогов). Обновления используются, чтобы добавить данные (адрес, промокоды, вариант доставки) и пересчитать суммы. Завершение — это момент, когда в ход идёт Delegated Payment и деньги действительно списываются.
Здесь важно понимать разделение ролей:
- ChatGPT инициирует создание, обновления и завершение сессии, основываясь на диалоге с пользователем.
- Ваш backend (мерчант) отвечает за корректную бизнес‑логику: проверку SKU, доступности, расчёт цен и налогов, смену статусов, создание заказов.
- PSP (Stripe и др.) проводит реальный платёж и выдаёт Shared Payment Token, который мерчант использует для списания средств.
Чуть дальше мы наложим на эту стейт‑диаграмму конкретные HTTP‑запросы и крошечные примеры кода.
5. Создание checkout_session: что именно ждёт от нас ChatGPT
Когда ChatGPT (или агент) решил, что пользователь реально хочет что‑то купить, он формирует набор line items на основе Product Feed: список SKU, количество, предполагаемая валюта и, возможно, дополнительные пожелания к доставке. Затем он вызывает ваш эндпоинт POST /checkout_sessions.
На стороне мерчанта в этот момент нужно:
- Провалидировать входные данные: убеждаемся, что все SKU существуют, доступны к продаже, не нарушают политику (например, нет алкоголя для несовершеннолетнего).
- Рассчитать цены и налоги на основе собственных правил.
- Подготовить варианты доставки (fulfillment options), если товар физический.
- Вернуть корректный checkout_session со статусом и суммами.
Простейший обработчик на Express для GiftGenius может выглядеть так:
// Псевдокод: создание упрощённой checkout_session
app.post('/checkout_sessions', async (req, res) => {
const items = req.body.lineItems as GGLineItem[]; // skuId + quantity
const pricedItems = await priceItems(items); // считаем total по каждому SKU
const grandTotal = sum(pricedItems.map(i => i.total));
const session: GGCheckoutSession = {
id: generateId(),
status: 'ready_for_payment', // для цифровых подарков можно сразу готовить к оплате
currency: 'usd',
lineItems: pricedItems,
grandTotal,
};
res.status(201).json(session);
});
Здесь мы делаем несколько вещей:
- Не доверяем входным ценам от клиента (ChatGPT) и пересчитываем их по своим данным — это критично для безопасности commerce.
- Генерируем собственный id сессии (например, префикс gg_chk_...).
- Возвращаем статус ready_for_payment, если у нас нет дополнительных шагов (нет доставки, автоматические налоги, простая модель).
В реальном ACP‑совместимом backend’е вы дополнительно вернёте messages, links и составной объект totals, а также заполните order (хотя бы в черновом виде), как это описано в спецификации.
6. Обновление checkout_session и идемпотентность
После создания сессии ChatGPT может попросить пользователя о дополнительных деталях: адрес доставки, применение купона, смена варианта исполнения. Когда эти данные появляются, платформа делает вызов POST /checkout_sessions/{id}, чтобы вы обновили расчёты.
С точки зрения кода это очень похоже на создание, но вместо генерации новой сессии вы:
- находите существующую по id;
- применяете изменения (например, меняете fulfillment_option_id или добавляете скидку);
- пересчитываете суммы;
- возвращаете обновлённый checkout_session.
Важно, что спецификация допускает повторные вызовы (из‑за сетевых сбоев или повторов со стороны ChatGPT). Поэтому, как и в более ранних модулях, где мы говорили об идемпотентности инструментов и webhooks, здесь рекомендуется использовать Idempotency-Key в заголовках запроса и обрабатывать повторы аккуратно.
Условный обработчик обновления мог бы выглядеть так:
app.post('/checkout_sessions/:id', async (req, res) => {
const id = req.params.id;
const key = req.header('Idempotency-Key'); // один и тот же key => один эффект
const existing = await loadSessionWithIdempotency(id, key, req.body);
// applyUpdates внутри может пересчитать цены, доставку и т.п.
const updated = await applyUpdates(existing, req.body);
await saveSession(updated, key);
res.json(updated);
});
Здесь мы не жёстко следуем конкретной структуре SPEC, а показываем идею: на входе — изменения и идемпотентный ключ, на выходе — консистентное состояние checkout_session. Если к вам придёт такой же запрос с тем же ключом, вы должны вернуть тот же результат, не создавая лишних заказов или дублей в логах.
7. Завершение checkout_session и Delegated Payment: как работает Shared Payment Token
Самый интересный и нервный момент — завершение checkout_session, когда деньги действительно списываются. Здесь вступает в игру вторая спецификация: Delegated Payment.
Идея Delegated Payment
Пользователь вводит или выбирает платёжные данные в интерфейсе ChatGPT (карта, кошелёк, сохранённый метод оплаты). Платформа не шлёт эти данные вам напрямую — вместо этого она запрашивает у PSP (например, Stripe) специальный токен, Shared Payment Token (SPT), который:
- однозначно связан с мерчантом и конкретной сессией;
- ограничен по сумме и времени жизни;
- не раскрывает вам реальный номер карты.
В результате получается такая картина:
| Актор | Видит платёжные реквизиты карты | Видит Shared Payment Token | Видит детали заказа (SKU, суммы) |
|---|---|---|---|
| Пользователь | Да (вводит их в UI) | Нет (не нужно) | Частично (что покупает и за сколько) |
| ChatGPT/OpenAI | Да (в процессе оплаты) | Да | Да |
| PSP (Stripe) | Да | Да | В рамках платежа |
| Мерчант | Нет | Да | Да |
Такой дизайн позволяет мерчанту избежать хранения платёжных реквизитов и сосредоточиться на бизнес‑логике заказа, оставив compliance‑проблемы PSP и платформе.
Insight
Смысл Shared Payment Token в том, чтобы скрыть от вашего бэкенда данные карты, но платеж проводили именно вы. Но можно смотреть на него и немного по другому.
Думаю вы сталкивались с ситуацией когда магазин или отель сначала hold'ит деньги на вашей карте, а затем через время списывает их. Так вот: рассматривайте Shared Payment Token как hold-токен. ChatGPT захолдил деньги на счету пользователя, но не списывал их. Он передал вам этот hold-токен и теперь вы можете переслать его Stripe и списать деньги.
Тут есть два важных нюанса:
- суммы hold и списания не должны сильно отличаться, а лучше вообще должны совпадать.
- вы можете продать через ChatGPT первый месяц подписки за $1, а потом списывать каждый месяц по $49.99
Запрос POST /checkout_sessions/{id}/complete
Когда пользователь нажимает кнопку подтверждения оплаты в Instant Checkout, ChatGPT:
- Запрашивает SPT у PSP (например, через Stripe ACP API).
- Отправляет этот токен в ваш backend через POST /checkout_sessions/{id}/complete вместе с данными покупателя.
Спека описывает тело запроса примерно так (ниже адаптированный и сокращённый пример из официальной документации):
POST /checkout_sessions/checkout_session_123/complete
{
"buyer": {
"first_name": "John",
"last_name": "Smith",
"email": "johnsmith@mail.com"
},
"payment_data": {
"token": "spt_123",
"provider": "stripe"
}
}
Ваш backend на это должен:
- Найти checkout_session с id checkout_session_123.
- Проверить, что статус позволяет завершение (обычно ready_for_payment).
- Создать платёж у PSP, используя токен spt_123 (способ зависит от PSP, в случае Stripe — определённый endpoint и тип payment method).
- Дождаться подтверждения платёжной операции.
- Обновить checkout_session до completed, создать и сохранить заказ, заполнить поле order в структуре сессии.
- Вернуть актуальный checkout_session в ответе.
В очень упрощённом TypeScript‑псевдокоде это могло бы выглядеть так:
app.post('/checkout_sessions/:id/complete', async (req, res) => {
const { id } = req.params;
const { buyer, payment_data } = req.body;
const session = await loadSession(id);
await chargeWithSharedToken(payment_data.token, session.grandTotal);
const completed = await markSessionCompleted(session, buyer);
res.json(completed);
});
В реальном мире между этими строками спрячутся обработка ошибок, повторные попытки, логирование и интеграция с вашей моделью заказов.
Если что‑то идёт не так (например, платёж отклонён), вы должны вернуть checkout_session со статусом not_ready_for_payment или canceled и заполнить messages так, чтобы ChatGPT мог корректно объяснить пользователю, что произошло.
8. Instant Checkout в ChatGPT: как всё собирается в один поток
Теперь давайте сложим эти кусочки в цельный сценарий «от намерения до оплаты» в ChatGPT. Лекцию можно воспринимать как «раскодирование» того, что скрывается за одной кнопкой «Купить» в виджете.
Упрощённый сценарий:
- Пользователь пишет: «Подбери цифровой подарок другу до $50 и сразу оформи покупку».
- Агент (или сам ChatGPT App) использует Product Feed, чтобы найти подходящие SKU в пределах бюджета.
- ChatGPT показывает в чате несколько карточек подарков (через ваш виджет GiftGenius) и предлагает выбрать один.
- После выбора ChatGPT формирует line items и вызывает POST /checkout_sessions на вашем ACP backend’е, получая checkout_session с суммами и статусом.
- В UI Instant Checkout пользователь видит итоговую сумму, название товара, политику возврата и кнопку подтверждения.
- При подтверждении ChatGPT получает Shared Payment Token у PSP и вызывает POST /checkout_sessions/{id}/complete, как мы обсуждали выше.
- Ваш backend проводит платёж, создаёт заказ, возвращает checkout_session со статусом completed.
- ChatGPT показывает пользователю подтверждение, а ваш backend (через webhooks по Agentic Checkout Spec) может отправить событие обратно в OpenAI, чтобы платформа знала о судьбе заказа.
В виде sequence‑диаграммы это выглядит так:
sequenceDiagram
actor U as Пользователь
participant GPT as ChatGPT
participant GG as GiftGenius ACP backend
participant PSP as Stripe (PSP)
U->>GPT: Хочу подарок до $50 и купить прямо здесь
GPT->>GG: POST /checkout_sessions (line_items)
GG-->>GPT: checkout_session (ready_for_payment)
GPT->>U: Показывает Instant Checkout (товар, цена, ToS)
U->>GPT: Нажимает «Подтвердить оплату»
GPT->>PSP: Запрос SPT для суммы и мерчанта
PSP-->>GPT: Shared Payment Token (spt_xxx)
GPT->>GG: POST /checkout_sessions/{id}/complete (token + buyer)
GG->>PSP: Платёж с SPT
PSP-->>GG: Платёж успешен
GG-->>GPT: checkout_session (completed + order)
GPT-->>U: Показывает подтверждение покупки
В этом сценарии нигде не появляется «произвольный» вызов вашей базы или странных внутренних эндпоинтов. Всё укладывается в строго описанный контракт ACP, где каждый участник знает свою роль.
9. Мини‑практика: упрощённый ACP backend для GiftGenius
Чтобы эта лекция не осталась чистой теорией, важно ментально «прокрутить» реализацию ACP‑слоя для нашего учебного проекта.
Представьте, что у GiftGenius уже есть:
- База SKU и цен, на основе которой мы формируем Product Feed (мы это моделировали в прошлых лекциях).
- Простая модель заказов: таблица orders с полями id, userId, skuId, amount, currency, status, createdAt.
- Интерфейс ChatGPT App и MCP‑слой, который умеет рекомендовать подарки (мы строили это в предыдущих модулях курса).
Теперь ваша задача — добавить поверх этого ещё один маленький сервис gg-acp:
- Эндпоинт POST /checkout_sessions:
- Принимает список SKU и количество.
- Пересчитывает суммы на основе вашей БД.
- Создаёт черновой заказ (например, со статусом pending) и checkout_session со статусом ready_for_payment.
- Возвращает checkout_session.
- Эндпоинт POST /checkout_sessions/{id}:
- Находит сессию и заказ.
- Применяет изменения (например, поддержка промокода, который уменьшает итоговую сумму).
- Возвращает обновлённый checkout_session.
- Эндпоинт POST /checkout_sessions/{id}/complete:
- Получает SPT, сумму и данные покупателя.
- В демо‑версии может просто помечать заказ как «оплачен» без реального интеграционного вызова к PSP (или вы можете симулировать Stripe).
- Обновляет checkout_session в статус completed и привязывает к нему order_id.
Весь этот сервис можно реализовать в одном маленьком Node/Express‑приложении или в эндпоинтах Next.js App Router. Главное — соблюдать контракт по формату и статусам, даже если вы эмулируете платёж.
Условная модель заказа в TypeScript может выглядеть так:
// Упрощённая модель заказа GiftGenius
type GGOrderStatus = 'pending' | 'paid' | 'canceled';
type GGOrder = {
id: string;
userId: string;
skuId: string;
amount: number;
currency: 'usd';
status: GGOrderStatus;
};
В продакшене поверх этого появятся связи с вашим Auth/Identity (чтобы знать, каким пользователем является чат), webhooks к OpenAI и более сложные сценарии возвратов. Но как учебный шаг в рамках этой лекции достаточно научиться уверенно ходить по кругу: создать сессию → обновить → завершить, не теряя при этом деньги и здравый смысл.
10. Типичные ошибки при проектировании ACP / Instant Checkout
Ошибка №1: смешивание ролей («ChatGPT — это мой магазин»).
Иногда разработчики мысленно назначают ChatGPT «центральной системой учёта» и пытаются хранить бизнес‑состояние заказа на стороне платформы: «ну там же есть checkout_session, значит и историю заказов я буду читать из OpenAI». Это путь в никуда. checkout_session — это объект протокола, а не источник правды о заказах. Источник правды — ваш commerce backend: именно там должны жить заказы, статусы, возвраты и отчёты. ChatGPT в этой схеме всего лишь доверенный «фронтенд в чате».
Ошибка №2: доверие входным ценам от ChatGPT.
Легко подумать: «агент уже подобрал SKU и даже подсчитал сумму, давайте просто примем эту сумму и спишем деньги». Так делать нельзя. Вход из ChatGPT (line items, предполагаемые цены) нужно воспринимать как предложение, а не как приказ. Ваш backend обязан сам проверить SKU, цены, наличие, применимость скидок и т.п., сравнив это с Product Feed и своей БД. Иначе у вас появится весёлый класс багов «пользователь купил товар за $0.01, потому что модель решила округлить».
Ошибка №3: игнорирование статусов и state‑машины.
В ранних прототипах часто делают «дырявую» реализацию: статус сессии всегда completed, или просто ok, а любые расхождения с фактическим платёжным состоянием скрывают внутри. В итоге ChatGPT не может корректно отобразить пользователю, что происходит: платёж ещё в пути, уже завершился или был отменён. Гораздо надёжнее честно реализовать стейт‑машину not_ready_for_payment → ready_for_payment → completed/canceled и возвращать реальный статус из backend’а, а не выдумывать свои ad‑hoc поля.
Ошибка №4: использование Shared Payment Token как «многоразовой карты».
SPT по задумке — одноразовый или строго ограниченный токен: он привязан к конкретной операции, сумме и мерчанту. Пытаться кэшировать его «на всякий случай» или использовать повторно для другой покупки — плохая идея. В лучшем случае PSP откажет во второй попытке; в худшем — вы запутаете учёт платежей и заказы. Для каждого checkout_session.complete должен быть свой свежий токен, а если платёж не удался — нужно запрашивать новый.
Ошибка №5: отсутствие идемпотентности в /checkout_sessions и webhooks.
В реальной сети запросы могут дублироваться: ChatGPT может повторить POST /checkout_sessions после таймаута, PSP может повторно отправить webhook после временной ошибки. Если ваша реализация каждый раз создаёт новый заказ и новую запись в базе, вы быстро получите хаос: двойные списания, дубли заказов и странные расхождения между системами. Использование Idempotency-Key, проверка повторов и хранение результатов предыдущих вызовов — это не «опциональная оптимизация», а необходимый элемент надёжной ACP‑интеграции.
Ошибка №6: забыли про связь с Product Feed.
Иногда ACP‑слой проектируют «в вакууме»: SKU и цены берут из каких‑то внутренних таблиц, которые не совпадают с тем, что попадает в Product Feed. В результате ChatGPT показывает пользователю одно (по фиду), а в checkout через ACP прокатывается совсем другое. Чтобы избежать таких сюрпризов, важно, чтобы ваша модель SKU и цен была единой: feed, ACP backend и внутренняя база должны смотреть на один и тот же источник правды, даже если поверх него есть разные проекции и кэши.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ