Quickstarts

Six flows, four ways to call them.

curl, Python, TypeScript, and MCP are all in this page. Markdown: /docs/quickstarts.md.

Create an inbox

POST /api/v1/onboard creates an account, a Bearer key, and username@cooperemail.com. The api_key is returned once.

curl
curl -s https://cooperemail.com/api/v1/onboard \
  -H 'content-type: application/json' \
  -d '{"username":"research-bot"}'
Python
import json, urllib.request
req = urllib.request.Request(
    "https://cooperemail.com/api/v1/onboard",
    data=json.dumps({"username": "research-bot"}).encode(),
    headers={"content-type": "application/json"},
    method="POST",
)
with urllib.request.urlopen(req) as res:
    body = json.load(res)
print(body["api_key"], body["inbox"]["email"], body["inbox"]["id"])
TypeScript
const response = await fetch("https://cooperemail.com/api/v1/onboard", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ username: "research-bot" }),
});
const created = await response.json();
const key = created.api_key as string;
const inboxId = created.inbox.id as string;
const email = created.inbox.email as string;
MCP
{
  "tool": "cooper_onboard",
  "arguments": { "username": "research-bot" }
}

Send an email

POST /api/v1/inboxes/{id}/messages sends from the inbox. client_id and Idempotency-Key stop a retry from sending twice.

curl
curl -s https://cooperemail.com/api/v1/inboxes/$INBOX/messages \
  -H "authorization: Bearer $COOP_KEY" \
  -H "content-type: application/json" \
  -H "idempotency-key: send-001" \
  -d '{"to":["ada@example.com"],"subject":"Hello","text":"Tuesday works.","client_id":"send-001"}'
Python
import json, os, urllib.request
req = urllib.request.Request(
    f"https://cooperemail.com/api/v1/inboxes/{os.environ['INBOX']}/messages",
    data=json.dumps({
        "to": ["ada@example.com"],
        "subject": "Hello",
        "text": "Tuesday works.",
        "client_id": "send-001",
    }).encode(),
    headers={
        "authorization": f"Bearer {os.environ['COOP_KEY']}",
        "content-type": "application/json",
        "idempotency-key": "send-001",
    },
    method="POST",
)
with urllib.request.urlopen(req) as res:
    print(json.load(res)["id"])
TypeScript
await fetch(`https://cooperemail.com/api/v1/inboxes/${inboxId}/messages`, {
  method: "POST",
  headers: {
    authorization: `Bearer ${key}`,
    "content-type": "application/json",
    "idempotency-key": "send-001",
  },
  body: JSON.stringify({
    to: ["ada@example.com"],
    subject: "Hello",
    text: "Tuesday works.",
    client_id: "send-001",
  }),
});
MCP
{
  "tool": "cooper_send_message",
  "arguments": {
    "inbox_id": "research-bot",
    "to": ["ada@example.com"],
    "subject": "Hello",
    "text": "Tuesday works.",
    "client_id": "send-001",
    "idempotency_key": "send-001"
  }
}

Wait for a verification code

GET /api/v1/inboxes/{id}/wait-for-code holds up to 55 seconds and returns the code and message id. The body is not returned.

curl
curl -s "https://cooperemail.com/api/v1/inboxes/$INBOX/wait-for-code?wait=25" \
  -H "authorization: Bearer $COOP_KEY"
Python
import json, os, urllib.request
url = f"https://cooperemail.com/api/v1/inboxes/{os.environ['INBOX']}/wait-for-code?wait=25"
req = urllib.request.Request(url, headers={"authorization": f"Bearer {os.environ['COOP_KEY']}"})
with urllib.request.urlopen(req) as res:
    body = json.load(res)
print(body["code"], body["message"]["id"])
TypeScript
const waiting = await fetch(
  `https://cooperemail.com/api/v1/inboxes/${inboxId}/wait-for-code?wait=25`,
  { headers: { authorization: `Bearer ${key}` } },
);
const code = await waiting.json();
console.log(code.code, code.message.id);
MCP
{
  "tool": "cooper_wait_for_code",
  "arguments": { "inbox_id": "research-bot", "wait": 25 }
}

Register a webhook

POST /api/v1/webhooks registers an HTTPS URL. Verify x-cooper-signature-v2 over the timestamp, a dot, and the raw body, and dedupe on x-cooper-delivery.

curl
curl -s https://cooperemail.com/api/v1/webhooks \
  -H "authorization: Bearer $COOP_KEY" \
  -H "content-type: application/json" \
  -d '{"url":"https://example.com/hooks/cooper","events":["message.received"],"client_id":"hook-1"}'
Python
import hashlib, hmac, json, os, urllib.request
req = urllib.request.Request(
    "https://cooperemail.com/api/v1/webhooks",
    data=json.dumps({
        "url": "https://example.com/hooks/cooper",
        "events": ["message.received"],
        "client_id": "hook-1",
    }).encode(),
    headers={
        "authorization": f"Bearer {os.environ['COOP_KEY']}",
        "content-type": "application/json",
    },
    method="POST",
)
with urllib.request.urlopen(req) as res:
    secret = json.load(res)["secret"]

def verify(secret, raw_body, timestamp, signature):
    expected = "v1=" + hmac.new(
        secret.encode(), f"{timestamp}.{raw_body}".encode(), hashlib.sha256
    ).hexdigest()
    return any(hmac.compare_digest(expected, part.strip()) for part in signature.split(","))
