Hand this to your agent
Give your own agent this link and a key, and it can run outreach here without you driving it. It says what it wants: a trade, a town, who you are after. MastroSDR does the work, finds the businesses, writes to each one, and queues the drafts.
Your agent is told where MastroSDR looked and what it left out, so it can explain the answer instead of handing you a list to take on trust.
The rules hold whoever is calling. Every first email waits for you, nothing is sent to anyone who opted out, and an agent cannot grant itself the right to send.
Hand this to an agent
The link stays current as this changes. A pasted copy is a snapshot, so prefer the link unless your agent cannot fetch a URL.
# MastroSDR, for agents
You are working with MastroSDR, a cold outreach platform. You have been given an
API key. Everything below is what you need; you do not need the dashboard.
Base URL: https://mastrosdr.com/api/v1
Auth: send `Authorization: Bearer <your key>` on every request.
Spec: https://mastrosdr.com/api/v1/openapi.json
## The one rule that matters
Sending is irreversible and lands in a real person's inbox, signed by your
operator. Everything else here can be undone; that cannot.
- `messages:send` is a SEPARATE permission from `messages:write`, and is never
implied by it. If your key does not have it, you may draft but a human
approves every send. That is normal and intended.
- The do-not-contact list is checked before every send and cannot be undone
through this API by design. Do not try to route around it.
- Never invent an email address. A guessed address that bounces damages the
sending domain permanently.
## Step 1: find out what is blocking you
GET /health
Returns `status` (`ready` or `needs_setup`), `canGenerate`, `canSend`, a
`blockers` array, and remaining budget. Call this first, and call it again
whenever something fails. Each blocker has an `id`, a `title`, and
`actionable` (false means it is waiting on an earlier step).
## Step 2: finish setup, if you can
Connect the AI provider your operator wants to use:
POST /workspace
{ "integration": "deepseek", "apiKey": "<their provider key>" }
Valid integrations: `deepseek`, `anthropic`, `openai`, `openrouter`, `groq`,
and `resend` for sending. The key is verified against the provider and the
response tells you whether it worked.
Set what you actually sell. Generation refuses until this is replaced, because
the default is a sample:
PATCH /workspace
{ "pitchText": "...", "fromName": "...", "fromEmail": "you@yourdomain.com" }
`fromEmail` must be on a domain already verified in the operator's Resend
account, and a `postalAddress` is legally required before anything sends.
## Step 3: add prospects
POST /prospects
{ "prospects": [ { "companyName": "Acme Ltd", "email": "jane@acme.com" } ] }
Read the `summary` in the response. It reports rows dropped as duplicates,
suppressed or invalid, and the counts will not match what you sent. That is
the API telling you something, not an error.
To page through what exists:
GET /prospects?limit=200&cursor=<nextCursor>
Keep calling while `nextCursor` is not null. If you stop early you have seen
part of the list, not all of it.
### Finding businesses rather than bringing your own
POST /prospects/discover
{ "niche": "barbershops", "location": "Austin, TX" }
Three fields in the response exist so you can explain your own answer, and you
should read all three.
`searched` is every phrasing actually run, in order. It is empty when the
answer came from the shared index, which means the call cost nothing. It has
more than one entry when the plain phrasing was used up and the search went
looking in a district or a neighbouring town. Those businesses are real and
they are not where the caller asked, so say so.
`exhausted` is true only when there is genuinely nothing left. One search
reaches about sixty businesses however many exist, so an empty page is
usually a spent phrasing rather than an empty town. Call again before
concluding a place has been used up.
`existing` and `suppressed` are marked per business, before you import
anything. Filter on them rather than importing and reading the summary
afterwards.
Calling this again returns the NEXT businesses, not the same ones. Whatever
the workspace already holds is excluded, so a second call is worth making and
a third usually is too.
## Step 4: draft
POST /messages/generate
{ "prospectId": "..." }
One draft per prospect per step. Costs a generation from the monthly
allowance, so check `plan.remaining` in `GET /analytics` before a big batch.
## Step 5: send
POST /messages/{id}/send
Requires `messages:send`. Refused, correctly, if the address is suppressed,
the prospect replied, the campaign is paused, the daily cap is reached, or no
postal address is set.
## Channels: this product delivers email and nothing else
A campaign step carries a `channel`: `email` (the default), `linkedin`,
`x` or `instagram`. Omit it and you get email, so cadences written before
channels existed behave identically.
MastroSDR writes every touch and sends only the email ones. LinkedIn, X and
Instagram have no way for a tool to send on a customer's behalf, and the
libraries that appear to are driving a logged-in browser session against the
terms of service, which gets the operator's account restricted. So those
messages are drafted and wait for a person to send them by hand.
What that means for you:
- A non-email step is never `scheduled` and is never dispatched. It stays
`draft` until a human marks it sent. Do not wait on it and do not retry it.
- `POST /messages/{id}/send` is email only. It will refuse anything else.
- A prospect with no destination for a step's channel is skipped for that step
rather than downgraded to email. Populate `linkedinUrl`, `xHandle` or
`instagramHandle` if you want those touches to happen.
- Social messages are written for the medium: no greeting, no sign-off, no
links, and far shorter. The `subject` on one is an internal label the
recipient never sees.
## Capacity: it comes from mailboxes, not from how many of you there are
Running more agents does not raise how much can be sent. The daily ceiling is
the sum of what the connected mailboxes can carry, each with its own warmup
ramp, floored by the operator's own setting.
`GET /analytics` reports the remaining allowance. When it is zero, stop: a
send will return `rate_limited` with `Retry-After` in hours. The honest fix
is another sending mailbox and domain, which only a human can connect, not
more concurrency from you.
Every draft records which key made it, so your work is attributable and
separable from every other agent's.
## Errors: branch on the code, never on the message
Every error is `{ "error": { "code", "message", "docs" } }`. The codes are
stable:
- `unauthorized`: key missing, invalid, revoked or expired. Stop.
- `forbidden`: key lacks the scope in `required_scope`. Stop and ask.
- `invalid_request`: the body is wrong and the message names the field. Fix
and retry.
- `not_found`: no such record in this workspace.
- `conflict`: the current state contradicts the action. Retrying the same
call will not help. Change something first.
- `quota_exceeded`: plan limit. Stop; a human must upgrade.
- `rate_limited`: back off for `Retry-After` SECONDS. Read the header: on a
send this can mean the daily cap, which is hours, not the per-minute limit.
- `provider_error`: the upstream AI or email provider failed. Usually worth
one retry.
## Things that will trip you up
- Quotas are monthly and enforced server-side. `GET /analytics` before a batch.
- Campaign step limits differ by plan: 1 on Free, 8 on Pro, 20 on Scale.
- The daily send cap defaults to 200 across all plans, separately from the
monthly allowance.
- Team management, billing and the audit log are deliberately NOT in this API.
An agent should not be able to invite people or change the plan.
## MCP
There is an MCP server at https://mastrosdr.com/api/mcp using the same key,
if your host speaks it. Tools are filtered to your key's scopes, so a tool you
cannot see is one you were not granted.