Data API
Every market reading your agents take (companies, people, search terms, rankings, links, AI answers, ads) as 25 calls on one key, priced per result.
- Calls
- 25
- Noun groups
- 7
- Shared types
- 9
How many US property managers with 20 to 200 staff are there? Spend nothing.
curl -X POST "https: //api.sb/ leaseline.app/ organizations/ count? mode= try" \
-H "Authorization: Bearer $SB_TOKEN" \
-H "Content-Type: application/ json" \
-d '{"where": {"industries": ["property management"], "headcount": {"min": 20, "max": 200}, "countries": ["US"]}, "maxCost": 0}'
npx startup-builder do Organization.count --try --data '{"where": {"industries": ["property management"], "headcount": {"min": 20, "max": 200}, "countries": ["US"]}, "maxCost": 0}'
{
"name": "do",
"arguments": {
"mode": "try",
"code": "return await $.Organization.count({ where: { industries: ['property management'], headcount: { min: 20, max: 200 }, countries: ['US'] }, maxCost: 0 })"
}
}
import { $ } from 'startup-builder'
const result = await $.try.Organization.count({ where: { industries: ['property management'], headcount: { min: 20, max: 200 }, countries: ['US'] }, maxCost: 0 })
{
"count": 4180,
"ignored": [],
"observedAt": "2026-10-27T14: 02: 11Z",
"method": "estimated"
}
A Data Verb reads the market and returns a reading. A Software Verb changes your records and emits an Event.
The overview, in a founder's words: Data: every reading names when, how and what it cost.
A first pass costs about $3
Size the market, list the companies that fit with the quote that shows why, find the person at each and check their address, measure 500 search terms, read 30 results pages, ask four answer engines 20 buying questions, and diff three rivals’ links. Each line is priced from its call’s own record, so the total moves when a price does. One live run found and checked 50 heads of operations at US property managers for 29¢.
| Step | Call | Cost |
|---|---|---|
| How many property managers fit | Organization.count | Free |
| 100 that fit, each with the quote that shows why | Organization.find | About 20¢ |
| What happened lately at each of them | Organization.enrich | About $1.00 |
| The person to write to at each | Prospect.find | About 5¢ |
| 100 work emails, found and checked | Prospect.verify | About 60¢ |
| 500 search terms measured | Keyword.measure | About 90¢ |
| 30 results pages read | Keyword.search | About 0.9¢ |
| 20 buying questions on 4 answer engines | Keyword.ask | About 32¢ |
| The sites linking to 3 rivals and not to you | Domain.findLinks | About 12¢ |
| In all | About $3.20 |
Each call answers one question a founder asks
One call returns one shape, so each has one price unit, one schema and one MCP tool an agent can call without reading the docs twice. The success rate is the measured share of calls that returned an answer, with its sample; a documented Workflow’s is the product of its steps’.
The word you searched for is one of these calls
| You searched for | The call |
|---|---|
| Company search, account list | Organization.find |
| Total addressable market, market size | Organization.count |
| Lookalike companies | Organization.find |
| Company enrichment, firmographics | Organization.enrich |
| Technographics, what software a company uses | Organization.enrich |
| Buying triggers, funding news, hiring news | Organization.enrich |
| Website visitor identification, reverse IP | Organization.resolve |
| People search, contact search | Prospect.find |
| Email finder | ContactPoint.findEmail |
| Email verification | ContactPoint.verifyEmail |
| Catch-all verification | ContactPoint.verifyEmail |
| Phone finder, direct dials | ContactPoint.findPhone |
| Contact enrichment | Contact.enrich |
| Keyword research, keyword ideas | Keyword.find |
| Competitor keywords | Keyword.find |
| Competitor PPC keywords | Keyword.find |
| Search volume, CPC | Keyword.measure |
| Keyword difficulty | Keyword.measure |
| PPC forecast, Keyword Planner forecast | Keyword.forecast |
| SERP, search results | Keyword.search |
| Social listening, brand mentions | Keyword.search |
| AEO rankings, AI visibility, GEO | Keyword.ask |
| Domain authority | Domain.benchmark |
| Competitor analysis | Domain.findCompetitors |
| Backlinks | Domain.findLinks |
| Link gap, link intersect | Domain.findLinks |
| Index status, indexing check | Domain.verify |
| SEO audit of one page | Page.audit |
| Site audit, site crawl | Site.audit |
| Ad library, competitor ads | Ad.find |
| Rank tracking | Monitor.create |
Every data call keeps the same contract
Every row says where it came from. Each row carries observedAt, method (sampled, estimated or reported) and, where it has them, venue and locale. The Observed type is the same on every call.
A blank is unknown, never zero. Long-tail terms often have no volume reading; that reads searches:. A 0 means the source measured zero.
You pay per result returned. Never per row asked for, and never a one-row minimum on an empty answer. Counting is free. A miss is free wherever a route allows it: an email or phone find that finds nothing, and a check that ends catch-all or unknown.
Every call has a cap. maxCost defaults per call. A call that would pass it is refused before it spends, or stops early and returns what it has. Every response carries cost: what it spent, the most it could have, the units billed and the misses.
Falling back is the platform’s job. On a 429, a 5xx or a timeout the next route answers, and you pay for one. A 4xx is never retried elsewhere: the input is wrong, so the call says so.
A filter is applied or listed. A filter no route can apply comes back in ignored, never silently dropped. Send strict: to be refused instead, at $0. Filter coverage shows how often each one applies.
Readings are reused. A reading younger than maxAge returns at $0. Market readings (volume, results pages, authority, links) are shared across startups for the same subject and locale; readings about people never are.
Every call emits one Event. Its data holds the rows, so a Trigger can fire on it and Metric.query can read it back. In try mode the read runs and returns real rows, and nothing is saved or billed.
{
"mode": "try",
"stored": false,
"api": "https://api.sb/leaseline.app" ,
"action": { "$id": "https://api.sb/leaseline.app/actions/action_KO1jlJbtig" , "operation": "Organization.find", "status": "done" },
"organizations": [ "…the call's own value, documented on its page" ],
"links": { "next": "https://api.sb/leaseline.app/organizations/find?after=org_9Qe2" }
}Ten calls a second per key, and an array counts as one
| Behavior | Rule |
|---|---|
| Rate | 10 calls a second and 600 a minute, per key, per call. Over it, a 429 with Retry-After. |
| Batch | An array input (keywords, hostnames, addresses) counts as one call, up to the call’s stated maximum, under one maxCost and one cost. |
| Pages | A list returns after and total when the source knows it. limit defaults to 25 and tops out at 100 a page unless the call says otherwise. |
| Async | A call whose median run is over 10 seconds, such as an AI answer or a site crawl, returns a pending Action in send mode; its Event delivers the result. |
| Idempotency | The same Idempotency-Key with the same input replays the first answer at $0; with other input it returns 409. |
When a call says no, it names the fix and the next call
Every refusal is the hub’s RFC 9457 problem, served as application/: type, title, status, detail, fix, next (the call that clears it, with its input) and instance. Nothing is spent on a refusal. A miss is not a refusal: it returns 200 with the field null.
{
"type": "https://startupbuilder.dev/errors/max-cost" ,
"title": "The call would spend more than its cap",
"status": 422,
"detail": "100 rows from a sentence cost about $0.20; maxCost is $0.05." ,
"fix": "Lower limit, or raise maxCost to the price the detail quotes." ,
"next": { "verb": "Organization.find", "input": { "question": "US property managers with 20 to 400 units who still track rent in spreadsheets", "limit": 25, "maxCost": 0.05 } },
"instance": "https://api.sb/leaseline.app/actions/action_Pq7wD2"
}| Code | Status | Refuses when | Fix | Then call |
|---|---|---|---|---|
filter-unsupported | 422 | a call sent with strict: and a filter no route can apply | Drop that filter, or send without strict and read what comes back in ignored. | Organization.count |
input-missing | 422 | a data call given none of the input sets that name its subject, such as a person’s name without their company’s domain | Send one of the input sets the call’s Inputs table lists; nothing was spent. | Organization.resolve |
limit-exceeded | 422 | any data call once the startup’s monthly usage Limit for data is spent | Raise the Limit once an Approval allows it, or wait for the month to reset. | Limit.raise |
max-cost | 422 | a data call whose price for what you asked is above its maxCost, checked before anything is spent | Lower limit, or raise maxCost to the price the refusal’s detail quotes. | Organization.find |
rate-limited | 429 | more than 10 calls a second, or 600 a minute, to one call on one key | Wait the Retry-After seconds and retry; an array input counts as one call. | Action.retry |
sources-busy | 503 | a call whose every route failed or is out of capacity; nothing is spent | Retry after retryAfter with the same Idempotency-Key, or send it and watch for the Event. | Action.retry |
venue-unsupported | 422 | a search engine, answer engine or network the call does not read, such as ChatGPT sent to Keyword.measure | Pick one the call’s venue input lists; AI answers are Keyword.ask, posts are Keyword.search. | Keyword.ask |
Test values answer the same way every time, for free
A sk_test_ key answers these subjects the same way on every call, at $0, so an agent’s branches can be tested before a live key spends anything. Each call’s page repeats its own rows under Example.
| Test value | Call | Always returns | What it is for |
|---|---|---|---|
headline: |
Ad.create |
Refuses: venue-rejected |
The test ad account rejects it at review with the reason Misleading claims, passed through in detail, so your agent’s rewrite path runs. |
query: |
Audience.create |
Refuses: audience-guard |
One of its three test members sits on a Suppression, so nothing uploads until that member leaves the query. |
venue: |
Campaign.create |
{"state": |
Opens a Campaign on a test ad account that never serves or spends, and returns the same campaign id every time. |
budget: |
Campaign.create |
Refuses: over-mandate |
A $999,999 budget is over the Limit of every test standing approval, so the call refuses before a Campaign or a Bill exists. |
campaign_test_linkedin |
Campaign.update |
Refuses: over-mandate |
A raise to $999,999 is over the Limit of every test standing approval, so it refuses and the test Campaign keeps its budget. |
metric: |
Metric.query |
{"rows": |
One a day for seven days, every time, so a readout or a stop rule can be tested against a known line. |
lead_test_strong |
Lead.qualify |
{"fit": |
A strong-fit test lead opens the same $1,788 deal every time, so your pipeline steps run against a known value. |
limit: |
Mandate.authorize |
{"status": |
A test authorizer holds a $5,000 spend Permission, so a wider Limit comes back narrowed to it: a standing approval never widens a Permission. |
int_test_needs_reauth |
Integration.call |
Refuses: needs-reauth |
Every call through it answers as if the account had ended the sign-in, so your reconnect path runs. |
int_test_x |
Integration.call |
Refuses: grant-missing |
It holds only the post-and-reply grant, so a call to change follows refuses before it leaves the platform. |
int_test_revoked |
Handle.connect |
Refuses: account-not-connected |
The Integration reads revoked, so the bind refuses before any call leaves the platform. |
venue: |
Integration.connect |
{"state": |
Skips the approval page and returns a connected test Integration that reaches no real mailbox, so the calls after it run end to end. |
audience_test_waitlist |
Message.broadcast |
{"counts": |
Four test Contacts, one of them on a Suppression, so your agent sees the skip without sending to anyone. |
audience_test_spam_rate |
Message.broadcast |
Refuses: spam-brake |
Sends as if the Domain’s complaint rate were over the brake, so the broadcast refuses and nothing goes out. |
msg_test_unwarmed |
Message.schedule |
Refuses: mailbox-warm |
Its Mailbox is on day 6 of a 14-day warm-up, so the first cold email holds until the warm-up ends. |
contact_test_busy |
Meeting.book |
Refuses: availability |
Its test calendar is busy every weekday from 2:00 to 3:00 p.m. Central, so a booking then refuses and names the next free slot. |
contact_test_active |
Contact.enroll |
{"status": |
Enrolls and renders the first step without sending, so your agent can read the run it starts. |
contact_test_suppressed |
Contact.enroll |
Refuses: suppressed-contact |
The address sits on a Suppression, so the enroll refuses before any step is planned. |
contact_test_no_consent |
Contact.enroll |
Refuses: no-consent |
The Contact gave no Consent for the Sequence’s purpose, so the enroll refuses. |
post_test_published |
Post.publish |
{"status": |
Publishes to a test account and returns the same network id every time, so your agent’s follow-up steps run. |
post_test_unresolved |
Post.publish |
Refuses: claims-resolve |
Its one claim names no live record, so the publish refuses before anything reaches the network. |
post_test_removed |
Post.publish |
Refuses: venue-rejected |
The network takes the Post down as it goes live, and its reason comes back in detail, so your removal path runs. |
post_test_launch |
Post.schedule |
{"status": |
Queues on a test account and never publishes, so a launch-day Workflow runs end to end before the real one. |
US property managers with 20 to 200 staff |
Organization.count |
{"count": |
The market Leaseline sells to counts 4,180 companies, every time, for free. |
harborview-pm.com |
Organization.enrich |
{"name": |
Enriches to the same Harborview profile every time: 34 staff, founded 2009, in Washington. |
dana@harborview-pm.com |
ContactPoint.verifyEmail |
{"result": |
Always deliverable: a person’s own mailbox that accepts mail. |
leasing@crestline-residential.com |
ContactPoint.verifyEmail |
{"result": |
Always catch-all: the domain accepts every address, so the check cannot confirm this one, and it costs nothing. |
former.manager@harborview-pm.com |
ContactPoint.verifyEmail |
{"result": |
Always invalid: the mailbox does not exist, as when someone has left the company. |
office@quietpine-rentals.test |
ContactPoint.verifyEmail |
{"result": |
Always unknown: the mail server never answered, so check again with depth deep; it costs nothing. |
rent ledger template |
Keyword.measure |
{"term": |
1,900 searches a month, $2.40 a click and difficulty 18, every time. |
maxCost: |
Any priced call | Refuses: max-cost |
Any priced call with a maxCost of 0.0001 is refused before it spends: the price is above the cap. |
rate-limited.test |
Any priced call | Refuses: rate-limited |
Any call naming this hostname answers 429 with Retry-After, so your retry logic runs. |
sources-busy.test |
Any priced call | Refuses: sources-busy |
Any call naming this hostname answers 503 as if every route were busy, and nothing is spent. |
Read what you measured with one query
Answer share and rank curves are not calls of their own. They are Metric.query over the Events these calls emit: a week of Keyword.asked split by site is your answer share on each engine, and a month of Keyword.searched for one term is its rank curve.
import { $ } from 'startup-builder'
const share = await $.Metric.query({ event: 'Keyword.asked', shape: 'trend', breakdown: 'venue', window: '8w' })The calls compose into Workflows, agent tasks and Services
Each Workflow, agent task and Service names its steps as these calls, so its price is the sum of the prices printed here. The accounts your agents act through are Integrations, and the shared Types say what every field means.