The research is ready for your review.
", "replyTo": { "email": "research-agent@inbox.mailactor.com", "name": "Research Agent" }, "metadata": { "taskId": "research-42" }, "delivery": { "submissionId": "00000000-0000-4000-8000-000000000001", "status": "submitted", "failureCode": null }, "createdAt": "2026-09-08T12:00:00.000Z", "updatedAt": "2026-09-08T12:00:00.000Z" } ], "nextCursor": null } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "410": { "$ref": "#/components/responses/Gone" }, "413": { "$ref": "#/components/responses/PayloadTooLarge" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X GET \\\n \"https://api.mailactor.com/v1/inboxes/$INBOX_ID/export\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\"" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/inboxes/${process.env.INBOX_ID}/export`, {\n method: 'GET',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY },\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # Get delivery metrics (/docs/api-reference/getDeliveryMetrics) GET /v1/metrics/delivery Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "getDeliveryMetrics", "summary": "Get delivery metrics", "x-mailactor-required-scope": "delivery:read", "parameters": [ { "name": "hours", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 720, "default": 24 } } ], "responses": { "200": { "description": "Delivery metrics", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliveryHealthMetrics" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X GET \\\n \"https://api.mailactor.com/v1/metrics/delivery\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\"" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/metrics/delivery`, {\n method: 'GET',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY },\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # Get a domain (/docs/api-reference/getDomain) GET /v1/domains/{domainId} Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "getDomain", "summary": "Get a domain", "x-mailactor-required-scope": "domain:manage", "parameters": [ { "name": "domainId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Domain and DNS instructions", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendingDomain" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X GET \\\n \"https://api.mailactor.com/v1/domains/$DOMAIN_ID\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\"" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/domains/${process.env.DOMAIN_ID}`, {\n method: 'GET',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY },\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # Get domain usage (/docs/api-reference/getDomainTraffic) GET /v1/domains/{domainId}/traffic Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "getDomainTraffic", "summary": "Get domain usage", "x-mailactor-required-scope": "domain:manage", "parameters": [ { "name": "domainId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Domain traffic status", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DomainTrafficView" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X GET \\\n \"https://api.mailactor.com/v1/domains/$DOMAIN_ID/traffic\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\"" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/domains/${process.env.DOMAIN_ID}/traffic`, {\n method: 'GET',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY },\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # Get an inbox (/docs/api-reference/getInbox) GET /v1/inboxes/{inboxId} Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "getInbox", "summary": "Get an inbox", "x-mailactor-required-scope": "mailbox:read", "parameters": [ { "name": "inboxId", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/InboxId" } } ], "responses": { "200": { "description": "Inbox", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Inbox" }, "example": { "id": "inb_0123456789abcdef01234567", "localPart": "research-agent", "domainId": null, "domainKind": "managed", "domain": "inbox.mailactor.com", "address": "research-agent@inbox.mailactor.com", "displayName": "Research Agent", "status": "active", "createdAt": "2026-09-08T12:00:00.000Z", "updatedAt": "2026-09-08T12:00:00.000Z" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X GET \\\n \"https://api.mailactor.com/v1/inboxes/$INBOX_ID\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\"" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/inboxes/${process.env.INBOX_ID}`, {\n method: 'GET',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY },\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # Get a submission (/docs/api-reference/getSubmission) GET /v1/submissions/{submissionId} Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "getSubmission", "summary": "Get a submission", "x-mailactor-required-scope": "delivery:read", "parameters": [ { "name": "submissionId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Delivery submission", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliverySubmission" }, "example": { "id": "00000000-0000-4000-8000-000000000001", "from": { "email": "research-agent@inbox.mailactor.com", "name": "Research Agent" }, "recipients": [ { "email": "you@example.com", "status": "delivered", "response": "250 2.0.0 Message accepted", "updatedAt": "2026-09-08T12:01:00.000Z" } ], "subject": "Your research update", "metadata": { "taskId": "research-42" }, "status": "delivered", "queueId": "mailactor-example-queue", "response": "250 2.0.0 Message accepted", "failureCode": null, "createdAt": "2026-09-08T12:00:00.000Z", "updatedAt": "2026-09-08T12:01:00.000Z" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X GET \\\n \"https://api.mailactor.com/v1/submissions/$SUBMISSION_ID\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\"" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/submissions/${process.env.SUBMISSION_ID}`, {\n method: 'GET',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY },\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # Account & access (/docs/api-reference/getTenantAccessProfile) GET /v1/me Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "getTenantAccessProfile", "summary": "Account & access", "description": "Available to every valid tenant key without an additional scope. Returns effective policy metadata but never the key secret, hash, prefix, source fingerprint, or operator data.", "responses": { "200": { "description": "Effective tenant access profile", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TenantAccessProfile" }, "example": { "organization": { "id": "00000000-0000-4000-8000-000000000003", "status": "active", "sendLimits": { "messagesPerMinute": 10, "messagesPerDay": 100, "recipientsPerDay": 100 } }, "apiKey": { "id": "00000000-0000-4000-8000-000000000004", "kind": "agent", "scopes": [ "mailbox:manage", "mailbox:read", "mailbox:send", "delivery:read", "webhook:manage" ], "inboxIds": null, "allowedIps": null, "sendMode": "new_and_reply", "allowedRecipientDomains": null, "expiresAt": null, "requestsPerMinute": 60 }, "managedStarter": { "limit": 10, "used": 0, "remaining": 10 }, "platformLimits": { "recipientsPerMessage": 10, "messageContentBytes": 1000000, "inboxes": 10, "domains": 5, "webhookEndpoints": 5 } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X GET \\\n \"https://api.mailactor.com/v1/me\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\"" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/me`, {\n method: 'GET',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY },\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # Get a conversation (/docs/api-reference/getThread) GET /v1/threads/{threadId} Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "getThread", "summary": "Get a conversation", "x-mailactor-required-scope": "mailbox:read", "parameters": [ { "name": "threadId", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/ThreadId" } }, { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100 } }, { "name": "cursor", "in": "query", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Thread and messages", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Thread" }, "example": { "id": "thr_0123456789abcdef01234567", "inboxId": "inb_0123456789abcdef01234567", "subject": "Your research update", "participants": [ "research-agent@inbox.mailactor.com", "you@example.com" ], "messageCount": 2, "lastMessageAt": "2026-09-08T12:01:00.000Z", "messages": [ { "id": "msg_0123456789abcdef01234567", "inboxId": "inb_0123456789abcdef01234567", "threadId": "thr_0123456789abcdef01234567", "direction": "inbound", "internetMessageId": "The research is ready for your review.
", "replyTo": { "email": "research-agent@inbox.mailactor.com", "name": "Research Agent" }, "metadata": { "taskId": "research-42" }, "delivery": { "submissionId": "00000000-0000-4000-8000-000000000001", "status": "submitted", "failureCode": null }, "createdAt": "2026-09-08T12:00:00.000Z", "updatedAt": "2026-09-08T12:00:00.000Z" } ], "nextCursor": null } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "410": { "$ref": "#/components/responses/Gone" }, "413": { "$ref": "#/components/responses/PayloadTooLarge" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X GET \\\n \"https://api.mailactor.com/v1/threads/$THREAD_ID\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\"" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/threads/${process.env.THREAD_ID}`, {\n method: 'GET',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY },\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # List complaints (/docs/api-reference/listComplaints) GET /v1/complaints Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "listComplaints", "summary": "List complaints", "x-mailactor-required-scope": "delivery:read", "responses": { "200": { "description": "Complaints", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ComplaintList" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X GET \\\n \"https://api.mailactor.com/v1/complaints\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\"" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/complaints`, {\n method: 'GET',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY },\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # List domains (/docs/api-reference/listDomains) GET /v1/domains Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "listDomains", "summary": "List domains", "x-mailactor-required-scope": "domain:manage", "responses": { "200": { "description": "Registered domains", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DomainList" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X GET \\\n \"https://api.mailactor.com/v1/domains\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\"" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/domains`, {\n method: 'GET',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY },\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # List conversations (/docs/api-reference/listInboxThreads) GET /v1/inboxes/{inboxId}/threads Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "listInboxThreads", "summary": "List conversations", "x-mailactor-required-scope": "mailbox:read", "parameters": [ { "name": "inboxId", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/InboxId" } }, { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100 } }, { "name": "cursor", "in": "query", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Cursor-paginated thread summaries", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ThreadPage" }, "example": { "threads": [ { "id": "thr_0123456789abcdef01234567", "inboxId": "inb_0123456789abcdef01234567", "subject": "Your research update", "participants": [ "research-agent@inbox.mailactor.com", "you@example.com" ], "messageCount": 2, "lastMessageAt": "2026-09-08T12:01:00.000Z" } ], "nextCursor": null } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "413": { "$ref": "#/components/responses/PayloadTooLarge" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X GET \\\n \"https://api.mailactor.com/v1/inboxes/$INBOX_ID/threads\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\"" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/inboxes/${process.env.INBOX_ID}/threads`, {\n method: 'GET',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY },\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # List inboxes (/docs/api-reference/listInboxes) GET /v1/inboxes Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "listInboxes", "summary": "List inboxes", "x-mailactor-required-scope": "mailbox:read", "parameters": [ { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100 } }, { "name": "cursor", "in": "query", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Cursor-paginated inboxes", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InboxPage" }, "example": { "inboxes": [ { "id": "inb_0123456789abcdef01234567", "localPart": "research-agent", "domainId": null, "domainKind": "managed", "domain": "inbox.mailactor.com", "address": "research-agent@inbox.mailactor.com", "displayName": "Research Agent", "status": "active", "createdAt": "2026-09-08T12:00:00.000Z", "updatedAt": "2026-09-08T12:00:00.000Z" } ], "nextCursor": null } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X GET \\\n \"https://api.mailactor.com/v1/inboxes\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\"" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/inboxes`, {\n method: 'GET',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY },\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # List delivery events (/docs/api-reference/listSubmissionEvents) GET /v1/submissions/{submissionId}/events Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "listSubmissionEvents", "summary": "List delivery events", "x-mailactor-required-scope": "delivery:read", "parameters": [ { "name": "submissionId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Delivery events", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliveryEventList" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X GET \\\n \"https://api.mailactor.com/v1/submissions/$SUBMISSION_ID/events\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\"" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/submissions/${process.env.SUBMISSION_ID}/events`, {\n method: 'GET',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY },\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # List submissions (/docs/api-reference/listSubmissions) GET /v1/submissions Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "listSubmissions", "summary": "List submissions", "x-mailactor-required-scope": "delivery:read", "responses": { "200": { "description": "Delivery submissions", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliverySubmissionList" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X GET \\\n \"https://api.mailactor.com/v1/submissions\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\"" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/submissions`, {\n method: 'GET',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY },\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # List suppressions (/docs/api-reference/listSuppressions) GET /v1/suppressions Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "listSuppressions", "summary": "List suppressions", "x-mailactor-required-scope": "delivery:read", "responses": { "200": { "description": "Suppressions", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuppressionList" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X GET \\\n \"https://api.mailactor.com/v1/suppressions\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\"" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/suppressions`, {\n method: 'GET',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY },\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # List webhooks (/docs/api-reference/listWebhooks) GET /v1/webhooks Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "listWebhooks", "summary": "List webhooks", "x-mailactor-required-scope": "webhook:manage", "parameters": [ { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100 } }, { "name": "cursor", "in": "query", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Webhook endpoints", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookPage" }, "example": { "endpoints": [ { "id": "whk_0123456789abcdef01234567", "url": "https://your-app.example.com/webhooks/mailactor", "events": [ "message.received" ], "status": "active", "createdAt": "2026-09-08T12:00:00.000Z", "updatedAt": "2026-09-08T12:00:00.000Z" } ], "nextCursor": null } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X GET \\\n \"https://api.mailactor.com/v1/webhooks\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\"" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/webhooks`, {\n method: 'GET',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY },\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # Register a domain (/docs/api-reference/registerDomain) POST /v1/domains Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "registerDomain", "summary": "Register a domain", "x-mailactor-required-scope": "domain:manage", "description": "Returns TXT ownership, DKIM and MX instructions. Inbound and outbound capabilities verify independently.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "required": [ "domain" ], "properties": { "domain": { "type": "string", "format": "hostname", "minLength": 1, "maxLength": 253, "description": "A fully qualified customer-owned hostname. Mailactor trims, removes one trailing dot, converts IDNs to ASCII, and lowercases it.", "example": "agents.example.com" } } }, "example": { "domain": "agents.example.com" } } } }, "responses": { "201": { "description": "Domain and DNS instructions", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendingDomain" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "409": { "$ref": "#/components/responses/Conflict" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X POST \\\n \"https://api.mailactor.com/v1/domains\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"domain\": \"agents.example.com\"\n}'" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/domains`, {\n method: 'POST',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY, 'Content-Type': \"application/json\" },\n body: JSON.stringify({\n \"domain\": \"agents.example.com\"\n}),\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # Reply to a message (/docs/api-reference/replyToThread) POST /v1/threads/{threadId}/replies Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "replyToThread", "summary": "Reply to a message", "x-mailactor-required-scope": "mailbox:send", "parameters": [ { "name": "threadId", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/ThreadId" } }, { "name": "idempotency-key", "in": "header", "required": true, "schema": { "type": "string", "minLength": 8, "maxLength": 200, "pattern": "^[A-Za-z0-9._:-]+$" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "anyOf": [ { "required": [ "targetMessageId", "text" ], "title": "Plain text" }, { "required": [ "targetMessageId", "html" ], "title": "HTML" } ], "required": [ "targetMessageId" ], "properties": { "targetMessageId": { "$ref": "#/components/schemas/MessageId", "description": "Bind the reply to this exact message. Clients must supply the selected message ID so concurrent arrivals cannot change the recipient." }, "text": { "type": "string" }, "html": { "type": "string" }, "metadata": { "$ref": "#/components/schemas/Metadata" } } }, "example": { "targetMessageId": "msg_0123456789abcdef01234567", "text": "Thanks, Alex. I have everything I need." } } } }, "responses": { "202": { "description": "Durable reply and delivery submission", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MailboxSendResult" }, "example": { "message": { "id": "msg_2123456789abcdef01234567", "inboxId": "inb_0123456789abcdef01234567", "threadId": "thr_0123456789abcdef01234567", "direction": "outbound", "internetMessageId": "The research is ready for your review.
", "replyTo": { "email": "research-agent@inbox.mailactor.com", "name": "Research Agent" }, "metadata": { "taskId": "research-42" } } } } }, "responses": { "202": { "description": "Durable mailbox message and delivery submission", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MailboxSendResult" }, "example": { "message": { "id": "msg_1123456789abcdef01234567", "inboxId": "inb_0123456789abcdef01234567", "threadId": "thr_0123456789abcdef01234567", "direction": "outbound", "internetMessageId": "The research is ready for your review.
", "replyTo": { "email": "research-agent@inbox.mailactor.com", "name": "Research Agent" }, "metadata": { "taskId": "research-42" }, "delivery": { "submissionId": "00000000-0000-4000-8000-000000000001", "status": "submitted", "failureCode": null }, "createdAt": "2026-09-08T12:00:00.000Z", "updatedAt": "2026-09-08T12:00:00.000Z" }, "submission": { "id": "00000000-0000-4000-8000-000000000001", "from": { "email": "research-agent@inbox.mailactor.com", "name": "Research Agent" }, "recipients": [ { "email": "you@example.com", "status": "pending", "response": null, "updatedAt": "2026-09-08T12:00:00.000Z" } ], "subject": "Your research update", "metadata": { "taskId": "research-42" }, "status": "pending", "queueId": null, "response": null, "failureCode": null, "createdAt": "2026-09-08T12:00:00.000Z", "updatedAt": "2026-09-08T12:00:00.000Z" }, "replayed": false } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "409": { "$ref": "#/components/responses/Conflict" }, "410": { "description": "The requested mailbox content was deleted and cannot be returned.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "idempotency_result_expired", "message": "The original mailbox result was deleted; this idempotency key cannot be reused" } } } }, "413": { "$ref": "#/components/responses/PayloadTooLarge" }, "422": { "$ref": "#/components/responses/UnprocessableEntity" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\n# Generate once per logical action; preserve this key for retries.\nexport IDEMPOTENCY_KEY=\"request-$(uuidgen)\"\ncurl --fail-with-body --silent --show-error -X POST \\\n \"https://api.mailactor.com/v1/inboxes/$INBOX_ID/messages\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\" \\\n -H \"idempotency-key: $IDEMPOTENCY_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"to\": [\n {\n \"email\": \"you@example.com\",\n \"name\": \"Alex\"\n }\n ],\n \"subject\": \"Your research update\",\n \"text\": \"The research is ready for your review.\",\n \"html\": \"The research is ready for your review.
\",\n \"replyTo\": {\n \"email\": \"research-agent@inbox.mailactor.com\",\n \"name\": \"Research Agent\"\n },\n \"metadata\": {\n \"taskId\": \"research-42\"\n }\n}'" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\n// Save this value with the operation and reuse it for retries.\nconst idempotencyKey = `request-${crypto.randomUUID()}`;\nconst response = await fetch(`https://api.mailactor.com/v1/inboxes/${process.env.INBOX_ID}/messages`, {\n method: 'POST',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY, 'idempotency-key': idempotencyKey, 'Content-Type': \"application/json\" },\n body: JSON.stringify({\n \"to\": [\n {\n \"email\": \"you@example.com\",\n \"name\": \"Alex\"\n }\n ],\n \"subject\": \"Your research update\",\n \"text\": \"The research is ready for your review.\",\n \"html\": \"The research is ready for your review.
\",\n \"replyTo\": {\n \"email\": \"research-agent@inbox.mailactor.com\",\n \"name\": \"Research Agent\"\n },\n \"metadata\": {\n \"taskId\": \"research-42\"\n }\n}),\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # Submit a message (/docs/api-reference/submitEmail) POST /v1/send Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "submitEmail", "summary": "Submit a message", "x-mailactor-required-scope": "delivery:send", "parameters": [ { "name": "idempotency-key", "in": "header", "required": true, "schema": { "type": "string", "minLength": 8, "maxLength": 200, "pattern": "^[A-Za-z0-9._:-]+$" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "required": [ "from", "to", "subject" ], "anyOf": [ { "required": [ "from", "to", "subject", "text" ], "title": "Plain text" }, { "required": [ "from", "to", "subject", "html" ], "title": "HTML" } ], "properties": { "from": { "$ref": "#/components/schemas/Mailbox" }, "to": { "type": "array", "minItems": 1, "maxItems": 1000, "description": "The current deployment limit is returned by GET /v1/me as platformLimits.recipientsPerMessage.", "items": { "$ref": "#/components/schemas/Mailbox" } }, "subject": { "type": "string", "minLength": 1, "maxLength": 998, "pattern": "\\S" }, "text": { "type": "string" }, "html": { "type": "string" }, "replyTo": { "$ref": "#/components/schemas/Mailbox" }, "headers": { "type": "object", "additionalProperties": false, "maxProperties": 3, "properties": { "x-campaign": { "type": "string", "maxLength": 998, "pattern": "^[^\\r\\n]*$" }, "x-campaign-id": { "type": "string", "maxLength": 998, "pattern": "^[^\\r\\n]*$" }, "x-entity-ref-id": { "type": "string", "maxLength": 998, "pattern": "^[^\\r\\n]*$" } }, "description": "Only these three non-routing customer headers are accepted." }, "metadata": { "$ref": "#/components/schemas/Metadata" } } }, "example": { "from": { "email": "agent@agents.example.com" }, "to": [ { "email": "you@example.com" } ], "subject": "Hello from Mailactor", "text": "A message from my verified domain." } } } }, "responses": { "202": { "description": "Durable delivery submission", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliverySubmissionAccepted" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "409": { "$ref": "#/components/responses/Conflict" }, "413": { "$ref": "#/components/responses/PayloadTooLarge" }, "422": { "$ref": "#/components/responses/UnprocessableEntity" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\n# Generate once per logical action; preserve this key for retries.\nexport IDEMPOTENCY_KEY=\"request-$(uuidgen)\"\ncurl --fail-with-body --silent --show-error -X POST \\\n \"https://api.mailactor.com/v1/send\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\" \\\n -H \"idempotency-key: $IDEMPOTENCY_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"from\": {\n \"email\": \"agent@agents.example.com\"\n },\n \"to\": [\n {\n \"email\": \"you@example.com\"\n }\n ],\n \"subject\": \"Hello from Mailactor\",\n \"text\": \"A message from my verified domain.\"\n}'" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\n// Save this value with the operation and reuse it for retries.\nconst idempotencyKey = `request-${crypto.randomUUID()}`;\nconst response = await fetch(`https://api.mailactor.com/v1/send`, {\n method: 'POST',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY, 'idempotency-key': idempotencyKey, 'Content-Type': \"application/json\" },\n body: JSON.stringify({\n \"from\": {\n \"email\": \"agent@agents.example.com\"\n },\n \"to\": [\n {\n \"email\": \"you@example.com\"\n }\n ],\n \"subject\": \"Hello from Mailactor\",\n \"text\": \"A message from my verified domain.\"\n}),\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # Validate an address (/docs/api-reference/validateEmail) POST /v1/validate Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "validateEmail", "summary": "Validate an address", "x-mailactor-required-scope": "validation:use", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "maxLength": 320 } } }, "example": { "email": "alex@example.com" } } } }, "responses": { "200": { "description": "Validation result", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EmailValidationResult" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X POST \\\n \"https://api.mailactor.com/v1/validate\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"email\": \"alex@example.com\"\n}'" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/validate`, {\n method: 'POST',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY, 'Content-Type': \"application/json\" },\n body: JSON.stringify({\n \"email\": \"alex@example.com\"\n}),\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # Validate in bulk (/docs/api-reference/validateEmailsBulk) POST /v1/validate/bulk Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "validateEmailsBulk", "summary": "Validate in bulk", "x-mailactor-required-scope": "validation:use", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "required": [ "emails" ], "properties": { "emails": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "string", "minLength": 1, "maxLength": 320 } } } }, "example": { "emails": [ "alex@example.com", "sam@example.com" ] } } } }, "responses": { "200": { "description": "Validation results", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationResultList" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X POST \\\n \"https://api.mailactor.com/v1/validate/bulk\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"emails\": [\n \"alex@example.com\",\n \"sam@example.com\"\n ]\n}'" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/validate/bulk`, {\n method: 'POST',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY, 'Content-Type': \"application/json\" },\n body: JSON.stringify({\n \"emails\": [\n \"alex@example.com\",\n \"sam@example.com\"\n ]\n}),\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # Verify domain DNS (/docs/api-reference/verifyDomain) POST /v1/domains/{domainId}/verify Resolve component references with https://docs.mailactor.com/openapi.json. ```json { "operationId": "verifyDomain", "summary": "Verify domain DNS", "x-mailactor-required-scope": "domain:manage", "parameters": [ { "name": "domainId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Checks plus independent capabilities.outbound and capabilities.inbound results", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DomainVerification" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" }, "500": { "$ref": "#/components/responses/InternalError" }, "503": { "$ref": "#/components/responses/Unavailable" } }, "x-codeSamples": [ { "lang": "bash", "label": "cURL", "source": "# Set the API key and replace resource IDs with your saved IDs.\ncurl --fail-with-body --silent --show-error -X POST \\\n \"https://api.mailactor.com/v1/domains/$DOMAIN_ID/verify\" \\\n -H \"x-api-key: $MAILACTOR_API_KEY\"" }, { "lang": "js", "label": "JavaScript", "source": "// Node.js: configure your key and saved resource IDs in the environment.\nconst response = await fetch(`https://api.mailactor.com/v1/domains/${process.env.DOMAIN_ID}/verify`, {\n method: 'POST',\n headers: { 'x-api-key': process.env.MAILACTOR_API_KEY },\n});\n// Handle errors before reading the success response.\nif (!response.ok) throw new Error(`Mailactor returned ${response.status}`);\nconst contentType = response.headers.get(\"content-type\") ?? \"\";\nconst result = response.status === 204 ? null : contentType.includes(\"application/json\")\n ? await response.json() : await response.arrayBuffer();" } ] } ``` # Connect your own domain (/docs/domains/setup) Customer-domain inboxes let you send and receive as an address such as `research-agent@agents.example.com`. They do not consume the managed starter allowance, but verified-domain policy and traffic limits still apply. You need control of the domain's DNS, `domain:manage` to configure it, and `mailbox:manage` to create an inbox. Use an organization enabled for customer-domain inboxes; if your trial is restricted, contact the team. ## 1. Register a dedicated subdomain [#1-register-a-dedicated-subdomain] We recommend a dedicated subdomain such as `agents.example.com`, especially if your root domain already uses Google Workspace or Microsoft 365. ```bash curl --fail-with-body --silent --show-error \ "$MAILACTOR_API_URL/v1/domains" \ -H "x-api-key: $MAILACTOR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain":"agents.example.com"}' ``` Save the returned domain `id` as `DOMAIN_ID`. After a lost response, list domains and match the normalized domain name before registering again. ## 2. Publish the returned DNS records [#2-publish-the-returned-dns-records] The domain response includes `dnsRecords`. Publish each record exactly as returned. TXT records use `name` and `value`; MX records also include `priority`. The instructions cover ownership, DKIM, and inbound mail routing. Follow any additional onboarding instructions for SPF and DMARC. Your DNS provider may append your zone name automatically, so check whether it expects a relative or fully qualified record name.The research is ready for your review.
", "replyTo": { "email": "research-agent@inbox.mailactor.com", "name": "Research Agent" }, "metadata": { "taskId": "research-42" } } ``` | Field | Required | Shape | | ---------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `to` | Yes | Array of `{email, name?}` objects. At least one; use `/v1/me` → `platformLimits.recipientsPerMessage` for your live cap (schema maximum 1,000). | | `subject` | Yes | String, 1–998 characters, not whitespace-only. | | `text` | One of `text` / `html` | Plain-text string. | | `html` | One of `text` / `html` | HTML string. Both body formats are allowed. | | `replyTo` | No | One `{email, name?}` object; directs normal email replies. Omit to receive replies at the inbox's sender address. | | `metadata` | No | Up to 20 string values, keys 1–100 characters, values at most 500 characters. | Every mailbox needs a valid `email`; optional `name` is 1–200 characters. Unknown fields are rejected. There are no outbound attachment, CC, or BCC fields. The sender is your inbox's returned `address`; you do not supply `from`. Treat outgoing HTML as application-authored content, and respect the live `messageContentBytes` limit. ## Save the response identifiers [#save-the-response-identifiers] | Field | Use | | ------------------ | ------------------------------------------- | | `message.id` | Identify the email in your mailbox. | | `message.threadId` | Read this conversation and receive replies. | | `submission.id` | Track outbound delivery. | A `202` response means accepted for asynchronous delivery. Poll the [submission endpoint](/docs/reliability/delivery) for the outcome. A mailbox message's `delivery.status: "submitted"` does not mean the recipient's server accepted it. ### HTTP 202 example [#http-202-example] The example above can return this complete envelope. `replayed: false` means a new operation; `true` means the same operation was recovered by its idempotency key. Status may already have advanced when you read a replay. ```json title="Response: sendInboxMessage 202" { "message": { "id": "msg_1123456789abcdef01234567", "inboxId": "inb_0123456789abcdef01234567", "threadId": "thr_0123456789abcdef01234567", "direction": "outbound", "internetMessageId": "The research is ready for your review.
", "replyTo": { "email": "research-agent@inbox.mailactor.com", "name": "Research Agent" }, "metadata": { "taskId": "research-42" }, "delivery": { "submissionId": "00000000-0000-4000-8000-000000000001", "status": "submitted", "failureCode": null }, "createdAt": "2026-09-08T12:00:00.000Z", "updatedAt": "2026-09-08T12:00:00.000Z" }, "submission": { "id": "00000000-0000-4000-8000-000000000001", "from": { "email": "research-agent@inbox.mailactor.com", "name": "Research Agent" }, "recipients": [ { "email": "you@example.com", "status": "pending", "response": null, "updatedAt": "2026-09-08T12:00:00.000Z" } ], "subject": "Your research update", "metadata": { "taskId": "research-42" }, "status": "pending", "queueId": null, "response": null, "failureCode": null, "createdAt": "2026-09-08T12:00:00.000Z", "updatedAt": "2026-09-08T12:00:00.000Z" }, "replayed": false } ``` ## Retry safely [#retry-safely] For an interrupted or retryable request, use the **same idempotency key and semantic body**. Do not create a new key merely because the first response was lost. A new key represents a new message and can send a duplicate. If the retry deadline expires, retain the key and body and report acceptance as unknown. There is no lookup endpoint by idempotency key; matching a message by subject or content is not proof that it belongs to this operation. [Idempotency and safe retries](/docs/reliability/idempotency) ## Choose the correct sending route [#choose-the-correct-sending-route] `POST /v1/inboxes/{inboxId}/messages` stores a mailbox message and creates the thread needed for normal inbox workflows. `POST /v1/send` is a lower-level delivery endpoint for integrations supplying their own authorized From mailbox. It requires `delivery:send` and a verified sending domain. It does not create the inbox conversation for you. Never switch routes when retrying one logical send. [Full inbox send reference](/docs/api-reference/sendInboxMessage) # Receive webhook notifications (/docs/webhooks/overview) A webhook sends an HTTPS request to your application after an inbound message is available. The current event type is **`message.received`**. Outbound delivery status is read through [submissions](/docs/reliability/delivery). Required scope: `webhook:manage`. Your application also needs `mailbox:read` to fetch message content. Set `MAILACTOR_API_URL` and `MAILACTOR_API_KEY` as shown in [quickstart](/docs/quickstart). Examples below use illustrative IDs and values; use the values returned for your organization. ## Prepare your receiver [#prepare-your-receiver] Use a public HTTPS endpoint on port 443. Localhost, private addresses, redirects, embedded URL credentials, and URL fragments are not supported. Your receiver must preserve the raw request body for signature verification and persist verified event IDs for deduplication. If you do not have a public receiver yet, [poll your inbox](/docs/messages/receive). ## Register an endpoint [#register-an-endpoint] ```bash export WEBHOOK_KEY="webhook-$(uuidgen)" curl --fail-with-body --silent --show-error \ "$MAILACTOR_API_URL/v1/webhooks" \ -H "x-api-key: $MAILACTOR_API_KEY" \ -H "idempotency-key: $WEBHOOK_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://your-app.example.com/webhooks/mailactor","events":["message.received"]}' ``` Replace the URL with your actual receiver. Store the returned `id`, `signingSecret`, and `signingSecretReplayUntil` securely. Avoid running the command in shared or logged terminals because its response contains the signing secret. An exact retry with the same idempotency key and input can recover the same secret only until `signingSecretReplayUntil`. Listing webhooks does not return it. After that deadline, replace an endpoint whose secret you have lost. ### Request and response [#request-and-response] | Field | Required | Shape | | -------- | -------- | -------------------------------------------------------------------- | | `url` | Yes | Public HTTPS URL on port 443, at most 2,048 characters. | | `events` | Yes | Exactly `["message.received"]`; other event types are not supported. | ```json title="Request: createWebhook" { "url": "https://your-app.example.com/webhooks/mailactor", "events": ["message.received"] } ``` New registration returns **201** with the full object below. Exact replay returns **200**, the same ID and secret, and `replayed: true`. The replay deadline shown is illustrative: use the returned `signingSecretReplayUntil`, never a hardcoded duration. ```json title="Response: createWebhook 201" { "id": "whk_0123456789abcdef01234567", "url": "https://your-app.example.com/webhooks/mailactor", "events": ["message.received"], "status": "active", "createdAt": "2026-09-08T12:00:00.000Z", "updatedAt": "2026-09-08T12:00:00.000Z", "signingSecret": "whsec_EXAMPLE_ONLY_DO_NOT_USE", "signingSecretReplayUntil": "2026-09-08T12:15:00.000Z", "replayed": false } ``` ## Process an event [#process-an-event] The JSON payload contains `id`, `type`, `createdAt`, and `data` with `inboxId`, `threadId`, and `messageId`. It contains no subject, sender address, message body, or attachments. ```json title="Event: message.received" { "id": "evt_0123456789abcdef01234567", "type": "message.received", "createdAt": "2026-09-08T12:01:00.000Z", "data": { "messageId": "msg_0123456789abcdef01234567", "inboxId": "inb_0123456789abcdef01234567", "threadId": "thr_0123456789abcdef01234567" } } ``` 1. [Verify the signature and timestamp](/docs/webhooks/verify) against the exact raw body. 2. Persist the verified event using a unique constraint on your endpoint identity plus the **signed event ID**. Record the delivery ID for tracing; that header alone is not a safe deduplication key. 3. Return `2xx` after durable acceptance. A duplicate already persisted should also receive `2xx`. 4. In a background job, fetch `GET /v1/threads/{threadId}` and locate `data.messageId`, following pagination as needed. Delivery is at least once. Transport failures and non-2xx responses are retried with exponential backoff. Design side effects, including automatic replies, to be idempotent. ## List and remove endpoints [#list-and-remove-endpoints] `GET /v1/webhooks` returns `endpoints` and `nextCursor`. Use `DELETE /v1/webhooks/{webhookId}` to remove an endpoint. ```bash curl --fail-with-body --silent --show-error \ "$MAILACTOR_API_URL/v1/webhooks?limit=20" \ -H "x-api-key: $MAILACTOR_API_KEY" # WEBHOOK_ID is the saved endpoint id (whk_...), not an event or delivery id. curl --fail-with-body --silent --show-error -X DELETE \ "$MAILACTOR_API_URL/v1/webhooks/$WEBHOOK_ID" \ -H "x-api-key: $MAILACTOR_API_KEY" ``` List success is `200`; [its full response](/docs/api-reference/listWebhooks#response-examples) contains no signing secret. Delete success is `204` with no body. There is no individual webhook GET or update endpoint; verify removal by paging the complete list. Deletion can return `409` while a delivery is in flight. Retry with bounded backoff and confirm absence from the complete endpoint inventory. To change a URL or replace a secret, create a replacement at a **distinct receiver URL** (a new path is enough), store its secret, then delete the old endpoint. A URL must be unique within the organization. If you must reuse the exact URL, delete the old endpoint first, then register it with a fresh key; reconcile the resulting notification gap by polling. During replacement, endpoint-level deduplication alone cannot prevent both endpoints from triggering the same action. Add a business-action key such as organization + event ID + action name, shared by both receivers, and preserve the same send/reply idempotency key. Use polling to reconcile any gap. If recovery returns `409 webhook_secret_replay_expired`, replace the endpoint with a new key; never invent or rotate a secret locally. [Webhook creation reference](/docs/api-reference/createWebhook) ## Registration conflicts and expiry [#registration-conflicts-and-expiry] | Status and `error` | Action | | ----------------------------------- | --------------------------------------------------------------------------------- | | `409 idempotency_key_conflict` | Restore the original persisted input for that key. | | `409 webhook_endpoint_exists` | The URL is already registered; list endpoints to reconcile. | | `409 webhook_secret_replay_expired` | The secret recovery window expired. Replace the endpoint if you lost its secret. | | `410 webhook_endpoint_deleted` | That key belongs to a deleted endpoint. Register a new endpoint with a fresh key. | ```json title="Response: createWebhook 409" { "error": "webhook_secret_replay_expired", "message": "The signing-secret replay window for this idempotency key has expired" } ``` ```json title="Response: createWebhook 410" { "error": "webhook_endpoint_deleted", "message": "The webhook endpoint created by this idempotency key was deleted" } ``` ## Delivery retry boundary [#delivery-retry-boundary] Webhook retries are finite. Request timeout and maximum attempts are deployment settings; the public contract does not promise an exact retry schedule or delivery deadline. After attempts are exhausted, that delivery stops retrying. There is no public delivery replay or dead-letter inspection endpoint. A receiver must not rely on callbacks as its only record of incoming mail: periodically reconcile inbox threads and messages, including after outages. Arrival order is not guaranteed. # Verify webhook signatures (/docs/webhooks/verify) Mailactor signs each webhook using HMAC-SHA256 and the signing secret returned when the endpoint was created. This authenticates the webhook transport, not the identity or intent of the person who sent the email. ## Webhook headers [#webhook-headers] | Header | Meaning | | ------------------------- | -------------------------------------------------------- | | `x-mailactor-event` | `message.received` | | `x-mailactor-event-id` | Stable source event ID. | | `x-mailactor-delivery-id` | Stable delivery ID for this endpoint. | | `x-mailactor-timestamp` | Unix timestamp in seconds. | | `x-mailactor-signature` | `v1=` followed by the lowercase hexadecimal HMAC digest. | Compute the HMAC over these bytes in order: ```text timestamp + "." + eventId + "." + exactRawBody ``` Use the complete signing-secret string as the HMAC key. Do not decode its prefix or reserialize parsed JSON. Even harmless whitespace changes alter the signature. ## Use the Node.js verifier [#use-the-nodejs-verifier] [Download the verifier](/examples/verify-webhook.mjs). It uses Node's built-in crypto module, checks timestamp freshness, performs a constant-time signature comparison, and validates event identity before returning the parsed event. ```javascript import { verifyMailactorWebhook } from './verify-webhook.mjs'; // request is a Web Request. Read bytes before any JSON middleware. const rawBody = new Uint8Array(await request.arrayBuffer()); const verified = verifyMailactorWebhook({ rawBody, headers: request.headers, secret: process.env.MAILACTOR_WEBHOOK_SECRET, }); if (!verified) { return new Response('Invalid webhook', { status: 401 }); } // Persist using a unique constraint on endpoint identity + verified.event.id. // Store verified.deliveryId too. A duplicate should be acknowledged without // executing the same work twice. Return 503 if durable storage is unavailable. await persistEventOnce(verified); return new Response(null, { status: 204 }); ``` `persistEventOnce` is your application-owned durable queue/database operation. The example is a handler fragment; connect it to your web framework, enforce a request-size limit before buffering the body, and implement persistence before using it in production. An in-memory Set will not survive restarts. ## Reject stale requests and duplicates [#reject-stale-requests-and-duplicates] Reject timestamps more than five minutes in the past or future relative to your verifier's clock. Keep the host clock synchronized. The body and event ID are signed; the delivery-ID header is not independently covered by the HMAC. Record the delivery ID, and use the signed event ID with your endpoint identity as the durable deduplication key so changing a header cannot cause a repeated action. A valid duplicate must not produce another reply or another business action. Persist before acknowledging, then process asynchronously. Use the same logical send idempotency key if your event worker retries an automatic reply. ## Test your receiver [#test-your-receiver] [Download a signed fixture](/examples/webhook-fixture.json). It contains a fake secret, exact raw body, matching headers, and a fixed verification time. The headers for that body are: ```http content-type: application/json x-mailactor-event: message.received x-mailactor-event-id: evt_0123456789abcdef01234567 x-mailactor-delivery-id: whd_0123456789abcdef01234567 x-mailactor-timestamp: 1788868860 x-mailactor-signature: v1=9e34323b31594245ed9541aca26f1e48e79fbff68299b54d1560548cf34dc8ef ``` Run this beside the downloaded fixture and verifier. Only this fixture test overrides the clock; production uses the current time and the endpoint's real secret. ```javascript import { readFile } from 'node:fs/promises'; import assert from 'node:assert/strict'; import { verifyMailactorWebhook } from './verify-webhook.mjs'; const fixture = JSON.parse(await readFile('./webhook-fixture.json', 'utf8')); const verified = verifyMailactorWebhook({ rawBody: new TextEncoder().encode(fixture.rawBody), headers: new Headers(fixture.headers), secret: fixture.secret, now: fixture.now, }); assert.equal(verified?.event.id, fixture.headers['x-mailactor-event-id']); ``` Verify four separate cases: one valid event is accepted, a modified body is rejected, a stale timestamp is rejected, and a valid replay is acknowledged without repeating work. Keep polling as a reconciliation path after receiver outages. # Delivery status (/docs/reliability/delivery) Every outbound send returns a submission ID. Read that submission with `delivery:read`: ```bash curl --fail-with-body --silent --show-error \ "$MAILACTOR_API_URL/v1/submissions/$SUBMISSION_ID" \ -H "x-api-key: $MAILACTOR_API_KEY" ``` ## Interpret the result [#interpret-the-result] | Status | Meaning / next step | | --------------------- | ------------------------------------------------------------------------ | | `pending` | Stored; delivery work has not finished. Continue polling. | | `queued` | Accepted into the delivery queue. Continue polling. | | `deferred` | Temporary delivery problem. Continue polling; do not send a duplicate. | | `delivered` | The recipient mail server accepted the message. | | `partially_delivered` | Recipients have mixed outcomes. Inspect every recipient. | | `bounced` | Permanent recipient/server rejection. Inspect the response. | | `expired` | Delivery could not be confirmed before the delivery window ended. | | `unknown` | The transport outcome is ambiguous. Reconcile before deciding to resend. | | `failed` | The operation failed; inspect `failureCode` and `response`. | Inspect the `recipients` array even when the aggregate status appears final. A mixed submission can contain recipients still in a nonterminal state. Wait until every recipient has a terminal outcome, or stop at your deadline and report the remaining uncertainty. Later transport evidence can refine an `expired` or `unknown` result. `delivered` is **server acceptance**, not proof of inbox placement, opening, or reading. Mailactor does not provide read receipts here. ### HTTP 200 example [#http-200-example] ```json title="Response: getSubmission 200" { "id": "00000000-0000-4000-8000-000000000001", "from": { "email": "research-agent@inbox.mailactor.com", "name": "Research Agent" }, "recipients": [ { "email": "you@example.com", "status": "delivered", "response": "250 2.0.0 Message accepted", "updatedAt": "2026-09-08T12:01:00.000Z" } ], "subject": "Your research update", "metadata": { "taskId": "research-42" }, "status": "delivered", "queueId": "mailactor-example-queue", "response": "250 2.0.0 Message accepted", "failureCode": null, "createdAt": "2026-09-08T12:00:00.000Z", "updatedAt": "2026-09-08T12:01:00.000Z" } ``` ### Mixed recipients [#mixed-recipients] For `partially_delivered`, inspect `recipients` rather than retrying the entire send: | Recipient state | Action | | ------------------------------- | ------------------------------------------------------------------------------------------ | | `pending`, `queued`, `deferred` | Continue polling the same submission. | | `delivered` | Report server acceptance for this recipient. Do not send it again. | | `bounced` | Report permanent rejection with its `response`. | | `expired`, `unknown` | Retain the submission ID and report delivery as unconfirmed; later evidence may refine it. | An aggregate `failed` is a failed operation; retain `failureCode` and `response`. At your deadline, return a per-recipient result including any unresolved states. Any new send after failure is an explicit application decision, not an automatic retry with a fresh key. ### Reference-client stopping rule [#reference-client-stopping-rule] The optional runtime reference client labels `partially_delivered` as an aggregate terminal status. Its wait helpers can therefore return while an individual recipient is still `deferred`. That label does not mean every recipient finished. Inspect the returned `recipients`; for this guide's per-recipient completion rule, continue `GET /v1/submissions/{submissionId}` within your deadline whenever any recipient is `pending`, `queued`, or `deferred`. `GET /v1/submissions/{submissionId}/events` returns `{events: [...]}` and has **no cursor pagination**. Do not pass `limit` or `cursor` or expect `nextCursor` on that operation. ## Poll responsibly [#poll-responsibly] Wait 1 second before the first request, then 2, 4, and at most 5 seconds between checks. Honor a longer `Retry-After` when supplied. Choose a deadline; retain the submission ID when it expires. Do not turn a deferred or unknown outcome into a success message. `GET /v1/submissions/{submissionId}/events` provides delivery events. `GET /v1/metrics/delivery` exposes your organization's delivery metrics. Neither requires access to another tenant's data. ## Why a send may be blocked [#why-a-send-may-be-blocked] Sending requires an active organization, an authorized sender domain, sufficient key permissions, and available traffic quota. A recipient may also be suppressed after a hard bounce, complaint, or manual suppression. An organization-wide managed starter allowance cannot be reset by creating a new inbox. Inspect [limits](/docs/limits) and the error's recovery fields before retrying. # Errors & troubleshooting (/docs/reliability/errors) API errors carry a stable `error` code and recovery information such as `retryable` and `action_required`. Inspect the returned body instead of relying only on an HTTP status. Keep a request ID when provided so the team can help investigate. | HTTP status | What to do | | ------------- | ----------------------------------------------------------------------------------------------------- | | `400` | Correct the input. Check required fields and ID formats. | | `401` | Check your key, expiry, revocation, and allowed source IPs. | | `403` | Check scopes, inbox restrictions, recipient policy, and organization/domain state. | | `404` | Refresh the resource from its owning list. Verify you are using the right organization's key. | | `409` | Inspect conflict or deletion-pending state. Do not change an idempotency key to conceal a conflict. | | `410` | Previously stored content or an idempotency result was removed. Reconcile before recreating anything. | | `413` | Reduce message content or page size as appropriate. | | `422` | Check suppressed recipients or the lack of a safe reply recipient. | | `423` | An attachment is quarantined. Stop; do not bypass the policy. | | `429` | Honor `Retry-After` when present. Preserve the same key for retries. | | `500` / `503` | Retry only when safe, with bounded backoff and the original idempotency key. | ## I cannot create an inbox [#i-cannot-create-an-inbox] Check `GET /v1/me`, especially the key's scopes, inbox restrictions, and `platformLimits.inboxes`. For a managed name conflict, list your inboxes and either recover the existing one or choose a different local part. For a custom domain, verify both inbound and outbound capabilities. ## My email was accepted, but I cannot find it [#my-email-was-accepted-but-i-cannot-find-it] A `202` response is not delivery confirmation. Read the returned submission. If it is delivered, inspect the recipient mailbox's spam filtering. If it is deferred, keep polling within your deadline. Do not create a new logical send to resolve uncertainty. ## I do not see incoming mail yet [#i-do-not-see-incoming-mail-yet] Confirm the sender used the exact inbox `address` and that the inbox is active. Allow time for asynchronous processing. List threads and read their messages; checking only the first thread page can miss arrivals. For custom domains, verify MX and inbound capability. ## My webhook is not working [#my-webhook-is-not-working] Confirm the URL is public HTTPS on port 443 and does not redirect. Verify your secret, raw-body handling, and system clock. Ensure your receiver persists the event and returns a 2xx response. Reconcile the inbox through polling while investigating. [Contact the Mailactor team](/docs/support) if you need a key or organization-policy change. # Idempotency & retries (/docs/reliability/idempotency) A network timeout does not tell you whether the server accepted a request. Idempotency gives one logical operation a stable identity so you can retry it safely. ## Where keys are required [#where-keys-are-required] | Operation | Header | | ----------------------- | ----------------- | | Send from an inbox | `idempotency-key` | | Reply to a thread | `idempotency-key` | | Lower-level direct send | `idempotency-key` | | Create a webhook | `idempotency-key` | Use 8–200 characters matching `^[A-Za-z0-9._:-]+$`. A prefix followed by a UUID is a good choice. ```bash export SEND_KEY="send-$(uuidgen)" ``` Create the key **once per logical action** and persist it with the request data. Do not regenerate it inside a retry loop. ## Retry rules [#retry-rules] * Retry ambiguous requests on the same route with the same key and semantic body. * A different message or reply needs a fresh key. * Reusing a key with changed content causes a conflict. * A previously failed operation can replay its original failure; a retry does not automatically bypass the original policy decision. * After `410`, reconcile what was deleted. Do not blindly recreate the operation with a fresh key. Use bounded backoff: wait 1, 2, 4, then at most 5 seconds between attempts. Honor `Retry-After` when present, even if it is longer than that normal schedule, provided the wait fits your deadline. Stop when the deadline expires. ## Resources without a create key [#resources-without-a-create-key] For inbox creation, choose an explicit `localPart`. After an ambiguous response, match it plus `domainKind: "managed"` (or your exact customer `domainId`) across every inbox page, then save the returned address. See [creation recovery](/docs/inboxes/create#recover-from-an-interrupted-request). For domain registration, list and match the normalized domain. Repeated registration of the same domain converges, but still retain the original domain ID. For DELETE, the goal is confirmed absence. Retry pending deletion and verify absence using the owning read/list operation. Do not assume an interrupted DELETE failed. ## A mailbox replay returns 410 [#a-mailbox-replay-returns-410] | `error` | Meaning | Recovery | | ---------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | `idempotency_result_expired` | The original mailbox result was deleted; its key cannot be reused. | Stop automatic send/reply retries. If you saved `submission.id`, read that submission to reconcile delivery. | | `mailbox_message_deleted` | The stored message is being deleted or was deleted. | Stop replaying or replying to that message. Refresh its owning thread; do not select a replacement target automatically. | ```json title="Response: sendInboxMessage 410" { "error": "idempotency_result_expired", "message": "The original mailbox result was deleted; this idempotency key cannot be reused" } ``` Deletion of a mailbox record does **not** prove that an email was never delivered. If the submission is also unavailable or its ID was lost, retain the original key/input and report delivery as unknown; there is no idempotency-key lookup endpoint. A new key would create a new logical send and requires a deliberate application decision. The same rules apply to inbox sends and thread replies. Webhook registration uses its [own conflict/expiry codes](/docs/webhooks/overview#registration-conflicts-and-expiry). # Pagination (/docs/reliability/pagination) Inbox lists, thread lists, messages within a thread, inbox exports, and webhook lists support cursor pagination. Consult each endpoint for supported parameters; not every list is paginated. ## Request the next page [#request-the-next-page] ```bash curl --fail-with-body --silent --show-error --get \ "$MAILACTOR_API_URL/v1/inboxes" \ -H "x-api-key: $MAILACTOR_API_KEY" \ --data-urlencode "limit=20" \ --data-urlencode "cursor=$NEXT_CURSOR" ``` Omit `cursor` for the first request. Pass back the response's `nextCursor` unchanged for the next request. Stop when it is `null`. `limit` is between 1 and 100; smaller pages may help if content-heavy reads return `413`. Cursors are opaque and scoped to their owning tenant/resource. Never decode, modify, or reuse one across inboxes or organizations. ## Know the response container [#know-the-response-container] | Operation | Collection | | ------------------ | ----------- | | List inboxes | `inboxes` | | List inbox threads | `threads` | | Get a thread | `messages` | | Export an inbox | `messages` | | List webhooks | `endpoints` | Lists are newest-first. A thread summary does not contain `messages`; call the thread endpoint to read its messages. ## Read every page [#read-every-page] [Download the Node.js pagination reader](/examples/read-pages.mjs). It passes cursors with URL encoding, stops only at `nextCursor: null`, and throws if the deadline expires, the response is invalid, or any request fails. It does not silently return a partial inventory as complete. ```javascript import { readMailactorPages } from './read-pages.mjs'; for await (const inbox of readMailactorPages('/v1/inboxes', 'inboxes', { apiKey: process.env.MAILACTOR_API_KEY, deadline: Date.now() + 60_000, })) { // Persist this inbox before continuing; a later page can still fail. console.log(inbox.id, inbox.address); } ``` Use `baseUrl` only when targeting a trusted API deployment. This is a read-only helper: retries and durable storage belong to your application. On `429`, honor the helper error's `retryAfter` property, which copies the HTTP `Retry-After` header (it is not a field in the API JSON error body); on a content-size `413`, retry a new traversal with a smaller `limit`. If a traversal fails, preserve what you saved, mark it incomplete, and restart with ID deduplication. Never delete an inbox based on an incomplete export. A nonfinal response contains a string in `nextCursor` (for example, `"opaque-cursor-from-server"`); that example text is not a usable cursor. The next request must reuse the actual server value. A final page returns JSON `null`, not the string `"null"`. ## Handle concurrent arrivals [#handle-concurrent-arrivals] A new message can move a thread toward the top of the list while you are paging. Deduplicate by resource ID. After a complete pass, restart at page one and reconcile until a complete pass adds no unseen IDs, or your deadline expires. Pagination is not a snapshot guarantee. For a continuously active inbox, use webhook notifications plus periodic reconciliation and do not wait forever for a perfectly stable inventory.