How it works

Create, invite, and let the replies come back to you.

The calendar sits on the inbox, so invitations go out from the agent's own address and every answer comes back to the same place.

1

Create an event

POST /v1/inboxes/{inbox_id}/calendar/events with a title and a start time. Nothing is sent yet. The inbox is the organizer unless you say otherwise, and the event gets a stable ical_uid.

2

Send it as an invitation

POST .../events/{event_id}/invite emails every attendee a proper iCalendar REQUEST. They see Accept and Decline in their mail client and the event lands on their calendar. Reschedule with PATCH, call invite again, and it goes out as an update, not a duplicate.

3

Answers arrive on their own

When an attendee replies, the inbox receives the REPLY, records their response on the event, and fires calendar.event.updated. When someone invites your agent, the event appears on the calendar and the agent can rsvp.

the event object
{
  "event_id": "9b0e4c2a-7d13-4f58-a6e1-2c8b5d9f0a47",
  "inbox_id": "6f1c2d3e-8a4b-4c5d-9e6f-0a1b2c3d4e5f",
  "ical_uid": "b4f6e0c2d8a1@deadsimple.email",   // stable across updates, on both ends
  "title": "Kickoff: Q4 onboarding",
  "start_at": "2026-09-22T15:00:00Z",
  "end_at": "2026-09-22T15:30:00Z",
  "timezone": "America/New_York",
  "all_day": false,
  "status": "confirmed",                    // confirmed | tentative | cancelled
  "organizer": { "email": "ops_9c4e1f2a@box1.deadsimple.email", "name": "Ops Agent" },
  "attendees": [
    { "email": "dana@example.com", "response": "accepted" },
    { "email": "sam@example.com",  "response": "needs-action", "optional": true }
  ],
  "source": "sent",                         // api | sent | inbound
  "sequence": 1,                             // bumped on every change after the first invite
  "message_id": "c41f8a2e-6b3d-4e9a-8f7c-0d2b5a9e1c63",               // the invitation we sent, or the inbound message
  "invited_at": "2026-09-17T14:02:11Z",
  "created_at": "2026-09-17T14:01:40Z",
  "updated_at": "2026-09-17T16:20:05Z"
}

Invitations that arrive by email land on the calendar

Most calendars for agents stop at sending. This one reads too. Every inbound message is checked for a text/calendar part, and when one is there the calendar applies it before the message is stored:

  • A new invitation (METHOD:REQUEST) creates the event with source: inbound, the sender as organizer, and your inbox listed among the attendees with needs-action. Webhook: calendar.event.created.
  • An update to an invitation you already have is matched on its ical_uid and applied in place: new time, new title, new attendee list. Webhook: calendar.event.updated. Stale copies with a lower SEQUENCE are ignored.
  • A cancellation (METHOD:CANCEL) marks the event cancelled. Webhook: calendar.event.cancelled.
  • A reply (METHOD:REPLY) to an invitation your agent sent records that attendee's accepted, declined or tentative on the event. Webhook: calendar.event.updated.

The message itself carries a compact calendar_event summary so an agent reading its mail sees the event without a second call. action says what the message did to the calendar.

GET /v1/inboxes/{inbox_id}/messages/{message_id}
{
  "message_id": "e7a2c9d1-4b8f-4c3e-9d6a-5f1b0c8e2a74",
  "from_email": "dana@example.com",
  "subject": "Invitation: Vendor review @ Tue Sep 22, 2026 11am",
  "calendar_method": "REQUEST",
  "calendar_event": {
    "event_id": "3d7a1f9c-52e4-4b8d-9a6f-1e0c7b2d8f35",
    "inbox_id": "6f1c2d3e-8a4b-4c5d-9e6f-0a1b2c3d4e5f",
    "ical_uid": "7f3a9c1e-2b4d-4e6f-8a0b@google.com",
    "title": "Vendor review",
    "start_at": "2026-09-22T15:00:00Z",
    "end_at": "2026-09-22T15:45:00Z",
    "status": "confirmed",
    "organizer": { "email": "dana@example.com", "name": "Dana" },
    "action": "created"                  // created | updated | cancelled | replied | unchanged
  },
  ...
}

