Developer guide

How to get AI research and outreach emails for a lead with the Coolie API

Send POST /api/v1/agents/{agentId}/leads/actions with action research and up to 50 leads, each { runId, prospectId }. Coolie answers 202 with how many it queued, researches each lead in the background (a minute or two a lead, 1 credit each) and writes its sequence: the lead's research (insights, persona) and its email and LinkedIn messages then appear in GET /api/v1/leads, and webhooks subscribed to lead.researched receive the lead with them.

Updated

Before you start

  • An API key: in Coolie, open Settings → API keys, name the key and click Create key. It is shown once, so keep it in an environment variable (COOLIE_API_KEY in these examples). It acts as you, with your plan, your credits and your agents.
  • Credits: 1 credit a lead researched, charged when its research is saved. The balance must cover the batch.
  • Leads with a contact and no research yet (status enriched), from GET /api/v1/leads: any other lead is skipped, at no charge.

Queue the research

curl
curl https://trycoolie.ai/api/v1/agents/agent-3f2a91c0/leads/actions \
  -H "Authorization: Bearer $COOLIE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action": "research", "leads": [ { "runId": "ui-5b9e2c41", "prospectId": "p-41b7c2d9e0aa" } ] }'
Answer (202)
{
  "queued": 1,
  "skipped": 0
}

Asking again for leads still queued resumes them, at no extra charge. A researched lead whose messages are missing gets them written again, free. Leads that cannot be researched are counted in skipped, and cost nothing.

Wait for the result

Subscribe a webhook to lead.researched, which carries the researched lead, or poll the agent's leads until the lead's research and sequence are filled in. A lead keeps its place in the newest-first list, so a recent one is on the first page.

Python
import os
import time

import requests

API = "https://trycoolie.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['COOLIE_API_KEY']}"}
AGENT_ID = "agent-3f2a91c0"
LEAD = {"runId": "ui-5b9e2c41", "prospectId": "p-41b7c2d9e0aa"}

queued = requests.post(f"{API}/agents/{AGENT_ID}/leads/actions", headers=HEADERS,
                       json={"action": "research", "leads": [LEAD]}, timeout=60)
queued.raise_for_status()
print(queued.json())  # {'queued': 1, 'skipped': 0}

lead = None
while lead is None or lead["research"] is None or lead["sequence"] is None:
    time.sleep(30)
    page = requests.get(f"{API}/agents/{AGENT_ID}/leads", headers=HEADERS,
                        params={"limit": 100}, timeout=60).json()
    lead = next((item for item in page["leads"] if item["prospectId"] == LEAD["prospectId"]), None)

for insight in lead["research"]["insights"]:
    print(insight["id"], insight["type"], "-", insight["insight"])
for message in lead["sequence"]:
    print()
    print(f"[{message['channel']}, step {message['step']}] {message['subject'] or ''}")
    print(message["body"])

Write the messages again from your own insight

Add what you know with POST /api/v1/agents/{agentId}/leads/insights (5,000 characters at most, five a lead, free): it answers 201 with its id, c1 for the first. Then POST /api/v1/agents/{agentId}/leads/sequence with the insight ids to write from: the new messages replace the lead's, free, and the request waits for them, up to a minute.

curl
curl https://trycoolie.ai/api/v1/agents/agent-3f2a91c0/leads/insights \
  -H "Authorization: Bearer $COOLIE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "runId": "ui-5b9e2c41", "prospectId": "p-41b7c2d9e0aa", "text": "Met Camille at a trade show: their two sites still plan shifts by hand." }'

curl https://trycoolie.ai/api/v1/agents/agent-3f2a91c0/leads/sequence \
  -H "Authorization: Bearer $COOLIE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "runId": "ui-5b9e2c41", "prospectId": "p-41b7c2d9e0aa", "insightIds": ["c1", "s1", "w1"] }'

What the research holds

  • companyContext and roleTension: the company in a few lines, and the problem the contact's role points at; confidence high, medium or low.
  • insights: the agent's, best first (s1 the buying signal, w1… the web, p1… the lead's posts), then yours (c1…, custom true), each with its date, why it matters, a score out of 10 and its sources.
  • persona: the pain point, the current solution, the risks, the differentiation and the benefits.
  • sequence: the messages in order, each with its channel, its step, a subject for emails, and its body.

Researching sends nothing: messages go out only from a campaign you launch.

A researched lead

The lead as GET /api/v1/leads gives it once researched. The values are examples.

