Suscripciones que inicia un agente y aprueba una persona
- Para quién
- Quienes crean agentes de IA cuyo propietario aún no tiene clave de Cold Leads
- El problema
- Un agente que necesita verificar correos nunca debería tener una tarjeta ni suscribirse por su cuenta. Mandar a la persona a registrarse, elegir un plan y copiar una clave interrumpe la tarea, y pegar claves en chats hace que se filtren.
- La solución
- El agente llama a un endpoint sin clave y recibe un enlace de Stripe Checkout para su propietario más un token de reclamación secreto. El propietario revisa el plan y decide. Tras el pago, el agente recoge la clave de API exactamente una vez con el token de reclamación, mediante sondeo o tras un callback firmado.
- Qué obtiene
- Una clave secreta de API operativa para la nueva cuenta Business del propietario, entregada al agente una sola vez, y un correo al propietario con un enlace para abrir la aplicación web de Cold Leads.
Las direcciones, los identificadores y los resultados de los ejemplos son ilustrativos. example.com está reservado para documentación, así que una comprobación real de estas direcciones devuelve invalid.
Los ejemplos de código son iguales en todos los idiomas; sus comentarios están en inglés.
El flujo de un vistazo
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) | |El mismo flujo está integrado en el SDK de Node.js (agent.provision, agent.waitForActivation, verifyCallbackSignature) y en el servidor MCP local stdio, como las herramientas provision_account_and_get_payment_link y check_provisioning_status. El endpoint MCP alojado necesita una clave en cada petición, así que un agente sin clave usa REST, el SDK o el servidor stdio para este paso.
Paso 1: pida un enlace de pago (no hace falta clave)
owner_email es la persona que será propietaria de la cuenta y la pagará; agent_id da nombre a su agente o producto y se muestra en la página de pago. callback_url es opcional y debe ser https en un host público.
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 | Cuándo |
|---|---|---|
| 400 | invalid_owner_email, agent_id_required, invalid_callback_url, bad_body | El cuerpo no es JSON válido, el correo del propietario no supera la comprobación de sintaxis, agent_id está vacío o callback_url no es https en un host público. |
| 409 | account_exists | Este correo ya tiene un espacio de trabajo de Cold Leads. En su lugar, pida al propietario una clave secreta de Ajustes → Claves de API. |
| 429 | rate_limited | Más de 10 peticiones por hora desde una misma dirección IP, o más de 3 peticiones al día para el mismo correo de propietario. |
| 502 | gateway | Stripe no pudo crear la sesión de pago; inténtelo de nuevo más tarde. |
Paso 2: el propietario decide en Stripe
- Muestre checkout_url a la persona y deje que decida. No abra, pague ni reenvíe el enlace por su cuenta.
- La página de Stripe nombra el producto Cold Leads Business (API access) e indica el nombre del agente, el precio de $99 al mes, la renovación mensual hasta la cancelación y enlaces a las condiciones. En esta vía no hay periodo de prueba, y pueden añadirse impuestos al pagar.
- El enlace es válido durante 24 horas. Después, el estado pasa a expired y el agente puede pedir un enlace nuevo.
- Cuando el pago se completa, Cold Leads activa la cuenta y la clave y envía al propietario por correo un enlace de un solo uso para abrir la aplicación web, donde puede gestionar la suscripción o regenerar la clave del agente.
Paso 3: recoja la clave una sola vez
Consulte GET /api/agent/status?session_id=… con la cabecera X-Claim-Token cada 15 a 30 segundos mientras el estado sea pending_payment. La primera respuesta tras el pago a una petición con un token de reclamación válido contiene api_key; ninguna respuesta posterior lo incluye.
| status | Campos además de session_id y plan |
|---|---|
| pending_payment | expires_at; checkout_url cuando el token de reclamación es válido |
| active (primera llamada con el token de reclamación) | activated_at, api_key, api_key_prefix, mcp_url, api_base, message |
| active (cualquier llamada posterior) | activated_at, api_key_prefix, mcp_url, api_base, key_delivered_at, message |
| active (sin token de reclamación válido) | solo activated_at |
| expired | message: pida un enlace nuevo con POST /api/agent/provision |
| suspended | ninguno |
{
"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 secondsOpcional: un callback firmado en lugar de sondeo
- Cuando el propietario ha pagado, Cold Leads envía un único POST a callback_url con un tiempo de espera de 5 segundos. No sigue redirecciones ni reintenta, así que mantenga el sondeo como respaldo.
- El cuerpo JSON contiene event (coldleads.account.activated), provision_id, session_id, status (active) y status_url. Nunca contiene la clave: recójala con la petición de estado del paso 3.
- Cabeceras: X-Coldleads-Event con el nombre del evento, y X-Coldleads-Signature con sha256= seguido de un digest hexadecimal: HMAC-SHA256 sobre el cuerpo en bruto de la petición, con el digest hexadecimal SHA-256 de su token de reclamación como clave.
// 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 "", 204Notas de seguridad
- Trate claim_token como una contraseña. Se devuelve una sola vez, solo se guarda su hash y es la única forma de recoger la clave: sin él, ninguna respuesta de estado contiene la clave, tanto si la consulta usa session_id como owner_email.
- La clave de API se entrega exactamente una vez. Si se pierde, el propietario inicia sesión y la regenera en Ajustes → Claves de API.
- Pagar no demuestra que quien paga controle el buzón del propietario. El acceso web a la cuenta solo se concede a owner_email: mediante el enlace de un solo uso enviado a esa dirección, o iniciando sesión en Cold Leads con esa dirección una vez verificada.
- Verifique la firma del callback sobre el cuerpo en bruto antes de actuar, aunque el callback no lleve secretos.
- Las solicitudes sin pagar no se quedan pendientes: el enlace caduca a las 24 horas, y los registros de aprovisionamiento sin pagar se eliminan a los 7 días junto con su cuenta y su clave sin usar.
Límites y costes
- Plan: Business con acceso a la API, $99 al mes en dólares estadounidenses, con renovación mensual hasta que se cancele; 10 000 créditos de verificación y API al mes; los paquetes extra de 1 000 créditos cuestan $5. Pueden aplicarse impuestos.
- Aprovisionamiento: 10 peticiones por hora por dirección IP y 3 peticiones al día por correo de propietario. Peticiones de estado: 120 por minuto por dirección IP.
- Tras la activación, la clave tiene el límite normal de la API de 120 peticiones por minuto.
Preguntas frecuentes
¿Puede el agente pagar o elegir un plan más barato?
No. El enlace es para el plan Business, el único plan que incluye la API, y solo la persona puede pagarlo en la página de Stripe. La tarea del agente es mostrar el enlace y esperar.
¿Y si el propietario nunca paga?
El enlace de pago caduca a las 24 horas y el estado pasa a expired. El agente puede pedir un enlace nuevo (hasta 3 peticiones al día por correo de propietario). Los registros sin pagar se eliminan a los 7 días.
¿Y si el propietario ya usa Cold Leads?
La petición falla con 409 account_exists. El propietario crea una clave secreta en Ajustes → Claves de API (plan Business) y se la entrega al agente como COLDLEADS_API_KEY.
¿Funciona esto por MCP?
Sí, con el servidor local stdio (npx -y --allow-git=root github:anttka4cz/mcp-server-coldleads). Arranca sin clave, ofrece provision_account_and_get_payment_link y check_provisioning_status, y pasa a usar la clave nueva en cuanto la recoge. El endpoint alojado en coldleads.app/api/mcp exige una clave en cada petición.