Send a calendar invite to someone using only their phone number, and get back their real email once they accept — over a REST API, an MCP server, or a tokenized email address. This is the reference for all three.
403 api_requires_paid_plan.
Every integration path drives the same core action: a phone-only person is invited to an event, maeil texts them a one-tap accept link, and on accept their real email is returned to you.
| Surface | Base | Best for |
|---|---|---|
| REST API | https://api.maeil.cc | Any backend, CRM, or platform that can make an HTTPS request. |
| MCP server | https://mcp.maeil.cc/mcp | AI assistants and agents (Claude, Cursor, VS Code, ChatGPT, …). |
| Email on-ramp | <phone>+<token>@maeil.cc | Email-based schedulers (Cal.com, Howie, …) — zero code. |
All three authenticate with the same mk_live_… API key. US phone numbers only in v1.
Two credentials, two purposes:
| Credential | Used for | How you get it |
|---|---|---|
API key mk_live_… | The send endpoints (/v1/invite) and MCP | Minted from your account — POST /v1/keys |
| Account JWT | Key management (/v1/keys) | POST /auth/login with your account email + password |
Send the API key as either header:
Authorization: Bearer mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-API-Key: mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Authenticate with your account JWT (not an API key). Max 20 active keys per account.
curl -X POST https://api.maeil.cc/v1/keys \
-H "Authorization: Bearer <YOUR_ACCOUNT_JWT>" \
-H "Content-Type: application/json" \
-d '{"name":"production key"}'
Response — 201 Created:
{
"id": "key_...",
"key": "mk_live_...", // shown ONCE — copy it now
"prefix": "mk_live_abcd",
"name": "production key",
"email_token": "mp_Xa9kQ3v7bN2pLw...", // handle for the email on-ramp
"email_address_template": "<phone>+mp_Xa9kQ3v7bN2pLw...@maeil.cc"
}
GET /v1/keys lists your keys (prefix, name, per-key limits, usage, revoked flag). DELETE /v1/keys/:id revokes one. Revoking a key also disables its email on-ramp address immediately.
Authenticate with an API key.
curl -X POST https://api.maeil.cc/v1/invite \
-H "Authorization: Bearer mk_live_..." \
-H "Content-Type: application/json" \
-d '{
"phone": "+15551234567",
"event": {
"title": "Intro call",
"start": "2026-06-15T14:00:00Z",
"end": "2026-06-15T14:30:00Z",
"location": "Phone",
"description": "Quick 15-min intro",
"organizer": "agent@firm.com"
},
"consent_attestation": "client booked by phone on 6/1",
"callback_url": "https://your-app.example/webhooks/maeil"
}'
| Field | Required | Notes |
|---|---|---|
phone | yes | US only. Accepts +15551234567, 15551234567, 5551234567, (555) 123-4567. |
event.title | yes | Shown in the SMS and on the accept page. |
event.start | yes | ISO 8601 datetime (…T14:00:00Z or with offset), or a date (2026-06-15) for all-day. |
event.organizer | yes | Valid email. The RSVP and the revealed email are delivered here. |
consent_attestation | yes | true, or a string describing the basis (e.g. "met at conference, gave card"). Stored on the invite and in the audit log. See Compliance. |
event.end | no | Same formats as start. |
event.location | no | Free text. |
event.description | no | Free text. |
callback_url | no | Public https:// URL for the response webhook. Requires the Builder plan (else 403 webhook_requires_builder). Private/loopback hosts are rejected (SSRF guard). |
Response — 201 Created:
{ "invite_id": "V1StGXR8_Z5jdHi6B-myT", "status": "sent", "accept_url": "https://app.maeil.cc/a/V1StGXR8_Z5jdHi6B-myT" }
Status and the revealed email once accepted. Scoped to the key's owning account.
{
"invite_id": "V1StGXR8_Z5jdHi6B-myT",
"status": "accepted",
"recipient_phone": "+15551234567",
"event_title": "Intro call",
"event_start": "2026-06-15T14:00:00Z",
"revealed_email": "dana@example.com",
"created_at": "2026-05-24T16:00:00.000Z"
}
status is one of sent · accepted · declined · failed. revealed_email is null until the recipient accepts and provides it.
If you passed callback_url (Builder plan), maeil POSTs it when the recipient responds:
{
"invite_id": "V1StGXR8_Z5jdHi6B-myT",
"status": "accepted",
"recipient_email": "dana@example.com",
"recipient_phone": "+15551234567",
"event_title": "Intro call",
"responded_at": "2026-05-24T17:02:11.000Z"
}
Verify the signature. Each callback carries X-Maeil-Signature: sha256=<hex> — an HMAC-SHA256 of the exact request body. Recompute it with your shared secret and compare before trusting the payload.
POST /v1/invite creates a new invite and consumes one unit of quota. Don't blind-retry a send; reconcile with GET /v1/invite/:id first. callback_url must be a public https host — localhost, dotless, .local/.internal, and private/loopback/link-local/CGNAT IPs are rejected at create time.Errors return a JSON body { "error": "...", "code": "..." } with the HTTP status below.
| Status | code | Meaning & what to do |
|---|---|---|
| 400 | consent_required | consent_attestation was missing or false. Add it and resend. |
| 400 | — | Invalid phone, missing/invalid event field, bad date, non-https callback_url, or malformed JSON. Fix the payload. |
| 401 | — | Missing, invalid, or revoked API key. Check the Authorization header. |
| 403 | suppressed | Recipient texted STOP and is globally opted out. Never re-texted; do not retry. |
| 403 | webhook_requires_builder | You passed callback_url without webhook access (Builder or Business required). Upgrade, or drop the callback and poll status. |
| 403 | api_requires_paid_plan | API/MCP access needs the Power plan or above (Power, Builder, Business; returned when minting a key). Upgrade the account. |
| 409 | key_limit_reached | You already have 20 active keys. Revoke an unused one first. |
| 429 | rate_limited | This key's — or your plan's daily — send cap is reached. Back off and retry later. |
| 429 | fair_use_reached | Your plan's monthly fair-use pool is exhausted (non-metered plans). Upgrade to Builder for metered volume. |
| 502 | — | Invite stored, but the carrier rejected the SMS. Response is { status: "failed", … }. |
Two layers enforce sending: a per-key cap and your account's monthly pool. API keys draw from the same account pool as the dashboard and your personal MCP key — one total, not split.
| Plan | Daily | Monthly (included) | Above included |
|---|---|---|---|
| Free | 3 | 3 to start, then 1 / month | — |
| Standard | 25 | 150 | hard cap |
| Power | 40 | 250 | hard cap |
| Builder | 200 | 1,500 | metered — $0.05 / send |
| Business | 400 | 5,000 | metered — $0.05 / send |
New keys are seeded with per-key limits derived from your plan. On metered plans (Builder, Business) the per-key monthly bound is effectively unlimited so a single key can reach your full included volume and beyond — the account pool plus metered overage governs. Hitting a limit returns 429 (rate_limited for a daily/per-key cap, fair_use_reached for a non-metered monthly pool).
maeil runs as a Model Context Protocol server so assistants can invite by phone and read back the email without leaving the conversation. It exposes exactly two tools:
| Tool | Arguments | Returns |
|---|---|---|
send_calendar_invite | phone, title, start, organizer, consent_attestation (all required); end, location, description, callback_url (optional) | The invite_id and initial status. |
get_invite_status | invite_id (required) | Status (sent/accepted/declined/failed) and the revealed email once accepted. |
Protocol revision 2025-06-18 (Streamable HTTP). consent_attestation is required at the tool — a send without it is refused before any network call.
Any client that can attach an Authorization header can point at https://mcp.maeil.cc/mcp:
{
"mcpServers": {
"maeil": {
"url": "https://mcp.maeil.cc/mcp",
"headers": { "Authorization": "Bearer mk_live_REPLACE_ME" }
}
}
}
For clients that can't set a header (or require OAuth), launch the bundled Node connector with your key in the environment (Node 18+). Example — Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"maeil": {
"command": "node",
"args": ["/path/to/workers/mcp/dist/maeil-mcp-stdio.mjs"],
"env": { "MAEIL_API_KEY": "mk_live_REPLACE_ME" }
}
}
}
Discover the tools over plain JSON-RPC:
curl -s https://mcp.maeil.cc/mcp \
-H "Authorization: Bearer mk_live_REPLACE_ME" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Authorization header shown above. ChatGPT's web connector may require an OAuth 2.1 flow and can refuse a static Bearer key (OAuth is a planned follow-up); until then, connect ChatGPT through the local connector. Any client that can send a Bearer header works against the remote endpoint.An email-based scheduler (Cal.com, Howie, Reclaim, Motion, …) can use maeil with zero code — just add a tokenized maeil address as a guest on the calendar invite:
<phone>+<email_token>@maeil.cc
e.g. 5551234567+mp_Xa9kQ3v7bN2pLw...@maeil.cc
<phone> — the recipient's US number (digits only).+<email_token> — your attributable handle (mp_…), returned when you create an API key (email_address_template). It marks the send as yours and applies your key's limits.The plain consumer address 5551234567@maeil.cc is unrelated; the token is what attributes a send to a contracted partner. Revoke the key and the address stops resolving immediately.
consent_attestation; it's stored on the invite and in the audit log.403 suppressed.Questions the docs don't answer? Email maeil@cb-industries.co or visit Help.