Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Dead Simple Email Calendar API
version: 0.1.0
description: 'Email API for AI agents. Create inboxes, send and receive email,
manage threads, and get real-time webhook notifications.
## Authentication
All authenticated endpoints use Bearer token auth:
```
Authorization: Bearer dse_your_api_key
```
## Agent self-signup
Autonomous agents can provision an account, a key, and a live inbox in one
unauthenticated call — no human, no verification email:
```
POST /v1/auth/agent-signup
```
That returns a `trial` account (1 inbox, 10 sends/hour, 25 sends/day) with a
scoped key. `POST /v1/auth/claim` attaches a verified email and moves it to
the Free plan, keeping the same key, inboxes and history.
## Sign in with Dead Simple (OpenID Connect)
`https://id.deadsimple.email` is an OpenID Connect provider where the
identity is an agent''s inbox: `sub` is the `inbox_id`, `email` is the inbox
address. Discovery at `/.well-known/openid-configuration`, JWKS at
`/.well-known/jwks.json`, ES256 id_tokens, authorization-code + PKCE S256,
RFC 7591 registration at `/register`. Open clients (client_id = the app''s
https origin) need no registration. An agent authenticates at `/authorize`
with `Authorization: Bearer dse_...` plus `inbox_id=`, or with an ES256
assertion signed by a key enrolled via `/v1/inboxes/{inbox_id}/identity/keys`;
the one-call helper `POST /v1/inboxes/{inbox_id}/identity/sign-in` wraps
this for SDKs and the MCP server. Endpoints on the `id.deadsimple.email`
host are tagged **Identity Provider** below and carry their own `servers`
entry.
## Pay per request (no API key)
Twelve priced routes can be called with no credentials at all. They answer
`402 Payment Required` carrying an x402 v2 `PAYMENT-REQUIRED` header (USDC
on Base, Polygon or Solana, `exact` scheme, PayAI facilitator; the same
JSON is repeated in the body for v1 clients) and, on `POST /v1/inboxes`,
a `WWW-Authenticate: Payment` challenge for Stripe''s Machine Payments
Protocol. Retry with `PAYMENT-SIGNATURE` (x402 v2), `X-PAYMENT` (v1) or
`Authorization: Payment ` (MPP). x402 payments are verified
before the work and settled after it; MPP charges first and refunds if
the work fails. Prices: inbox $1.00, send/reply/reply-all/forward $0.01,
reads $0.005. The paying wallet keys a `payg` account, and the first paid
inbox returns `credentials` (an API key) in the response so later reads
and sends within the plan are free. See the reusable `402` response and
https://deadsimple.email/pay-per-request.html.
## Idempotency
POST endpoints accept an `Idempotency-Key` header. Same key within 24h
returns the cached response without re-executing.
## Rate Limits
Every response includes rate limit headers:
- `X-RateLimit-Limit` — requests per window
- `X-RateLimit-Remaining` — remaining requests
- `X-RateLimit-Reset` — window reset timestamp
On `429`, a `Retry-After` header gives the seconds to wait. Per-account
limits scale with plan (`rate_limit_rpm`); public/unauthenticated endpoints
are additionally limited per IP.
## Webhook Reliability
Every event is a durable delivery: the first attempt runs immediately and
failures are retried on an exponential-backoff schedule (configurable per
webhook via `/v1/webhooks/{id}/config`; default `[1, 30, 300, 1800, 7200,
86400]` seconds). Deliveries are HMAC-SHA256 signed on **all plans**. The
per-webhook delivery log (`/v1/webhooks/{id}/deliveries`) records every
attempt with status and error, and any delivery can be replayed. Endpoints
that fail repeatedly are auto-disabled.
## WebSocket Streaming
Connect to `wss://ws.deadsimple.email?api_key=dse_...` for real-time events.
After connecting, subscribe to specific inboxes/events:
```json
{"action": "subscribe", "inbox_ids": ["..."], "events": ["message.received"]}
```
Events are pushed as JSON with the same payload as webhooks. Send `{"action": "ping"}`
to keep the connection alive (10-minute idle timeout).'
contact:
url: https://deadsimple.email
license:
name: MIT
servers:
- url: https://api.deadsimple.email
description: Production
security:
- BearerAuth: []
tags:
- name: Calendar
paths:
/v1/calendar/events:
get:
summary: List calendar events across the account
description: 'Every event on every inbox the key can see, sorted by start_at. Same filters as the per-inbox listing plus an optional comma-separated `inbox_id` list. Scoped to the key''s workspace when it has one. SDK: `calendar.list_all` / `calendar.listAll`.'
tags:
- Calendar
parameters:
- name: from
in: query
schema:
type: string
description: ISO 8601 date or datetime. Events starting at or after this.
- name: to
in: query
schema:
type: string
description: ISO 8601 date or datetime. Events starting at or before this (a bare date means the end of that day).
- name: status
in: query
schema:
type: string
enum:
- confirmed
- tentative
- cancelled
- name: inbox_id
in: query
schema:
type: string
description: One or more inbox ids, comma-separated.
- name: limit
in: query
schema:
type: integer
default: 50
maximum: 100
- name: cursor
in: query
schema:
type: string
responses:
'200':
description: Events.
content:
application/json:
schema:
$ref: '#/components/schemas/CalendarEventList'
operationId: getV1CalendarEvents
x-operation-id-source: derived
/v1/inboxes/{inbox_id}/calendar/events:
get:
summary: List an inbox's calendar events
description: 'Sorted by start_at. `from`/`to` bound the start time; `status` filters. Cursor pagination like the rest of the API. SDK: `calendar.list`.'
tags:
- Calendar
parameters:
- name: inbox_id
in: path
required: true
schema:
type: string
- name: from
in: query
schema:
type: string
- name: to
in: query
schema:
type: string
- name: status
in: query
schema:
type: string
enum:
- confirmed
- tentative
- cancelled
- name: limit
in: query
schema:
type: integer
default: 50
maximum: 100
- name: cursor
in: query
schema:
type: string
responses:
'200':
description: Events.
content:
application/json:
schema:
$ref: '#/components/schemas/CalendarEventList'
operationId: getV1InboxesByInboxIdCalendarEvents
x-operation-id-source: derived
post:
summary: Create a calendar event
description: 'Puts an event on the inbox''s calendar. Nothing is sent: call `invite` to send it to the attendees as a real invitation. The inbox is the organizer unless `organizer` says otherwise. Emits `calendar.event.created`. SDK: `calendar.create`.'
tags:
- Calendar
parameters:
- name: inbox_id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/CalendarEventInput'
- required:
- title
- start
responses:
'201':
description: The event.
content:
application/json:
schema:
$ref: '#/components/schemas/CalendarEvent'
'400':
description: Missing title or start, a naive time with no timezone, an unknown zone, end before start, or a duplicate ical_uid on this inbox.
operationId: postV1InboxesByInboxIdCalendarEvents
x-operation-id-source: derived
/v1/inboxes/{inbox_id}/calendar/events/{event_id}:
get:
summary: Get a calendar event
tags:
- Calendar
parameters:
- name: inbox_id
in: path
required: true
schema:
type: string
- name: event_id
in: path
required: true
schema:
type: string
responses:
'200':
description: The event.
content:
application/json:
schema:
$ref: '#/components/schemas/CalendarEvent'
'404':
description: No such event on this inbox.
operationId: getV1InboxesByInboxIdCalendarEventsByEventId
x-operation-id-source: derived
patch:
summary: Update a calendar event
description: 'Partial update; fields left out keep their values, and a moved `start` keeps the event''s length unless `end` or `duration_minutes` is given. Once the event has been sent as an invitation, every update raises `sequence`, so the next `invite` goes out as a revision that calendar clients apply to the existing entry. Setting `status` to cancelled emits `calendar.event.cancelled`; anything else emits `calendar.event.updated`. SDK: `calendar.update`.'
tags:
- Calendar
parameters:
- name: inbox_id
in: path
required: true
schema:
type: string
- name: event_id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CalendarEventInput'
responses:
'200':
description: The updated event.
content:
application/json:
schema:
$ref: '#/components/schemas/CalendarEvent'
operationId: patchV1InboxesByInboxIdCalendarEventsByEventId
x-operation-id-source: derived
delete:
summary: Delete a calendar event
description: 'Removes the row. Attendees are not told: to withdraw an invitation you sent, PATCH `status` to cancelled and call `invite` (which then sends a CANCEL) before deleting. Emits `calendar.event.cancelled` unless the event was already cancelled. SDK: `calendar.delete`.'
tags:
- Calendar
parameters:
- name: inbox_id
in: path
required: true
schema:
type: string
- name: event_id
in: path
required: true
schema:
type: string
responses:
'204':
description: Deleted.
operationId: deleteV1InboxesByInboxIdCalendarEventsByEventId
x-operation-id-source: derived
/v1/inboxes/{inbox_id}/calendar/events/{event_id}/invite:
post:
summary: Send the event to its attendees as an invitation
description: 'Sends a real iCalendar invitation (METHOD:REQUEST) from the inbox to every attendee, through the normal send path (quota, guardrails and suppression apply; needs `messages.send`). The recipient sees Accept/Decline and the event lands on their calendar. The event keeps its `ical_uid`, so calling this again after a PATCH sends an update with a higher SEQUENCE that replaces the earlier entry rather than duplicating it. When the event''s status is cancelled the message goes out as METHOD:CANCEL instead. Marks the event `source: sent` and links the message. The inbox must be the organizer. SDK: `calendar.invite`.'
tags:
- Calendar
parameters:
- name: inbox_id
in: path
required: true
schema:
type: string
- name: event_id
in: path
required: true
schema:
type: string
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
subject:
type: string
description: 'Defaults to "Invitation: {title}", "Updated invitation: {title}" or "Cancelled: {title}".'
text_body:
type: string
description: Covering text. Defaults to a line naming the event, when and where.
html_body:
type: string
attendees:
type: array
description: Replace the attendee list before sending.
items:
oneOf:
- type: string
format: email
- $ref: '#/components/schemas/CalendarAttendee'
in_reply_to:
type: string
description: A message_id to thread the invitation under.
responses:
'201':
description: Sent.
content:
application/json:
schema:
$ref: '#/components/schemas/CalendarInviteResult'
'400':
description: No attendees, or the inbox is not the event's organizer (answer with rsvp instead).
operationId: postV1InboxesByInboxIdCalendarEventsByEventIdInvite
x-operation-id-source: derived
/v1/inboxes/{inbox_id}/calendar/events/{event_id}/rsvp:
post:
summary: Answer an invitation
description: 'Sends an iCalendar REPLY to the event''s organizer with this inbox''s PARTSTAT (through the normal send path; needs `messages.send`) and records the answer on the event''s attendee entry. Only for events the inbox does not organize, which includes every invitation that arrived by email. Emits `calendar.event.updated`. SDK: `calendar.rsvp`.'
tags:
- Calendar
parameters:
- name: inbox_id
in: path
required: true
schema:
type: string
- name: event_id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- response
properties:
response:
type: string
enum:
- accepted
- declined
- tentative
comment:
type: string
description: Sent as the body of the reply.
subject:
type: string
description: 'Defaults to "Accepted: {title}" etc.'
responses:
'201':
description: Replied.
content:
application/json:
schema:
$ref: '#/components/schemas/CalendarRsvpResult'
'400':
description: Invalid response, no organizer, or the inbox organizes this event itself.
operationId: postV1InboxesByInboxIdCalendarEventsByEventIdRsvp
x-operation-id-source: derived
/v1/inboxes/{inbox_id}/calendar/feed:
get:
summary: Get the inbox's calendar subscription URL
description: 'Returns the .ics feed URL (https and webcal) for this inbox, creating its feed token on first request. The same URL comes back on every call until it is rotated. Hand it to a human to add the agent''s calendar to Google Calendar, Apple Calendar or Outlook. The token is the only credential on the feed, so treat the URL as a secret. SDK: `calendar.feed`.'
tags:
- Calendar
parameters:
- name: inbox_id
in: path
required: true
schema:
type: string
responses:
'200':
description: Feed URLs.
content:
application/json:
schema:
$ref: '#/components/schemas/CalendarFeed'
operationId: getV1InboxesByInboxIdCalendarFeed
x-operation-id-source: derived
/v1/inboxes/{inbox_id}/calendar/feed/rotate:
post:
summary: Rotate the calendar feed token
description: 'Issues a new feed token and invalidates the old URL immediately. Anyone subscribed to the old URL stops receiving updates. SDK: `calendar.rotate_feed` / `calendar.rotateFeed`.'
tags:
- Calendar
parameters:
- name: inbox_id
in: path
required: true
schema:
type: string
responses:
'201':
description: New feed URLs.
content:
application/json:
schema:
$ref: '#/components/schemas/CalendarFeed'
operationId: postV1InboxesByInboxIdCalendarFeedRotate
x-operation-id-source: derived
/v1/inboxes/{inbox_id}/calendar.ics:
get:
summary: The calendar feed (iCalendar)
description: A `text/calendar` VCALENDAR of every non-cancelled event on the inbox, for calendar apps to subscribe to. Authenticated by the `token` query parameter alone (no API key), rate limited per IP. Times are UTC instants; `X-WR-TIMEZONE` carries the zone most events were given in.
tags:
- Calendar
security: []
parameters:
- name: inbox_id
in: path
required: true
schema:
type: string
- name: token
in: query
required: true
schema:
type: string
description: From GET /v1/inboxes/{inbox_id}/calendar/feed.
responses:
'200':
description: The feed.
content:
text/calendar:
schema:
type: string
'401':
description: Missing or invalid token.
operationId: getV1InboxesByInboxIdCalendarIcs
x-operation-id-source: derived
components:
schemas:
CalendarInviteResult:
allOf:
- $ref: '#/components/schemas/CalendarEvent'
- type: object
properties:
message_id:
type: string
description: The sent message.
method:
type: string
enum:
- REQUEST
- CANCEL
description: CANCEL when the event's status is cancelled; REQUEST otherwise.
sent_to:
type: array
items:
type: string
CalendarRsvpResult:
allOf:
- $ref: '#/components/schemas/CalendarEvent'
- type: object
properties:
message_id:
type: string
description: The REPLY message sent to the organizer.
response:
type: string
enum:
- accepted
- declined
- tentative
CalendarFeed:
type: object
properties:
inbox_id:
type: string
email:
type: string
feed_url:
type: string
description: https URL of the .ics feed, token included. Paste into Google Calendar ("From URL"), Apple Calendar or Outlook.
webcal_url:
type: string
description: The same feed as a webcal:// link, which most calendar apps open as a subscription.
token_created_at:
type: string
rotated:
type: boolean
description: True when this call issued a new token (the first request, or a rotate).
CalendarOrganizer:
type: object
properties:
email:
type: string
format: email
name:
type: string
CalendarEvent:
type: object
properties:
event_id:
type: string
inbox_id:
type: string
workspace_id:
type: string
nullable: true
ical_uid:
type: string
description: Stable across updates. What an update, cancellation or reply is matched on, on both ends.
title:
type: string
description:
type: string
location:
type: string
start_at:
type: string
description: ISO 8601 UTC, e.g. 2026-09-15T19:00:00Z
end_at:
type: string
description: ISO 8601 UTC. Exclusive for all-day events.
timezone:
type: string
all_day:
type: boolean
status:
type: string
enum:
- confirmed
- tentative
- cancelled
organizer:
$ref: '#/components/schemas/CalendarOrganizer'
attendees:
type: array
items:
$ref: '#/components/schemas/CalendarAttendee'
source:
type: string
enum:
- api
- sent
- inbound
description: api = created here; sent = created here and sent as an invitation; inbound = arrived by email.
sequence:
type: integer
message_id:
type: string
nullable: true
description: The invitation we sent, or the inbound message that last created, updated or cancelled the event.
invited_at:
type: string
nullable: true
created_at:
type: string
updated_at:
type: string
CalendarAttendee:
type: object
required:
- email
properties:
email:
type: string
format: email
name:
type: string
response:
type: string
enum:
- needs-action
- accepted
- declined
- tentative
description: The attendee's answer as last seen (our own rsvp, or their REPLY).
optional:
type: boolean
description: OPT-PARTICIPANT rather than REQ-PARTICIPANT on the invitation.
CalendarEventInput:
type: object
description: 'Fields for creating or (all optional, on PATCH) updating an event. Times are ISO 8601 and need a zone: a trailing Z, an offset, or `timezone`. A bare date makes an all-day event. Stored and returned as UTC instants.'
properties:
title:
type: string
maxLength: 500
start:
type: string
description: '"2026-09-15T15:00:00Z", "2026-09-15T15:00:00-04:00", "2026-09-15T15:00:00" plus timezone, or "2026-09-15".'
end:
type: string
description: Omit to use duration_minutes (default 30). On PATCH, a moved start keeps the event's length.
duration_minutes:
type: integer
minimum: 1
timezone:
type: string
description: IANA zone for a local start/end with no offset, e.g. America/New_York. Also sets X-WR-TIMEZONE on the feed.
all_day:
type: boolean
description:
type: string
location:
type: string
status:
type: string
enum:
- confirmed
- tentative
- cancelled
default: confirmed
organizer:
$ref: '#/components/schemas/CalendarOrganizer'
description: Defaults to the inbox itself. Only events the inbox organizes can be sent with invite; only events it does not can be answered with rsvp.
attendees:
type: array
maxItems: 100
items:
oneOf:
- type: string
format: email
- $ref: '#/components/schemas/CalendarAttendee'
ical_uid:
type: string
description: Create only. Supply your own iCalendar UID; otherwise one is generated as {hex}@deadsimple.email.
sequence:
type: integer
description: iCalendar SEQUENCE. Managed for you; set it only to take over from a calendar system that already sent this event.
CalendarEventList:
type: object
properties:
events:
type: array
items:
$ref: '#/components/schemas/CalendarEvent'
count:
type: integer
cursor:
type: string
description: Present when there is another page. Pass it back as ?cursor=.
securitySchemes:
BearerAuth:
type: http
scheme: bearer
description: API key (dse_...), session token (JWT), or OAuth 2.1 access token (dse_oat_..., obtained via the authorization-code + PKCE flow below)
OAuth2:
type: oauth2
description: OAuth 2.1 for MCP clients. Public clients only (PKCE S256 required, no client secret). Supports RFC 7591 dynamic client registration at /oauth/register and discovery via /.well-known/oauth-authorization-server. Access tokens carry inbox/message permissions only (no billing, team, or API-key administration).
flows:
authorizationCode:
authorizationUrl: https://api.deadsimple.email/oauth/authorize
tokenUrl: https://api.deadsimple.email/oauth/token
refreshUrl: https://api.deadsimple.email/oauth/token
scopes:
email: Send, receive, and manage email inboxes
OpenIDConnect:
type: openIdConnect
openIdConnectUrl: https://id.deadsimple.email/.well-known/openid-configuration
description: 'Sign in with Dead Simple. Relying parties use this to authenticate an agent by its inbox; it is not used to call the Dead Simple API itself. Scopes: openid, email, profile (open clients); owner_email, org, offline_access (registered clients only).'