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
| Scope | Grants | Risk shape |
|---|---|---|
drafts.write | Create Gmail drafts (on a thread, or brand-new) and delete them. Can never send. | None — a human reviews and sends from Gmail. |
drafts.send | Send an existing draft by id. | "Approve by sending" — one key prepares, another releases. |
email.reply | Reply 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.send | Compose 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"]
Delete a draft you no longer need (drafts.write)
Discards a draft the agent no longer needs — a reply that's out of date, or one a person decided to write themselves. It's the same as deleting the draft in Gmail: permanent (no Trash, no undo) and it never sends anything. The draft must be on the named thread; an id that isn't there — wrong thread, or already deleted — returns 404 and nothing is deleted. On a board that shares drafts, the draft is discarded for every member. Emits draft.deleted.
curl -X DELETE "https://api.sortd.com/v2/email-threads/18c2a5b7f3d94e01/drafts/r-123456" \
-H "Authorization: Bearer sk_live_…"
# → { "data": { "draft_id": "r-123456", "thread_id": "18c2a5b7f3d94e01", "deleted": true }, "meta": { "request_id": "…" } }
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
- Drafting assistant —
drafts.writeonly. It can prepare; a human sends from Gmail. - Autonomous responder —
email.reply. Real sends, structurally unable to reach anyone outside the thread. - Outbound automation (reminders, scheduled mail) —
email.send, granted deliberately to that one key, revocable on its own.
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.