Cold Leads

Agent-initiated, human-approved subscriptions

Who it is for
Builders of AI agents whose owner has no Cold Leads key yet
The problem
An agent that needs e-mail verification should never hold a card or subscribe on its own. Sending the human off to sign up, pick a plan and copy a key breaks the task, and pasting keys into chats leaks them.
The solution
The agent calls one endpoint without a key and receives a Stripe Checkout link for its owner plus a secret claim token. The owner reviews the plan and decides. After payment, the agent collects the API key exactly once with the claim token, by polling or after a signed callback.
What you get
A working secret API key for the owner's new Business account, delivered to the agent once, and an e-mail to the owner with a link to open the Cold Leads web app.

Addresses, IDs and results in the examples are illustrative. example.com is reserved for documentation, so a real check of these addresses returns invalid.

The flow at a glance

Sequence
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)                 |                     |

The same flow is built into the Node.js SDK (agent.provision, agent.waitForActivation, verifyCallbackSignature) and into the local stdio MCP server as the tools provision_account_and_get_payment_link and check_provisioning_status. The hosted MCP endpoint needs a key on every request, so a keyless agent uses REST, the SDK or the stdio server for this step.

Step 1: request a payment link (no key needed)

owner_email is the human who will own and pay for the account; agent_id names your agent or product and is shown on the payment page. callback_url is optional and must be https on a public host.

Request
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"
  }'
200 response
{
  "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). …"
}
HTTPerrorWhen
400invalid_owner_email, agent_id_required, invalid_callback_url, bad_bodyThe body is not valid JSON, the owner e-mail fails the syntax check, agent_id is empty, or callback_url is not https on a public host.
409account_existsThis e-mail already has a Cold Leads workspace. Ask the owner for a secret key from Settings → API keys instead.
429rate_limitedMore than 10 requests per hour from one IP address, or more than 3 requests for the same owner e-mail per day.
502gatewayStripe could not create the checkout; try again later.

Step 2: the owner decides on Stripe

  • Show checkout_url to the human and let them decide. Do not open, pay or forward the link yourself.
  • The Stripe page names the product Cold Leads Business (API access) and states the agent's name, the price of $99 per month, monthly renewal until cancelled, and links to the terms. There is no trial on this path, and tax may be added at checkout.
  • The link is valid for 24 hours. After that the status turns expired and the agent can request a new link.
  • Once the payment goes through, Cold Leads activates the account and the key and e-mails the owner a one-time link to open the web app, where they can manage the subscription or rotate the agent's key.

Step 3: collect the key once

Poll GET /api/agent/status?session_id=… with the header X-Claim-Token every 15 to 30 seconds while the status is pending_payment. The first answer after payment that carries a valid claim token contains api_key; no later answer does.

statusFields besides session_id and plan
pending_paymentexpires_at; checkout_url when the claim token is valid
active (first call with the claim token)activated_at, api_key, api_key_prefix, mcp_url, api_base, message
active (any later call)activated_at, api_key_prefix, mcp_url, api_base, key_delivered_at, message
active (without a valid claim token)activated_at only
expiredmessage: request a new link with POST /api/agent/provision
suspendednone
First answer after payment
{
  "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>."
}
poll_status.py
# 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

Optional: a signed callback instead of polling

  • When the owner has paid, Cold Leads sends one POST to callback_url with a 5-second timeout. It does not follow redirects and does not retry, so keep polling as a fallback.
  • The JSON body is event (coldleads.account.activated), provision_id, session_id, status (active) and status_url. It never contains the key: collect it with the status request from step 3.
  • Headers: X-Coldleads-Event with the event name, and X-Coldleads-Signature with sha256= followed by a hex digest: HMAC-SHA256 over the raw request body, keyed with the SHA-256 hex digest of your claim token.
Node.js (Express)
// 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);
Python (Flask)
# 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

Security notes

  • Treat claim_token like a password. It is returned only once, only its hash is stored, and it is the only way to collect the key: without it, no status answer contains the key, whether the lookup uses session_id or owner_email.
  • The API key is delivered exactly once. If it is lost, the owner signs in and rotates it in Settings → API keys.
  • Paying does not prove that the payer controls the owner mailbox. Web access to the account is granted only to owner_email: through the one-time link e-mailed to it, or by signing in to Cold Leads with that address once it is verified.
  • Verify the callback signature on the raw body before acting on it, even though the callback carries no secrets.
  • Unpaid requests do not linger: the link expires after 24 hours, and unpaid provisioning records are deleted after 7 days together with their unused account and key.

Limits and costs

  • Plan: Business with API access, $99 per month in US dollars, renewing monthly until cancelled; 10,000 verification and API credits a month; extra packs of 1,000 credits cost $5. Tax may apply.
  • Provisioning: 10 requests per hour per IP address and 3 requests per owner e-mail per day. Status requests: 120 per minute per IP address.
  • After activation the key has the normal API limit of 120 requests per minute.

FAQ

Can the agent pay, or choose a cheaper plan?

No. The link is for the Business plan, the only plan that includes the API, and only the human can pay it on the Stripe page. The agent's job is to show the link and wait.

What if the owner never pays?

The checkout link expires after 24 hours and the status becomes expired. The agent may request a new link (up to 3 requests per owner e-mail per day). Unpaid records are deleted after 7 days.

What if the owner already uses Cold Leads?

The request fails with 409 account_exists. The owner creates a secret key in Settings → API keys (Business plan) and hands it to the agent as COLDLEADS_API_KEY.

Can this run over MCP?

Yes, with the local stdio server (npx -y --allow-git=root github:anttka4cz/mcp-server-coldleads). It starts without a key, offers provision_account_and_get_payment_link and check_provisioning_status, and switches to the new key as soon as it is collected. The hosted endpoint at coldleads.app/api/mcp requires a key for every request.