The same summary is the payload of the three calendar.event.* webhooks, so a handler that already routes message.received can subscribe to the calendar events and act on the event rather than re-parsing the mail. Calendar ingestion never loses a message: if parsing fails for any reason, the email is stored as normal and the calendar is simply left alone.

Endpoints

Everything is under the inbox.

Same authentication, pagination and error shape as the rest of the API. Reads need inboxes.read; creating, updating and deleting need inboxes.update; invite and rsvp send mail, so they need messages.send and count against your plan like any other send.

Method and pathWhat it doesSDK
GET /v1/calendar/eventsEvents across every inbox the key can see, sorted by start_at. Filters: from, to, status, comma-separated inbox_id. Honours workspace scope and inbox-scoped keys.calendar.list_all / listAll
GET /v1/inboxes/{inbox_id}/calendar/eventsOne inbox's events with the same filters and cursor pagination.calendar.list
POST /v1/inboxes/{inbox_id}/calendar/eventsCreate an event. title and start required; end or duration_minutes (default 30), timezone, all_day, description, location, status, organizer, attendees, your own ical_uid. Nothing is sent. Emits calendar.event.created.calendar.create
GET /v1/inboxes/{inbox_id}/calendar/events/{event_id}One event.calendar.get
PATCH /v1/inboxes/{inbox_id}/calendar/events/{event_id}Partial update. A moved start keeps the event's length. After an invitation has gone out, every change raises sequence so the next invite is a revision. status: cancelled emits calendar.event.cancelled; anything else calendar.event.updated.calendar.update
DELETE /v1/inboxes/{inbox_id}/calendar/events/{event_id}Removes the row. Attendees are not told: to withdraw an invitation, PATCH status to cancelled and call invite first, which sends a CANCEL.calendar.delete
POST /v1/inboxes/{inbox_id}/calendar/events/{event_id}/inviteSend the event to its attendees as an iCalendar REQUEST (or CANCEL when the event is cancelled) from the inbox. Optional subject, text_body, html_body, replacement attendees, in_reply_to to thread it. The inbox must be the organizer.calendar.invite
POST /v1/inboxes/{inbox_id}/calendar/events/{event_id}/rsvpAnswer an invitation the inbox received: response of accepted, declined or tentative, optional comment. Sends an iCalendar REPLY to the organizer and records the answer. Only for events the inbox does not organize.calendar.rsvp
GET /v1/inboxes/{inbox_id}/calendar/feedThe inbox's subscription URLs, feed_url (https) and webcal_url. Creates the feed token on first call and returns the same URLs until rotated.calendar.feed
POST /v1/inboxes/{inbox_id}/calendar/feed/rotateNew token. The old URL stops working immediately.calendar.rotate_feed / rotateFeed
GET /v1/inboxes/{inbox_id}/calendar.ics?token=...The feed itself: a text/calendar VCALENDAR of every non-cancelled event. No API key; the token is the credential. Rate limited per IP.(calendar apps call this)

In the SDKs and over MCP

Times are ISO 8601 and need a zone: a trailing Z, an offset, or a timezone argument next to a local time. A bare date makes an all-day event. Everything is stored and returned as UTC instants.

Python

calendar.py
from deadsimple import DeadSimple

client = DeadSimple(api_key="dse_...")

# 1. Put an event on the inbox's calendar. Nothing is sent yet.
event = client.calendar.create(
    inbox_id,
    title="Kickoff: Q4 onboarding",
    start="2026-09-22T11:00:00", timezone="America/New_York",
    duration_minutes=30,
    location="https://meet.example.com/kickoff",
    attendees=["dana@example.com", {"email": "sam@example.com", "optional": True}],
)

# 2. Send it as a real invitation (Accept / Decline in their mail client).
sent = client.calendar.invite(inbox_id, event["event_id"],
                             text_body="Kickoff for the Q4 onboarding project. Agenda attached Monday.")
print(sent["method"], sent["sent_to"], sent["message_id"])   # REQUEST [...] msg_...