TypeScript
await fetch("https://cooperemail.com/api/v1/webhooks", {
  method: "POST",
  headers: {
    authorization: `Bearer ${key}`,
    "content-type": "application/json",
  },
  body: JSON.stringify({
    url: "https://example.com/hooks/cooper",
    events: ["message.received"],
    client_id: "hook-1",
  }),
});
MCP
{
  "tool": "cooper_register_webhook",
  "arguments": {
    "url": "https://example.com/hooks/cooper",
    "events": ["message.received"]
  }
}

Buy a phone number and read an inbound text

POST /api/v1/inboxes/{id}/phone-number buys a US number for a Starter or Pro inbox that has a verified owner. Inbound texts are stored on the inbox. cooper_wait_for_sms waits for a text. GET events?types=message.received&wait=25 waits for the next received message on that inbox.

curl
curl -s https://cooperemail.com/api/v1/inboxes/$INBOX/phone-number \
  -H "authorization: Bearer $COOP_KEY" \
  -H "content-type: application/json" \
  -H "idempotency-key: phone-1" \
  -d '{"area_code":"415"}'

curl -s "https://cooperemail.com/api/v1/inboxes/$INBOX/events?types=message.received&wait=25" \
  -H "authorization: Bearer $COOP_KEY"
Python
import json, os, urllib.request
inbox = os.environ["INBOX"]
headers = {
    "authorization": f"Bearer {os.environ['COOP_KEY']}",
    "content-type": "application/json",
    "idempotency-key": "phone-1",
}
buy = urllib.request.Request(
    f"https://cooperemail.com/api/v1/inboxes/{inbox}/phone-number",
    data=json.dumps({"area_code": "415"}).encode(),
    headers=headers,
    method="POST",
)
with urllib.request.urlopen(buy) as res:
    print(json.load(res)["e164"])
wait = urllib.request.Request(
    f"https://cooperemail.com/api/v1/inboxes/{inbox}/events?types=message.received&wait=25",
    headers={"authorization": headers["authorization"]},
)
with urllib.request.urlopen(wait) as res:
    print(json.load(res))
TypeScript
await fetch(`https://cooperemail.com/api/v1/inboxes/${inboxId}/phone-number`, {
  method: "POST",
  headers: {
    authorization: `Bearer ${key}`,
    "content-type": "application/json",
    "idempotency-key": "phone-1",
  },
  body: JSON.stringify({ area_code: "415" }),
});
const inbound = await fetch(
  `https://cooperemail.com/api/v1/inboxes/${inboxId}/events?types=message.received&wait=25`,
  { headers: { authorization: `Bearer ${key}` } },
);
MCP
{
  "tool": "cooper_provision_phone_number",
  "arguments": { "inbox_id": "research-bot", "area_code": "415", "idempotency_key": "phone-1" }
}

{
  "tool": "cooper_wait_for_sms",
  "arguments": { "inbox_id": "research-bot", "wait": 20 }
}

Approve before send

A verified owner is required. PATCH send_mode to approval holds POST /messages, reply, reply-all, and forward as a pending draft and emails that owner Approve and Reject links. No verified owner returns HTTP 409 owner_required and leaves send_mode direct.

curl
curl -s -X PATCH https://cooperemail.com/api/v1/inboxes/$INBOX \
  -H "authorization: Bearer $COOP_KEY" \
  -H "content-type: application/json" \
  -d '{"send_mode":"approval"}'

curl -s https://cooperemail.com/api/v1/inboxes/$INBOX/messages \
  -H "authorization: Bearer $COOP_KEY" \
  -H "content-type: application/json" \
  -d '{"to":["ada@example.com"],"subject":"Quote","text":"Ready for review.","client_id":"draft-1"}'
Python
import json, os, urllib.request
inbox = os.environ["INBOX"]
key = os.environ["COOP_KEY"]

def post(path, payload, method="POST"):
    req = urllib.request.Request(
        f"https://cooperemail.com{path}",
        data=json.dumps(payload).encode(),
        headers={"authorization": f"Bearer {key}", "content-type": "application/json"},
        method=method,
    )
    with urllib.request.urlopen(req) as res:
        return json.load(res)

post(f"/api/v1/inboxes/{inbox}", {"send_mode": "approval"}, "PATCH")
post(f"/api/v1/inboxes/{inbox}/messages", {
    "to": ["ada@example.com"],
    "subject": "Quote",
    "text": "Ready for review.",
    "client_id": "draft-1",
})
TypeScript
await fetch(`https://cooperemail.com/api/v1/inboxes/${inboxId}`, {
  method: "PATCH",
  headers: {
    authorization: `Bearer ${key}`,
    "content-type": "application/json",
  },
  body: JSON.stringify({ send_mode: "approval" }),
});
await fetch(`https://cooperemail.com/api/v1/inboxes/${inboxId}/messages`, {
  method: "POST",
  headers: {
    authorization: `Bearer ${key}`,
    "content-type": "application/json",
  },
  body: JSON.stringify({
    to: ["ada@example.com"],
    subject: "Quote",
    text: "Ready for review.",
    client_id: "draft-1",
  }),
});
MCP
{
  "tool": "cooper_update_inbox",
  "arguments": { "inbox_id": "research-bot", "send_mode": "approval" }
}

{
  "tool": "cooper_create_draft",
  "arguments": {
    "inbox_id": "research-bot",
    "to": ["ada@example.com"],
    "subject": "Quote",
    "text": "Ready for review.",
    "client_id": "draft-1"
  }
}