Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions api-reference/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@
"domain": {
"type": "string",
"description": "Domain for the inbox address. Defaults to your account domain (e.g. `openmail.sh`). Pass a verified custom domain you own to create the inbox on it (e.g. `agent-mail.example.com`). The domain must already be added and verified for your account; the account default never switches automatically."
},
"webhookUrl": {
"type": "string",
"description": "Optional HTTPS URL to receive inbound events for this inbox. When set, OpenMail generates a per-inbox signing secret (returned once in the response). Falls back to the account webhook when omitted."
}
}
}
Expand Down Expand Up @@ -214,6 +218,11 @@
"nullable": true,
"maxLength": 200,
"description": "Sender display name. Set null to clear."
},
"webhookUrl": {
"type": "string",
"nullable": true,
"description": "Per-inbox webhook URL (HTTPS). Set a value to point this inbox at its own endpoint (a signing secret is generated and returned once on first set). Set null or an empty string to clear it and fall back to the account webhook."
}
}
}
Expand All @@ -240,6 +249,16 @@
}
}
}
},
"422": {
"description": "Invalid webhook URL (failed SSRF/format validation)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
},
Expand Down Expand Up @@ -273,6 +292,127 @@
}
}
},
"/v1/inboxes/{id}/webhook/rotate-secret": {
"post": {
"summary": "Rotate inbox webhook secret",
"description": "Generate a new HMAC signing secret for the inbox webhook. The inbox must already have a webhook URL set. The new secret is returned once.",
"operationId": "rotateInboxWebhookSecret",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "New secret",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"webhookSecret": {
"type": "string"
}
}
}
}
}
},
"400": {
"description": "No webhook URL set on this inbox",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/v1/inboxes/{id}/webhook/test": {
"post": {
"summary": "Send a test webhook",
"description": "Send a signed `setup.test` event to the inbox webhook URL to verify connectivity and signature handling.",
"operationId": "testInboxWebhook",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Test delivered",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"message": {
"type": "string"
}
}
}
}
}
},
"400": {
"description": "No webhook URL set on this inbox",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"502": {
"description": "The webhook endpoint did not accept the test event",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/v1/inboxes/{id}/send": {
"post": {
"summary": "Send email",
Expand Down Expand Up @@ -844,6 +984,15 @@
"description": "Sender display name",
"nullable": true
},
"webhookUrl": {
"type": "string",
"nullable": true,
"description": "Per-inbox webhook URL. When set, inbound events for this inbox are delivered here instead of the account webhook. Null means the account-level webhook is used."
},
"webhookSecret": {
"type": "string",
"description": "HMAC signing secret for the inbox webhook. Returned only in the response that creates or rotates it — store it securely; it is not returned again."
},
"createdAt": {
"type": "string",
"format": "date-time"
Expand Down
2 changes: 2 additions & 0 deletions concepts/inboxes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ Route inbound events by `inbox_id`. Map each inbox ID to your user/agent/contain

**Multiple inboxes per user** - You can create multiple inboxes for the same user. Webhooks include `inbox_id` so you can identify which inbox received the message.

**Per-inbox webhooks** - Instead of routing every event through one account webhook, you can give each inbox its own `webhookUrl` so each agent receives mail on its own endpoint. See [Webhooks](/concepts/webhooks#account-vs-per-inbox-webhooks).

<Tip>
See [Email deliverability](/best-practices/email-deliverability) for warm-up schedules and content best practices.
</Tip>
Expand Down
27 changes: 27 additions & 0 deletions concepts/webhooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,33 @@ Instead of constantly polling the API to check for new emails, you register a UR
- **Real-time** - Build agents that reply to emails in seconds.
- **Efficient** - No polling; saves compute and simplifies your logic.

## Account vs. per-inbox webhooks

You can configure webhooks at two levels:

- **Account webhook** - A single URL for your whole account, set in **Settings**. Every inbox without its own webhook delivers here.
- **Per-inbox webhook** - A URL set on an individual inbox. Useful when you provision many inboxes (for example one per agent) and want each to receive its own mail on its own endpoint.

OpenMail resolves the delivery target for each event in this order:

1. An active real-time WebSocket connection for the account
2. The inbox's own `webhookUrl` (if set)
3. The account `webhookUrl` (fallback)

Each webhook is signed with the secret that belongs to the URL it is delivered to. Set a per-inbox webhook when creating an inbox or by updating it:

```bash
# Create an inbox with its own webhook
curl -X POST https://api.openmail.sh/v1/inboxes \
-H "Authorization: Bearer $OPENMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "mailboxName": "agent-42", "webhookUrl": "https://hooks.example.com/agent-42" }'

# The response includes a one-time webhookSecret — store it.
```

Clear a per-inbox webhook (revert to the account webhook) by setting it to `null` via `PATCH /v1/inboxes/{id}`. Rotate its secret with `POST /v1/inboxes/{id}/webhook/rotate-secret`, and verify connectivity with `POST /v1/inboxes/{id}/webhook/test`.

## Delivery semantics

- **At-least-once delivery** - We may deliver the same event more than once. Use `event_id` to deduplicate.
Expand Down
93 changes: 93 additions & 0 deletions scripts/generate-openapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ const spec = {
description:
"Domain for the inbox address. Defaults to your account domain (e.g. `openmail.sh`). Pass a verified custom domain you own to create the inbox on it (e.g. `agent-mail.example.com`). The domain must already be added and verified for your account; the account default never switches automatically.",
},
webhookUrl: {
type: "string",
description:
"Optional HTTPS URL to receive inbound events for this inbox. When set, OpenMail generates a per-inbox signing secret (returned once in the response). Falls back to the account webhook when omitted.",
},
},
},
},
Expand Down Expand Up @@ -144,6 +149,12 @@ const spec = {
maxLength: 200,
description: "Sender display name. Set null to clear.",
},
webhookUrl: {
type: "string",
nullable: true,
description:
"Per-inbox webhook URL (HTTPS). Set a value to point this inbox at its own endpoint (a signing secret is generated and returned once on first set). Set null or an empty string to clear it and fall back to the account webhook.",
},
},
},
},
Expand All @@ -158,6 +169,10 @@ const spec = {
description: "Not found",
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } },
},
"422": {
description: "Invalid webhook URL (failed SSRF/format validation)",
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } },
},
},
},
delete: {
Expand All @@ -173,6 +188,73 @@ const spec = {
},
},
},
"/v1/inboxes/{id}/webhook/rotate-secret": {
post: {
summary: "Rotate inbox webhook secret",
description:
"Generate a new HMAC signing secret for the inbox webhook. The inbox must already have a webhook URL set. The new secret is returned once.",
operationId: "rotateInboxWebhookSecret",
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
responses: {
"200": {
description: "New secret",
content: {
"application/json": {
schema: {
type: "object",
properties: { webhookSecret: { type: "string" } },
},
},
},
},
"400": {
description: "No webhook URL set on this inbox",
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } },
},
"404": {
description: "Not found",
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } },
},
},
},
},
"/v1/inboxes/{id}/webhook/test": {
post: {
summary: "Send a test webhook",
description:
"Send a signed `setup.test` event to the inbox webhook URL to verify connectivity and signature handling.",
operationId: "testInboxWebhook",
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
responses: {
"200": {
description: "Test delivered",
content: {
"application/json": {
schema: {
type: "object",
properties: {
ok: { type: "boolean" },
message: { type: "string" },
},
},
},
},
},
"400": {
description: "No webhook URL set on this inbox",
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } },
},
"404": {
description: "Not found",
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } },
},
"502": {
description: "The webhook endpoint did not accept the test event",
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } },
},
},
},
},
"/v1/inboxes/{id}/send": {
post: {
summary: "Send email",
Expand Down Expand Up @@ -474,6 +556,17 @@ const spec = {
id: { type: "string", description: "OpenMail inbox ID" },
address: { type: "string", format: "email", description: "Full email address" },
displayName: { type: "string", description: "Sender display name", nullable: true },
webhookUrl: {
type: "string",
nullable: true,
description:
"Per-inbox webhook URL. When set, inbound events for this inbox are delivered here instead of the account webhook. Null means the account-level webhook is used.",
},
webhookSecret: {
type: "string",
description:
"HMAC signing secret for the inbox webhook. Returned only in the response that creates or rotates it — store it securely; it is not returned again.",
},
createdAt: { type: "string", format: "date-time" },
},
},
Expand Down