# 3. Reschedule: PATCH raises sequence, invite again sends an update, not a duplicate.
client.calendar.update(inbox_id, event["event_id"], start="2026-09-23T11:00:00", timezone="America/New_York")
client.calendar.invite(inbox_id, event["event_id"])

# 4. Answer an invitation that arrived by email.
for ev in client.calendar.list(inbox_id, start_from="2026-09-22")["events"]:
    if ev["source"] == "inbound" and ev["status"] == "confirmed":
        client.calendar.rsvp(inbox_id, ev["event_id"], "accepted", comment="See you then.")

# 5. Hand a human the subscription URL.
feed = client.calendar.feed(inbox_id)
print(feed["webcal_url"])     # webcal://api.deadsimple.email/v1/inboxes/.../calendar.ics?token=...

# Every event on every inbox the key can see:
client.calendar.list_all(start_from="2026-09-21", start_to="2026-09-27")

Node

calendar.ts
import { DeadSimple } from "@deadsimple/email"

const client = new DeadSimple({ apiKey: "dse_..." })

const event = await client.calendar.create(inboxId, {
  title: "Kickoff: Q4 onboarding",
  start: "2026-09-22T15:00:00Z",
  duration_minutes: 30,
  attendees: ["dana@example.com"],
})

const sent = await client.calendar.invite(inboxId, event.event_id, {
  textBody: "Kickoff for the Q4 onboarding project.",
})
// sent.method === "REQUEST", sent.sent_to, sent.message_id

// Answer something that arrived by email
const { events } = await client.calendar.list(inboxId, { status: "confirmed" })
for (const ev of events) {
  if (ev.source === "inbound") await client.calendar.rsvp(inboxId, ev.event_id, "accepted")
}

// Feed for a human's calendar app; rotate if the URL ever leaks
const feed = await client.calendar.feed(inboxId)      // feed.feed_url, feed.webcal_url
await client.calendar.rotateFeed(inboxId)

MCP

The hosted MCP server at https://api.deadsimple.email/mcp and the local python -m deadsimple.mcp both expose two calendar tools on top of the existing invite tools, for 19 tools in total.

MCP tools
list_calendar_events(inbox_id, from_date="", to_date="", status="", limit=20)
# Soonest first. Includes every invitation that arrived by email.

create_calendar_event(inbox_id, title, start, end="", duration_minutes=30,
                      timezone="", location="", description="",
                      attendees="", all_day=False, send_invites=False)
# send_invites=True also emails the attendees a real invitation.

# In Claude, Cursor or Grok Bot, that means:
"What is on your calendar this week?"
"Set up a 30 minute call with dana@example.com on Tuesday at 11 New York time and send the invite."

Subscribe a human to the agent's calendar

An agent's schedule is only useful if the people around it can see it. Each inbox has an iCalendar feed that any calendar app can subscribe to. Get the URLs once:

GET /v1/inboxes/{inbox_id}/calendar/feed
{
  "inbox_id": "6f1c2d3e-8a4b-4c5d-9e6f-0a1b2c3d4e5f",
  "email": "ops_9c4e1f2a@box1.deadsimple.email",
  "feed_url": "https://api.deadsimple.email/v1/inboxes/6f1c.../calendar.ics?token=dsecal_9f3c1a...e7",
  "webcal_url": "webcal://api.deadsimple.email/v1/inboxes/6f1c.../calendar.ics?token=dsecal_9f3c1a...e7",
  "token_created_at": "2026-09-17T14:05:40Z",
  "rotated": true          // true on the first call and after a rotate
}

Google Calendar

  1. Open Google Calendar on the web. In the left sidebar, next to Other calendars, click the plus sign and choose From URL.
  2. Paste the feed_url (the https one) and click Add calendar.
  3. The agent's calendar appears under Other calendars and syncs to the Google Calendar apps on your phone. Google refreshes subscribed calendars on its own schedule, typically every several hours.

Apple Calendar (Mac, iPhone, iPad)

  1. Open the webcal_url on the device, or in Calendar on a Mac choose File, New Calendar Subscription and paste it.
  2. Pick where to keep it (iCloud makes it available on every device) and set Auto-refresh to every 15 minutes or hourly.
  3. Click OK. New events, changes and cancellations arrive on each refresh.

