{
  "openapi": "3.1.0",
  "info": {
    "title": "Mailcore Email API",
    "version": "1.0.0",
    "description": "Transactional email for approved projects. At this documentation release, production sending is disabled; publishing documentation does not activate sending. Access is currently a controlled pilot: registration submits an application and does not issue credentials, enable sending, create a webhook, or provide a customer sandbox. After separate production activation, an operator approves the project, verified domain, exact sender and quotas, then securely supplies a key. Keep the key on your server. This describes the public send contract, not internal administration. When sending is enabled, a 202 response commits one message to the durable queue; it is not proof of delivery."
  },
  "servers": [{"url": "https://mailcore.uz", "description": "Public Mailcore endpoint"}],
  "security": [{"bearerAuth": []}],
  "tags": [{"name": "Emails"}, {"name": "Health"}],
  "paths": {
    "/api/email/v1/emails": {
      "post": {
        "operationId": "sendEmail",
        "tags": ["Emails"],
        "summary": "Queue one transactional email",
        "description": "Send exactly one recipient. The from address must be an active sender on this project and its domain must be verified. A supplied display name must match the sender binding; omitting it uses the configured name. Unknown JSON fields are rejected: attachments, headers, CC, BCC and SES options are unsupported. The HTTP request body cap defaults to 262144 bytes, including chunked bodies; combined text/html UTF-8 bytes must be at most 200000. Quotas are consumed atomically at key, project and tenant scopes for UTC hour/day/month windows. Rejected requests and committed idempotency replays do not consume quotas. Persist an idempotency key with the application event and reuse both the same key and unchanged payload after an uncertain HTTP result; changing the payload requires a new event/key. Do not send again under a new key to resolve an ambiguous delivery status. Revoked, expired or paused credentials usually fail authentication with 401.",
        "parameters": [{"name": "Idempotency-Key", "in": "header", "required": true, "description": "Required 16–128 ASCII characters from A–Z, a–z, 0–9, dot, underscore, colon and hyphen. Scoped to the authenticated API key and this operation. Same key/payload returns the original committed 202; different payload returns 409. No public fixed retention interval is promised.", "schema": {"type": "string", "minLength": 16, "maxLength": 128, "pattern": "^[A-Za-z0-9._:-]{16,128}$"}, "example": "order-8421-completed"}],
        "requestBody": {
          "required": true,
          "content": {"application/json": {"schema": {"$ref": "#/components/schemas/EmailCreate"}, "example": {"from": "Example Shop <orders@notify.example>", "to": "customer@recipient.example", "subject": "Order #8421 completed", "text": "Your order has been completed.", "html": "<p>Your order has been completed.</p>", "replyTo": "support@notify.example", "tags": {"order_id": "8421"}}}}
        },
        "responses": {
          "202": {"description": "Durably queued, or the original committed response replayed. Query message state separately.", "headers": {"Idempotency-Replayed": {"description": "Present with value true only for a committed replay.", "schema": {"type": "string", "const": "true"}}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/EmailQueued"}, "examples": {"queued": {"value": {"id": "eml_0123456789abcdef01234567", "status": "queued"}}}}}},
          "400": {"description": "Invalid Content-Length or missing/invalid idempotency key.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "examples": {"request_invalid": {"value": {"ok": false, "error": "request_invalid", "message": "Content-Length is invalid", "retryable": false}}, "idempotency_key_required": {"value": {"ok": false, "error": "idempotency_key_required", "message": "Idempotency-Key must contain 16-128 safe ASCII characters", "retryable": false}}}}}},
          "401": {"$ref": "#/components/responses/Unauthorized"},
          "403": {"description": "Sending paused during submission or by the volume circuit breaker. Contact support; do not loop retries.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "examples": {"account_paused": {"value": {"ok": false, "error": "account_paused", "message": "Sending is paused", "retryable": false}}, "volume_anomaly_paused": {"value": {"ok": false, "error": "volume_anomaly_paused", "message": "Sending paused after a volume anomaly", "retryable": false}}}}}},
          "409": {"description": "Same idempotency key reused with different request data.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "examples": {"idempotency_conflict": {"value": {"ok": false, "error": "idempotency_conflict", "message": "Idempotency-Key was reused with a different request", "retryable": false}}}}}},
          "413": {"$ref": "#/components/responses/TooLarge"},
          "422": {"description": "Malformed JSON, unsupported fields, invalid content, unauthorized sender or suppressed recipient. Correct the cause rather than automatically retrying.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "examples": {"request_invalid": {"value": {"ok": false, "error": "request_invalid", "message": "Check the request fields", "retryable": false}}, "sender_not_allowed": {"value": {"ok": false, "error": "sender_not_allowed", "message": "From address is not an active verified sender", "retryable": false}}, "recipient_suppressed": {"value": {"ok": false, "error": "recipient_suppressed", "message": "Recipient is suppressed", "retryable": false}}}}}},
          "429": {"description": "An approved key, project or tenant quota was reached. Back off; the response does not include Retry-After or quota-limit headers. Retryable is not an instruction for immediate retries.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "examples": {"quota_exceeded": {"value": {"ok": false, "error": "quota_exceeded", "message": "api_key hour quota exceeded", "retryable": true}}}}}},
          "503": {"description": "Sending disabled or durable-storage capacity guard active. Back off and contact support if persistent.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "examples": {"sending_disabled": {"value": {"ok": false, "error": "sending_disabled", "message": "Transactional sending is not enabled", "retryable": true}}, "storage_capacity_guard": {"value": {"ok": false, "error": "storage_capacity_guard", "message": "Storage capacity guard is active", "retryable": true}}}}}}
        }
      }
    },
    "/api/email/v1/emails/{message_id}": {
      "get": {
        "operationId": "getEmail", "tags": ["Emails"], "summary": "Read message state",
        "description": "Returns state only for the authenticated project. Another project's message and an unknown/malformed ID both return 404. The document contains sender and provider ID, but never the recipient, subject or body. A 202 response and the accepted state are not delivery confirmations. The ambiguous state means a send may have reached the provider; do not blindly resend it. Later verified provider events may resolve ambiguous state.",
        "parameters": [{"name": "message_id", "in": "path", "required": true, "schema": {"type": "string", "pattern": "^eml_[a-f0-9]{24}$"}, "example": "eml_0123456789abcdef01234567"}],
        "responses": {
          "200": {"description": "Current message state.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/EmailStatus"}, "examples": {"queued": {"value": {"id": "eml_0123456789abcdef01234567", "status": "queued", "from": "Example Shop <orders@notify.example>", "providerMessageId": null, "detail": null, "createdAt": "2026-10-14T10:00:00.000Z", "updatedAt": "2026-10-14T10:00:00.000Z"}}}}}},
          "401": {"$ref": "#/components/responses/Unauthorized"},
          "404": {"description": "Unknown or inaccessible message.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "examples": {"email_not_found": {"value": {"ok": false, "error": "email_not_found", "message": "Email not found", "retryable": false}}}}}}
        }
      }
    },
    "/api/email/v1/healthz": {
      "get": {
        "operationId": "getHealth", "tags": ["Health"], "summary": "Read minimal HTTP health", "security": [],
        "description": "Unauthenticated database reachability check. Does not attest that sending is enabled, that workers/events are healthy or that the project is approved.",
        "responses": {
          "200": {"description": "API database reachable.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Health"}, "example": {"ok": true}}}},
          "503": {"description": "API database not reachable.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Health"}, "example": {"ok": false}}}}
        }
      }
    }
  },
  "webhooks": {
    "emailEvent": {
      "post": {
        "operationId": "receiveEmailEvent", "summary": "Mailcore sends a signed event to your endpoint", "security": [],
"description": "Operator-configured endpoints only: no public webhook-management API exists. Destination must be public HTTPS port 443, without credentials, query strings or fragments. Redirects are not followed. Signatures use the literal signing secret as UTF-8 bytes (do not base64-decode it) and exact raw request bytes: HMAC-SHA256(secret, Webhook-Id + '.' + Webhook-Timestamp + '.' + rawBody). Signature is v1,<lowercase hex>. Verify in constant time before JSON parsing and reject stale/future timestamps (a 300-second tolerance is a receiver policy, not a provider promise). Use a persistent unique constraint on Webhook-Id and atomically persist the event and its durable processing job before acknowledging; process business effects transactionally and idempotently. Events may be duplicated or arrive out of order; retries have the same ID and body but a fresh timestamp/signature. Default timeout is 10 seconds and default maximum is 8 attempts; bounded exponential retries occur for non-2xx or transport errors. HTTP 410 disables the endpoint immediately; exhausted deliveries require operator inspection.",
        "parameters": [
          {"name": "Webhook-Id", "in": "header", "required": true, "schema": {"type": "string", "pattern": "^evt_[a-f0-9]{24}$"}},
          {"name": "Webhook-Timestamp", "in": "header", "required": true, "description": "Unix timestamp in seconds for this delivery attempt.", "schema": {"type": "string", "pattern": "^[0-9]+$"}},
          {"name": "Webhook-Signature", "in": "header", "required": true, "schema": {"type": "string", "pattern": "^v1,[a-f0-9]{64}$"}}
        ],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebhookEvent"}, "example": {"id": "evt_0123456789abcdef01234567", "type": "email.delivered", "createdAt": "2026-10-14T10:00:03.000Z", "data": {"emailId": "eml_0123456789abcdef01234567", "status": "delivered", "detail": null}}}}},
        "responses": {"200": {"description": "Receiver has durably accepted the event or it was already processed; any 2xx acknowledges."}, "410": {"description": "Receiver intentionally disables future deliveries to this endpoint."}, "default": {"description": "Non-2xx is a failed attempt. Retry up to the configured limit (except 410)."}}
      }
    }
  },
  "components": {
    "securitySchemes": {"bearerAuth": {"type": "http", "scheme": "bearer", "description": "Project-scoped Mailcore key supplied once after operator approval. Read it from server environment/secret storage; never expose it in frontend code, URLs, logs or exported collections."}},
    "responses": {
      "Unauthorized": {"description": "Missing, invalid, revoked, expired or inactive key/project/tenant. Authentication uses Bearer, not a website login cookie.", "headers": {"WWW-Authenticate": {"schema": {"type": "string", "const": "Bearer"}}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "examples": {"authentication_required": {"value": {"ok": false, "error": "authentication_required", "message": "Valid bearer authentication is required", "retryable": false}}}}}},
      "TooLarge": {"description": "HTTP body exceeds the configured cap (262144 bytes by default).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "examples": {"request_too_large": {"value": {"ok": false, "error": "request_too_large", "message": "Request body is too large", "retryable": false}}}}}}
    },
    "schemas": {
      "EmailCreate": {
        "type": "object", "additionalProperties": false, "required": ["from", "to", "subject"],
        "description": "Only these canonical public field names are documented. At least one of text/html must be a string; combined UTF-8 bytes <=200000. Mailboxes must be valid single addresses; the parser accepts an optional display name and rejects address lists/control characters.",
        "anyOf": [{"required": ["text"], "properties": {"text": {"type": "string"}}}, {"required": ["html"], "properties": {"html": {"type": "string"}}}],
        "not": {"required": ["text"], "properties": {"text": {"const": ""}, "html": {"enum": [null, ""]}}},
        "properties": {
          "from": {"type": "string", "minLength": 3, "maxLength": 320, "description": "Exact active verified sender; optional display name must match binding."},
          "to": {"type": "string", "minLength": 3, "maxLength": 320, "description": "One recipient as a string, never an array or comma-separated list."},
          "subject": {"type": "string", "minLength": 1, "maxLength": 998, "pattern": "^[^\\u0000-\\u0008\\u000A-\\u001F]*$", "description": "No CR, LF, NUL or ASCII control characters except tab."},
          "text": {"type": ["string", "null"], "maxLength": 200000},
          "html": {"type": ["string", "null"], "maxLength": 200000},
          "replyTo": {"type": ["string", "null"], "minLength": 3, "maxLength": 320, "description": "Optional single reply address; an optional display name is accepted."},
          "tags": {"type": "object", "maxProperties": 10, "default": {}, "propertyNames": {"type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[A-Za-z0-9_-]{1,64}$", "not": {"pattern": "^[Mm][Aa][Ii][Ll][Cc][Oo][Rr][Ee]_"}}, "additionalProperties": {"type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[A-Za-z0-9_-]{1,256}$"}, "description": "Up to 10 string tags. Names and values use ASCII letters, digits, underscore or hyphen; mailcore_ names are reserved case-insensitively."}
        },
        "x-max-combined-content-utf8-bytes": 200000,
        "x-default-max-http-body-bytes": 262144
      },
      "EmailQueued": {"type": "object", "additionalProperties": false, "required": ["id", "status"], "properties": {"id": {"$ref": "#/components/schemas/EmailId"}, "status": {"type": "string", "const": "queued"}}},
      "EmailId": {"type": "string", "pattern": "^eml_[a-f0-9]{24}$"},
      "MessageStatus": {"type": "string", "enum": ["queued", "sending", "accepted", "delayed", "delivered", "bounced", "complained", "rejected", "failed", "ambiguous"], "description": "accepted: provider acknowledged, not inbox delivery. ambiguous: provider may have accepted; never resend blindly. delivered: provider delivery event, not proof of reading/inbox placement."},
      "EmailStatus": {"type": "object", "additionalProperties": false, "required": ["id", "status", "from", "providerMessageId", "detail", "createdAt", "updatedAt"], "properties": {"id": {"$ref": "#/components/schemas/EmailId"}, "status": {"$ref": "#/components/schemas/MessageStatus"}, "from": {"type": "string"}, "providerMessageId": {"type": ["string", "null"]}, "detail": {"type": ["string", "null"]}, "createdAt": {"type": "string", "format": "date-time"}, "updatedAt": {"type": "string", "format": "date-time"}}},
      "Error": {"type": "object", "additionalProperties": false, "required": ["ok", "error", "message", "retryable"], "properties": {"ok": {"type": "boolean", "const": false}, "error": {"type": "string", "enum": ["request_invalid", "idempotency_key_required", "authentication_required", "account_paused", "volume_anomaly_paused", "email_not_found", "idempotency_conflict", "request_too_large", "sender_not_allowed", "recipient_suppressed", "quota_exceeded", "sending_disabled", "storage_capacity_guard"]}, "message": {"type": "string", "description": "Human-readable detail; branch on error rather than message text."}, "retryable": {"type": "boolean", "description": "Backoff may be appropriate when true; does not mean retry immediately or with a new idempotency key."}}},
      "Health": {"type": "object", "additionalProperties": false, "required": ["ok"], "properties": {"ok": {"type": "boolean"}}},
      "WebhookEvent": {"type": "object", "additionalProperties": false, "required": ["id", "type", "createdAt", "data"], "properties": {"id": {"type": "string", "pattern": "^evt_[a-f0-9]{24}$"}, "type": {"type": "string", "enum": ["email.accepted", "email.delayed", "email.delivered", "email.bounced", "email.complained", "email.rejected", "email.failed", "email.ambiguous"]}, "createdAt": {"type": "string", "format": "date-time"}, "data": {"type": "object", "additionalProperties": false, "required": ["emailId", "status", "detail"], "properties": {"emailId": {"$ref": "#/components/schemas/EmailId"}, "status": {"type": "string", "enum": ["accepted", "delayed", "delivered", "bounced", "complained", "rejected", "failed", "ambiguous"]}, "detail": {"type": ["string", "null"]}}}}}
    }
  }
}
