Built with InboxPlease: Outreach Please
By Chris Aylott, Founder
Outreach Please is a reference application for cold outreach: CSV contacts, mail merge templates, scheduled campaigns, follow-ups and engagement reports. It is built only on the public InboxPlease auth REST API and MCP server, with no access to InboxPlease's database or mailbox grants, to show how a real product uses the same tools your agents get.
Connect to the same MCP server
Outreach Please talks to the public MCP endpoint at https://api.inboxplease.com/mcp (MCP over Streamable HTTP), with an inp_ key in an Authorization header. Your own agent can connect to the same server from Claude Code:
# Connect InboxPlease to Claude Code claude mcp add --transport http inboxplease \ https://api.inboxplease.com/mcp \ --header "Authorization: Bearer inp_YOUR_API_KEY"
An app does the same with the official TypeScript SDK. Outreach Please creates the client on its server, so the key is never sent to the browser:
// Server-side only: the inp_ key never reaches the browser.
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const client = new Client({ name: "my-app", version: "1.0.0" });
await client.connect(new StreamableHTTPClientTransport(
new URL("https://api.inboxplease.com/mcp"),
{ requestInit: { headers: { Authorization: "Bearer inp_YOUR_API_KEY" } } },
));MCP access needs an Enterprise plan. Scopes, limits and the full tool list are on InboxPlease for AI agents.
Architecture: secrets stay on the server
Browser → Outreach Please server → InboxPlease REST and MCP.
- 01
Browser
Renders the dashboard and holds an opaque, HttpOnly session cookie. It never receives an InboxPlease token, an inp_ API key or a mailbox grant.
- 02
Outreach Please server (Next.js)
Owns sessions, validates every request, re-checks the organization and the key, and is the only part of the app that calls InboxPlease. Credentials, drafts and campaigns are stored encrypted in SQLite.
- 03
InboxPlease auth REST API
Sign-up, email/password or Google sign-in, and creating a dedicated, scoped inp_ key for the app once the user explicitly authorizes access. A user can instead import an inp_ key they already own.
- 04
InboxPlease MCP server
Everything else: mailboxes, quotas, two-phase sending, send status, Domain Health, warmup and placement tests. Plan, role and quota rules are enforced here, not re-implemented in the app.
The app keeps three credentials apart and never swaps one for another:
| Credential | Where it lives | How it is handled |
|---|---|---|
| InboxPlease sign-in session | Outreach server, encrypted | Used for sign-in and key management, never as permission to use another user's key |
| inp_ API key | Outreach server, encrypted | Either created for the app after explicit consent (expires after 30 days; disconnecting attempts to revoke it) or an existing key the user owns, imported and detached on disconnect |
| Mailbox provider grant | InboxPlease only | Outreach never sees it; a person connects the mailbox through an OAuth link |
Two-phase sending
send_email sends nothing. It validates and stages the email, then returns the exact preview with a single-use send_token that expires after 10 minutes. Only confirm_send dispatches, and Outreach Please calls it only after a person approves that exact preview; editing the draft needs a new preview.
// Your UI: show the exact preview to a person; resolve true only on an explicit yes.
declare function approvedByPerson(preview: unknown): Promise<boolean>;
// Tool results carry the status dict as structuredContent, or as JSON text.
const payload = (result: any) =>
result.structuredContent ?? JSON.parse(result.content[0].text);
async function sendWithApproval() {
// Phase 1: stage. Nothing is sent.
const preview = payload(await client.callTool({
name: "send_email",
arguments: {
from_email: "you@yourcompany.com",
to: "lead@example.com",
subject: "Quick question",
body: "Hi Sam, ...",
},
}));
if (preview.status !== "pending_confirmation") return preview; // refused: nothing staged
const { send_token, send_id } = preview; // single-use token, expires in 600 s
if (!(await approvedByPerson(preview))) return preview; // not approved: nothing is sent
// Phase 2: dispatch, only after approval.
try {
const result = payload(await client.callTool({ name: "confirm_send", arguments: { send_token } }));
// "sent" means the provider accepted it; rate_limited and similar are definite refusals.
if (result.status !== "expired_or_used") return result;
} catch {
// Lost response: the send may or may not have happened.
}
// expired_or_used or a lost response: an earlier attempt may have sent.
// Reconcile by send_id. Never restage or resend automatically.
return payload(await client.callTool({ name: "get_send_status", arguments: { send_id } }));
}The tools it uses
Each dashboard action calls a fixed tool. There is no general-purpose tool gateway.
| Tools | How Outreach Please uses them |
|---|---|
whoami | Confirms which user, organization and key the app is acting as before work starts |
list_mailboxes · connect_mailbox · check_mailbox_connection | Mailbox picker; connecting returns an OAuth link a person opens |
get_send_quota | Remaining recipient budget, shown before a send |
send_email → confirm_send | Composer and campaign sends: stage first, dispatch only after approval |
get_send_status | Reconciles unknown outcomes and resumes campaigns after a key rotation |
get_campaign_capabilities · get_send_engagement | Checks campaign support before launch; opens and clicks for reports |
get_deliverability_overview | Deliverability panel |
list_warmups · pause_warmup · resume_warmup | Warmup panel and the Reputation Guard warmup signal |
get_domain_health | Sender Readiness and the Reputation Guard domain-authentication signal |
start_placement_test · get_placement_result · list_placement_tests | Optional canary placement test |
analyze_email · domain_profile | Wired into the client; deliberately not used to approve or block a send |
What the reference app adds
What it does not claim
Connect your own agent
MCP access is available on Enterprise. Talk to us about connecting your agent or app.