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 Identity 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: Identity
paths:
/v1/identity/clients:
get:
summary: List registered identity clients
description: Relying parties this account registered for Sign in with Dead Simple. Secrets are never returned.
tags:
- Identity
responses:
'200':
description: Clients.
content:
application/json:
schema:
type: object
properties:
clients:
type: array
items:
$ref: '#/components/schemas/IdentityClient'
total:
type: integer
operationId: getV1IdentityClients
x-operation-id-source: derived
post:
summary: Register an identity client
description: Same registration as POST https://id.deadsimple.email/register, wrapped in the standard API envelope for the dashboard and SDKs (`identity.register_client` / `identity.registerClient`). Requires the api_keys.create permission.
tags:
- Identity
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/IdentityClientRegistration'
responses:
'201':
description: Client created. client_secret is shown once.
content:
application/json:
schema:
$ref: '#/components/schemas/IdentityClientCreated'
'400':
description: Validation error (redirect_uris missing or malformed, unsupported auth method).
'403':
description: Key lacks api_keys.create, or an MCP OAuth token was used.
operationId: postV1IdentityClients
x-operation-id-source: derived
/v1/identity/clients/{client_id}:
delete:
summary: Revoke an identity client
description: The client can no longer start or complete sign-ins.
tags:
- Identity
parameters:
- name: client_id
in: path
required: true
schema:
type: string
responses:
'204':
description: Revoked.
'404':
description: Client not found or not owned by this account.
operationId: deleteV1IdentityClientsByClientId
x-operation-id-source: derived
/v1/identity/request/{request_id}:
get:
summary: Describe a pending browser sign-in request (dashboard consent)
description: Used by the consent page at app.deadsimple.email/id/consent. Requires a dashboard session; API keys get 403. A request is valid for 10 minutes and is consumed by /v1/identity/approve.
tags:
- Identity
parameters:
- name: request_id
in: path
required: true
schema:
type: string
responses:
'200':
description: The relying party asking for sign-in.
content:
application/json:
schema:
type: object
properties:
request_id:
type: string
client_id:
type: string
client_name:
type: string
client_uri:
type: string
registered:
type: boolean
redirect_host:
type: string
scope:
type: string
inbox_hint:
type: string
'403':
description: Not a dashboard session.
'404':
description: Request expired or already handled.
operationId: getV1IdentityRequestByRequestId
x-operation-id-source: derived
/v1/identity/approve:
post:
summary: Approve or deny a browser sign-in request (dashboard consent)
description: 'Requires a dashboard session; API keys get 403. Consumes the request and returns the URL the browser should be sent to: redirect_uri with a code (approve) or with error=access_denied (deny).'
tags:
- Identity
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- request_id
- approve
properties:
request_id:
type: string
inbox_id:
type: string
description: Required when approving. Must belong to the session's account.
approve:
type: boolean
responses:
'201':
description: '{ redirect_to }'
'403':
description: Not a dashboard session.
'404':
description: Request expired or already handled.
operationId: postV1IdentityApprove
x-operation-id-source: derived
/v1/inboxes/{inbox_id}/identity/keys:
get:
summary: List enrolled agent keys
description: Public keys that may sign assertions for this inbox, with last_used_at per key.
tags:
- Identity
parameters:
- name: inbox_id
in: path
required: true
schema:
type: string
responses:
'200':
description: Keys.
content:
application/json:
schema:
type: object
properties:
keys:
type: array
items:
$ref: '#/components/schemas/IdentityKey'
total:
type: integer
operationId: getV1InboxesByInboxIdIdentityKeys
x-operation-id-source: derived
post:
summary: Enroll an agent public key
description: 'Enrolls a P-256 (secp256r1) public key so the agent can sign in with an ES256 assertion instead of an API key. Accepts a JWK object, a JWK string, or a PEM. A JWK carrying a private component (d) or a key on any other curve is rejected. The same key cannot be enrolled on two inboxes. SDK: `identity.enroll_key` / `identity.enrollKey`.'
tags:
- Identity
parameters:
- name: inbox_id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- public_key
properties:
public_key:
oneOf:
- type: object
description: JWK with kty EC, crv P-256, x, y
- type: string
description: A JWK as a JSON string, or a PEM public key
name:
type: string
maxLength: 80
description: Label, for example "worker-7".
responses:
'201':
description: Key enrolled. Use kid in the assertion's JWS header.
content:
application/json:
schema:
$ref: '#/components/schemas/IdentityKey'
'400':
description: Not a valid P-256 public key, or private material was sent.
'409':
description: This public key is already enrolled on another inbox.
operationId: postV1InboxesByInboxIdIdentityKeys
x-operation-id-source: derived
/v1/inboxes/{inbox_id}/identity/keys/{kid}:
delete:
summary: Delete an agent key
description: Assertions signed by this key are refused from now on. SDK `identity.delete_key` / `identity.deleteKey`.
tags:
- Identity
parameters:
- name: inbox_id
in: path
required: true
schema:
type: string
- name: kid
in: path
required: true
schema:
type: string
responses:
'204':
description: Deleted.
'404':
description: Key not found on this inbox.
operationId: deleteV1InboxesByInboxIdIdentityKeysByKid
x-operation-id-source: derived
/v1/inboxes/{inbox_id}/identity/connections:
get:
summary: List apps this inbox has signed in to
description: One row per relying party, with scope, first and last sign-in, and a count. SDK `identity.connections`.
tags:
- Identity
parameters:
- name: inbox_id
in: path
required: true
schema:
type: string
responses:
'200':
description: Connections, most recent first.
content:
application/json:
schema:
type: object
properties:
connections:
type: array
items:
$ref: '#/components/schemas/IdentityConnection'
total:
type: integer
operationId: getV1InboxesByInboxIdIdentityConnections
x-operation-id-source: derived
/v1/inboxes/{inbox_id}/identity/sign-in:
post:
summary: One-call sign-in helper
description: Hand over the relying party's authorization URL (the `https://id.deadsimple.email/authorize?...` link a browser would be sent to) and get back the URL to GET to finish signing in as this inbox. Validates the client, redirect_uri, PKCE and scopes exactly as /authorize does, then mints the code. Exposed as `identity.sign_in(inbox_id, authorization_url)` in the Python SDK, `identity.signIn(inboxId, authorizationUrl)` in the Node SDK, and the `sign_in_with_inbox` MCP tool.
tags:
- Identity
parameters:
- name: inbox_id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- authorization_url
properties:
authorization_url:
type: string
description: Must start with https://id.deadsimple.email/authorize and carry response_type=code and a code_challenge.
responses:
'201':
description: The redirect to follow. The code inside it lives 5 minutes.
content:
application/json:
schema:
$ref: '#/components/schemas/IdentitySignInResult'
'400':
description: Not our authorize URL, unknown client or unregistered redirect_uri, missing PKCE, or an unsupported scope.
'404':
description: Inbox not found on this account.
operationId: postV1InboxesByInboxIdIdentitySignIn
x-operation-id-source: derived
components:
schemas:
IdentityClient:
type: object
properties:
client_id:
type: string
example: dse_idc_3f9a1c77b2e04d5a6b8c
client_name:
type: string
client_uri:
type: string
logo_uri:
type: string
redirect_uris:
type: array
items:
type: string
token_endpoint_auth_method:
type: string
enum:
- none
- client_secret_basic
- client_secret_post
scope:
type: string
description: Space-separated scopes the client may request.
grant_types:
type: array
items:
type: string
response_types:
type: array
items:
type: string
created_at:
type: string
format: date-time
IdentityClientCreated:
allOf:
- $ref: '#/components/schemas/IdentityClient'
- type: object
properties:
client_secret:
type: string
description: dse_ics_ secret. Shown once; stored hashed. Absent when token_endpoint_auth_method is none.
client_secret_expires_at:
type: integer
description: 0 means the secret does not expire.
client_id_issued_at:
type: integer
warning:
type: string
issuer:
type: string
example: https://id.deadsimple.email
discovery_url:
type: string
example: https://id.deadsimple.email/.well-known/openid-configuration
IdentityConnection:
type: object
properties:
client_id:
type: string
client_name:
type: string
client_uri:
type: string
scope:
type: string
first_sign_in_at:
type: string
format: date-time
last_sign_in_at:
type: string
format: date-time
sign_in_count:
type: integer
IdentitySignInResult:
type: object
properties:
redirect_to:
type: string
description: The relying party's redirect_uri with code (and state) appended. GET it, following redirects.
client_id:
type: string
client_name:
type: string
scope:
type: string
expires_in:
type: integer
description: Seconds until the authorization code expires (300).
next_step:
type: string
IdentityClientRegistration:
type: object
required:
- redirect_uris
properties:
client_name:
type: string
client_uri:
type: string
logo_uri:
type: string
policy_uri:
type: string
tos_uri:
type: string
redirect_uris:
type: array
maxItems: 20
items:
type: string
description: https, loopback http, or app-scheme URLs.
scope:
type: string
default: openid email profile owner_email org
description: Any of openid, email, profile, owner_email, org, offline_access. openid is always included.
token_endpoint_auth_method:
type: string
enum:
- none
- client_secret_basic
- client_secret_post
default: client_secret_basic
IdentityKey:
type: object
properties:
kid:
type: string
description: RFC 7638 thumbprint prefix. Put it in the assertion's JWS header.
inbox_id:
type: string
email:
type: string
name:
type: string
jwk:
type: object
description: The enrolled P-256 public key (kty EC, crv P-256, x, y, kid).
created_at:
type: string
format: date-time
last_used_at:
type: string
format: date-time
nullable: true
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).'