Підписки, які ініціює агент і схвалює людина
- Для кого
- Розробники AI-агентів, чий власник ще не має ключа Cold Leads
- Проблема
- Агент, якому потрібна перевірка e-mail, ніколи не повинен мати платіжної картки чи оформлювати підписку сам. Якщо відправити людину реєструватися, обирати тариф і копіювати ключ, завдання переривається, а ключі, вставлені в чат, витікають.
- Рішення
- Агент викликає один ендпоінт без ключа й отримує посилання Stripe Checkout для свого власника та секретний claim-токен. Власник переглядає тариф і ухвалює рішення. Після оплати агент рівно один раз отримує API-ключ за claim-токеном — через опитування або після підписаного callback-запиту.
- Що ви отримаєте
- Робочий секретний API-ключ для нового акаунта Business власника, переданий агентові один раз, і лист власникові з посиланням, яке відкриває веб-застосунок Cold Leads.
Адреси, ідентифікатори та результати в прикладах ілюстративні. Домен example.com зарезервовано для документації, тож реальна перевірка цих адрес поверне invalid.
Приклади коду однакові для всіх мов, коментарі в них — англійською.
Процес коротко
Agent (no key) Cold Leads Human owner Stripe
| | | |
| POST /api/agent/provision | | |
| owner_email, agent_id, | | |
| callback_url (optional) | | |
|----------------------------->| pending account, disabled key, | |
| | Checkout session (24 h) --------------------------------->|
|<-----------------------------| checkout_url, session_id, | |
| | claim_token (shown once) | |
| shows checkout_url --------------------------------------------->| |
| | | reviews plan, pays->|
| |<-------------------------------------- payment webhook --|
| | activates account and key, | |
| | e-mails the owner a sign-in link ->| |
|<-- signed callback (optional, no secrets) -----------------------| |
| GET /api/agent/status | | |
| ?session_id=... | | |
| X-Claim-Token: clt_... | | |
|----------------------------->| | |
|<-----------------------------| status active + api_key | |
| | (first call only) | |Цей самий процес вбудовано в Node.js SDK (agent.provision, agent.waitForActivation, verifyCallbackSignature) і в локальний stdio MCP-сервер як інструменти provision_account_and_get_payment_link і check_provisioning_status. Хостованому MCP-ендпоінту ключ потрібен у кожному запиті, тож агент без ключа виконує цей крок через REST, SDK або stdio-сервер.
Крок 1: запит посилання на оплату (ключ не потрібен)
owner_email — людина, яка володітиме акаунтом і платитиме за нього; agent_id — назва вашого агента чи продукту, яку показують на сторінці оплати. callback_url необов’язковий і має бути https-адресою на публічному хості.
curl -s https://coldleads.app/api/agent/provision \
-H "Content-Type: application/json" \
-d '{
"owner_email": "owner@example.com",
"agent_id": "Acme Research Agent",
"callback_url": "https://agent.example.com/hooks/coldleads"
}'{
"status": "payment_required",
"checkout_url": "https://checkout.stripe.com/c/pay/cs_live_…",
"session_id": "cs_live_…",
"provision_id": "prov_…",
"claim_token": "clt_…",
"expires_at": "2026-10-01T09:30:00.000Z",
"plan": { "id": "business", "name": "Business", "price": "$99", "amount": 99, "currency": "usd", "interval": "month", "api": true },
"status_url": "https://coldleads.app/api/agent/status?session_id=cs_live_…",
"instructions_for_agent": "Show the human owner (owner@example.com) this payment link and let them decide: … Do not open, pay or forward the link yourself. Keep claim_token secret (it is shown only now). …"
}| HTTP | error | Коли |
|---|---|---|
| 400 | invalid_owner_email, agent_id_required, invalid_callback_url, bad_body | Тіло не є коректним JSON, e-mail власника не проходить перевірку синтаксису, agent_id порожній або callback_url не є https-адресою на публічному хості. |
| 409 | account_exists | Для цього e-mail уже існує робочий простір Cold Leads. Натомість попросіть у власника секретний ключ із Налаштування → API-ключі. |
| 429 | rate_limited | Понад 10 запитів на годину з однієї IP-адреси або понад 3 запити для того самого e-mail власника на добу. |
| 502 | gateway | Stripe не зміг створити сесію оплати; спробуйте пізніше. |
Крок 2: власник ухвалює рішення в Stripe
- Покажіть checkout_url людині й дайте їй вирішити. Не відкривайте, не оплачуйте й не пересилайте посилання самостійно.
- Сторінка Stripe називає продукт Cold Leads Business (API access) і вказує ім’я агента, ціну $99 на місяць, щомісячне поновлення до скасування та посилання на умови. Пробного періоду на цьому шляху немає, а під час оплати може додатися податок.
- Посилання дійсне 24 години. Після цього статус змінюється на expired, і агент може запросити нове посилання.
- Щойно оплата пройде, Cold Leads активує акаунт і ключ та надсилає власникові листом одноразове посилання, яке відкриває веб-застосунок, де можна керувати підпискою або перевипустити ключ агента.
Крок 3: одноразове отримання ключа
Опитуйте GET /api/agent/status?session_id=… із заголовком X-Claim-Token кожні 15–30 секунд, поки статус — pending_payment. Перша відповідь після оплати на запит із дійсним claim-токеном містить api_key; жодна наступна відповідь його не містить.
| status | Поля, крім session_id і plan |
|---|---|
| pending_payment | expires_at; checkout_url, якщо claim-токен дійсний |
| active (перший виклик із claim-токеном) | activated_at, api_key, api_key_prefix, mcp_url, api_base, message |
| active (будь-який наступний виклик) | activated_at, api_key_prefix, mcp_url, api_base, key_delivered_at, message |
| active (без дійсного claim-токена) | лише activated_at |
| expired | message: запросіть нове посилання через POST /api/agent/provision |
| suspended | немає |
{
"session_id": "cs_live_…",
"plan": "business",
"status": "active",
"activated_at": "2026-09-30T10:02:11.000Z",
"api_key_prefix": "sk_…9f3c",
"mcp_url": "https://coldleads.app/api/mcp",
"api_base": "https://coldleads.app/api/v1",
"api_key": "sk_…",
"message": "This is the only time the key is shown. Store it as COLDLEADS_API_KEY; send it as Authorization: Bearer <key>."
}# pip install httpx
import os
import time
import httpx
BASE = "https://coldleads.app"
SESSION_ID = os.environ["COLDLEADS_SESSION_ID"] # session_id from the provision response
CLAIM_TOKEN = os.environ["COLDLEADS_CLAIM_TOKEN"] # claim_token (clt_...), shown only once
def save_key(key: str) -> None:
path = os.path.expanduser("~/.coldleads_api_key") # or your secret manager
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w") as f:
f.write(key)
while True:
r = httpx.get(f"{BASE}/api/agent/status", params={"session_id": SESSION_ID},
headers={"X-Claim-Token": CLAIM_TOKEN}, timeout=30)
if r.status_code == 429: # 120 status requests per minute per IP
time.sleep(60)
continue
r.raise_for_status()
s = r.json()
if s["status"] == "active":
if "api_key" not in s:
raise SystemExit("The key was already collected; the owner can rotate it in Settings -> API keys.")
save_key(s["api_key"])
print("API key saved; prefix", s["api_key_prefix"])
break
if s["status"] in ("expired", "suspended"):
raise SystemExit(s.get("message", s["status"]))
time.sleep(20) # pending_payment: poll every 15-30 secondsНеобов’язково: підписаний callback замість опитування
- Коли власник оплатив, Cold Leads надсилає один POST на callback_url із тайм-аутом 5 секунд. Він не переходить за редиректами й не повторює спроб, тож залиште опитування як запасний варіант.
- JSON-тіло містить event (coldleads.account.activated), provision_id, session_id, status (active) і status_url. Ключа в ньому ніколи немає: отримайте його запитом статусу з кроку 3.
- Заголовки: X-Coldleads-Event з назвою події та X-Coldleads-Signature зі значенням sha256=, за яким іде hex-дайджест: HMAC-SHA256 від сирого тіла запиту з ключем, яким є SHA-256 hex-дайджест вашого claim-токена.
// npm install express
import express from "express";
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
const CLAIM_TOKEN = process.env.COLDLEADS_CLAIM_TOKEN; // clt_... from the provision response
// X-Coldleads-Signature: sha256=<hex HMAC-SHA256 of the raw body>, key = SHA-256 hex digest of the claim token
function validSignature(rawBody, header, claimToken) {
if (!Buffer.isBuffer(rawBody) || typeof header !== "string" || !header.startsWith("sha256=")) return false;
const key = createHash("sha256").update(claimToken).digest("hex");
const expected = createHmac("sha256", key).update(rawBody).digest();
const given = Buffer.from(header.slice("sha256=".length), "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}
const app = express();
app.post("/hooks/coldleads", express.raw({ type: "application/json" }), (req, res) => {
if (!validSignature(req.body, req.get("x-coldleads-signature"), CLAIM_TOKEN)) return res.sendStatus(401);
const event = JSON.parse(req.body.toString("utf8"));
res.sendStatus(204); // answer fast: Cold Leads waits 5 seconds and does not retry
if (event.event === "coldleads.account.activated") {
// the callback carries no key: collect it now with GET /api/agent/status + X-Claim-Token
console.log("owner paid, session", event.session_id);
}
});
app.listen(3000);# pip install flask
import hashlib
import hmac
import json
import os
from flask import Flask, request
CLAIM_TOKEN = os.environ["COLDLEADS_CLAIM_TOKEN"] # clt_... from the provision response
app = Flask(__name__)
def valid_signature(raw_body: bytes, header, claim_token: str) -> bool:
if not header or not header.startswith("sha256="):
return False
key = hashlib.sha256(claim_token.encode()).hexdigest().encode()
expected = hmac.new(key, raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header[len("sha256="):])
@app.post("/hooks/coldleads")
def coldleads_callback():
raw = request.get_data() # the raw bytes, before any JSON parsing
if not valid_signature(raw, request.headers.get("X-Coldleads-Signature"), CLAIM_TOKEN):
return "", 401
event = json.loads(raw)
if event.get("event") == "coldleads.account.activated":
# the callback carries no key: collect it with GET /api/agent/status + X-Claim-Token
print("owner paid, session", event["session_id"])
return "", 204Безпека
- Поводьтеся з claim_token як із паролем. Він повертається лише один раз, зберігається лише його хеш, і це єдиний спосіб отримати ключ: без нього жодна відповідь статусу не містить ключа, хоч за session_id, хоч за owner_email.
- API-ключ видається рівно один раз. Якщо його втрачено, власник входить в акаунт і перевипускає ключ у Налаштування → API-ключі.
- Оплата не доводить, що платник контролює скриньку власника. Вебдоступ до акаунта надається лише owner_email: через одноразове посилання, надіслане на цю адресу, або через вхід у Cold Leads із цією адресою, щойно її підтверджено.
- Перевіряйте підпис callback на сирому тілі, перш ніж щось робити, хоча callback не містить секретів.
- Неоплачені запити не висять довго: посилання спливає через 24 години, а неоплачені записи про підключення видаляються через 7 днів разом із невикористаними акаунтом і ключем.
Ліміти та вартість
- Тариф: Business із доступом до API, $99 на місяць у доларах США, щомісячне поновлення до скасування; 10 000 кредитів на перевірку та API на місяць; додаткові пакети по 1 000 кредитів коштують $5. Може додаватися податок.
- Підключення: 10 запитів на годину з IP-адреси та 3 запити на e-mail власника на добу. Запити статусу: 120 на хвилину з IP-адреси.
- Після активації ключ має звичайний ліміт API — 120 запитів на хвилину.
Питання
Чи може агент оплатити або вибрати дешевший тариф?
Ні. Посилання веде на тариф Business, єдиний тариф з API, і оплатити його може лише людина на сторінці Stripe. Завдання агента — показати посилання й чекати.
А якщо власник так і не оплатить?
Посилання на оплату спливає через 24 години, і статус стає expired. Агент може запросити нове посилання (до 3 запитів на e-mail власника на добу). Неоплачені записи видаляються через 7 днів.
А якщо власник уже користується Cold Leads?
Запит завершується помилкою 409 account_exists. Власник створює секретний ключ у Налаштування → API-ключі (тариф Business) і передає його агентові як COLDLEADS_API_KEY.
Чи працює це через MCP?
Так, із локальним stdio-сервером (npx -y --allow-git=root github:anttka4cz/mcp-server-coldleads). Він запускається без ключа, пропонує provision_account_and_get_payment_link і check_provisioning_status і переходить на новий ключ, щойно його отримано. Хостований ендпоінт coldleads.app/api/mcp вимагає ключ у кожному запиті.