shiply.now

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 broadcast

Zero-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
      }
    }
  }
}
FieldDefaultEffect
emailField"email"Which field holds the address
confirmtrueSend double-opt-in confirmation email
notifytrueNotify the site owner on each submission
audiencetrueAdd 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

ToolPurpose
send_emailSend a transactional email from a site (Bearer)
list_site_inboxRead inbox threads (optional slug filter)
set_mailboxConfigure a collection's mailbox settings
list_mailbox_contactsList audience contacts, filter by status
send_mailbox_broadcastBroadcast 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

RuleValue
Site must be ownedanonymous sites get 403 email_requires_account — claim first
Capture body16 KB max, 30 fields max
Honeypotnon-empty company field → silent drop (bot protection)
Double opt-inon by default; visitors must click confirm to join audience
Public pagecan capture only — cannot send or read inbox
Bearer keyrequired to send, read inbox, configure mailbox, or broadcast
Broadcast minimum≥ 1 confirmed subscriber required
Unsubscribe footerauto-added to every broadcast (cannot be removed)

Errors

StatusBody errorFix
403email_requires_accountclaim the site / publish with a Bearer key
400email_field_missingPOST body must include the configured emailField
400email_invalidvalue is not a valid email address
413email_body_too_largebody exceeds 16 KB or 30 fields
422broadcast_no_audienceno confirmed subscribers yet
429rate_limitedback off and retry