Developer Documentation
Inbox API
Programmatic inbox operations for automation pipelines.
Authentication
- Method: Bearer API key (
Authorization: Bearer <api_key>) orX-Api-Key. - Permissions:
inboundforPOST /api/v1/mail/inboundreadfor email retrieval endpointswritefor read/delete/update actionswebhookfor webhook registration endpoints
inboundforPOST /api/v1/mail/inboundreadfor email retrieval endpointswritefor read/delete/update actionswebhookfor webhook registration endpoints
Common errors:
Error Response
{ "status": "error", "reason": "API key is required." }
Error Response
{ "status": "error", "reason": "API key does not have webhook permission." }
Mailbox Bulk Task Behavior
For POST /api/v1/create/mailbox/bulk and the follow-up GET /api/v1/get/mailbox/bulk task polling:
- Existing mailbox addresses are skipped automatically and do not fail the task.
- Task completion still returns
status=completedwhen every requested address already exists. - In that all-existing case,
total_emailis0andresultsis empty.
Inbound Ingestion
POST /api/v1/mail/inbound
Receives inbound email payload from upstream mail provider.
Required request fields:
message_idstring (unique per mailbox)from_emailstring emailto_emailstring emailsubjectstringreceived_atISO timestamp
Optional:
from_name,cc[],bcc[],body_text,body_html,headers,in_reply_to,references[]attachments[](filename,mime_type,size, and eithercontentbase64 orcontent_url)
Request Example
curl -X POST "$APP_URL/api/v1/mail/inbound" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message_id": "msg-12345",
"from_email": "sender@example.com",
"to_email": "support@mails.now",
"subject": "Need help",
"body_text": "Hello team",
"received_at": "2026-04-27T12:10:00Z"
}'
Success Response
{
"status": "accepted",
"message_id": "msg-12345"
}
Error examples:
401invalid/missing API key403missinginboundpermission422unknown mailbox or validation error
Mailcow IMAP bridge rollout
IMAP polling ingests mail in-process in the application that runs the poller (mailbridge:poll-inbound): messages are normalized and dispatched without an HTTP loopback or bridge-specific API keys.
- Set
MAILBRIDGE_POLL_ENABLED=truewhen you want Laravel’s scheduler to runmailbridge:poll-inboundevery 30 minutes (seeroutes/console.php). - Set
MAILBRIDGE_QUEUE=inboundand run a queue worker that listens to theinboundqueue (recommended: a dedicated Supervisor program separate fromdefault/mailcowworkers). - Only mailboxes with Active status and Ready provisioning (
provisioning_status = active) that have IMAP credentials or a stored password are scheduled for polling. - Run
php artisan mailbridge:poll-inboundmanually and inspect bridge logs/dead-letter rows until behavior is stable. - Roll out mailbox-by-mailbox (e.g.
--mailbox=ID) before relying on scheduler-driven polling for all boxes. - Users can still trigger an immediate poll from the Inbox Refresh button; those jobs are queued on
inboundfor all pollable active mailboxes in their tenant.
Tenant integrations may still POST to /api/v1/mail/inbound with a normal workspace API key and inbound permission—that path is unrelated to IMAP polling.
Dead-letter replay:
Example
php artisan mailbridge:replay-deadletters --id=123 php artisan mailbridge:replay-deadletters --mailbox=10 php artisan mailbridge:replay-deadletters --all
Email Retrieval API
GET /api/v1/emails
Query params:
mailbox_idunread=true|falsesince=ISO_TIMESTAMPbefore=ISO_TIMESTAMPq=keyword- cursor pagination via
cursor
Example:
Request Example
curl -H "Authorization: Bearer $API_KEY" \ "$APP_URL/api/v1/emails?mailbox_id=10&unread=true&q=invoice"
Response:
Success Response
{
"data": [
{
"id": 101,
"mailbox_id": 10,
"thread_id": 44,
"message_id": "msg-123",
"from_email": "billing@example.com",
"to_email": "ops@mails.now",
"subject": "Invoice",
"body_text": "Please review",
"body_html": "<p>Please review</p>",
"is_read": false,
"is_deleted": false,
"is_spam": false,
"spam_score": 0,
"received_at": "2026-04-27T10:00:00Z",
"updated_at": "2026-04-27T10:00:01Z",
"attachments": []
}
],
"meta": {
"next_cursor": null,
"prev_cursor": null
}
}
GET /api/v1/emails/{id}
Returns one email scoped to tenant.
GET /api/v1/emails/sync?since=ISO_TIMESTAMP
Returns only records where updated_marker_at >= since for incremental polling clients.
POST /api/v1/emails/{id}/read
Marks email read and emits email.read webhook event.
Request Example
curl -X POST -H "Authorization: Bearer $API_KEY" \ "$APP_URL/api/v1/emails/101/read"
DELETE /api/v1/emails/{id}
Soft-deletes email and emits email.deleted.
GET /api/v1/emails/{id}/attachments/{attachment_id}
Returns a short-lived signed URL.
Response:
Success Response
{
"data": {
"url": "https://app.test/api/v1/downloads/attachments/9?expires=...",
"expires_at": "2026-04-27T12:15:00Z"
}
}
Webhook Management API
POST /api/v1/webhooks
Registers tenant webhook endpoint.
Payload:
url(required URL)events[](email.received,email.read,email.deleted)is_activeoptional bool
Example:
Request Example
curl -X POST "$APP_URL/api/v1/webhooks" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://client.example/webhooks/email","events":["email.received","email.deleted"]}'
GET /api/v1/webhooks
Lists current tenant webhook endpoints.
PATCH /api/v1/webhooks/{id}
Updates url/events/is_active.
DELETE /api/v1/webhooks/{id}
Deletes webhook.
Outbound Webhook Delivery Format
Every webhook request body:
Success Response
{
"event": "email.received",
"timestamp": 1777290021,
"data": {
"email_id": 101,
"mailbox_id": 10
}
}
Headers:
X-Webhook-Signature:HMAC_SHA256(timestamp + "." + raw_json_payload, webhook_secret)X-Webhook-Timestamp: unix timestamp
Delivery is queued and retried with exponential backoff.