My Leads
Read the research brief, research companies with your own model, and push rich leads the team can work, assign, convert and export.
My Leads is a lead database with an AI research option. A lead is a company record with structured research sections, a fit score, a status, an owner, tags, notes and a full status history. In the dashboard a member presses Find leads and SoftSolz researches the companies itself. Through the public API an external model does the research: an assistant connected through the hosted MCP server, or your own server holding an sk_*key, reads the workspace's ideal-customer brief, researches companies with its own model and its own web search, and pushes the results as leads. SoftSolz stores them, catches duplicates, tracks status, owner and history, hands them to the CRM and Outreach, and exports them. Base path:
https://app.softsolz.uk/api/v1/services/my-leadsNo endpoint spends SoftSolz AI credits
Every call on this API is free of SoftSolz AI credits, without exception. Reading the brief, adding leads one at a time or in bulk, changing status, tags, owner and scores, writing notes, converting and exporting are all done without a SoftSolz model. The research is yours, with your own model and your own credits. The one thing that does spend SoftSolz credits is the Find leads research in the dashboard, and it cannot be started through the API. Every lead an app adds is labelled in the dashboard as Added by <app> (or Added via API with the key name), so the team can always tell who researched what.
Connecting Claude, ChatGPT, Gemini or Cursor takes one address and no code; see Connect an AI assistant. Everything below applies equally to a connected assistant (each endpoint is one tool) and to a server calling the REST API with a key.
The handoff
- Read
GET /settings. It returns the workspace's ideal-customer profile as a plain-language brief, field-by-field guidance on what to put in a lead, the valid statuses and the section keys. - Research companies with your own model and web search, from each company's own website first and public sources second. Score fit against the brief and keep every source URL.
- Save what you found with
POST /leads/bulk. The default merges into any lead that already exists, so researching a company again refreshes it instead of duplicating it. - Work the leads: move the status with a reason, tag them, give them an owner, adjust the scores, add notes.
- Hand a qualified lead to the CRM as a company, contact and deal, put it in an Outreach contact book, or export the list as CSV.
Worked flow: research and push leads
1. Read the brief
curl https://app.softsolz.uk/api/v1/services/my-leads/settings \ -H "Authorization: Bearer sk_live_your_key"{ "settings": { "company_name": "Northwind Software", "company_website": "https://northwind.example", "offering": "Bookkeeping software for independent retailers", "ideal_customer": "Independent retailers with 5 to 50 staff and more than one shop", "target_industries": "Retail, Hospitality", "avoid_industries": "Gambling", "switching_targets": "Sage, Excel", "default_countries": "United Kingdom", "default_count": 10, "default_deep_verify": false, "kb_sections": ["overview", "contact", "insights"] }, "icp": { "company_name": "Northwind Software", "company_website": "https://northwind.example", "offering": "Bookkeeping software for independent retailers", "ideal_customer": "Independent retailers with 5 to 50 staff and more than one shop", "target_industries": ["Retail", "Hospitality"], "avoid_industries": ["Gambling"], "switching_targets": ["Sage", "Excel"], "default_countries": ["United Kingdom"], "default_count": 10 }, "brief": "We are Northwind Software and we offer: Bookkeeping software for independent retailers. Our ideal customer: Independent retailers with 5 to 50 staff and more than one shop. Prefer these industries: Retail, Hospitality. Avoid: Gambling. Companies using Sage, Excel are strong switch candidates. Default markets: United Kingdom. Research each company from its own website first, then public sources. Score fit 0-100 against this profile, keep contact details you could verify on the official site, and cite every source URL. Save results with POST /leads/bulk using on_duplicate \"merge\" so re-research refreshes a company instead of duplicating it.", "output_schema_hint": { "name": "Company name (required unless website is given).", "website": "Official website URL.", "external_id": "Your own stable id for this company; used first for dedupe.", "fit_score": "Integer 0-100, how well the company matches the brief.", "lead_score": "Integer 0-100, how ready they are to buy now.", "confidence": "Number 0-1, how confident you are in the data.", "verification_tier": "verified, probable or unverified.", "sections": { "description": "One paragraph on what the company does.", "contact": "contact_email, sales_email, support_email, phone, whatsapp, contact_form_url.", "insights": "why_fit, pain_points[], best_angle, first_outreach_message, expected_objections[], demo_focus, estimated_deal_size.", "sources": "Array of source URLs you used." } }, "kb_sections": ["overview", "contact", "social", "insights", "fit_breakdown", "verification", "company_size", "technology", "financial", "website_intel", "registry", "activity", "hiring", "reviews", "sources"], "lead_statuses": ["new", "not_called", "called", "contacted", "follow_up", "qualified", "won", "lost"], "section_keys": ["contact", "social", "insights", "fit_breakdown", "analysis", "size_detail", "tech_stack", "financial_indicators", "website_intel", "registry_info", "activity", "hiring", "reviews", "sources", "cities_operating", "description", "employee_estimate", "revenue_estimate_usd", "num_locations", "crm", "erp"]}brief is written for a model: paste it into the system prompt of the session that does the research. output_schema_hint says what belongs in every field and section, and section_keys is the full list the API accepts inside sections. If the workspace has not filled in its profile yet the brief says so; ask the person before researching rather than guessing the fit. PUT /settings updates the profile, and only the keys you send change.
2. Research with your own model
This step happens entirely on your side. Read each company's own website first, then public sources. Score fit_score against the brief and lead_score for readiness to buy, both 0 to 100, and explain the fit in fit_breakdown. Mark verification_tier honestly: only call a lead verified when the contact details came from the official site. Put every page you read in sections.sources; the first one becomes the lead's primary source. Give each company a stable external_id from your own system when you have one, because it is the first thing the duplicate check looks at.
3. Push the leads in one batch
Up to 200 leads per call. Each row has the same shape as the single POST /leads body.
curl -X POST https://app.softsolz.uk/api/v1/services/my-leads/leads/bulk \ -H "Authorization: Bearer sk_live_your_key" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: research-manchester-2026-09-17" \ -d '{ "on_duplicate": "merge", "leads": [ { "external_id": "crm-1001", "name": "Harbour Lane Bakery", "website": "harbourlanebakery.example", "industry": "Food retail", "city": "Manchester", "state": "Greater Manchester", "country": "United Kingdom", "employee_band": "11-50", "revenue_band": "1M-10M", "fit_score": 87, "lead_score": 64, "confidence": 0.8, "verification_tier": "verified", "tags": ["manchester", "multi-site"], "sections": { "description": "Independent bakery chain with four shops across Manchester and Salford and a wholesale arm supplying cafes.", "contact": { "contact_email": "hello@harbourlanebakery.example", "phone": "+44 161 000 0000", "contact_name": "Sam Baker" }, "social": { "linkedin": "https://linkedin.com/company/harbour-lane-bakery" }, "insights": { "why_fit": "Four shops and a wholesale arm, still invoicing from spreadsheets.", "pain_points": ["manual wholesale invoicing", "stock counted by hand"], "best_angle": "One place for shop takings and wholesale invoices.", "expected_objections": ["Busy season, no time to switch"], "demo_focus": "Wholesale invoicing and multi-shop reporting", "estimated_deal_size": "Small", "first_outreach_message": "Hi Sam, congratulations on the Salford shop. We help multi-site bakeries move wholesale invoicing off spreadsheets. Would a ten-minute look be useful?" }, "fit_breakdown": { "industry": { "weight": 0.4, "subscore": 90 }, "size": { "weight": 0.3, "subscore": 85 }, "switching_signal": { "weight": 0.3, "subscore": 80 } }, "analysis": { "what_they_do": "Retail and wholesale bakery", "decision_maker": "Owner" }, "tech_stack": { "cms": "Shopify", "technologies": ["Klaviyo"] }, "activity": { "latest_news": ["Opened a fourth shop in Salford in 2026"] }, "hiring": { "hiring_active": true, "open_roles": ["Shop manager"] }, "num_locations": 4, "cities_operating": ["Manchester", "Salford"], "sources": [ "https://harbourlanebakery.example/about", "https://harbourlanebakery.example/contact", "https://harbourlanebakery.example/wholesale" ] } }, { "external_id": "crm-1002", "name": "Peak District Outfitters", "website": "peakoutfitters.example", "industry": "Outdoor retail", "city": "Sheffield", "country": "United Kingdom", "employee_band": "11-50", "fit_score": 74, "lead_score": 41, "confidence": 0.6, "verification_tier": "probable", "tags": ["sheffield"], "sections": { "description": "Two-shop outdoor clothing retailer with an online store.", "contact": { "contact_form_url": "https://peakoutfitters.example/contact" }, "insights": { "why_fit": "Two shops plus online, growing; the brief prefers retail.", "pain_points": ["reconciling online and in-store sales"] }, "fit_breakdown": { "industry": { "weight": 0.4, "subscore": 80 }, "size": { "weight": 0.3, "subscore": 70 }, "switching_signal": { "weight": 0.3, "subscore": 70 } }, "tech_stack": { "cms": "WooCommerce" }, "num_locations": 2, "sources": ["https://peakoutfitters.example/about"] } } ] }'{ "results": [ { "index": 0, "action": "created", "matched_on": null, "lead": { "id": 42, "run_id": null, "name": "Harbour Lane Bakery", "website": "https://harbourlanebakery.example", "domain": "harbourlanebakery.example", "external_id": "crm-1001", "source": "api", "via": "connected_app", "via_label": "Claude", "fit_score": 87, "lead_score": 64, "confidence": 0.8, "verification_tier": "verified", "verified_contact_count": 2, "lead_status": "new", "industry": "Food retail", "city": "Manchester", "state": "Greater Manchester", "country": "United Kingdom", "employee_band": "11-50", "revenue_band": "1M-10M", "source_url": "https://harbourlanebakery.example/about", "owner_user_id": null, "owner_name": null, "owner_email": null, "tags": ["manchester", "multi-site"], "last_contacted_at": null, "company_id": null, "contact_id": null, "deal_id": null, "result": { "description": "Independent bakery chain ...", "contact": { "...": "..." }, "sources": ["..."] }, "created_at": "2026-09-17T09:30:00.000Z", "updated_at": "2026-09-17T09:30:00.000Z" }, "error": null }, { "index": 1, "action": "merged", "matched_on": "domain", "lead": { "id": 17, "name": "Peak District Outfitters", "domain": "peakoutfitters.example", "...": "..." }, "error": null } ], "counts": { "created": 1, "merged": 1, "skipped": 0, "failed": 0 }}Every row is saved on its own and reported in request order as created, merged, skipped or failed; a failed row carries { code, message } in errorand never stops the others. The second lead above matched a lead the team already had on its domain, so the new research was merged into it. When the batch would cross the free plan's lead cap the whole call answers 402 free_plan_limit_reached and nothing is written.
4. Move a lead and say why
curl -X POST https://app.softsolz.uk/api/v1/services/my-leads/leads/42/status \ -H "Authorization: Bearer sk_live_your_key" \ -H "Content-Type: application/json" \ -d '{"status":"contacted","reason":"Replied to our email, wants a demo next week."}'The response is the updated lead. The reason is kept in the status history with who made the change and through which channel. called, contacted and follow_up also stamp last_contacted_at, and won notifies the owner. An unknown status answers 400 invalid_status.
5. Give it an owner
curl -X POST https://app.softsolz.uk/api/v1/services/my-leads/leads/42/assign \ -H "Authorization: Bearer sk_live_your_key" \ -H "Content-Type: application/json" \ -d '{"owner_email":"priya@northwind.example"}'Send owner_user_id when you know the member id, owner_email when you do not, or neither to unassign. The new owner gets an in-app notification and an email. A person who is not an active member of the workspace answers 400 owner_not_member.
6. Hand it to the CRM
curl -X POST https://app.softsolz.uk/api/v1/services/my-leads/leads/42/convert \ -H "Authorization: Bearer sk_live_your_key" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: convert-lead-42" \ -d '{ "create_deal": true, "deal": { "title": "Harbour Lane Bakery - bookkeeping", "value_cents": 120000, "currency": "GBP", "expected_close_on": "2026-11-30" } }'{ "lead": { "id": 42, "name": "Harbour Lane Bakery", "lead_status": "contacted", "company_id": "6f1c2a1e-1234-4c2b-9f2a-0c8a1e2b3c4d", "contact_id": "9a2b3c4d-1234-4c2b-9f2a-0c8a1e2b3c4d", "deal_id": 318, "...": "..." }, "links": { "company_id": "6f1c2a1e-1234-4c2b-9f2a-0c8a1e2b3c4d", "contact_id": "9a2b3c4d-1234-4c2b-9f2a-0c8a1e2b3c4d", "deal_id": 318 }, "created": { "company": true, "contact": true, "deal": true }}Converting creates a CRM company and contact from the research, or reuses ones that already match on domain and email, and links them back to the lead, so converting twice never duplicates anything. The contact defaults to the researched contact details; send contact with { full_name, email, phone } to override it. A deal is optional: create_deal opens one on the default sales board, or pipeline_id and stage_id pick a specific board and stage. Opening a deal additionally needs the service.pipelines.create scope and answers 403 deal_not_allowed without it; a workspace with no sales board yet answers 409 no_pipeline.
POST /leads/{id}/outreachis the other hand-off: it puts the lead's primary contact into an Outreach contact book so a campaign can reach them. Send contact_book_id, or contact_book_name to reuse or create a book by name. The response reports how many people were added, how many were already in the book and any entry that was refused, for example a phone number without a country code. A lead with neither an email nor a phone answers 400 no_channel, and the Outreach service must be installed in the workspace.
7. List what matters
curl -G https://app.softsolz.uk/api/v1/services/my-leads/leads \ -H "Authorization: Bearer sk_live_your_key" \ -d status=qualified,contacted \ -d min_fit_score=70 \ -d owner_user_id=unassigned \ -d tags_any=manchester,sheffield \ -d sort=fit \ -d limit=50{ "data": [ { "id": 42, "name": "Harbour Lane Bakery", "fit_score": 87, "lead_status": "contacted", "owner_user_id": null, "tags": ["manchester", "multi-site"], "...": "..." } ], "meta": { "limit": 50, "offset": 0, "total": 1, "has_more": false }}Every filter is optional and they combine: status and source take one or more comma-separated values, owner_user_id takes a member id or the word unassigned, tags_any and tags_all match on tags, min_fit_score and min_lead_score set a floor, industry, city, state and country match case-insensitively, technology finds a word anywhere in the stored research (a CRM name, for instance), search looks in the name, domain and industry, run_id limits to one research run, converted splits leads already in the CRM from the rest, and updated_since or created_since take an ISO timestamp. Sort with fit (the default), score, recent, updated or name. To keep another system in sync, poll with updated_since set to the time of your last read.
8. Export
curl "https://app.softsolz.uk/api/v1/services/my-leads/leads/export.csv?status=qualified&min_fit_score=70" \ -H "Authorization: Bearer sk_live_your_key" \ -o leads.csvThe same filters as the list, returned as a CSV download with one row per lead: id, name, website, domain, industry, city, state, country, employee band, revenue band, fit score, lead score, verified contacts, status, owner, tags, source, who added it, primary contact email and phone, last contacted, created and updated.
The lead object
GET /leads/{id} returns the whole record. The flat fields are the ones you filter and sort on: name, website and domain (the bare domain, lower case), external_id, industry, city, state, country, employee_band, revenue_band, fit_score, lead_score, confidence, verification_tier, verified_contact_count, lead_status, owner_user_id with owner_name and owner_email, tags, last_contacted_at, the CRM links company_id, contact_id and deal_id, and run_id when a SoftSolz research run produced it. source says how the lead got here (research, api, import or manual) and via with via_label say which channel and which app or key last wrote it. The research itself lives in result, grouped by section.
PATCH /leads/{id} changes any field from the create body and only touches the keys you send. Sections merge key by key into what is already stored, so you can add one section without resending the rest. Send expected_updated_at from your last read and a stale write is refused with 409 STALE_VERSION. The status, tags, assign and score endpoints exist for those specific changes because each of them does something extra: history, notifications, or normalising the tags.
Sections
Every key is optional. Use the same keys on create and update. The dashboard renders each section on the lead's Overview tab, and the Knowledge Hub export uses the ones the workspace picked.
| Key | Holds |
|---|---|
description | One paragraph on what the company does. |
contact | contact_email, sales_email, support_email, phone, whatsapp, contact_form_url, contact_name. |
social | Profile URLs: linkedin, facebook, instagram, x_twitter, youtube, tiktok, github. |
insights | Sales insights: why_fit, pain_points[], best_angle, first_outreach_message, expected_objections[], demo_focus, estimated_deal_size. |
fit_breakdown | A map of factor name to { weight, subscore } explaining fit_score. |
analysis | what_they_do, business_model, revenue_model, decision_maker, growth_signal, switch_barrier. |
size_detail | Extra size notes such as years_in_business. |
tech_stack | cms, hosting, analytics, crm, erp, payment_gateways[], technologies[]. |
financial_indicators | Funding, revenue and growth signals as key to value. |
website_intel | website_quality, mobile_friendly, has_pricing_page, has_careers_page, has_blog. |
registry_info | legal_name, registration_number, legal_entity_type, incorporation_date. |
activity | latest_news[], expansion, partnerships[], awards[]. |
hiring | hiring_active, open_roles[]. |
reviews | google_rating, google_review_count, trustpilot_rating, review_summary. |
sources | The URLs you read. The first one becomes the lead's primary source. |
employee_estimate, revenue_estimate_usd, num_locations | Integers: headcount, annual revenue in USD, number of locations. |
cities_operating | The cities the company serves. |
crm, erp | The software in use, if known. |
Duplicates
Before a lead is written, SoftSolz looks for an existing lead in this order: the same external_id, then the same website domain, then any contact email in the sections. The first match wins, and the response says which one in matched_on. What happens next is up to on_duplicate:
| Value | Effect |
|---|---|
merge | Fill blank fields, merge the sections key by key and union the tags. Existing values are kept. The default for POST /leads/bulk, so re-researching a company refreshes it. |
error | Answer 409 duplicate_lead with existing_id, so you can read and PATCH that lead instead. The default for POST /leads. In a bulk call the row is reported as failed and the rest go through. |
skip | Leave the existing lead untouched and report the row as skipped. |
create | Write a second record anyway. Use it only when two records are genuinely different companies. |
POST /leads answers 201 when it created the lead and 200 when it merged into or skipped an existing one. Both single and bulk creates accept an Idempotency-Key, which is the right way to make a retry safe.
Notes and history
POST /leads/{id}/notes writes a free-text note of up to 8000 characters, for example a call summary or the reasoning behind a score, and GET /leads/{id}/notes lists them newest first. Each note shows who wrote it: a member, or your app or key by name.
GET /leads/{id}/historyis the status trail: every change with the previous and new status, who made it, when, through which channel, and the reason given. The first row is the creation. The dashboard shows the same trail on the lead's History tab.
{ "id": 9, "lead_id": 42, "from_status": "new", "to_status": "contacted", "reason": "Replied to our email, wants a demo next week.", "changed_by": 7, "changed_by_name": "Priya", "via": "connected_app", "via_label": "Claude", "changed_at": "2026-09-17T10:05:00.000Z"}Research runs
A research run is a piece of research SoftSolz performed itself from the dashboard, when a member pressed Find leads. The API can read runs and cancel them, and nothing more:
GET /runslists them with their status, how many companies were found and researched, and the SoftSolz credits each one spent. Filter bystatusormode.GET /runs/{id}returns one run with the leads it produced, best fit first.POST /runs/{id}/cancelstops a queued or running run. A queued run ends at once; a running run stops after the company it is on and keeps the leads already saved. A finished run answers409 run_not_cancellable.
Starting a run is deliberately dashboard-only. A run spends the workspace's SoftSolz AI credits, and the whole point of this API is that nothing on it can. So a member with the Start AI research permission starts research in the dashboard, and an app or key researches with its own model and pushes the results. Read the runs before you research a list yourself, so you do not repeat work the team already paid for.
Scopes
| Scope | Allows |
|---|---|
service.my-leads.view | Read leads. List and read leads, notes, status history, research runs and the research brief. |
service.my-leads.create | Add leads. Save leads you researched with your own model, one at a time or in bulk. Never spends SoftSolz AI credits. |
service.my-leads.edit | Update leads. Edit leads, change status, tags, owner and scores, add notes, cancel research runs and update the research brief. |
service.my-leads.delete | Delete leads. Remove leads one at a time or in bulk. |
service.my-leads.export | Export leads. Download leads as a CSV file. |
service.my-leads.convert | Hand off leads. Convert a lead into a CRM company, contact and deal, or add it to an Outreach contact book. |
A connected assistant only ever holds the scopes the member ticked on the consent screen, and a member can only grant scopes their own role already holds. A lead in another workspace answers 404.
Two permissions to know about
Start AI researchis the permission behind the dashboard's Find leads button, the one action that spends SoftSolz AI credits. It is not an API scope and can never be granted to an app or key. Convert leads is the permission behind the CRM and Outreach hand-off; its API scope is service.my-leads.convertabove. Both are set per role by a workspace admin in Members & Access, so an assistant can only convert leads when the member who connected it can.
Events
| Event | When it fires |
|---|---|
my-leads.lead.created | A lead was added, by research, the API or a member. |
my-leads.lead.updated | Lead fields, sections, tags or scores changed. |
my-leads.lead.status_changed | A lead moved to a new status. |
my-leads.lead.assigned | A lead was assigned to a member or unassigned. |
my-leads.lead.converted | A lead was handed to the CRM as a company, contact or deal. |
my-leads.run.completed | A SoftSolz research run finished. |
my-leads.run.failed | A SoftSolz research run failed. |
my-leads.run.cancelled | A SoftSolz research run was cancelled. |
my-leads.task.started | A research task started processing. |
my-leads.task.completed | A research task finished. |
my-leads.task.failed | A research task failed. |
my-leads.task.cancelled | A research task was cancelled. |
{ "lead_id": "42", "name": "Harbour Lane Bakery", "domain": "harbourlanebakery.example", "website": "https://harbourlanebakery.example", "source": "api", "via": "connected_app", "lead_status": "contacted", "fit_score": 87, "owner_user_id": 7, "tags": ["manchester", "multi-site"], "run_id": null, "from_status": "new", "to_status": "contacted", "reason": "Replied to our email, wants a demo next week."}Every lead event carries the same summary (lead_id, name, domain, website, source, channel, status, fit score, owner, tags and run), plus what changed: changed lists the fields on an update, from_status, to_status and reason come with a status change, previous_owner_user_id with an assignment, and company_id, contact_id and deal_id with a conversion. Run events carry the run id, title, mode, status and counts, and a failure adds error_message. Every delivery is signed; follow Webhooks for the signature check before you trust a payload.
Sandbox
An sk_test_*key, or a connected app that chose the Sandbox workspace, acts on the sandbox twin: its own leads, runs and settings, never the live ones. Everything above works there, which makes it the right place to develop a research prompt or an integration. Converting in the sandbox creates the company, contact and deal in the sandbox's own CRM, and the free plan's lead cap applies to each workspace separately.
Next
Every endpoint, parameter, response shape and error code is generated into the API Reference, with samples in cURL, Node, Python and the SDKs. Connect an assistant in a minute with Connect an AI assistant, or read the product walkthrough on docs.softsolz.uk.