JSON
{
  "runId": "ui-5b9e2c41",
  "prospectId": "p-41b7c2d9e0aa",
  "agentId": "agent-3f2a91c0",
  "status": "written",
  "processedAt": "2026-10-19T09:02:13.511Z",
  "icpMatch": true,
  "aiScore": 8.4,
  "feedback": null,
  "signal": {
    "type": "hiring",
    "name": "Hiring",
    "headline": "Operations Manager",
    "detail": null,
    "url": "https://jobs.example.com/12345",
    "postedAt": "2026-10-17"
  },
  "company": {
    "name": "Northwind Health",
    "domain": "northwind.example",
    "website": "https://northwind.example",
    "linkedinUrl": "https://www.linkedin.com/company/example",
    "industry": "Hospitals and Health Care",
    "employees": 320,
    "country": "France",
    "locations": [
      "Lyon, France"
    ]
  },
  "contact": {
    "name": "Camille Martin",
    "title": "COO",
    "seniority": "c_suite",
    "linkedinUrl": "https://www.linkedin.com/in/example",
    "country": "France",
    "email": "camille.martin@northwind.example",
    "emailConfidence": 0.92,
    "phone": null
  },
  "research": {
    "companyContext": "Northwind Health runs two clinics around Lyon and is hiring an Operations Manager to run scheduling across both sites.",
    "roleTension": "As COO, Camille owns the clinics' scheduling while the team grows faster than its tools.",
    "confidence": "high",
    "insights": [
      {
        "id": "s1",
        "custom": false,
        "type": "Hiring Activity",
        "insight": "Northwind Health is hiring: Operations Manager",
        "date": "2026-10-17",
        "relevancy": "The buying signal that brought this lead in.",
        "score": null,
        "sources": [
          "https://jobs.example.com/12345"
        ],
        "hiddenProblem": null
      },
      {
        "id": "w1",
        "custom": false,
        "type": "Company News",
        "insight": "Northwind Health opened its second clinic in September.",
        "date": "2026-09-12",
        "relevancy": "A second site makes shift planning by hand harder.",
        "score": 8,
        "sources": [
          "https://news.example.com/northwind-second-clinic"
        ],
        "hiddenProblem": "Schedules kept in spreadsheets per site."
      }
    ],
    "persona": {
      "painPoint": "Planning shifts across two clinics by hand.",
      "currentSolution": "Spreadsheets per site.",
      "risks": "Gaps in cover as the team grows.",
      "differentiation": "One schedule for every site.",
      "benefits": "Hours back each week for the operations team."
    }
  },
  "sequence": [
    {
      "channel": "email",
      "step": 1,
      "subject": "Two clinics, one schedule",
      "body": "Hi Camille, congratulations on opening the second clinic. …"
    },
    {
      "channel": "linkedin",
      "step": 2,
      "subject": null,
      "body": "Hi Camille, I wrote to you about scheduling across your two sites. …"
    }
  ]
}

Errors

An error answers with its HTTP status and a body { "error": code, "message": sentence }: branch on the code, show or log the message.

StatusCodeWhat to do
400invalid_bodyaction is not research or delete, or leads is empty or holds more than 50.
402no_creditsThe balance does not cover the batch; the answer says needed and balance.
404agent_not_foundNo agent with this id in your account.
409not_researchedInsights and sequences need a researched lead: research it first.
409researchingThe lead's research or writing is under way: try again once it is done.
400insight_too_longAn insight holds 5,000 characters at most.
400too_many_insightsA lead holds five insights of yours at most.
401missing_api_keyNo key in the request: send Authorization: Bearer coolie_sk_… or an x-api-key header.
401invalid_api_keyNot a Coolie key, or a key that is unknown or revoked: copy it again from Settings → API keys.
429rate_limitedMore than 60 requests in the minute for your account: wait the seconds Retry-After gives, then try again.

Frequently asked questions

How much does researching a lead cost?

1 credit a lead researched, charged when its research is saved. Adding your own insights and writing the messages again are free.

How long does the research take?

A minute or two a lead, in the background: the call answers at once with how many leads it queued.

Which leads can be researched?

Leads with a contact and no research yet (status enriched), and researched leads whose messages are missing, which are written again for free. The others are counted in skipped, at no charge.

Does researching a lead send anything?

No. It writes the messages; nothing goes out until the lead is in a campaign you launch.

Can I get the result by webhook?

Yes: subscribe a webhook to lead.researched. The event carries the lead with its research and its sequence.

Every endpoint, field and error is in the API reference, and more guides are in the developer hub.

Your next conversation is already happening somewhere.

Set up an agent in five minutes. The first leads arrive with their reason to write.