Outlook

In Outlook on the web, choose Add calendar, then Subscribe from web, and paste the https URL. Desktop Outlook accepts the same URL under Open Calendar, From Internet.

Treat the URL as a secret. The token in the query string is the only credential on the feed, and calendar apps fetch it with no API key. If it is pasted somewhere it should not be, call POST /v1/inboxes/{inbox_id}/calendar/feed/rotate (calendar.rotate_feed): the old URL stops answering at once and anyone subscribed to it stops receiving updates. The feed lists confirmed and tentative events only; cancelled ones are dropped so they disappear from subscribers on the next refresh. X-WR-TIMEZONE carries the zone most events were created in, so all-day events and local times render the way you expect.

What it does not do yet

Being clear about the edges saves everyone time.

  • No booking page. There is no public link where a human picks a free slot and the event is created for them. Nylas ships this as Scheduler alongside Agent Accounts, and it is a real convenience if your agent's main job is taking bookings. AgentMail has no calendar at all. On Dead Simple the equivalent today is done by email: the agent proposes times in a message, and when the reply comes back it creates the event and sends the invitation.
  • No free/busy lookup across other people's calendars. The calendar knows what the agent has been invited to and what it has scheduled. It does not query Google or Microsoft for someone else's availability.
  • No recurrence expansion yet. Events are single occurrences. A weekly meeting is one event per week, which the agent can create in a loop; a repeating invitation that arrives by email is stored as one event at its first start, without the rule.
  • Feeds are read-only. A human subscribed in Google or Apple Calendar sees the agent's events; editing them there does not write back. Changes go through the API, the SDKs or an MCP tool.

If a booking page would change your decision, tell us. It is the obvious next step and the ordering is driven by who asks.

FAQ

Questions people ask first.

Every inbox has one. There is nothing to enable and nothing extra to pay for: the calendar is part of the inbox on every plan, including Free. Events live under /v1/inboxes/{inbox_id}/calendar/events, and GET /v1/calendar/events lists events across every inbox the key can see.

The invitation is parsed on arrival and the event is created on the inbox's calendar with source: inbound. The message record carries a calendar_event summary with action: created, and a calendar.event.created webhook fires. If the organizer later sends an update or a cancellation, the same event is matched on its iCalendar UID and updated or marked cancelled, with calendar.event.updated or calendar.event.cancelled. When an attendee replies to an invitation your agent sent, their response is recorded on the event.

POST /v1/inboxes/{inbox_id}/calendar/events/{event_id}/rsvp with response set to accepted, declined or tentative, and an optional comment. The API sends a standard iCalendar REPLY to the organizer through the inbox's normal send path, so it shows up as a real response in Google Calendar, Outlook or Apple Calendar, and records the answer on the event. In the SDKs this is calendar.rsvp; it needs the messages.send permission.

Yes. GET /v1/inboxes/{inbox_id}/calendar/feed returns an https feed URL and a webcal URL for the inbox. Paste the https URL into Google Calendar under Other calendars, From URL, or open the webcal link on a Mac or iPhone to subscribe in Apple Calendar. The feed is a text/calendar VCALENDAR of every non-cancelled event and is authenticated by the token in the URL alone, so treat it as a secret and rotate it with POST /v1/inboxes/{inbox_id}/calendar/feed/rotate if it leaks.

Not yet. Today the calendar covers events, invitations, RSVPs, automatic ingestion of inbound invitations and a subscribable feed. There is no public booking page where a human picks a slot, which Nylas offers as Scheduler alongside Agent Accounts. If your agent needs to offer times, it can do so by email today: propose slots in a message, create the event when the reply comes back, and send the invitation.

send_calendar_invite attaches a one-off iCalendar part to an outgoing message and stops there; nothing is stored. The calendar keeps the event: it tracks sequence numbers so a rescheduled event goes out as an update rather than a duplicate, records attendee responses as replies arrive, appears on the ICS feed, and can be listed, updated and cancelled later. Use create_calendar_event with send_invites for anything the agent should remember.

Give your agent a calendar

Create an inbox, create an event, send the invite. On every plan, including Free.

Get Started Free Read the worked example API Reference