Sortd Docs

Drafts & sending email

Outbound email is governed by a four-scope ladder, so a key holds exactly as much send capability as its owner meant to grant — from "prepare drafts only" up to "compose to anyone". You choose the rung when you create the key.

The scope ladder

ScopeGrantsRisk shape
drafts.writeCreate Gmail drafts (on a thread, or brand-new). Can never send.None — a human reviews and sends from Gmail.
drafts.sendSend an existing draft by id."Approve by sending" — one key prepares, another releases.
email.replyReply on an existing thread, body only — recipients are derived server-side and cannot be supplied.Content can never leave the thread's audience. The safe send for agents.
email.sendCompose and send to arbitrary recipients.The widest capability — grant deliberately.

Sends are irreversible: always pass an Idempotency-Key so a retried request can't send twice. No attachments on any compose operation (v1 — deliberate).

1. Create a draft for a human to send (drafts.write)

On a thread, only body is required — recipients default to the conversation and the subject to Re: <thread subject>. The draft lands in Gmail's Drafts folder, where the user can edit, send or discard it.

curl — draft a reply onto a thread

curl -X POST "https://api.sortd.com/v2/email-threads/18c2a5b7f3d94e01/drafts" \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: <unique-key>" \
  -d '{ "body": "Thanks — updated proposal attached to the previous mail. Does Thursday work?" }'

# curl — a brand-new draft (fresh conversation): to + subject required
curl -X POST "https://api.sortd.com/v2/email/drafts" \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: <unique-key>" \
  -d '{ "to": ["jane@acme.com"], "subject": "Q3 renewal", "body": "Hi Jane — …" }'

JavaScript (fetch)

const res = await fetch(
  "https://api.sortd.com/v2/email-threads/18c2a5b7f3d94e01/drafts",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer sk_live_…",
      "Content-Type": "application/json",
      "Idempotency-Key": crypto.randomUUID(),
    },
    body: JSON.stringify({ body: "Thanks — does Thursday work?" }),
  }
);
const { data } = await res.json(); // data.id is the Gmail draft id

Python (requests)

import requests, uuid
res = requests.post(
    "https://api.sortd.com/v2/email-threads/18c2a5b7f3d94e01/drafts",
    headers={
        "Authorization": "Bearer sk_live_…",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={"body": "Thanks — does Thursday work?"},
)
draft = res.json()["data"]

2. Approve by sending (drafts.send)

Sends a previously created draft. The split is the human-in-the-loop pattern for automation: an agent key with drafts.write prepares, and a human — or a separate, more trusted key with drafts.send — releases.

curl -X POST "https://api.sortd.com/v2/email-threads/18c2a5b7f3d94e01/drafts/r-123456/send" \
  -H "Authorization: Bearer sk_live_…" \
  -H "Idempotency-Key: <unique-key>"
# List a thread's drafts first: GET /email-threads/{threadId}/drafts

3. Reply without being able to leak (email.reply)

You control only the body. Recipients are derived server-side from the thread (honouring Reply-To); a request containing to/cc/bcc is rejected with a 400. So even a prompt-injected agent can't mail thread content to an outside address — it can only reach people already on the conversation. mode is reply (sender only, the default) or reply_all (keep the To/Cc audience).

curl

curl -X POST "https://api.sortd.com/v2/email-threads/18c2a5b7f3d94e01/reply" \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: <unique-key>" \
  -d '{ "body": "Confirmed for Thursday 10:00.", "mode": "reply_all" }'

JavaScript (fetch)

await fetch("https://api.sortd.com/v2/email-threads/18c2a5b7f3d94e01/reply", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_live_…",
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({ body: "Confirmed for Thursday 10:00." }),
});

Python (requests)

import requests, uuid
requests.post(
    "https://api.sortd.com/v2/email-threads/18c2a5b7f3d94e01/reply",
    headers={
        "Authorization": "Bearer sk_live_…",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={"body": "Confirmed for Thursday 10:00.", "mode": "reply"},
)

4. Compose and send (email.send)

A new message to recipients you specify, from the key owner's mailbox — optionally onto an existing thread via thread_id. This is the highest-risk email scope: anything that can read the mailbox and send to arbitrary addresses can exfiltrate it, so prefer a reply or a draft when they'd do.

curl

curl -X POST "https://api.sortd.com/v2/email/send" \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: <unique-key>" \
  -d '{
    "to": ["jane@acme.com"],
    "cc": ["sam@acme.com"],
    "subject": "Q3 renewal — proposal",
    "body": "Hi Jane — proposal below. …"
  }'

JavaScript (fetch)

await fetch("https://api.sortd.com/v2/email/send", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_live_…",
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    to: ["jane@acme.com"],
    subject: "Q3 renewal — proposal",
    body: "Hi Jane — proposal below. …",
  }),
});

Python (requests)

import requests, uuid
requests.post(
    "https://api.sortd.com/v2/email/send",
    headers={
        "Authorization": "Bearer sk_live_…",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={"to": ["jane@acme.com"], "subject": "Q3 renewal — proposal", "body": "…"},
)

Choosing a rung for an agent

The same ladder governs the MCP tools: a connected assistant only ever sees the send tools its key's scopes allow.

Next: receive webhooks — including thread.reply_received to hear when the answer arrives.