Agent Email
Every owned site has a real inbox — send, receive, capture signups, and broadcast from one endpoint
Every owned shiply site has a real email address. From that single identity it can capture signups, receive replies, send transactional messages, and broadcast to a confirmed audience — no external email provider account needed. Think of it as AgentMail built into the site.
The mental model
The site's inbox is its email identity. Visitors submit to the site; shiply routes to the inbox, pings the owner, and optionally sends a double-opt-in confirmation. Authenticated callers (Bearer key) can read the inbox and send outbound. Broadcasts go to the confirmed audience collected via capture.
visitor POST /.shiply/email → inbox + owner ping + opt-in email (zero config)
Bearer POST /api/v1/email/send → transactional outbound
Bearer GET /api/v1/email/inbox → read threads
Bearer POST …/broadcast → audience broadcastZero-config capture
No manifest. No setup. Just POST to the relative path from any published page:
<form id="f">
<input id="email" type="email" required placeholder="you@example.com">
<button>Join waitlist</button>
</form>
<script>
document.getElementById('f').onsubmit = async (e) => {
e.preventDefault()
const res = await fetch('/.shiply/email', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email: document.getElementById('email').value }),
})
alert(res.ok ? 'Check your inbox to confirm!' : (await res.json()).message)
}
</script>On success (201):
- Record saved to the site's inbox.
- Owner notification sent.
- Double-opt-in confirmation email sent to the visitor (default on).
Send (authenticated only)
The public page can capture but never send. Outbound requires a Bearer key:
POST https://shiply.now/api/v1/email/send
Authorization: Bearer shp_…
Content-Type: application/json
{
"slug": "my-site",
"to": "user@example.com",
"subject": "Welcome",
"html": "<p>Thanks for signing up!</p>",
"text": "Thanks for signing up!"
}Response: { "messageId": "..." }
Read inbox
GET https://shiply.now/api/v1/email/inbox
GET https://shiply.now/api/v1/email/inbox?slug=my-site
Authorization: Bearer shp_…Returns paginated threads. The dashboard Inbox section shows the same view.
Typed email block (optional)
For typed fields and granular control, declare an email block on a
.shiply/data.json collection. Then POST to /.shiply/data/<collection> as
usual — the email block activates the email layer on that collection.
{
"collections": {
"signups": {
"fields": {
"email": { "type": "email", "required": true },
"name": { "type": "string", "maxLength": 200 }
},
"access": { "read": "owner", "insert": "public" },
"email": {
"emailField": "email",
"confirm": true,
"notify": true,
"audience": true
}
}
}
}| Field | Default | Effect |
|---|---|---|
emailField | "email" | Which field holds the address |
confirm | true | Send double-opt-in confirmation email |
notify | true | Notify the site owner on each submission |
audience | true | Add confirmed addresses to the broadcast audience |
Confirm and unsubscribe
shiply embeds these links automatically in confirmation and broadcast emails:
GET /api/email/<slug>/<collection>/confirm?token=<token>
GET /api/email/<slug>/<collection>/unsubscribe?email=<address>You don't call these directly — they're in the emails shiply sends.
Broadcast
Confirmed (double-opt-in) subscribers form an audience. To broadcast:
POST /api/v1/publishes/<slug>/mailboxes/<collection>/broadcast
Authorization: Bearer shp_…
Content-Type: application/json
{
"subject": "We launched!",
"html": "<p>It's live — <a href=\"https://example.com\">check it out</a>.</p>"
}shiply spam-checks the content, appends an unsubscribe footer, and sends. Requires at least one confirmed subscriber.
Configure a mailbox
GET /api/v1/publishes/<slug>/mailboxes/<collection>
PATCH /api/v1/publishes/<slug>/mailboxes/<collection>
Authorization: Bearer shp_…List contacts
GET /api/v1/publishes/<slug>/mailboxes/<collection>/contacts?status=confirmed
Authorization: Bearer shp_…status values: pending, confirmed, unsubscribed.
MCP tools
| Tool | Purpose |
|---|---|
send_email | Send a transactional email from a site (Bearer) |
list_site_inbox | Read inbox threads (optional slug filter) |
set_mailbox | Configure a collection's mailbox settings |
list_mailbox_contacts | List audience contacts, filter by status |
send_mailbox_broadcast | Broadcast to confirmed audience (spam-checked) |
CLI
# Send a transactional email
shiply email send --site <slug> --to <address> --subject <subject> --html <html>
# Read inbox
shiply email inbox [--site <slug>]
# Configure a mailbox
shiply mailbox set <slug> <collection> [--confirm] [--notify] [--from <domain>]
# Broadcast to confirmed audience
shiply mailbox broadcast <slug> <collection> --subject <subject> --html <html>Rules and limits
| Rule | Value |
|---|---|
| Site must be owned | anonymous sites get 403 email_requires_account — claim first |
| Capture body | 16 KB max, 30 fields max |
| Honeypot | non-empty company field → silent drop (bot protection) |
| Double opt-in | on by default; visitors must click confirm to join audience |
| Public page | can capture only — cannot send or read inbox |
| Bearer key | required to send, read inbox, configure mailbox, or broadcast |
| Broadcast minimum | ≥ 1 confirmed subscriber required |
| Unsubscribe footer | auto-added to every broadcast (cannot be removed) |
Errors
| Status | Body error | Fix |
|---|---|---|
| 403 | email_requires_account | claim the site / publish with a Bearer key |
| 400 | email_field_missing | POST body must include the configured emailField |
| 400 | email_invalid | value is not a valid email address |
| 413 | email_body_too_large | body exceeds 16 KB or 30 fields |
| 422 | broadcast_no_audience | no confirmed subscribers yet |
| 429 | rate_limited | back off and retry |