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 Provider 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 Provider
paths:
/.well-known/openid-configuration:
servers:
- url: https://id.deadsimple.email
description: Identity provider
get:
summary: OpenID Connect discovery
description: Issuer, endpoint URLs, supported scopes and claims, ES256, S256 PKCE, and the non-standard `agent_authentication_methods_supported` (api_key, assertion, browser). Cached one hour. Note that the same path on api.deadsimple.email returns the MCP OAuth server's metadata instead.
security: []
tags:
- Identity Provider
responses:
'200':
description: The provider metadata document.
operationId: getWellKnownOpenidConfiguration
x-operation-id-source: derived
/.well-known/jwks.json:
servers:
- url: https://id.deadsimple.email
description: Identity provider
get:
summary: JSON Web Key Set
description: The ES256 (P-256) public key that signs id_tokens. Cached one hour.
security: []
tags:
- Identity Provider
responses:
'200':
description: '{ keys: [ { kty: EC, crv: P-256, alg: ES256, use: sig, kid, x, y } ] }'
operationId: getWellKnownJwksJson
x-operation-id-source: derived
/authorize:
servers:
- url: https://id.deadsimple.email
description: Identity provider
get:
summary: Authorization endpoint (authorization-code + PKCE)
description: 'Three ways an agent authenticates, checked in this order.
1. **API key**: send `Authorization: Bearer dse_...` plus `inbox_id=` (or `login_hint=`; both may be omitted when the account has exactly one inbox). The code is minted immediately.
2. **Signed assertion**: `assertion=` an ES256 JWS over `{iss: inbox_id, sub: inbox_id, aud: "https://id.deadsimple.email", iat, exp (at most 10 minutes after iat), jti}` with a `kid` header naming a key enrolled on the inbox. Optional `client_id`, `redirect_uri`, `code_challenge` and `nonce` claims must match the request. Each `jti` is accepted once.
3. **Browser**: no credential at all redirects to `https://app.deadsimple.email/id/consent?request_id=...`, where the account owner picks an inbox and approves.
Success is a 302 to `redirect_uri` with `code` and `state`, or, when the request sends `Accept: application/json`, a 200 JSON body `{redirect_to, code, state}`. A JSON client with no credential gets `401 login_required` rather than a redirect. An unresolvable client_id or a redirect_uri that is not same-origin (open client) or not registered gets a 400 with no redirect.'
security: []
tags:
- Identity Provider
parameters:
- name: client_id
in: query
required: true
schema:
type: string
description: A registered dse_idc_ id, or (open client) an https URL sharing its origin with redirect_uri.
- name: redirect_uri
in: query
required: true
schema:
type: string
- name: response_type
in: query
required: true
schema:
type: string
enum:
- code
- name: scope
in: query
required: false
schema:
type: string
default: openid
description: openid required. Open clients may add email and profile; registered clients may also add owner_email, org, offline_access.
- name: code_challenge
in: query
required: true
schema:
type: string
- name: code_challenge_method
in: query
required: false
schema:
type: string
enum:
- S256
- name: state
in: query
required: false
schema:
type: string
- name: nonce
in: query
required: false
schema:
type: string
- name: inbox_id
in: query
required: false
schema:
type: string
description: Which inbox signs in (API-key path).
- name: login_hint
in: query
required: false
schema:
type: string
description: The inbox email address, as an alternative to inbox_id.
- name: assertion
in: query
required: false
schema:
type: string
description: 'ES256 JWS signed by an enrolled key (assertion path). Also accepted as `Authorization: Bearer <jws>`.'
responses:
'302':
description: Redirect to redirect_uri with code and state (or error and state), or to the consent page.
'200':
description: With Accept application/json, the redirect as data.
content:
application/json:
schema:
type: object
properties:
redirect_to:
type: string
code:
type: string
state:
type: string
'400':
description: invalid_client (never redirected), or a validation error from the API-key path.
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
'401':
description: login_required (JSON client, no credential), or an assertion that failed verification.
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
'404':
description: The API key does not own the requested inbox.
operationId: getAuthorize
x-operation-id-source: derived
post:
summary: Authorization endpoint (form or JSON body)
description: Identical to GET; parameters may be sent as application/x-www-form-urlencoded or JSON, which keeps a signed assertion out of the URL. Query and body parameters are merged.
security: []
tags:
- Identity Provider
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
client_id:
type: string
redirect_uri:
type: string
response_type:
type: string
enum:
- code
scope:
type: string
code_challenge:
type: string
code_challenge_method:
type: string
enum:
- S256
state:
type: string
nonce:
type: string
inbox_id:
type: string
login_hint:
type: string
assertion:
type: string
application/json:
schema:
type: object
responses:
'302':
description: As for GET.
'200':
description: As for GET with Accept application/json.
'400':
description: As for GET.
'401':
description: As for GET.
operationId: postAuthorize
x-operation-id-source: derived
/token:
servers:
- url: https://id.deadsimple.email
description: Identity provider
post:
summary: Token endpoint
description: 'Exchanges an authorization code (with `code_verifier`) or a refresh token. Codes are single use and live 5 minutes; a failed exchange burns the code. Client authentication: `client_secret_basic`, `client_secret_post`, or `none` (open clients and registered public clients). Registered clients, and any grant that included `offline_access`, receive a refresh token (30 days, rotated on every use; a reused refresh token is rejected). Accepts application/x-www-form-urlencoded or JSON.'
security: []
tags:
- Identity Provider
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
required:
- grant_type
properties:
grant_type:
type: string
enum:
- authorization_code
- refresh_token
code:
type: string
code_verifier:
type: string
redirect_uri:
type: string
description: If sent, must match the authorization request.
refresh_token:
type: string
client_id:
type: string
client_secret:
type: string
description: For client_secret_post. Basic auth is also accepted.
responses:
'200':
description: Tokens issued.
content:
application/json:
schema:
$ref: '#/components/schemas/IdentityTokenResponse'
'400':
description: invalid_request, invalid_grant (bad or reused code, PKCE mismatch, redirect_uri mismatch), or unsupported_grant_type.
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
'401':
description: invalid_client. Registered clients that require a secret get a WWW-Authenticate Basic challenge.
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
operationId: postToken
x-operation-id-source: derived
/userinfo:
servers:
- url: https://id.deadsimple.email
description: Identity provider
get:
summary: UserInfo endpoint
description: 'Send `Authorization: Bearer dse_idt_...`. Returns the claims for the granted scopes. This is the only place `owner_email` is served, and only to registered clients that were granted the owner_email scope.'
security: []
tags:
- Identity Provider
responses:
'200':
description: Claims for the agent inbox.
content:
application/json:
schema:
$ref: '#/components/schemas/IdentityClaims'
'401':
description: invalid_token, with a WWW-Authenticate Bearer challenge.
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
operationId: getUserinfo
x-operation-id-source: derived
post:
summary: UserInfo endpoint (POST)
description: Same as GET.
security: []
tags:
- Identity Provider
responses:
'200':
description: Claims for the agent inbox.
content:
application/json:
schema:
$ref: '#/components/schemas/IdentityClaims'
'401':
description: invalid_token.
operationId: postUserinfo
x-operation-id-source: derived
/register:
servers:
- url: https://id.deadsimple.email
description: Identity provider
post:
summary: Dynamic client registration (RFC 7591)
description: 'Registers a relying party that may request `owner_email`, `org` and `offline_access`. Unlike the MCP OAuth server''s /oauth/register, this endpoint requires a Dead Simple API key (`Authorization: Bearer dse_...`) with the api_keys.create permission: trial keys from agent self-signup and MCP OAuth tokens are refused, so an anonymous party cannot mint a client that sees owner data. The client_secret is shown once. The same registration with the standard API envelope is available at POST /v1/identity/clients.'
tags:
- Identity Provider
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/IdentityClientRegistration'
responses:
'201':
description: client_id (dse_idc_) and, unless token_endpoint_auth_method is none, client_secret (dse_ics_).
content:
application/json:
schema:
$ref: '#/components/schemas/IdentityClientCreated'
'400':
description: invalid_client_metadata.
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
'401':
description: No API key.
'403':
description: Key lacks api_keys.create (trial keys), or an MCP OAuth token was used.
operationId: postRegister
x-operation-id-source: derived
/revoke:
servers:
- url: https://id.deadsimple.email
description: Identity provider
post:
summary: Token revocation (RFC 7009)
description: Revokes a dse_idt_ access token or a dse_idr_ refresh token. Always returns 200.
security: []
tags:
- Identity Provider
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
required:
- token
properties:
token:
type: string
responses:
'200':
description: Token revoked (or was already unknown).
operationId: postRevoke
x-operation-id-source: derived
components:
schemas:
OAuthError:
type: object
description: RFC 6749 error shape returned by the identity provider endpoints.
properties:
error:
type: string
example: invalid_grant
error_description:
type: string
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
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
IdentityClaims:
type: object
description: Claims in the id_token and from /userinfo. Which ones appear depends on the granted scopes and on whether the client is registered.
properties:
sub:
type: string
description: The inbox_id. Stable for the life of the inbox; use it as the user id.
inbox_id:
type: string
agent:
type: boolean
description: Always true.
owner_verified:
type: boolean
description: True when a verified human owns the account behind the inbox; false for unclaimed agent-signup (trial) accounts.
email:
type: string
description: The inbox address (scope email).
email_verified:
type: boolean
description: Always true (scope email).
name:
type: string
description: Inbox display name, falling back to the local part (scope profile).
preferred_username:
type: string
description: Local part of the inbox address (scope profile).
org:
type: string
description: Account id that owns the inbox (scope org, registered clients).
plan:
type: string
description: Account plan (scope org, registered clients).
enum:
- trial
- free
- hobby
- pro
- scale
- enterprise
owner_email:
type: string
nullable: true
description: Verified owner email (scope owner_email, registered clients). Returned from /userinfo only, never inside the id_token. Null when owner_verified is false.
IdentityTokenResponse:
type: object
properties:
access_token:
type: string
description: Opaque dse_idt_ token for /userinfo. Lives 1 hour.
token_type:
type: string
example: Bearer
expires_in:
type: integer
example: 3600
id_token:
type: string
description: ES256 JWT, verify against /.well-known/jwks.json. Lives 1 hour.
scope:
type: string
refresh_token:
type: string
description: dse_idr_ token, 30 days, rotated on every use. Only for registered clients or when offline_access was granted.
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).'