Reference application

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:

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.ts
// 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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:

CredentialWhere it livesHow it is handled
InboxPlease sign-in sessionOutreach server, encryptedUsed for sign-in and key management, never as permission to use another user's key
inp_ API keyOutreach server, encryptedEither 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 grantInboxPlease onlyOutreach 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.

send.ts
// 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.

ToolsHow Outreach Please uses them
whoamiConfirms which user, organization and key the app is acting as before work starts
list_mailboxes · connect_mailbox · check_mailbox_connectionMailbox picker; connecting returns an OAuth link a person opens
get_send_quotaRemaining recipient budget, shown before a send
send_email → confirm_sendComposer and campaign sends: stage first, dispatch only after approval
get_send_statusReconciles unknown outcomes and resumes campaigns after a key rotation
get_campaign_capabilities · get_send_engagementChecks campaign support before launch; opens and clicks for reports
get_deliverability_overviewDeliverability panel
list_warmups · pause_warmup · resume_warmupWarmup panel and the Reputation Guard warmup signal
get_domain_healthSender Readiness and the Reputation Guard domain-authentication signal
start_placement_test · get_placement_result · list_placement_testsOptional canary placement test
analyze_email · domain_profileWired into the client; deliberately not used to approve or block a send

What the reference app adds

  • Sender Readiness pre-flight

    Before a send is staged or a campaign launches, the app reads the sender domain's latest stored Domain Health scan. It blocks only on fresh, verified, critical sender-authentication failures; other issues, such as stale scans, unmonitored domains, missing scopes or unknown results, warn and need an explicit acknowledgement. It checks the sender's DNS authentication, not the campaign's content, and does not predict where a message will land.

  • Canary placement test (optional)

    With an explicit confirmation, the app runs one InboxPlease placement test from the connected mailbox and shows inbox, tabs, spam and missing per provider. It sends one real email and uses one test from the plan's monthly allowance. It tests the sender and mailbox with InboxPlease's own test content, not the campaign's exact copy.

  • Reputation Guard circuit breaker

    Before each campaign dispatch, the worker checks InboxPlease signals. By default it pauses the campaign on a fresh, verified sender-authentication failure, while warmup inbox rate and bounce rate run in observe mode and only report what would have paused. Each signal can be switched to enforce. The warmup signal needs observation metadata (sample count and last observation time) from the InboxPlease API; without it the signal reports unknown and never pauses. Unknown, stale or low-sample signals never pause a campaign.

  • Key-rotation-safe resume

    App keys rotate every 30 days. A campaign resumes on the new key only after the app re-verifies the user, organization and mailbox and proves with get_send_status that the new key can read the earlier sends. Every earlier send is reconciled before anything new is dispatched, and an unknown send is never resent.

  • Developer X-Ray

    A live timeline of the REST and MCP calls behind each dashboard action, with scopes, timings, send states and quota windows. Arguments and results are reduced to an allowlist of safe fields, so keys, tokens, subjects and bodies are never recorded, and addresses are kept only in masked form (a***@domain). History is kept in memory per process for up to 30 minutes. Each area also offers copyable @modelcontextprotocol/sdk and claude mcp add snippets with placeholder values.

  • Agent staging bridge

    External agents and lead-enrichment pipelines can stage contacts and one draft campaign with a revocable, staging-only token that is not an inp_ key. The bridge never previews, approves, launches or sends: a person opens the draft, previews it and approves the launch in the app.

What it does not claim

  • Outreach Please is a reference implementation, not a hosted InboxPlease product.
  • MCP access needs an InboxPlease Enterprise plan, and sending needs a key created by an organization owner or admin.
  • "Sent" means the mailbox provider accepted the message. It is not proof of delivery or inbox placement.
  • Sender Readiness checks DNS authentication from the last stored scan. It does not read or score the campaign's content.
  • X-Ray history lives in one server process's memory and is lost on restart.
  • The app runs as one Node.js process with a private data directory. Running several instances needs shared storage and locking first.

Connect your own agent

MCP access is available on Enterprise. Talk to us about connecting your agent or app.