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 Webhooks 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: Webhooks
paths:
/v1/webhooks:
post:
summary: Register webhook
tags:
- Webhooks
parameters:
- $ref: '#/components/parameters/WorkspaceHeader'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- url
- events
properties:
url:
type: string
format: uri
events:
type: array
items:
type: string
enum:
- message.received
- message.sent
- message.delivered
- message.bounced
- message.complained
- message.rejected
- message.opened
- message.link_clicked
- inbox.created
- inbox.deleted
- domain.verified
description:
type: string
headers:
type: object
additionalProperties:
type: string
maxProperties: 10
description: 'Optional custom headers sent on every delivery attempt and every retry — for endpoints that enforce their own auth (Bearer tokens, gateway keys). Values are write-only and are masked in every response. Reserved names (Content-Type, Host, User-Agent, hop-by-hop headers, and any `X-DSE-*` header) are rejected, as are values with control characters or non-ASCII bytes. Max 10 headers, 1024 characters per value.
'
endpoint_secret:
type: string
pattern: ^whsec_[A-Za-z0-9+/]+={0,2}$
description: 'The receiving service''s own Standard Webhooks secret (`whsec_...`), for endpoints that verify every request against it, such as Grok Automations. Each delivery then also carries `webhook-id` (the delivery id, stable across retries), `webhook-timestamp` and `webhook-signature` (`v1,<base64 HMAC-SHA256 of "id.timestamp.body">`) per standardwebhooks.com. Such receivers answer an unsigned request with HTTP 400. Write-only. A `whsec_` value supplied as `Authorization: Bearer whsec_...` in headers is treated the same way and not sent as a header.
'
inbox_ids:
type: array
maxItems: 100
items:
type: string
format: uuid
description: 'Optional inbox scope. A webhook with inbox_ids fires only for events whose payload names one of those inboxes (so never for inbox-less events such as domain.verified). Every id must belong to the account. Omit or pass an empty list for an account-wide webhook. An inbox-scoped API key may only register webhooks for its own inboxes and gets them by default.
'
example:
url: https://your-app.com/webhooks/email
events:
- message.received
description: Notify on new inbound email
inbox_ids:
- b1a7f2c0-3f44-4c2b-9d0e-6a1e2f3c4d5e
headers:
Authorization: Bearer eyJhbGciOi...
responses:
'201':
description: Webhook created with signing secret (shown once)
operationId: postV1Webhooks
x-operation-id-source: derived
get:
summary: List webhooks
tags:
- Webhooks
parameters:
- $ref: '#/components/parameters/WorkspaceHeader'
- name: workspace_id
in: query
required: false
schema:
type: string
format: uuid
description: Filter webhooks by workspace
responses:
'200':
description: List of webhooks
operationId: getV1Webhooks
x-operation-id-source: derived
/v1/webhooks/{webhook_id}:
get:
summary: Get webhook
tags:
- Webhooks
parameters:
- name: webhook_id
in: path
required: true
schema:
type: string
responses:
'200':
description: Webhook (signing secret never returned; header values masked)
content:
application/json:
schema:
$ref: '#/components/schemas/Webhook'
'404':
description: No such webhook on this account
operationId: getV1WebhooksByWebhookId
x-operation-id-source: derived
patch:
summary: Update webhook
description: 'Partial update. Any of url, events, description, inbox_ids, is_active and endpoint_secret may be supplied; unknown fields are ignored and at least one known field is required. `inbox_ids: []` clears the inbox scope. Setting `is_active` to true also resets the consecutive-failure counter, so an auto-disabled endpoint starts over with a full disable budget.'
tags:
- Webhooks
parameters:
- name: webhook_id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
url:
type: string
format: uri
events:
type: array
items:
type: string
description:
type: string
inbox_ids:
type: array
maxItems: 100
items:
type: string
format: uuid
is_active:
type: boolean
endpoint_secret:
type: string
pattern: ^whsec_[A-Za-z0-9+/]+={0,2}$
description: 'The receiving service''s own Standard Webhooks secret (`whsec_...`), for endpoints that verify every request against it, such as Grok Automations. Each delivery then also carries `webhook-id` (the delivery id, stable across retries), `webhook-timestamp` and `webhook-signature` (`v1,<base64 HMAC-SHA256 of "id.timestamp.body">`) per standardwebhooks.com. Such receivers answer an unsigned request with HTTP 400. Write-only. A `whsec_` value supplied as `Authorization: Bearer whsec_...` in headers is treated the same way and not sent as a header. `null` removes it.
'
example:
inbox_ids:
- b1a7f2c0-3f44-4c2b-9d0e-6a1e2f3c4d5e
is_active: true
responses:
'200':
description: Updated webhook
content:
application/json:
schema:
$ref: '#/components/schemas/Webhook'
'400':
description: Invalid field, unknown inbox, or nothing to update
operationId: patchV1WebhooksByWebhookId
x-operation-id-source: derived
delete:
summary: Delete webhook
tags:
- Webhooks
parameters:
- name: webhook_id
in: path
required: true
schema:
type: string
- $ref: '#/components/parameters/WorkspaceHeader'
responses:
'204':
description: Webhook deleted
operationId: deleteV1WebhooksByWebhookId
x-operation-id-source: derived
/v1/webhooks/{webhook_id}/config:
get:
summary: Get webhook retry configuration
tags:
- Webhooks
parameters:
- name: webhook_id
in: path
required: true
schema:
type: string
responses:
'200':
description: Retry/backoff configuration
operationId: getV1WebhooksByWebhookIdConfig
x-operation-id-source: derived
put:
summary: Update webhook retry configuration
description: Tune delivery reliability. `retry_schedule` is the exponential-backoff delay (seconds) before each retry; failed deliveries are retried by the webhook-retry processor until `max_retries` is reached. Endpoints that fail `disable_on_failures` times in a row are auto-disabled.
tags:
- Webhooks
parameters:
- name: webhook_id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
max_retries:
type: integer
minimum: 0
maximum: 10
retry_schedule:
type: array
items:
type: integer
timeout_seconds:
type: integer
minimum: 5
maximum: 60
disable_on_failures:
type: integer
minimum: 0
example:
max_retries: 6
retry_schedule:
- 1
- 30
- 300
- 1800
- 7200
- 86400
timeout_seconds: 30
disable_on_failures: 50
responses:
'200':
description: Updated configuration
operationId: putV1WebhooksByWebhookIdConfig
x-operation-id-source: derived
/v1/webhooks/{webhook_id}/headers:
get:
summary: Get webhook custom delivery headers
description: Header names installed on the webhook. Values are masked.
tags:
- Webhooks
parameters:
- name: webhook_id
in: path
required: true
schema:
type: string
responses:
'200':
description: Custom headers (values masked)
operationId: getV1WebhooksByWebhookIdHeaders
x-operation-id-source: derived
put:
summary: Replace webhook custom delivery headers
description: 'Replaces the webhook''s custom headers wholesale; send `{"headers": {}}` to clear them. These headers are sent on every delivery attempt and every retry, so an endpoint that requires its own `Authorization` header can be used without polling. Values are write-only — responses mask them.'
tags:
- Webhooks
parameters:
- name: webhook_id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- headers
properties:
headers:
type: object
additionalProperties:
type: string
maxProperties: 10
example:
headers:
Authorization: Bearer eyJhbGciOi...
responses:
'200':
description: Updated headers (values masked)
operationId: putV1WebhooksByWebhookIdHeaders
x-operation-id-source: derived
/v1/webhooks/{webhook_id}/deliveries:
get:
summary: List webhook deliveries
description: Delivery log for a webhook (newest first). Each entry records the event, status (pending/delivered/failed), attempt count, last HTTP status and error, and next scheduled retry.
tags:
- Webhooks
parameters:
- name: webhook_id
in: path
required: true
schema:
type: string
- name: limit
in: query
required: false
schema:
type: integer
default: 50
maximum: 200
responses:
'200':
description: Recent delivery attempts
operationId: getV1WebhooksByWebhookIdDeliveries
x-operation-id-source: derived
/v1/webhooks/{webhook_id}/deliveries/{delivery_id}:
get:
summary: Get a webhook delivery
description: A single delivery record, including the payload that was (or will be) sent.
tags:
- Webhooks
parameters:
- name: webhook_id
in: path
required: true
schema:
type: string
- name: delivery_id
in: path
required: true
schema:
type: string
responses:
'200':
description: Delivery record
operationId: getV1WebhooksByWebhookIdDeliveriesByDeliveryId
x-operation-id-source: derived
/v1/webhooks/{webhook_id}/deliveries/{delivery_id}/replay:
post:
summary: Replay a webhook delivery
description: Re-attempt a delivery immediately, e.g. after fixing a broken endpoint. The retry budget is reset so the full backoff schedule is available again.
tags:
- Webhooks
parameters:
- name: webhook_id
in: path
required: true
schema:
type: string
- name: delivery_id
in: path
required: true
schema:
type: string
responses:
'200':
description: Delivery re-attempted
operationId: postV1WebhooksByWebhookIdDeliveriesByDeliveryIdReplay
x-operation-id-source: derived
/v1/event-subscriptions:
post:
summary: Create or refresh an MCP Events subscription
description: 'Backs `events/subscribe` on the hosted MCP server (OpenAI''s MCP Events, used by ChatGPT Work chats and dots). Idempotent: the subscription id is derived from the account, callback URL, event name and arguments, so the same request refreshes the same subscription. A new subscription, or one whose secret changed, is activated only after the callback answers a signed `{"type":"verification","challenge":...}` POST by echoing the challenge. Events are then POSTed as `{eventId, name, timestamp, data, cursor}` with Standard Webhooks headers (`webhook-id` = eventId, stable across retries) and `X-MCP-Subscription-Id`. 410 and 413 responses are not retried; a 410 deletes the subscription. Field names follow the MCP Events spec.'
tags:
- Webhooks
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- name
- delivery
properties:
name:
type: string
enum:
- message.received
- message.bounced
arguments:
type: object
additionalProperties: false
description: 'Filters. `inbox_id` (both events; omitted = every inbox the credential can see, required for inbox-scoped keys), `from_contains` and `subject_contains` (message.received only, case-insensitive substrings).
'
properties:
inbox_id:
type: string
from_contains:
type: string
maxLength: 200
subject_contains:
type: string
maxLength: 200
delivery:
type: object
required:
- url
- secret
properties:
mode:
type: string
enum:
- webhook
url:
type: string
format: uri
description: HTTPS callback; must resolve to public addresses.
secret:
type: string
description: Standard Webhooks secret, `whsec_` + base64 of 24-64 bytes.
ttlMs:
type:
- number
- 'null'
description: 'Requested lifetime. Omitted = 30 days; capped at 90 days; null (no expiry) is granted as 90 days.
'
cursor:
description: Accepted and ignored; these events are not replayable.
responses:
'201':
description: Subscription active
content:
application/json:
schema:
type: object
properties:
id:
type: string
example: sub_3f0c2a9d4b1e8f7a6c5d4e3f2a1b0c9d
refreshBefore:
type: string
format: date-time
cursor:
type: 'null'
truncated:
type: boolean
'400':
description: Unknown event, unknown argument, bad secret or callback URL
'422':
description: '`callback_verification_failed`: the callback did not echo the challenge. `details[0].reason` is `challenge_failed` or `timeout`.
'
operationId: postV1EventSubscriptions
x-operation-id-source: derived
get:
summary: List MCP Events subscriptions
tags:
- Webhooks
responses:
'200':
description: The account's subscriptions (callback host only, never the secret)
operationId: getV1EventSubscriptions
x-operation-id-source: derived
/v1/event-subscriptions/unsubscribe:
post:
summary: Remove an MCP Events subscription by identity
description: Backs `events/unsubscribe`. Idempotent; removing one that does not exist succeeds.
tags:
- Webhooks
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- name
- delivery
properties:
name:
type: string
arguments:
type: object
delivery:
type: object
properties:
url:
type: string
format: uri
responses:
'201':
description: Removed (empty object)
operationId: postV1EventSubscriptionsUnsubscribe
x-operation-id-source: derived
/v1/event-subscriptions/{subscription_id}:
delete:
summary: Delete an MCP Events subscription
tags:
- Webhooks
parameters:
- name: subscription_id
in: path
required: true
schema:
type: string
responses:
'200':
description: Deleted
'404':
description: No such subscription on this account
operationId: deleteV1EventSubscriptionsBySubscriptionId
x-operation-id-source: derived
components:
parameters:
WorkspaceHeader:
name: X-Workspace-Id
in: header
required: false
schema:
type: string
format: uuid
description: Target a specific workspace. Required for account-wide API keys that want workspace-scoped results.
schemas:
Webhook:
type: object
properties:
webhook_id:
type: string
format: uuid
workspace_id:
type: string
format: uuid
url:
type: string
format: uri
events:
type: array
items:
type: string
enum:
- message.received
- message.sent
- message.delivered
- message.bounced
- message.complained
- message.rejected
- message.opened
- message.link_clicked
- inbox.created
- inbox.deleted
- domain.verified
is_active:
type: boolean
headers:
type: object
additionalProperties:
type: string
description: Custom delivery headers, values masked (write-only).
endpoint_secret_set:
type: boolean
description: 'True when deliveries are also signed with the receiver''s own Standard Webhooks secret (see endpoint_secret on create). The secret itself is write-only and never returned.
'
inbox_ids:
type: array
items:
type: string
format: uuid
description: Inboxes this webhook is restricted to. Empty means every inbox.
created_at:
type: string
format: date-time
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).'