{
  "openapi": "3.1.0",
  "info": {
    "title": "mxgate API",
    "version": "0.3.0",
    "description": "mxgate receives mail for domains you own and forwards it to confirmed mailboxes. It sends mail as addresses at those domains, through SMTP or one API call. Requests and answers are JSON. Every error is JSON `{error, message, fix}`: a code to switch on, a sentence for a person, and the next step. Every endpoint can also answer 404 `not_found`, 405 `method_not_allowed` and 500 `internal`; an endpoint that takes a body can answer 400 `invalid_json` and 413 `body_too_large`. The hosted MCP server is at `/mcp` (Streamable HTTP): it takes the API key, or a token from OAuth; see https://mxgate.io/docs/mcp.",
    "contact": {
      "name": "mxgate",
      "email": "hello@mxgate.io",
      "url": "https://mxgate.io"
    }
  },
  "servers": [
    {
      "url": "https://api.mxgate.io"
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "Create an account and read its plan, limits and usage."
    },
    {
      "name": "Sign-in",
      "description": "Sign in to the web dashboard with a code that is mailed to the account address."
    },
    {
      "name": "Keys",
      "description": "List, recover and replace the API keys of the account."
    },
    {
      "name": "Connected apps",
      "description": "The apps that the owner connected through OAuth, for the hosted MCP server at /mcp."
    },
    {
      "name": "Domains",
      "description": "Add a domain, get the DNS records to publish, and check them."
    },
    {
      "name": "Aliases",
      "description": "Addresses at a domain and the mailboxes they forward to."
    },
    {
      "name": "Destinations",
      "description": "The mailboxes you forward to; each one must confirm before it gets mail."
    },
    {
      "name": "Send-as",
      "description": "SMTP logins that send mail as addresses at a domain."
    },
    {
      "name": "Send",
      "description": "Send one plain-text message with one API call."
    },
    {
      "name": "Logs",
      "description": "One row for each message, with what the other server answered."
    },
    {
      "name": "Notifications",
      "description": "An email digest and a signed webhook for mail that failed."
    },
    {
      "name": "Billing",
      "description": "Links for a person to pay for the paid plan and to manage the subscription."
    },
    {
      "name": "Status",
      "description": "The list of endpoints and the health of the service."
    }
  ],
  "security": [
    {
      "bearer": []
    }
  ],
  "paths": {
    "/": {
      "get": {
        "tags": [
          "Status"
        ],
        "operationId": "getIndex",
        "summary": "List the endpoints",
        "description": "Returns the name and the version of the service, the setup steps, and one line for each endpoint. GET /v1 gives the same answer. No auth.",
        "security": [],
        "responses": {
          "200": {
            "description": "The service and its endpoints.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IndexResponse"
                },
                "example": {
                  "name": "mxgate",
                  "version": "0.3.0",
                  "about": "Email for a domain, set up by an agent. Receive mail for domains you own and forward it to confirmed mailboxes.",
                  "docs": "https://mxgate.io/llms.txt",
                  "reference": "https://mxgate.io/reference",
                  "openapi": "https://mxgate.io/openapi.json",
                  "auth": "Authorization: Bearer <api_key>",
                  "flow": [
                    "POST /v1/signup {\"email\"} -> api_key; confirm the link mailed to that address",
                    "POST /v1/domains {\"name\"} -> the DNS records to add",
                    "POST /v1/domains/{domain}/verify -> checks the records; the domain takes mail once ownership is proven",
                    "PUT /v1/domains/{domain}/aliases/{alias} {\"to\":[\"you@example.org\"]} -> \"*\" is the catch-all"
                  ],
                  "endpoints": {
                    "POST /v1/signup": "create an account; no auth",
                    "GET /v1/account": "plan, limits, usage, billing",
                    "GET /v1/domains": "your domains",
                    "POST /v1/domains": "add a domain",
                    "POST /v1/domains/{domain}/verify": "check DNS now",
                    "PUT /v1/domains/{domain}/aliases/{alias}": "create or replace an alias"
                  },
                  "mx_hosts": [
                    "mx1.mxgate.io"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/status": {
      "get": {
        "tags": [
          "Status"
        ],
        "operationId": "getStatus",
        "summary": "Get the service status",
        "description": "Returns the health of the service for the last 30 days. The numbers come from a monitor on another machine that checks the server every 2 minutes: port 25 takes mail and refuses to relay, the TLS certificates on ports 25, 587 and 465 are valid, and the API answers over HTTPS. `stale` is true when the monitor sent no report for 10 minutes; `state` is then `unknown`. A check is down after two failed runs in a row. The answer has no customer data. No auth.",
        "security": [],
        "responses": {
          "200": {
            "description": "The state now and the checks of the last 30 days.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusView"
                },
                "example": {
                  "state": "operational",
                  "updated_at": "2026-11-14T10:42:07.000Z",
                  "window_days": 30,
                  "days": [
                    "2026-10-16",
                    "2026-10-17",
                    "2026-10-18",
                    "2026-10-19",
                    "2026-10-20",
                    "2026-10-21",
                    "2026-10-22",
                    "2026-10-23",
                    "2026-10-24",
                    "2026-10-25",
                    "2026-10-26",
                    "2026-10-27",
                    "2026-10-28",
                    "2026-10-29",
                    "2026-10-30",
                    "2026-10-31",
                    "2026-11-01",
                    "2026-11-02",
                    "2026-11-03",
                    "2026-11-04",
                    "2026-11-05",
                    "2026-11-06",
                    "2026-11-07",
                    "2026-11-08",
                    "2026-11-09",
                    "2026-11-10",
                    "2026-11-11",
                    "2026-11-12",
                    "2026-11-13",
                    "2026-11-14"
                  ],
                  "checks": [
                    {
                      "name": "mail in (mx1.mxgate.io:25)",
                      "ok": true,
                      "detail": "accepts postmaster@mxgate.io, refuses to relay",
                      "last_check_at": "2026-11-14T10:42:07.000Z",
                      "last_ok_at": "2026-11-14T10:42:07.000Z",
                      "success_rate": 99.98,
                      "daily": [
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        99.44,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100
                      ]
                    },
                    {
                      "name": "mail in TLS (mx1.mxgate.io:25)",
                      "ok": true,
                      "detail": "certificate valid, 61 days left",
                      "last_check_at": "2026-11-14T10:42:07.000Z",
                      "last_ok_at": "2026-11-14T10:42:07.000Z",
                      "success_rate": 99.98,
                      "daily": [
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        99.44,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100
                      ]
                    },
                    {
                      "name": "send-as (smtp.mxgate.io:587)",
                      "ok": true,
                      "detail": "certificate valid, 61 days left",
                      "last_check_at": "2026-11-14T10:42:07.000Z",
                      "last_ok_at": "2026-11-14T10:42:07.000Z",
                      "success_rate": 100,
                      "daily": [
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100
                      ]
                    },
                    {
                      "name": "send-as (smtp.mxgate.io:465)",
                      "ok": true,
                      "detail": "certificate valid, 61 days left",
                      "last_check_at": "2026-11-14T10:42:07.000Z",
                      "last_ok_at": "2026-11-14T10:42:07.000Z",
                      "success_rate": 100,
                      "daily": [
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100
                      ]
                    },
                    {
                      "name": "API (api.mxgate.io)",
                      "ok": true,
                      "detail": "HTTP 200, certificate valid, 61 days left",
                      "last_check_at": "2026-11-14T10:42:07.000Z",
                      "last_ok_at": "2026-11-14T10:42:07.000Z",
                      "success_rate": 100,
                      "daily": [
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100,
                        100
                      ]
                    }
                  ],
                  "stale": false
                }
              }
            }
          }
        }
      }
    },
    "/v1/signup": {
      "post": {
        "tags": [
          "Account"
        ],
        "operationId": "signUp",
        "summary": "Create an account",
        "description": "Creates an account for an email address and returns its first API key. The key is shown once in the answer and is mailed to the address. The mail has a link: the owner opens it and presses \"Confirm forwarding\", and the address becomes a forwarding destination. Disposable addresses are refused. No auth. Limits: 3 signups a day and 10 requests an hour from one IP address.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailRequest"
              },
              "example": {
                "email": "ada@example.org"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The account was made.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignupResponse"
                },
                "example": {
                  "api_key": "mxg_0123456789abcdef0123456789abcdef01234567",
                  "account": {
                    "email": "ada@example.org",
                    "plan": "free",
                    "status": "active",
                    "status_reason": null,
                    "send_hold": null,
                    "limits": {
                      "domains": 1,
                      "aliases_per_domain": 10,
                      "destinations_per_alias": 3,
                      "send_per_day": 50,
                      "send_per_month": null
                    },
                    "usage": {
                      "domains": 0,
                      "sent_today": 0,
                      "sent_this_month": 0
                    },
                    "billing": null,
                    "created_at": "2026-11-13T09:12:44.318Z"
                  },
                  "mailed": true,
                  "message": "The key is shown once here and was mailed to you, with a link that confirms the address as a forwarding destination.",
                  "next_steps": [
                    "Open the confirmation link mailed to ada@example.org and press \"Confirm forwarding\".",
                    "Add a domain: POST /v1/domains {\"name\":\"example.com\"}.",
                    "Add the DNS records from the response, then POST /v1/domains/{domain}/verify.",
                    "Create an alias: PUT /v1/domains/{domain}/aliases/hello {\"to\":[\"ada@example.org\"]}."
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`: the body is not a JSON object. `invalid_email`: `email` is missing or is not an email address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`already_registered`: the address has an account already. Use its key, or recover the key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`body_too_large`: the body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`disposable_email`: the address is at a disposable mail service.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: more than 10 signup requests in an hour from this IP address. The answer has `retry_after_seconds`. `too_many_signups`: 3 accounts were made from this IP address in the last 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/account": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getAccount",
        "summary": "Get the account",
        "description": "Returns the plan, the limits, the usage, the status and the state of the subscription.",
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "The account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountView"
                },
                "example": {
                  "email": "ada@example.org",
                  "plan": "free",
                  "status": "active",
                  "status_reason": null,
                  "send_hold": null,
                  "limits": {
                    "domains": 1,
                    "aliases_per_domain": 10,
                    "destinations_per_alias": 3,
                    "send_per_day": 50,
                    "send_per_month": null
                  },
                  "usage": {
                    "domains": 1,
                    "sent_today": 3,
                    "sent_this_month": 41
                  },
                  "billing": null,
                  "created_at": "2026-11-13T09:12:44.318Z"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/login": {
      "post": {
        "tags": [
          "Sign-in"
        ],
        "operationId": "requestLogin",
        "summary": "Mail a sign-in code",
        "description": "Mails a six-digit sign-in code to the account address. The answer is the same when the address has no account. The code works once, for 10 minutes and for 5 attempts. One mail for one account in a minute. No auth. Limit: 10 requests an hour from one IP address.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailRequest"
              },
              "example": {
                "email": "ada@example.org"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The request was taken. The answer does not say if the address has an account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoginResponse"
                },
                "example": {
                  "message": "If ada@example.org has an mxgate account, a six-digit code was mailed to it. The code works once, for 10 minutes. One mail per account in a minute.",
                  "next_step": "POST /v1/login/confirm {\"email\",\"code\"} gives a session token. No account yet? POST /v1/signup {\"email\"}."
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`: the body is not a JSON object. `invalid_email`: `email` is missing or is not an email address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`body_too_large`: the body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: more than 10 sign-in requests in an hour from this IP address. The answer has `retry_after_seconds`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/login/confirm": {
      "post": {
        "tags": [
          "Sign-in"
        ],
        "operationId": "confirmLogin",
        "summary": "Get a session token",
        "description": "Exchanges the sign-in code for a session token. The session token is a Bearer credential for the web dashboard: it works like an API key for 30 days. A code works once. No auth. Limit: 10 requests an hour from one IP address.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginConfirmRequest"
              },
              "example": {
                "email": "ada@example.org",
                "code": "482913"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The session was made.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionResponse"
                },
                "example": {
                  "session_token": "mxs_0123456789abcdef0123456789abcdef0123456789abcdef",
                  "expires_at": "2026-12-14T10:05:31.902Z",
                  "account": {
                    "email": "ada@example.org",
                    "plan": "free",
                    "status": "active",
                    "status_reason": null,
                    "send_hold": null,
                    "limits": {
                      "domains": 1,
                      "aliases_per_domain": 10,
                      "destinations_per_alias": 3,
                      "send_per_day": 50,
                      "send_per_month": null
                    },
                    "usage": {
                      "domains": 1,
                      "sent_today": 3,
                      "sent_this_month": 41
                    },
                    "billing": null,
                    "created_at": "2026-11-13T09:12:44.318Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`: the body is not a JSON object. `invalid_login_code`: the code is not valid, was used already, had 5 wrong attempts, or is older than 10 minutes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`body_too_large`: the body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: more than 10 sign-in attempts in an hour from this IP address. The answer has `retry_after_seconds`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/logout": {
      "post": {
        "tags": [
          "Sign-in"
        ],
        "operationId": "logout",
        "summary": "End the session",
        "description": "Signs out: the session token of this call stops working. Send the session token as the Bearer credential. An API key has no sign-out: POST /v1/keys/rotate makes a key stop working.",
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "The session token stopped working.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LogoutResponse"
                },
                "example": {
                  "signed_out": true
                }
              }
            }
          },
          "400": {
            "description": "`not_a_session`: the call has an API key, not a session token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/keys": {
      "get": {
        "tags": [
          "Keys"
        ],
        "operationId": "listKeys",
        "summary": "List the API keys",
        "description": "Returns the API keys of the account that work now. A key is shown in full only when it is made; later only its first 12 characters.",
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "The API keys that work now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyListResponse"
                },
                "example": {
                  "keys": [
                    {
                      "prefix": "mxg_01234567",
                      "created_at": "2026-11-13T09:12:44.318Z",
                      "last_used_at": "2026-11-14T10:01:17.553Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/keys/recover": {
      "post": {
        "tags": [
          "Keys"
        ],
        "operationId": "recoverKey",
        "summary": "Mail a key recovery token",
        "description": "For a lost API key: mails a recovery token to the account address. The answer is the same when the address has no account. The token works once, for 60 minutes. One mail for one account in 10 minutes. No auth. Limit: 10 requests an hour from one IP address.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailRequest"
              },
              "example": {
                "email": "ada@example.org"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The request was taken. The answer does not say if the address has an account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecoverResponse"
                },
                "example": {
                  "message": "If ada@example.org has an mxgate account, a recovery token was mailed to it. The token works once, for 60 minutes. One mail per account in 10 minutes.",
                  "next_step": "Run `mxgate recover --token <token>`, or POST /v1/keys/recover/confirm {\"token\":\"<token>\"}."
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`: the body is not a JSON object. `invalid_email`: `email` is missing or is not an email address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`body_too_large`: the body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: more than 10 recovery requests in an hour from this IP address. The answer has `retry_after_seconds`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/keys/recover/confirm": {
      "post": {
        "tags": [
          "Keys"
        ],
        "operationId": "confirmKeyRecovery",
        "summary": "Get a new key with the token",
        "description": "Exchanges the recovery token for a new API key. Every older key and every session token of the account stop working. Domains, aliases and SMTP logins do not change. The key is shown once in the answer. No auth. Limit: 10 requests an hour from one IP address.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecoverConfirmRequest"
              },
              "example": {
                "token": "mxr_0123456789abcdef0123456789abcdef0123456789abcdef"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewKeyResponse"
                },
                "example": {
                  "api_key": "mxg_0123456789abcdef0123456789abcdef01234567",
                  "account": {
                    "email": "ada@example.org",
                    "plan": "free",
                    "status": "active",
                    "status_reason": null,
                    "send_hold": null,
                    "limits": {
                      "domains": 1,
                      "aliases_per_domain": 10,
                      "destinations_per_alias": 3,
                      "send_per_day": 50,
                      "send_per_month": null
                    },
                    "usage": {
                      "domains": 1,
                      "sent_today": 3,
                      "sent_this_month": 41
                    },
                    "billing": null,
                    "created_at": "2026-11-13T09:12:44.318Z"
                  },
                  "revoked_keys": 1,
                  "message": "The key is shown once here: store it now. 1 older key stopped working. Domains, aliases and SMTP logins did not change."
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`: the body is not a JSON object. `invalid_recovery_token`: the token is not valid, was used already, or is older than 60 minutes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`body_too_large`: the body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: more than 10 recovery attempts in an hour from this IP address. The answer has `retry_after_seconds`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/keys/rotate": {
      "post": {
        "tags": [
          "Keys"
        ],
        "operationId": "rotateKey",
        "summary": "Replace the API key",
        "description": "Returns a new API key for the account. The key of this call and every older key stop working. The key is shown once in the answer. A mail tells the account address that the key was replaced. A suspended account can do this too. Limit: 60 changes at once for one account, then 1 each second.",
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "201": {
            "description": "The new key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewKeyResponse"
                },
                "example": {
                  "api_key": "mxg_0123456789abcdef0123456789abcdef01234567",
                  "account": {
                    "email": "ada@example.org",
                    "plan": "free",
                    "status": "active",
                    "status_reason": null,
                    "send_hold": null,
                    "limits": {
                      "domains": 1,
                      "aliases_per_domain": 10,
                      "destinations_per_alias": 3,
                      "send_per_day": 50,
                      "send_per_month": null
                    },
                    "usage": {
                      "domains": 1,
                      "sent_today": 3,
                      "sent_this_month": 41
                    },
                    "billing": null,
                    "created_at": "2026-11-13T09:12:44.318Z"
                  },
                  "revoked_keys": 1,
                  "message": "The key is shown once here: store it now. 1 older key stopped working. Domains, aliases and SMTP logins did not change."
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`not_for_apps`: the call was made with the token of a connected app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: too many changes from this account. The answer has `retry_after_seconds`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/billing/checkout": {
      "post": {
        "tags": [
          "Billing"
        ],
        "operationId": "createCheckout",
        "summary": "Get a payment link",
        "description": "Returns a link to a Stripe payment page for the paid plan: 9 USD a month or 79 USD a year. The link works for 24 hours. Show it to the person who owns the account: an agent cannot pay. The paid plan is on about a minute after the payment; GET /v1/account shows it. Limit: 60 changes at once for one account, then 1 each second.",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutRequest"
              },
              "example": {
                "interval": "year"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The payment link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutResponse"
                },
                "example": {
                  "url": "https://checkout.stripe.com/c/pay/cs_live_a1B2c3D4e5F6g7H8",
                  "interval": "year",
                  "price_usd": 79,
                  "message": "A person opens the URL and pays 79 USD a year by card. The link works for 24 hours. The paid plan is on about a minute after the payment; GET /v1/account shows it."
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`: the body is not a JSON object. `invalid_interval`: `interval` is not `month` or `year`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`account_suspended`: the account is suspended. Reads still work; changes are blocked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`already_subscribed`: the account has a subscription to the paid plan already. Use POST /v1/billing/portal to change it. `already_paid`: a person turned the paid plan on for this account; it needs no payment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`body_too_large`: the body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: too many changes from this account. The answer has `retry_after_seconds`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`payment_provider_failed`: the payment provider did not answer as expected. Retry in a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`payments_not_configured`: this server takes no payments.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/billing/portal": {
      "post": {
        "tags": [
          "Billing"
        ],
        "operationId": "createPortalLink",
        "summary": "Get a subscription management link",
        "description": "Returns a link to a Stripe page for the person who owns the account: change the card, switch between monthly and yearly, see invoices, or cancel. The link works for a short time. A cancelled plan stays on until the end of the paid period; then the account is on the free plan and mail is still forwarded. Limit: 60 changes at once for one account, then 1 each second.",
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "The link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortalResponse"
                },
                "example": {
                  "url": "https://billing.stripe.com/p/session/live_a1B2c3D4e5F6g7H8",
                  "message": "A person opens the URL to change the card, switch between monthly and yearly, see invoices, or cancel. The link works for a short time."
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`no_subscription`: the account never opened a checkout. Use POST /v1/billing/checkout first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: too many changes from this account. The answer has `retry_after_seconds`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`payment_provider_failed`: the payment provider did not answer as expected. Retry in a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`payments_not_configured`: this server takes no payments.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/domains": {
      "get": {
        "tags": [
          "Domains"
        ],
        "operationId": "listDomains",
        "summary": "List the domains",
        "description": "Returns every domain of the account, with its DNS records, its last check and its send-as state.",
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "The domains of the account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainListResponse"
                },
                "example": {
                  "domains": [
                    {
                      "name": "example.com",
                      "status": "verified",
                      "created_at": "2026-11-13T09:20:05.871Z",
                      "verified_at": "2026-11-13T09:31:48.264Z",
                      "records": [
                        {
                          "type": "TXT",
                          "name": "_mxgate.example.com",
                          "value": "mxgate-verify=5d1f0a9c3b7e2468ace0f135",
                          "required": true,
                          "purpose": "proves that you control the domain; mxgate accepts no mail for it before this record exists"
                        },
                        {
                          "type": "MX",
                          "name": "example.com",
                          "value": "mx1.mxgate.io",
                          "priority": 10,
                          "required": true,
                          "purpose": "sends the domain's mail to mxgate; remove other MX records on the same name"
                        },
                        {
                          "type": "TXT",
                          "name": "_dmarc.example.com",
                          "value": "v=DMARC1; p=none",
                          "required": false,
                          "purpose": "recommended when the domain has no DMARC record yet; keep an existing one"
                        },
                        {
                          "type": "TXT",
                          "name": "mxgate._domainkey.example.com",
                          "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx1Qm0u3T5kYc9o2fJ7hR4pLwV8dN6sZbE1aGtXyCqKiU0rMvHnOjPlSgDfWeBzA2IDAQAB",
                          "required": false,
                          "purpose": "the DKIM key for send-as; needed only to send mail as addresses at this domain"
                        }
                      ],
                      "checks": {
                        "ownership": "ok",
                        "mx": "ok",
                        "mx_found": [
                          "10 mx1.mxgate.io"
                        ],
                        "dkim": "ok",
                        "checked_at": "2026-11-13T09:31:48.264Z",
                        "problems": []
                      },
                      "aliases": 2,
                      "send_as": {
                        "ready": true,
                        "reasons": [],
                        "host": "smtp.mxgate.io",
                        "ports": [
                          {
                            "port": 587,
                            "security": "starttls"
                          },
                          {
                            "port": 465,
                            "security": "tls"
                          }
                        ],
                        "credentials": [
                          {
                            "username": "mxg-4f2a9c1e@example.com",
                            "created_at": "2026-11-13T10:02:11.507Z",
                            "last_used_at": "2026-11-14T07:45:30.112Z"
                          }
                        ]
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Domains"
        ],
        "operationId": "addDomain",
        "summary": "Add a domain",
        "description": "Adds a domain to the account and returns the DNS records to publish: TXT `_mxgate.<domain>` (ownership, required), MX `<domain>` to mx1.mxgate.io with priority 10 (required), TXT `mxgate._domainkey.<domain>` (DKIM, for send-as only) and TXT `_dmarc.<domain>` (only if the domain has none). mxgate takes no mail for the domain before the ownership record is checked. A domain that the account has already is returned with status 200 and `created` false. A domain that another account added but did not verify becomes free after 24 hours. The free plan has 1 domain. Limit: 60 changes at once for one account, then 1 each second.",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddDomainRequest"
              },
              "example": {
                "name": "example.com"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The account had the domain already. Nothing changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddDomainResponse"
                },
                "example": {
                  "domain": {
                    "name": "example.com",
                    "status": "verified",
                    "created_at": "2026-11-13T09:20:05.871Z",
                    "verified_at": "2026-11-13T09:31:48.264Z",
                    "records": [
                      {
                        "type": "TXT",
                        "name": "_mxgate.example.com",
                        "value": "mxgate-verify=5d1f0a9c3b7e2468ace0f135",
                        "required": true,
                        "purpose": "proves that you control the domain; mxgate accepts no mail for it before this record exists"
                      },
                      {
                        "type": "MX",
                        "name": "example.com",
                        "value": "mx1.mxgate.io",
                        "priority": 10,
                        "required": true,
                        "purpose": "sends the domain's mail to mxgate; remove other MX records on the same name"
                      },
                      {
                        "type": "TXT",
                        "name": "_dmarc.example.com",
                        "value": "v=DMARC1; p=none",
                        "required": false,
                        "purpose": "recommended when the domain has no DMARC record yet; keep an existing one"
                      },
                      {
                        "type": "TXT",
                        "name": "mxgate._domainkey.example.com",
                        "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx1Qm0u3T5kYc9o2fJ7hR4pLwV8dN6sZbE1aGtXyCqKiU0rMvHnOjPlSgDfWeBzA2IDAQAB",
                        "required": false,
                        "purpose": "the DKIM key for send-as; needed only to send mail as addresses at this domain"
                      }
                    ],
                    "checks": {
                      "ownership": "ok",
                      "mx": "ok",
                      "mx_found": [
                        "10 mx1.mxgate.io"
                      ],
                      "dkim": "ok",
                      "checked_at": "2026-11-13T09:31:48.264Z",
                      "problems": []
                    },
                    "aliases": 2,
                    "send_as": {
                      "ready": true,
                      "reasons": [],
                      "host": "smtp.mxgate.io",
                      "ports": [
                        {
                          "port": 587,
                          "security": "starttls"
                        },
                        {
                          "port": 465,
                          "security": "tls"
                        }
                      ],
                      "credentials": [
                        {
                          "username": "mxg-4f2a9c1e@example.com",
                          "created_at": "2026-11-13T10:02:11.507Z",
                          "last_used_at": "2026-11-14T07:45:30.112Z"
                        }
                      ]
                    }
                  },
                  "created": false
                }
              }
            }
          },
          "201": {
            "description": "The domain was added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddDomainResponse"
                },
                "example": {
                  "domain": {
                    "name": "example.com",
                    "status": "pending",
                    "created_at": "2026-11-13T09:20:05.871Z",
                    "verified_at": null,
                    "records": [
                      {
                        "type": "TXT",
                        "name": "_mxgate.example.com",
                        "value": "mxgate-verify=5d1f0a9c3b7e2468ace0f135",
                        "required": true,
                        "purpose": "proves that you control the domain; mxgate accepts no mail for it before this record exists"
                      },
                      {
                        "type": "MX",
                        "name": "example.com",
                        "value": "mx1.mxgate.io",
                        "priority": 10,
                        "required": true,
                        "purpose": "sends the domain's mail to mxgate; remove other MX records on the same name"
                      },
                      {
                        "type": "TXT",
                        "name": "_dmarc.example.com",
                        "value": "v=DMARC1; p=none",
                        "required": false,
                        "purpose": "recommended when the domain has no DMARC record yet; keep an existing one"
                      },
                      {
                        "type": "TXT",
                        "name": "mxgate._domainkey.example.com",
                        "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx1Qm0u3T5kYc9o2fJ7hR4pLwV8dN6sZbE1aGtXyCqKiU0rMvHnOjPlSgDfWeBzA2IDAQAB",
                        "required": false,
                        "purpose": "the DKIM key for send-as; needed only to send mail as addresses at this domain"
                      }
                    ],
                    "checks": null,
                    "aliases": 0,
                    "send_as": {
                      "ready": false,
                      "reasons": [
                        "example.com is not verified yet",
                        "the DKIM record mxgate._domainkey.example.com is not confirmed yet: add it (see records), then call verify"
                      ],
                      "host": "smtp.mxgate.io",
                      "ports": [
                        {
                          "port": 587,
                          "security": "starttls"
                        },
                        {
                          "port": 465,
                          "security": "tls"
                        }
                      ],
                      "credentials": []
                    }
                  },
                  "created": true,
                  "next_step": "Add the required records, then POST /v1/domains/example.com/verify."
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`: the body is not a JSON object. `invalid_domain`: `name` is missing or is not a registered domain name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "`plan_limit`: the plan has no more domains. The `fix` says how to get the paid plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`account_suspended`: the account is suspended. Reads still work; changes are blocked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`domain_taken`: another account holds the domain. A domain that is not verified becomes free after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`body_too_large`: the body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`reserved_domain`: mxgate uses the domain itself.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: too many changes from this account. The answer has `retry_after_seconds`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/domains/{domain}": {
      "get": {
        "tags": [
          "Domains"
        ],
        "operationId": "getDomain",
        "summary": "Get one domain",
        "description": "Returns one domain with the DNS records it needs, the result of its last check and its send-as state.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "description": "The domain name, for example `example.com`.",
            "schema": {
              "type": "string"
            },
            "example": "example.com"
          }
        ],
        "responses": {
          "200": {
            "description": "The domain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainResponse"
                },
                "example": {
                  "domain": {
                    "name": "example.com",
                    "status": "verified",
                    "created_at": "2026-11-13T09:20:05.871Z",
                    "verified_at": "2026-11-13T09:31:48.264Z",
                    "records": [
                      {
                        "type": "TXT",
                        "name": "_mxgate.example.com",
                        "value": "mxgate-verify=5d1f0a9c3b7e2468ace0f135",
                        "required": true,
                        "purpose": "proves that you control the domain; mxgate accepts no mail for it before this record exists"
                      },
                      {
                        "type": "MX",
                        "name": "example.com",
                        "value": "mx1.mxgate.io",
                        "priority": 10,
                        "required": true,
                        "purpose": "sends the domain's mail to mxgate; remove other MX records on the same name"
                      },
                      {
                        "type": "TXT",
                        "name": "_dmarc.example.com",
                        "value": "v=DMARC1; p=none",
                        "required": false,
                        "purpose": "recommended when the domain has no DMARC record yet; keep an existing one"
                      },
                      {
                        "type": "TXT",
                        "name": "mxgate._domainkey.example.com",
                        "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx1Qm0u3T5kYc9o2fJ7hR4pLwV8dN6sZbE1aGtXyCqKiU0rMvHnOjPlSgDfWeBzA2IDAQAB",
                        "required": false,
                        "purpose": "the DKIM key for send-as; needed only to send mail as addresses at this domain"
                      }
                    ],
                    "checks": {
                      "ownership": "ok",
                      "mx": "ok",
                      "mx_found": [
                        "10 mx1.mxgate.io"
                      ],
                      "dkim": "ok",
                      "checked_at": "2026-11-13T09:31:48.264Z",
                      "problems": []
                    },
                    "aliases": 2,
                    "send_as": {
                      "ready": true,
                      "reasons": [],
                      "host": "smtp.mxgate.io",
                      "ports": [
                        {
                          "port": 587,
                          "security": "starttls"
                        },
                        {
                          "port": 465,
                          "security": "tls"
                        }
                      ],
                      "credentials": [
                        {
                          "username": "mxg-4f2a9c1e@example.com",
                          "created_at": "2026-11-13T10:02:11.507Z",
                          "last_used_at": "2026-11-14T07:45:30.112Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_domain`: the domain in the path is not a domain name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`domain_not_found`: the domain is not a domain of this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Domains"
        ],
        "operationId": "deleteDomain",
        "summary": "Remove a domain",
        "description": "Removes the domain, its aliases and its SMTP logins from the account. mxgate stops taking mail for the domain. The DNS records at the DNS host stay: remove them there.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "description": "The domain name, for example `example.com`.",
            "schema": {
              "type": "string"
            },
            "example": "example.com"
          }
        ],
        "responses": {
          "200": {
            "description": "The domain was removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeletedResponse"
                },
                "example": {
                  "deleted": "example.com"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_domain`: the domain in the path is not a domain name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`account_suspended`: the account is suspended. Reads still work; changes are blocked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`domain_not_found`: the domain is not a domain of this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/domains/{domain}/verify": {
      "post": {
        "tags": [
          "Domains"
        ],
        "operationId": "verifyDomain",
        "summary": "Check the DNS records",
        "description": "Checks the DNS records of the domain now: the ownership TXT record, the MX records and the DKIM record. The domain becomes verified when the ownership record is found, and it stays verified. Mail arrives when the MX record points to mxgate. The DKIM record is for send-as only. Repeat the call until `verified` is true: DNS changes can take minutes. Limit: 20 DNS checks and webhook tests in 10 minutes for one account.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "description": "The domain name, for example `example.com`.",
            "schema": {
              "type": "string"
            },
            "example": "example.com"
          }
        ],
        "responses": {
          "200": {
            "description": "The check ran. Read `verified` and `domain.checks.problems`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifyResponse"
                },
                "example": {
                  "domain": {
                    "name": "example.com",
                    "status": "verified",
                    "created_at": "2026-11-13T09:20:05.871Z",
                    "verified_at": "2026-11-13T09:31:48.264Z",
                    "records": [
                      {
                        "type": "TXT",
                        "name": "_mxgate.example.com",
                        "value": "mxgate-verify=5d1f0a9c3b7e2468ace0f135",
                        "required": true,
                        "purpose": "proves that you control the domain; mxgate accepts no mail for it before this record exists"
                      },
                      {
                        "type": "MX",
                        "name": "example.com",
                        "value": "mx1.mxgate.io",
                        "priority": 10,
                        "required": true,
                        "purpose": "sends the domain's mail to mxgate; remove other MX records on the same name"
                      },
                      {
                        "type": "TXT",
                        "name": "_dmarc.example.com",
                        "value": "v=DMARC1; p=none",
                        "required": false,
                        "purpose": "recommended when the domain has no DMARC record yet; keep an existing one"
                      },
                      {
                        "type": "TXT",
                        "name": "mxgate._domainkey.example.com",
                        "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx1Qm0u3T5kYc9o2fJ7hR4pLwV8dN6sZbE1aGtXyCqKiU0rMvHnOjPlSgDfWeBzA2IDAQAB",
                        "required": false,
                        "purpose": "the DKIM key for send-as; needed only to send mail as addresses at this domain"
                      }
                    ],
                    "checks": {
                      "ownership": "ok",
                      "mx": "ok",
                      "mx_found": [
                        "10 mx1.mxgate.io"
                      ],
                      "dkim": "ok",
                      "checked_at": "2026-11-13T09:31:48.264Z",
                      "problems": []
                    },
                    "aliases": 2,
                    "send_as": {
                      "ready": true,
                      "reasons": [],
                      "host": "smtp.mxgate.io",
                      "ports": [
                        {
                          "port": 587,
                          "security": "starttls"
                        },
                        {
                          "port": 465,
                          "security": "tls"
                        }
                      ],
                      "credentials": [
                        {
                          "username": "mxg-4f2a9c1e@example.com",
                          "created_at": "2026-11-13T10:02:11.507Z",
                          "last_used_at": "2026-11-14T07:45:30.112Z"
                        }
                      ]
                    }
                  },
                  "verified": true,
                  "message": "example.com is verified and its MX points to mxgate. Mail to its aliases is forwarded now."
                }
              }
            }
          },
          "400": {
            "description": "`invalid_domain`: the domain in the path is not a domain name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`domain_not_found`: the domain is not a domain of this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: more than 20 DNS checks in 10 minutes. The answer has `retry_after_seconds`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/domains/{domain}/aliases": {
      "get": {
        "tags": [
          "Aliases"
        ],
        "operationId": "listAliases",
        "summary": "List the aliases",
        "description": "Returns the aliases of the domain. Each alias says if mail to it is forwarded now and, if not, why.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "description": "The domain name, for example `example.com`.",
            "schema": {
              "type": "string"
            },
            "example": "example.com"
          }
        ],
        "responses": {
          "200": {
            "description": "The aliases of the domain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AliasListResponse"
                },
                "example": {
                  "aliases": [
                    {
                      "alias": "hello",
                      "address": "hello@example.com",
                      "domain": "example.com",
                      "to": [
                        {
                          "email": "ada@example.org",
                          "confirmed": true
                        }
                      ],
                      "active": true,
                      "inactive_reasons": [],
                      "created_at": "2026-11-13T09:35:12.440Z",
                      "updated_at": "2026-11-13T09:35:12.440Z"
                    },
                    {
                      "alias": "*",
                      "address": "*@example.com",
                      "domain": "example.com",
                      "to": [
                        {
                          "email": "team@example.org",
                          "confirmed": false
                        }
                      ],
                      "active": false,
                      "inactive_reasons": [
                        "no destination is confirmed yet: confirm on the page linked in the email to team@example.org"
                      ],
                      "created_at": "2026-11-13T09:40:02.019Z",
                      "updated_at": "2026-11-13T09:40:02.019Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_domain`: the domain in the path is not a domain name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`domain_not_found`: the domain is not a domain of this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/domains/{domain}/aliases/{alias}": {
      "put": {
        "tags": [
          "Aliases"
        ],
        "operationId": "setAlias",
        "summary": "Create or replace an alias",
        "description": "Creates the alias, or replaces the destinations of an alias that exists. The alias `*` is the catch-all: it gets mail for every address of the domain that has no alias. A destination that is new to the account gets a confirmation mail; no mail is forwarded to it before its owner confirms on the linked page. A destination cannot be at the same domain or at another domain that mxgate forwards. Free plan: 10 aliases for one domain plus a catch-all, 3 destinations for one alias. Paid plan: no alias limit, 10 destinations. The status is 201 for a new alias and 200 for a replaced one. Limits: 10 confirmation mails a day for one account; 60 changes at once, then 1 each second.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "description": "The domain name, for example `example.com`.",
            "schema": {
              "type": "string"
            },
            "example": "example.com"
          },
          {
            "name": "alias",
            "in": "path",
            "required": true,
            "description": "The part before the @, for example `hello`, or `*` for the catch-all. Letters, digits and `. _ + -`; at most 64 characters.",
            "schema": {
              "type": "string"
            },
            "example": "hello"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PutAliasRequest"
              },
              "example": {
                "to": [
                  "ada@example.org",
                  "team@example.org"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The alias was replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PutAliasResponse"
                },
                "example": {
                  "alias": {
                    "alias": "hello",
                    "address": "hello@example.com",
                    "domain": "example.com",
                    "to": [
                      {
                        "email": "ada@example.org",
                        "confirmed": true
                      }
                    ],
                    "active": true,
                    "inactive_reasons": [],
                    "created_at": "2026-11-13T09:35:12.440Z",
                    "updated_at": "2026-11-14T09:02:55.731Z"
                  },
                  "created": false,
                  "confirmation_mailed_to": []
                }
              }
            }
          },
          "201": {
            "description": "The alias was made.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PutAliasResponse"
                },
                "example": {
                  "alias": {
                    "alias": "hello",
                    "address": "hello@example.com",
                    "domain": "example.com",
                    "to": [
                      {
                        "email": "ada@example.org",
                        "confirmed": true
                      },
                      {
                        "email": "team@example.org",
                        "confirmed": false
                      }
                    ],
                    "active": true,
                    "inactive_reasons": [],
                    "created_at": "2026-11-13T09:35:12.440Z",
                    "updated_at": "2026-11-13T09:35:12.440Z"
                  },
                  "created": true,
                  "confirmation_mailed_to": [
                    "team@example.org"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`: the body is not a JSON object. `invalid_domain`: the domain in the path is not a domain name. `invalid_alias`: the alias in the path is not a valid name. `invalid_destination`: `to` is missing, is empty, or has a text that is not an email address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "`plan_limit`: the alias has more destinations than the plan allows, or the domain has no more aliases on this plan. The `fix` says how to get the paid plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`account_suspended`: the account is suspended. Reads still work; changes are blocked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`domain_not_found`: the domain is not a domain of this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`body_too_large`: the body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`forwarding_loop`: a destination is at the same domain, or at another domain that mxgate forwards.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: too many changes from this account, or more than 10 confirmation mails in a day. The answer has `retry_after_seconds`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Aliases"
        ],
        "operationId": "deleteAlias",
        "summary": "Remove an alias",
        "description": "Removes the alias. Mail to its address is refused after that, unless the domain has a catch-all. The destinations stay in the account.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "description": "The domain name, for example `example.com`.",
            "schema": {
              "type": "string"
            },
            "example": "example.com"
          },
          {
            "name": "alias",
            "in": "path",
            "required": true,
            "description": "The part before the @, for example `hello`, or `*` for the catch-all. Letters, digits and `. _ + -`; at most 64 characters.",
            "schema": {
              "type": "string"
            },
            "example": "hello"
          }
        ],
        "responses": {
          "200": {
            "description": "The alias was removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeletedResponse"
                },
                "example": {
                  "deleted": "hello@example.com"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_domain`: the domain in the path is not a domain name. `invalid_alias`: the alias in the path is not a valid name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`account_suspended`: the account is suspended. Reads still work; changes are blocked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`domain_not_found`: the domain is not a domain of this account. `alias_not_found`: the domain has no alias with this name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/domains/{domain}/smtp": {
      "get": {
        "tags": [
          "Send-as"
        ],
        "operationId": "getSendAs",
        "summary": "Get the send-as state",
        "description": "Returns the send-as state of the domain: if it is ready and why not, the SMTP host smtp.mxgate.io, the ports 587 (STARTTLS) and 465 (TLS), and the SMTP logins. Passwords are not shown.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "description": "The domain name, for example `example.com`.",
            "schema": {
              "type": "string"
            },
            "example": "example.com"
          }
        ],
        "responses": {
          "200": {
            "description": "The send-as state of the domain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendAsResponse"
                },
                "example": {
                  "send_as": {
                    "ready": true,
                    "reasons": [],
                    "host": "smtp.mxgate.io",
                    "ports": [
                      {
                        "port": 587,
                        "security": "starttls"
                      },
                      {
                        "port": 465,
                        "security": "tls"
                      }
                    ],
                    "credentials": [
                      {
                        "username": "mxg-4f2a9c1e@example.com",
                        "created_at": "2026-11-13T10:02:11.507Z",
                        "last_used_at": "2026-11-14T07:45:30.112Z"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_domain`: the domain in the path is not a domain name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`domain_not_found`: the domain is not a domain of this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Send-as"
        ],
        "operationId": "createSmtpCredential",
        "summary": "Create an SMTP login",
        "description": "Makes a new SMTP login for the domain. The password is shown once in the answer. The login can send as every address at the domain; the From header must be at the domain too. The domain must be verified with its DKIM record. One domain has at most 5 SMTP logins. Each recipient counts against the plan limit: 50 a day on the free plan, 3,000 a month on the paid plan. The call has no body. Limit: 60 changes at once for one account, then 1 each second.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "description": "The domain name, for example `example.com`.",
            "schema": {
              "type": "string"
            },
            "example": "example.com"
          }
        ],
        "responses": {
          "201": {
            "description": "The new SMTP login.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SmtpCredentialCreated"
                },
                "example": {
                  "credential": {
                    "username": "mxg-4f2a9c1e@example.com",
                    "created_at": "2026-11-13T10:02:11.507Z",
                    "last_used_at": null
                  },
                  "password": "Zk3vQ9xT1mB7cN5pR8sW2yA4dF6gH0jL",
                  "host": "smtp.mxgate.io",
                  "ports": [
                    {
                      "port": 587,
                      "security": "starttls"
                    },
                    {
                      "port": 465,
                      "security": "tls"
                    }
                  ],
                  "message": "Log in to smtp.mxgate.io (port 587 with STARTTLS, or 465 with TLS) as mxg-4f2a9c1e@example.com. The password is shown once. You can send as any address at example.com; the From header must be at example.com too. The free plan allows 50 recipients per day."
                }
              }
            }
          },
          "400": {
            "description": "`invalid_domain`: the domain in the path is not a domain name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`account_suspended`: the account is suspended. Reads still work; changes are blocked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`domain_not_found`: the domain is not a domain of this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`send_as_not_ready`: the domain is not verified, its DKIM record is not confirmed, or sending is paused for the account. `too_many_credentials`: the domain has 5 SMTP logins already. Remove one first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: too many changes from this account. The answer has `retry_after_seconds`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/domains/{domain}/smtp/{username}": {
      "delete": {
        "tags": [
          "Send-as"
        ],
        "operationId": "deleteSmtpCredential",
        "summary": "Remove an SMTP login",
        "description": "Removes one SMTP login of the domain. Its password stops working at once.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "description": "The domain name, for example `example.com`.",
            "schema": {
              "type": "string"
            },
            "example": "example.com"
          },
          {
            "name": "username",
            "in": "path",
            "required": true,
            "description": "The SMTP username, as GET /v1/domains/{domain}/smtp shows it.",
            "schema": {
              "type": "string"
            },
            "example": "mxg-4f2a9c1e@example.com"
          }
        ],
        "responses": {
          "200": {
            "description": "The SMTP login was removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeletedResponse"
                },
                "example": {
                  "deleted": "mxg-4f2a9c1e@example.com"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_domain`: the domain in the path is not a domain name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`account_suspended`: the account is suspended. Reads still work; changes are blocked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`domain_not_found`: the domain is not a domain of this account. `credential_not_found`: the domain has no SMTP login with this username.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/send": {
      "post": {
        "tags": [
          "Send"
        ],
        "operationId": "sendMessage",
        "summary": "Send one message",
        "description": "Sends one plain-text message from an address at a domain of the account. The domain must be verified with its DKIM record. There is no HTML and no tracking: a link arrives as written. The JSON body of the call is at most 64 KB. One message goes to at most 50 addresses in `to`, `cc` and `bcc` together; an address that is in two lists gets one copy. The status is 202 when the mail server took the message; GET /v1/logs?direction=out shows what the server of each recipient answered. With an idempotency key, a repeated call sends nothing and returns the first answer with status 200 and `replayed` true. Each recipient counts against the plan limit: 50 a day on the free plan (resets at 00:00 UTC), 3,000 a month on the paid plan. A message over the limit is refused as a whole. Limit: 60 messages at once for one account, then 1 each second.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "The same as `idempotency_key` in the body, which comes first when both are sent. 1 to 200 characters. Kept 7 days.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            },
            "example": "offer-reply-1"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendRequest"
              },
              "example": {
                "from": "Ada <ada@example.com>",
                "to": [
                  "bob@example.org"
                ],
                "subject": "Re: Offer",
                "text": "Hello Bob,\n\nThank you for the offer. We accept it.\n\nAda",
                "in_reply_to": "<c7a1e0b2-55f3@example.org>",
                "copy_to_owner": true,
                "idempotency_key": "offer-reply-1"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A repeated call with the same idempotency key. Nothing was sent again; this is the first answer with `replayed` true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendResponse"
                },
                "example": {
                  "message_id": "9b2f4c6e8a0d1f3579bdf02468ace135@example.com",
                  "from": "ada@example.com",
                  "to": [
                    "bob@example.org"
                  ],
                  "cc": [],
                  "bcc": [
                    "ada@example.org"
                  ],
                  "status": "queued",
                  "replayed": true,
                  "message": "Queued for 2 recipients. What each mail server answered: GET /v1/logs?direction=out&domain=example.com."
                }
              }
            }
          },
          "202": {
            "description": "The mail server took the message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendResponse"
                },
                "example": {
                  "message_id": "9b2f4c6e8a0d1f3579bdf02468ace135@example.com",
                  "from": "ada@example.com",
                  "to": [
                    "bob@example.org"
                  ],
                  "cc": [],
                  "bcc": [
                    "ada@example.org"
                  ],
                  "status": "queued",
                  "replayed": false,
                  "message": "Queued for 2 recipients. What each mail server answered: GET /v1/logs?direction=out&domain=example.com."
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`: the body is not a JSON object. `invalid_from`: `from` is missing or is not an address. `invalid_recipient`: `to` has no address, or `to`, `cc`, `bcc` or `reply_to` has a text that is not an address. `too_many_recipients`: the message has more than 50 addresses. `invalid_subject`: `subject` is missing, is empty, or has more than 255 characters. `invalid_text`: `text` is missing or is empty. `invalid_message_id`: `in_reply_to` or `references` has a text that is not a Message-ID, or more than 50 of them. `invalid_idempotency_key`: the idempotency key is empty or has more than 200 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "`plan_limit`: the message would go over the daily or monthly recipient limit of the plan. Nothing was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`account_suspended`: the account is suspended. Reads still work; changes are blocked. `from_not_allowed`: the domain of `from` is not a domain of this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`send_as_not_ready`: the domain is not verified, its DKIM record is not confirmed, or sending is paused for the account. `idempotency_key_reused`: the idempotency key was used for a different message. `send_in_progress`: a call with this idempotency key has not finished.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`body_too_large`: the body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`recipient_refused`: the mail server did not accept one recipient. Nothing was sent. `message_refused`: the mail server did not accept the message, for example because it looks like spam. Nothing was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: too many messages from this account. The answer has `retry_after_seconds`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`send_unavailable`: the mail server did not take the message, or this server cannot send mail. Nothing was sent. Retry in a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/logs": {
      "get": {
        "tags": [
          "Logs"
        ],
        "operationId": "listLogs",
        "summary": "List the message log",
        "description": "Returns the per-message log, newest first: one row for each message and recipient, with the answer of the other server and a plain explanation. Forwarded mail (`in`) and mail sent through smtp.mxgate.io or POST /v1/send (`out`) are in the same log. No subject and no body are stored. The log is kept 7 days on the free plan and 90 days on the paid plan. For the next page, pass `next_before` as `before`.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": false,
            "description": "Only rows of this domain. It must be a domain of the account.",
            "schema": {
              "type": "string"
            },
            "example": "example.com"
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "description": "`in`: mail that mxgate forwards to you. `out`: mail you sent.",
            "schema": {
              "type": "string",
              "enum": [
                "in",
                "out"
              ]
            },
            "example": "in"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only rows with this status.",
            "schema": {
              "type": "string",
              "enum": [
                "forwarded",
                "sent",
                "deferred",
                "bounced",
                "expired",
                "rejected"
              ]
            },
            "example": "bounced"
          },
          {
            "name": "address",
            "in": "query",
            "required": false,
            "description": "Only rows where this address is the recipient, the destination or the sender.",
            "schema": {
              "type": "string"
            },
            "example": "hello@example.com"
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "description": "Only rows older than this ISO 8601 time. Use `next_before` of the page before.",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "example": "2026-11-14T07:58:40.000Z"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "The number of rows in one page: 1 to 500. The default is 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            },
            "example": 50
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LogsResponse"
                },
                "example": {
                  "entries": [
                    {
                      "received_at": "2026-11-14T08:15:02.000Z",
                      "updated_at": "2026-11-14T08:15:04.000Z",
                      "direction": "in",
                      "status": "forwarded",
                      "sender": "bob@example.org",
                      "recipient": "hello@example.com",
                      "destination": "ada@example.org",
                      "domain": "example.com",
                      "reason": "250 2.0.0 OK: queued as 4XkT2p1Wz9",
                      "explanation": null,
                      "dsn": "2.0.0",
                      "relay": "mail.example.org[203.0.113.25]:25",
                      "client": "mail.example.org[203.0.113.25]",
                      "message_id": "c7a1e0b2-55f3@example.org",
                      "attempts": 1
                    },
                    {
                      "received_at": "2026-11-14T07:58:40.000Z",
                      "updated_at": "2026-11-14T07:58:43.000Z",
                      "direction": "out",
                      "status": "bounced",
                      "sender": "ada@example.com",
                      "recipient": "nobody@example.org",
                      "destination": "",
                      "domain": "example.com",
                      "reason": "550 5.1.1 The email account that you tried to reach does not exist",
                      "explanation": "The recipient's mailbox does not exist.",
                      "dsn": "5.1.1",
                      "relay": "mail.example.org[203.0.113.25]:25",
                      "client": "localhost[127.0.0.1]",
                      "message_id": "9b2f4c6e8a0d1f3579bdf02468ace135@example.com",
                      "attempts": 1
                    }
                  ],
                  "next_before": null,
                  "retention_days": 7
                }
              }
            }
          },
          "400": {
            "description": "`invalid_domain`: `domain` is not a domain name. `invalid_status`: `status` is not one of the statuses. `invalid_direction`: `direction` is not `in` or `out`. `invalid_before`: `before` is not an ISO 8601 time.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`domain_not_found`: `domain` is not a domain of this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/notifications": {
      "get": {
        "tags": [
          "Notifications"
        ],
        "operationId": "getNotifications",
        "summary": "Get the notification settings",
        "description": "Returns the notification settings and the 20 newest notifications. Notifications are for mail that failed: there is no notification for each received message. The default is an hourly email digest for bounced and expired mail. Only the first 12 characters of the webhook secret are shown.",
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "The settings and the recent notifications.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationSettingsView"
                },
                "example": {
                  "email": true,
                  "events": [
                    "bounced",
                    "expired"
                  ],
                  "webhook_url": "https://app.example.com/hooks/mxgate",
                  "webhook_secret": "whsec_012345...",
                  "signature": "Header mxgate-signature: t=<unix seconds>,v1=<hex>. v1 is HMAC-SHA256 of the string \"<t>.<raw body>\" with your webhook_secret. Refuse calls whose t is more than 5 minutes old.",
                  "recent": [
                    {
                      "event": "bounced",
                      "created_at": "2026-11-14T07:58:44.120Z",
                      "recipient": "nobody@example.org",
                      "webhook_status": "delivered",
                      "webhook_attempts": 1,
                      "webhook_last_error": null,
                      "emailed": true
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Notifications"
        ],
        "operationId": "setNotifications",
        "summary": "Change the notification settings",
        "description": "Changes the email digest, the events and the webhook. A field that is not sent stays as it is. The webhook URL must be a public https:// URL with no credentials in it. A new webhook URL gets a new secret, and that answer shows the secret in full: store it. A webhook gets one signed JSON call for each event (header `mxgate-signature`), and a failed call is retried 5 times in about 8 hours. Limit: 60 changes at once for one account, then 1 each second.",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NotificationSettingsRequest"
              },
              "example": {
                "email": true,
                "events": [
                  "bounced",
                  "expired",
                  "rejected"
                ],
                "webhook_url": "https://app.example.com/hooks/mxgate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The settings as they are now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationSettingsView"
                },
                "example": {
                  "email": true,
                  "events": [
                    "bounced",
                    "expired",
                    "rejected"
                  ],
                  "webhook_url": "https://app.example.com/hooks/mxgate",
                  "webhook_secret": "whsec_0123456789abcdef0123456789abcdef0123456789abcdef",
                  "signature": "Header mxgate-signature: t=<unix seconds>,v1=<hex>. v1 is HMAC-SHA256 of the string \"<t>.<raw body>\" with your webhook_secret. Refuse calls whose t is more than 5 minutes old.",
                  "recent": [],
                  "message": "Store webhook_secret now: later responses show only its start. Check every call's signature with it."
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`: the body is not a JSON object. `invalid_setting`: `email` is not true or false, `events` has a name that is not an event, or `webhook_url` is not a text or null.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`account_suspended`: the account is suspended. Reads still work; changes are blocked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`body_too_large`: the body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`invalid_webhook_url`: `webhook_url` is not an https:// URL, has credentials in it, or points to a private address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: too many changes from this account. The answer has `retry_after_seconds`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/notifications/test": {
      "post": {
        "tags": [
          "Notifications"
        ],
        "operationId": "testWebhook",
        "summary": "Send a test webhook call",
        "description": "Sends one signed test call to the webhook URL now and returns what the webhook answered. The body of the call has the type `test`. The status of this endpoint is 200 also when the webhook failed: read `ok`. The call has no body. Limit: 20 webhook tests and DNS checks in 10 minutes for one account.",
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "The test call was made. `ok` says if the webhook took it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookTestResponse"
                },
                "example": {
                  "ok": true,
                  "status": 200,
                  "error": null,
                  "message": "The webhook answered 200."
                }
              }
            }
          },
          "400": {
            "description": "`no_webhook`: no webhook is set. Set one with PUT /v1/notifications.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: more than 20 webhook tests in 10 minutes. The answer has `retry_after_seconds`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/destinations": {
      "get": {
        "tags": [
          "Destinations"
        ],
        "operationId": "listDestinations",
        "summary": "List the destinations",
        "description": "Returns the mailboxes that the account can forward to, and if each one is confirmed.",
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "The destinations of the account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DestinationListResponse"
                },
                "example": {
                  "destinations": [
                    {
                      "email": "ada@example.org",
                      "confirmed": true,
                      "created_at": "2026-11-13T09:12:44.318Z",
                      "confirmed_at": "2026-11-13T09:14:20.905Z"
                    },
                    {
                      "email": "team@example.org",
                      "confirmed": false,
                      "created_at": "2026-11-13T09:40:02.019Z",
                      "confirmed_at": null
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Destinations"
        ],
        "operationId": "addDestination",
        "summary": "Add a destination",
        "description": "Adds a mailbox to forward to and mails a confirmation link to it. Forwarding to the mailbox starts when its owner opens the link and presses \"Confirm forwarding\". The status is 201 when this call sent a mail. The status is 200 when it sent none: the mailbox is confirmed already, or a link was mailed less than 10 minutes ago. Limit: 10 confirmation mails a day for one account.",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailRequest"
              },
              "example": {
                "email": "team@example.org"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "No mail was sent by this call.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DestinationResponse"
                },
                "example": {
                  "destination": {
                    "email": "ada@example.org",
                    "confirmed": true,
                    "created_at": "2026-11-13T09:12:44.318Z",
                    "confirmed_at": "2026-11-13T09:14:20.905Z"
                  },
                  "mailed": false,
                  "message": "ada@example.org is confirmed."
                }
              }
            }
          },
          "201": {
            "description": "A confirmation link was mailed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DestinationResponse"
                },
                "example": {
                  "destination": {
                    "email": "team@example.org",
                    "confirmed": false,
                    "created_at": "2026-11-13T09:40:02.019Z",
                    "confirmed_at": null
                  },
                  "mailed": true,
                  "message": "A confirmation link was mailed to team@example.org. Forwarding to it starts when its owner confirms on the linked page."
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`: the body is not a JSON object. `invalid_destination`: `email` is missing or is not an email address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`account_suspended`: the account is suspended. Reads still work; changes are blocked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`body_too_large`: the body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: more than 10 confirmation mails in a day from this account. The answer has `retry_after_seconds`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/login/google": {
      "get": {
        "tags": [
          "Sign-in"
        ],
        "operationId": "startGoogleLogin",
        "summary": "Sign in with Google",
        "description": "For a browser, not for a program. Sends the browser to Google (302). Google sends it back to the API, and the API sends it to `https://mxgate.io/login` with `grant` and `state` in the URL fragment, or with `error`. If the Google address has no account, mxgate makes one and mails the API key to it. No auth.",
        "security": [],
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "A random value of 16 to 64 characters (A-Z, a-z, 0-9, `-`, `_`) that the page made. It comes back with the grant, and the page accepts the grant only when it is the same."
          }
        ],
        "responses": {
          "302": {
            "description": "The `Location` header is the Google sign-in page."
          },
          "400": {
            "description": "`invalid_state`: there is no `state`, or it is not 16 to 64 characters of the permitted set.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`google_sign_in_off`: this server has no sign-in with Google.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/login/grant": {
      "post": {
        "tags": [
          "Sign-in"
        ],
        "operationId": "confirmLoginGrant",
        "summary": "Trade a Google grant for a session",
        "description": "Gives a session token for the one-time grant that the sign-in with Google put in the URL fragment. The grant works once, for 2 minutes. No auth. Limit: 10 requests an hour from one IP address.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginGrantRequest"
              },
              "example": {
                "grant": "mxl_3f9a1c0b7e2d4a6f8c1b3d5e7f90a2b4c6d8e0f1a3b5c7d9"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The session was made.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionResponse"
                },
                "example": {
                  "session_token": "mxs_0123456789abcdef0123456789abcdef0123456789abcdef",
                  "expires_at": "2026-12-14T10:05:31.902Z",
                  "account": {
                    "email": "ada@example.org",
                    "plan": "free",
                    "status": "active",
                    "status_reason": null,
                    "send_hold": null,
                    "limits": {
                      "domains": 1,
                      "aliases_per_domain": 10,
                      "destinations_per_alias": 3,
                      "send_per_day": 50,
                      "send_per_month": null
                    },
                    "usage": {
                      "domains": 1,
                      "sent_today": 3,
                      "sent_this_month": 41
                    },
                    "billing": null,
                    "created_at": "2026-11-13T09:12:44.318Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_login_grant`: the grant is not valid, was used already, or is older than 2 minutes. `invalid_json`: the body is not a JSON object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: too many attempts from this address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/oauth/apps": {
      "get": {
        "tags": [
          "Connected apps"
        ],
        "operationId": "listConnectedApps",
        "summary": "List connected apps",
        "description": "The apps that the owner allowed on the consent page, each with the date and the last use. The token of an app is never shown. A connected app cannot make this call.",
        "responses": {
          "200": {
            "description": "The connected apps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectedAppListResponse"
                },
                "example": {
                  "apps": [
                    {
                      "id": 3,
                      "name": "Claude",
                      "client_id": "mxc_Zk3vQ0s8mT1xYb7LwN2dR5aH",
                      "connected_at": "2026-11-14T10:02:11.000Z",
                      "last_used_at": "2026-11-14T11:40:52.000Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`not_for_apps`: the call was made with the token of a connected app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/oauth/apps/{id}": {
      "delete": {
        "tags": [
          "Connected apps"
        ],
        "operationId": "removeConnectedApp",
        "summary": "Disconnect an app",
        "description": "Removes a connected app. Its token stops working at once. The app can ask the owner to connect it again. A connected app cannot make this call.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "The `id` of the app from GET /v1/oauth/apps."
          }
        ],
        "responses": {
          "200": {
            "description": "The app is removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "removed": {
                      "type": "integer",
                      "description": "The id of the app that was removed."
                    }
                  },
                  "required": [
                    "removed"
                  ]
                },
                "example": {
                  "removed": 3
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key`: the request has no key. `invalid_api_key`: the key is not valid or was replaced. `invalid_session`: the session token is not valid or has ended. `invalid_token`: the token of a connected app is not valid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`not_for_apps`: the call was made with the token of a connected app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`app_not_found`: no connected app of this account has that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Send `Authorization: Bearer <key>`. The key is an API key (it starts with `mxg_`) or a session token from POST /v1/login/confirm (it starts with `mxs_`). The header `x-api-key: <key>` is also accepted. A session token of the website (`mxs_...`) and a token of a connected app (`mxo_...`) are sent in the same way."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every error body. Switch on `error`; show `message` to a person; `fix` is the next step.",
        "properties": {
          "error": {
            "type": "string",
            "description": "The error code, for example `plan_limit`. It does not change between versions."
          },
          "message": {
            "type": "string",
            "description": "One sentence for a person."
          },
          "fix": {
            "type": "string",
            "description": "What to do next, in one sentence an agent can follow. Not in every error."
          },
          "upgrade_url": {
            "type": "string",
            "description": "The pricing page. Only with `plan_limit`.",
            "format": "uri"
          },
          "retry_after_seconds": {
            "type": "integer",
            "description": "How long to wait before the next try. Only with `rate_limited`."
          }
        },
        "required": [
          "error",
          "message"
        ]
      },
      "IndexResponse": {
        "type": "object",
        "description": "The answer to GET /: what the service is and what it has.",
        "properties": {
          "name": {
            "type": "string",
            "description": "Always `mxgate`."
          },
          "version": {
            "type": "string",
            "description": "The version of the server."
          },
          "about": {
            "type": "string",
            "description": "One sentence about the service."
          },
          "docs": {
            "type": "string",
            "description": "The URL of the text for agents (llms.txt).",
            "format": "uri"
          },
          "reference": {
            "type": "string",
            "description": "The URL of the API reference for a person.",
            "format": "uri"
          },
          "openapi": {
            "type": "string",
            "description": "The URL of this OpenAPI document.",
            "format": "uri"
          },
          "auth": {
            "type": "string",
            "description": "How to send the API key."
          },
          "flow": {
            "type": "array",
            "description": "The setup steps, in order.",
            "items": {
              "type": "string"
            }
          },
          "endpoints": {
            "type": "object",
            "description": "One line for each endpoint. The key is the method and the path.",
            "additionalProperties": {
              "type": "string"
            }
          },
          "mx_hosts": {
            "type": "array",
            "description": "The MX hosts that a customer domain points to.",
            "items": {
              "type": "string"
            }
          },
          "mcp": {
            "type": "string",
            "description": "The address of the hosted MCP server."
          },
          "sign_in": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The sign-ins of the website: `code`, and `google` when the server has it."
          }
        },
        "required": [
          "name",
          "version",
          "about",
          "docs",
          "reference",
          "openapi",
          "auth",
          "flow",
          "endpoints",
          "mx_hosts"
        ]
      },
      "Limits": {
        "type": "object",
        "description": "The limits of the plan. null means no limit.",
        "properties": {
          "domains": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Domains in the account. null means no limit."
          },
          "aliases_per_domain": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Aliases for one domain; the catch-all is not counted. null means no limit."
          },
          "destinations_per_alias": {
            "type": "integer",
            "description": "Mailboxes that one alias forwards to."
          },
          "send_per_day": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Send-as recipients per UTC day. null means no daily limit."
          },
          "send_per_month": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Send-as recipients per UTC calendar month. null means no monthly limit."
          }
        },
        "required": [
          "domains",
          "aliases_per_domain",
          "destinations_per_alias",
          "send_per_day",
          "send_per_month"
        ]
      },
      "AccountUsage": {
        "type": "object",
        "description": "What the account uses now.",
        "properties": {
          "domains": {
            "type": "integer",
            "description": "Domains in the account."
          },
          "sent_today": {
            "type": "integer",
            "description": "Send-as recipients this UTC day."
          },
          "sent_this_month": {
            "type": "integer",
            "description": "Send-as recipients this UTC calendar month."
          }
        },
        "required": [
          "domains",
          "sent_today",
          "sent_this_month"
        ]
      },
      "BillingView": {
        "type": "object",
        "description": "The subscription to the paid plan, as the payment provider last reported it.",
        "properties": {
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "The state of the subscription: active, trialing, past_due, canceled, unpaid, incomplete and so on. null before the first payment."
          },
          "interval": {
            "type": [
              "string",
              "null"
            ],
            "description": "How often the plan is paid.",
            "enum": [
              "month",
              "year",
              null
            ]
          },
          "current_period_end": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "The end of the period that is paid for. The plan renews then, or ends then when `cancels_at_period_end` is true."
          },
          "cancels_at_period_end": {
            "type": "boolean",
            "description": "True when the subscription was cancelled and ends at `current_period_end`."
          }
        },
        "required": [
          "status",
          "interval",
          "current_period_end",
          "cancels_at_period_end"
        ]
      },
      "AccountView": {
        "type": "object",
        "description": "An account: its plan, limits, usage and state.",
        "properties": {
          "email": {
            "type": "string",
            "description": "The account address. Keys and notices are mailed to it.",
            "format": "email"
          },
          "plan": {
            "type": "string",
            "description": "The plan of the account.",
            "enum": [
              "free",
              "paid"
            ]
          },
          "status": {
            "type": "string",
            "description": "active: all works. warned: all works, and `status_reason` says what to correct. suspended: reads work, changes are blocked.",
            "enum": [
              "active",
              "warned",
              "suspended"
            ]
          },
          "status_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why the account is warned or suspended. null when it is active."
          },
          "send_hold": {
            "type": [
              "string",
              "null"
            ],
            "description": "The reason, when send-as is paused for review. Forwarding still works. null when sending is not paused."
          },
          "limits": {
            "$ref": "#/components/schemas/Limits",
            "description": "The limits of the plan."
          },
          "usage": {
            "$ref": "#/components/schemas/AccountUsage",
            "description": "What the account uses now."
          },
          "billing": {
            "description": "The subscription to the paid plan. null when the account never opened a checkout.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/BillingView"
              },
              {
                "type": "null"
              }
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the account was made."
          }
        },
        "required": [
          "email",
          "plan",
          "status",
          "status_reason",
          "send_hold",
          "limits",
          "usage",
          "created_at"
        ]
      },
      "EmailRequest": {
        "type": "object",
        "description": "An email address.",
        "properties": {
          "email": {
            "type": "string",
            "description": "The email address.",
            "format": "email"
          }
        },
        "required": [
          "email"
        ]
      },
      "SignupResponse": {
        "type": "object",
        "description": "The answer to POST /v1/signup.",
        "properties": {
          "api_key": {
            "type": "string",
            "description": "The API key. It starts with `mxg_`. It is shown once here: store it now."
          },
          "account": {
            "$ref": "#/components/schemas/AccountView",
            "description": "The new account."
          },
          "mailed": {
            "type": "boolean",
            "description": "True when the mail with the key and the confirmation link was sent."
          },
          "message": {
            "type": "string",
            "description": "What happened, in one or two sentences."
          },
          "next_steps": {
            "type": "array",
            "description": "What to do next, in order.",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "api_key",
          "account",
          "mailed",
          "message",
          "next_steps"
        ]
      },
      "LoginResponse": {
        "type": "object",
        "description": "The answer to POST /v1/login. The same for every address, with or without an account.",
        "properties": {
          "message": {
            "type": "string",
            "description": "What happened, in one or two sentences."
          },
          "next_step": {
            "type": "string",
            "description": "What to do next."
          }
        },
        "required": [
          "message",
          "next_step"
        ]
      },
      "LoginConfirmRequest": {
        "type": "object",
        "description": "The address and the code from the sign-in mail.",
        "properties": {
          "email": {
            "type": "string",
            "description": "The account address.",
            "format": "email"
          },
          "code": {
            "type": "string",
            "description": "The six-digit code from the mail."
          }
        },
        "required": [
          "email",
          "code"
        ]
      },
      "SessionResponse": {
        "type": "object",
        "description": "The answer to POST /v1/login/confirm: a session for the web dashboard.",
        "properties": {
          "session_token": {
            "type": "string",
            "description": "The session token. It starts with `mxs_`. Send it as `Authorization: Bearer <token>`; it works like an API key."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the session token stops working: 30 days after it was made."
          },
          "account": {
            "$ref": "#/components/schemas/AccountView",
            "description": "The account that signed in."
          }
        },
        "required": [
          "session_token",
          "expires_at",
          "account"
        ]
      },
      "LogoutResponse": {
        "type": "object",
        "description": "The answer to POST /v1/logout.",
        "properties": {
          "signed_out": {
            "type": "boolean",
            "description": "Always true."
          }
        },
        "required": [
          "signed_out"
        ]
      },
      "KeyView": {
        "type": "object",
        "description": "One API key of the account. The key itself is not shown.",
        "properties": {
          "prefix": {
            "type": "string",
            "description": "The first 12 characters of the key."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the key was made."
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the key was last used. null when it was never used."
          }
        },
        "required": [
          "prefix",
          "created_at",
          "last_used_at"
        ]
      },
      "KeyListResponse": {
        "type": "object",
        "description": "The answer to GET /v1/keys.",
        "properties": {
          "keys": {
            "type": "array",
            "description": "The API keys that work now.",
            "items": {
              "$ref": "#/components/schemas/KeyView"
            }
          }
        },
        "required": [
          "keys"
        ]
      },
      "RecoverResponse": {
        "type": "object",
        "description": "The answer to POST /v1/keys/recover. The same for every address, with or without an account.",
        "properties": {
          "message": {
            "type": "string",
            "description": "What happened, in one or two sentences."
          },
          "next_step": {
            "type": "string",
            "description": "What to do with the token from the mail."
          }
        },
        "required": [
          "message",
          "next_step"
        ]
      },
      "RecoverConfirmRequest": {
        "type": "object",
        "description": "The token from the recovery mail.",
        "properties": {
          "token": {
            "type": "string",
            "description": "The recovery token. It starts with `mxr_`."
          }
        },
        "required": [
          "token"
        ]
      },
      "NewKeyResponse": {
        "type": "object",
        "description": "A new API key from POST /v1/keys/recover/confirm or POST /v1/keys/rotate.",
        "properties": {
          "api_key": {
            "type": "string",
            "description": "The new API key. It is shown once here: store it now."
          },
          "account": {
            "$ref": "#/components/schemas/AccountView",
            "description": "The account of the key."
          },
          "revoked_keys": {
            "type": "integer",
            "description": "How many older keys stopped working."
          },
          "message": {
            "type": "string",
            "description": "What happened, in one or two sentences."
          }
        },
        "required": [
          "api_key",
          "account",
          "revoked_keys",
          "message"
        ]
      },
      "CheckoutRequest": {
        "type": "object",
        "description": "How often to pay for the paid plan.",
        "properties": {
          "interval": {
            "type": "string",
            "description": "month: 9 USD a month. year: 79 USD a year. The default is month.",
            "enum": [
              "month",
              "year"
            ],
            "default": "month"
          }
        }
      },
      "CheckoutResponse": {
        "type": "object",
        "description": "The answer to POST /v1/billing/checkout: a page where a person pays.",
        "properties": {
          "url": {
            "type": "string",
            "description": "The payment page. It works for 24 hours.",
            "format": "uri"
          },
          "interval": {
            "type": "string",
            "description": "How often the plan is paid.",
            "enum": [
              "month",
              "year"
            ]
          },
          "price_usd": {
            "type": "number",
            "description": "The price in USD for one interval: 9 or 79."
          },
          "message": {
            "type": "string",
            "description": "What the person does with the URL."
          }
        },
        "required": [
          "url",
          "interval",
          "price_usd",
          "message"
        ]
      },
      "PortalResponse": {
        "type": "object",
        "description": "The answer to POST /v1/billing/portal: a page to change the card, see invoices, or cancel.",
        "properties": {
          "url": {
            "type": "string",
            "description": "The page for the person. It works for a short time.",
            "format": "uri"
          },
          "message": {
            "type": "string",
            "description": "What the person can do on the page."
          }
        },
        "required": [
          "url",
          "message"
        ]
      },
      "DnsRecord": {
        "type": "object",
        "description": "One DNS record to publish at the DNS host of the domain.",
        "properties": {
          "type": {
            "type": "string",
            "description": "The record type.",
            "enum": [
              "MX",
              "TXT"
            ]
          },
          "name": {
            "type": "string",
            "description": "The full name of the record, for example `_mxgate.example.com`."
          },
          "value": {
            "type": "string",
            "description": "The value: the text of a TXT record, or the mail host of an MX record."
          },
          "priority": {
            "type": "integer",
            "description": "The priority of an MX record. Not in TXT records."
          },
          "required": {
            "type": "boolean",
            "description": "True when forwarding needs the record. False for the DMARC record and the DKIM record."
          },
          "purpose": {
            "type": "string",
            "description": "What the record is for."
          }
        },
        "required": [
          "type",
          "name",
          "value",
          "required",
          "purpose"
        ]
      },
      "DomainChecks": {
        "type": "object",
        "description": "The result of the last DNS check of a domain.",
        "properties": {
          "ownership": {
            "type": "string",
            "description": "ok when the TXT record `_mxgate.<domain>` has the expected value. fail when it does not.",
            "enum": [
              "ok",
              "warn",
              "fail"
            ]
          },
          "mx": {
            "type": "string",
            "description": "ok: all MX records point to mxgate. warn: some point to other servers. fail: none points to mxgate.",
            "enum": [
              "ok",
              "warn",
              "fail"
            ]
          },
          "mx_found": {
            "type": "array",
            "description": "The MX records that the check saw, for example `10 mx1.mxgate.io`.",
            "items": {
              "type": "string"
            }
          },
          "dkim": {
            "type": "string",
            "description": "ok when the TXT record `mxgate._domainkey.<domain>` has the key of the domain. fail when it does not. Only send-as needs it.",
            "enum": [
              "ok",
              "warn",
              "fail"
            ]
          },
          "checked_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the check ran."
          },
          "problems": {
            "type": "array",
            "description": "What is wrong, one sentence for each problem. Empty when all records are correct.",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "ownership",
          "mx",
          "mx_found",
          "checked_at",
          "problems"
        ]
      },
      "SmtpPort": {
        "type": "object",
        "description": "One port of the send-as SMTP host.",
        "properties": {
          "port": {
            "type": "integer",
            "description": "The port number: 587 or 465."
          },
          "security": {
            "type": "string",
            "description": "starttls: connect in clear, then STARTTLS. tls: TLS from the start.",
            "enum": [
              "starttls",
              "tls"
            ]
          }
        },
        "required": [
          "port",
          "security"
        ]
      },
      "SmtpCredentialView": {
        "type": "object",
        "description": "One SMTP login of a domain. The password is not shown.",
        "properties": {
          "username": {
            "type": "string",
            "description": "The SMTP username, for example `mxg-4f2a9c1e@example.com`."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the credential was made."
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it last logged in. null when it was never used."
          }
        },
        "required": [
          "username",
          "created_at",
          "last_used_at"
        ]
      },
      "SendAsView": {
        "type": "object",
        "description": "Send-as for one domain: sending mail as addresses of the domain, through smtp.mxgate.io.",
        "properties": {
          "ready": {
            "type": "boolean",
            "description": "True when the domain can send: it is verified, its DKIM record is confirmed, and the account can send."
          },
          "reasons": {
            "type": "array",
            "description": "What is missing, when `ready` is false. Empty when `ready` is true.",
            "items": {
              "type": "string"
            }
          },
          "host": {
            "type": "string",
            "description": "The SMTP host for a mail client: `smtp.mxgate.io`."
          },
          "ports": {
            "type": "array",
            "description": "The ports of the SMTP host.",
            "items": {
              "$ref": "#/components/schemas/SmtpPort"
            }
          },
          "credentials": {
            "type": "array",
            "description": "The SMTP logins of the domain.",
            "items": {
              "$ref": "#/components/schemas/SmtpCredentialView"
            }
          }
        },
        "required": [
          "ready",
          "reasons",
          "host",
          "ports",
          "credentials"
        ]
      },
      "SendAsResponse": {
        "type": "object",
        "description": "The answer to GET /v1/domains/{domain}/smtp.",
        "properties": {
          "send_as": {
            "$ref": "#/components/schemas/SendAsView",
            "description": "Send-as for the domain."
          }
        },
        "required": [
          "send_as"
        ]
      },
      "SmtpCredentialCreated": {
        "type": "object",
        "description": "A new SMTP login. Returned once, when the credential is made. The password is not stored in clear.",
        "properties": {
          "credential": {
            "$ref": "#/components/schemas/SmtpCredentialView",
            "description": "The new credential."
          },
          "password": {
            "type": "string",
            "description": "The SMTP password. It is shown once here: store it now."
          },
          "host": {
            "type": "string",
            "description": "The SMTP host: `smtp.mxgate.io`."
          },
          "ports": {
            "type": "array",
            "description": "The ports of the SMTP host.",
            "items": {
              "$ref": "#/components/schemas/SmtpPort"
            }
          },
          "message": {
            "type": "string",
            "description": "How to log in, and the send limit of the plan."
          }
        },
        "required": [
          "credential",
          "password",
          "host",
          "ports",
          "message"
        ]
      },
      "DomainView": {
        "type": "object",
        "description": "One domain of the account, with the DNS records it needs and the last check.",
        "properties": {
          "name": {
            "type": "string",
            "description": "The domain name, in lower case."
          },
          "status": {
            "type": "string",
            "description": "pending: ownership is not proven. verified: mxgate accepts mail for the domain.",
            "enum": [
              "pending",
              "verified"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the domain was added."
          },
          "verified_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When ownership was proven. null while the domain is pending."
          },
          "records": {
            "type": "array",
            "description": "The DNS records to publish.",
            "items": {
              "$ref": "#/components/schemas/DnsRecord"
            }
          },
          "checks": {
            "description": "The result of the last DNS check. null before the first call to verify.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/DomainChecks"
              },
              {
                "type": "null"
              }
            ]
          },
          "aliases": {
            "type": "integer",
            "description": "How many aliases the domain has, with the catch-all."
          },
          "send_as": {
            "$ref": "#/components/schemas/SendAsView",
            "description": "Send-as for the domain."
          }
        },
        "required": [
          "name",
          "status",
          "created_at",
          "verified_at",
          "records",
          "checks",
          "aliases",
          "send_as"
        ]
      },
      "DomainListResponse": {
        "type": "object",
        "description": "The answer to GET /v1/domains.",
        "properties": {
          "domains": {
            "type": "array",
            "description": "The domains of the account.",
            "items": {
              "$ref": "#/components/schemas/DomainView"
            }
          }
        },
        "required": [
          "domains"
        ]
      },
      "DomainResponse": {
        "type": "object",
        "description": "The answer to GET /v1/domains/{domain}.",
        "properties": {
          "domain": {
            "$ref": "#/components/schemas/DomainView",
            "description": "The domain."
          }
        },
        "required": [
          "domain"
        ]
      },
      "AddDomainRequest": {
        "type": "object",
        "description": "The domain to add.",
        "properties": {
          "name": {
            "type": "string",
            "description": "The domain name, for example `example.com`. No mailbox part."
          }
        },
        "required": [
          "name"
        ]
      },
      "AddDomainResponse": {
        "type": "object",
        "description": "The answer to POST /v1/domains.",
        "properties": {
          "domain": {
            "$ref": "#/components/schemas/DomainView",
            "description": "The domain, with the DNS records to publish."
          },
          "created": {
            "type": "boolean",
            "description": "True when this call added the domain. False when the account had it already."
          },
          "next_step": {
            "type": "string",
            "description": "What to do next. Only when `created` is true."
          }
        },
        "required": [
          "domain",
          "created"
        ]
      },
      "VerifyResponse": {
        "type": "object",
        "description": "The answer to POST /v1/domains/{domain}/verify.",
        "properties": {
          "domain": {
            "$ref": "#/components/schemas/DomainView",
            "description": "The domain, with the new `checks`."
          },
          "verified": {
            "type": "boolean",
            "description": "True when ownership is proven, now or before."
          },
          "message": {
            "type": "string",
            "description": "The result in one or two sentences."
          }
        },
        "required": [
          "domain",
          "verified",
          "message"
        ]
      },
      "DeletedResponse": {
        "type": "object",
        "description": "The answer to a DELETE call.",
        "properties": {
          "deleted": {
            "type": "string",
            "description": "The name of what was removed: the domain, the alias address, or the SMTP username."
          }
        },
        "required": [
          "deleted"
        ]
      },
      "AliasTarget": {
        "type": "object",
        "description": "One mailbox that an alias forwards to.",
        "properties": {
          "email": {
            "type": "string",
            "description": "The mailbox address.",
            "format": "email"
          },
          "confirmed": {
            "type": "boolean",
            "description": "True when the owner of the mailbox confirmed it. No mail is forwarded to it before that."
          }
        },
        "required": [
          "email",
          "confirmed"
        ]
      },
      "AliasView": {
        "type": "object",
        "description": "One alias: an address at a domain and the mailboxes it forwards to.",
        "properties": {
          "alias": {
            "type": "string",
            "description": "The part before the @, or `*` for the catch-all."
          },
          "address": {
            "type": "string",
            "description": "The full address, for example `hello@example.com`. The catch-all is `*@example.com`."
          },
          "domain": {
            "type": "string",
            "description": "The domain of the alias."
          },
          "to": {
            "type": "array",
            "description": "The mailboxes the alias forwards to.",
            "items": {
              "$ref": "#/components/schemas/AliasTarget"
            }
          },
          "active": {
            "type": "boolean",
            "description": "True when mail to this alias is forwarded now."
          },
          "inactive_reasons": {
            "type": "array",
            "description": "Why not, when `active` is false. Empty when `active` is true.",
            "items": {
              "type": "string"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the alias was made."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the alias was last changed."
          }
        },
        "required": [
          "alias",
          "address",
          "domain",
          "to",
          "active",
          "inactive_reasons",
          "created_at",
          "updated_at"
        ]
      },
      "AliasListResponse": {
        "type": "object",
        "description": "The answer to GET /v1/domains/{domain}/aliases.",
        "properties": {
          "aliases": {
            "type": "array",
            "description": "The aliases of the domain. The catch-all is last.",
            "items": {
              "$ref": "#/components/schemas/AliasView"
            }
          }
        },
        "required": [
          "aliases"
        ]
      },
      "PutAliasRequest": {
        "type": "object",
        "description": "The mailboxes that the alias forwards to.",
        "properties": {
          "to": {
            "type": [
              "string",
              "array"
            ],
            "description": "One email address, or a list of them. The list replaces the list that the alias had.",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "to"
        ]
      },
      "PutAliasResponse": {
        "type": "object",
        "description": "The answer to PUT /v1/domains/{domain}/aliases/{alias}.",
        "properties": {
          "alias": {
            "$ref": "#/components/schemas/AliasView",
            "description": "The alias as it is now."
          },
          "created": {
            "type": "boolean",
            "description": "True when this call made the alias. False when it replaced one."
          },
          "confirmation_mailed_to": {
            "type": "array",
            "description": "The destinations that got a confirmation mail from this call.",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "alias",
          "created",
          "confirmation_mailed_to"
        ]
      },
      "DestinationView": {
        "type": "object",
        "description": "One mailbox that the account can forward to.",
        "properties": {
          "email": {
            "type": "string",
            "description": "The mailbox address.",
            "format": "email"
          },
          "confirmed": {
            "type": "boolean",
            "description": "True when the owner of the mailbox confirmed it."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the destination was added."
          },
          "confirmed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it was confirmed. null while it waits for confirmation."
          }
        },
        "required": [
          "email",
          "confirmed",
          "created_at",
          "confirmed_at"
        ]
      },
      "DestinationListResponse": {
        "type": "object",
        "description": "The answer to GET /v1/destinations.",
        "properties": {
          "destinations": {
            "type": "array",
            "description": "The destinations of the account, in the order of the alphabet.",
            "items": {
              "$ref": "#/components/schemas/DestinationView"
            }
          }
        },
        "required": [
          "destinations"
        ]
      },
      "DestinationResponse": {
        "type": "object",
        "description": "The answer to POST /v1/destinations.",
        "properties": {
          "destination": {
            "$ref": "#/components/schemas/DestinationView",
            "description": "The destination."
          },
          "mailed": {
            "type": "boolean",
            "description": "True when this call sent a confirmation mail."
          },
          "message": {
            "type": "string",
            "description": "The state of the destination in one or two sentences."
          }
        },
        "required": [
          "destination",
          "mailed",
          "message"
        ]
      },
      "SendRequest": {
        "type": "object",
        "description": "One plain-text message from an address at a domain of the account.",
        "properties": {
          "from": {
            "type": "string",
            "description": "`Name <you@yourdomain>` or an address without a name. The domain must be ready for send-as."
          },
          "to": {
            "type": [
              "string",
              "array"
            ],
            "description": "One address or a list of them. At least one.",
            "items": {
              "type": "string"
            }
          },
          "cc": {
            "type": [
              "string",
              "array"
            ],
            "description": "One address or a list of them, shown in the Cc header.",
            "items": {
              "type": "string"
            }
          },
          "bcc": {
            "type": [
              "string",
              "array"
            ],
            "description": "One address or a list of them. In the envelope only: no header shows these addresses.",
            "items": {
              "type": "string"
            }
          },
          "subject": {
            "type": "string",
            "description": "The subject: 1 to 255 characters.",
            "minLength": 1,
            "maxLength": 255
          },
          "text": {
            "type": "string",
            "description": "The body, as plain text. Links arrive as written."
          },
          "reply_to": {
            "type": "string",
            "description": "The address that replies go to."
          },
          "in_reply_to": {
            "type": "string",
            "description": "The Message-ID of the message this one answers, so mail clients keep the thread. It is `message_id` in GET /v1/logs."
          },
          "references": {
            "type": [
              "string",
              "array"
            ],
            "description": "Older Message-IDs of the thread, oldest first. At most 50. The default is `in_reply_to`.",
            "items": {
              "type": "string"
            }
          },
          "copy_to_owner": {
            "type": "boolean",
            "description": "true adds the account address as Bcc, for a copy in your mailbox."
          },
          "idempotency_key": {
            "type": "string",
            "description": "1 to 200 characters. A repeated call with the same key sends nothing and returns the first answer. Kept 7 days. The header Idempotency-Key does the same.",
            "minLength": 1,
            "maxLength": 200
          }
        },
        "required": [
          "from",
          "to",
          "subject",
          "text"
        ]
      },
      "SendResponse": {
        "type": "object",
        "description": "The answer to POST /v1/send.",
        "properties": {
          "message_id": {
            "type": "string",
            "description": "The Message-ID without angle brackets, as `message_id` in the log shows it."
          },
          "from": {
            "type": "string",
            "description": "The From address, without the name."
          },
          "to": {
            "type": "array",
            "description": "The To addresses.",
            "items": {
              "type": "string"
            }
          },
          "cc": {
            "type": "array",
            "description": "The Cc addresses.",
            "items": {
              "type": "string"
            }
          },
          "bcc": {
            "type": "array",
            "description": "The Bcc addresses, with the account address when `copy_to_owner` was true.",
            "items": {
              "type": "string"
            }
          },
          "status": {
            "type": "string",
            "description": "queued: the mail server took the message. The log says what the server of each recipient answered.",
            "enum": [
              "queued"
            ]
          },
          "replayed": {
            "type": "boolean",
            "description": "True when this is the stored answer of an earlier call with the same idempotency key."
          },
          "message": {
            "type": "string",
            "description": "The result in one or two sentences."
          }
        },
        "required": [
          "message_id",
          "from",
          "to",
          "cc",
          "bcc",
          "status",
          "replayed",
          "message"
        ]
      },
      "LogEntryView": {
        "type": "object",
        "description": "One row of the per-message log. No subject and no body are stored.",
        "properties": {
          "received_at": {
            "type": "string",
            "format": "date-time",
            "description": "When mxgate got the message."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the row last changed, for example after a new delivery attempt."
          },
          "direction": {
            "type": "string",
            "description": "in: mail to your domain that mxgate forwards. out: mail you sent through smtp.mxgate.io or POST /v1/send.",
            "enum": [
              "in",
              "out"
            ]
          },
          "status": {
            "type": "string",
            "description": "forwarded: delivered to the destination (in). sent: delivered to the recipient (out). deferred: not accepted yet, mxgate retries for 2 days. bounced: the other server refused it. expired: no delivery in 2 days. rejected: mxgate refused it at SMTP time.",
            "enum": [
              "forwarded",
              "sent",
              "deferred",
              "bounced",
              "expired",
              "rejected"
            ]
          },
          "sender": {
            "type": [
              "string",
              "null"
            ],
            "description": "The original sender. null for bounce messages."
          },
          "recipient": {
            "type": "string",
            "description": "The alias address the mail was sent to (in), or the recipient (out)."
          },
          "destination": {
            "type": "string",
            "description": "The mailbox it was forwarded to. Empty when it was refused, and for sent mail."
          },
          "domain": {
            "type": "string",
            "description": "The domain of the account that the row belongs to."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the other server or mxgate said."
          },
          "explanation": {
            "type": [
              "string",
              "null"
            ],
            "description": "The same in one plain sentence, with what to do. null when there is nothing to explain."
          },
          "dsn": {
            "type": [
              "string",
              "null"
            ],
            "description": "The delivery status code, for example `2.0.0` or `5.1.1`."
          },
          "relay": {
            "type": [
              "string",
              "null"
            ],
            "description": "The server that mxgate delivered to."
          },
          "client": {
            "type": [
              "string",
              "null"
            ],
            "description": "The server that gave the message to mxgate."
          },
          "message_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The Message-ID of the message, without angle brackets."
          },
          "attempts": {
            "type": "integer",
            "description": "How many delivery attempts were made."
          }
        },
        "required": [
          "received_at",
          "updated_at",
          "direction",
          "status",
          "sender",
          "recipient",
          "destination",
          "domain",
          "reason",
          "explanation",
          "dsn",
          "relay",
          "client",
          "message_id",
          "attempts"
        ]
      },
      "LogsResponse": {
        "type": "object",
        "description": "The answer to GET /v1/logs.",
        "properties": {
          "entries": {
            "type": "array",
            "description": "The rows, newest first.",
            "items": {
              "$ref": "#/components/schemas/LogEntryView"
            }
          },
          "next_before": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Pass it as `before` for the next page. null at the end."
          },
          "retention_days": {
            "type": "integer",
            "description": "How long the log is kept: 7 days on the free plan, 90 on the paid plan."
          }
        },
        "required": [
          "entries",
          "next_before",
          "retention_days"
        ]
      },
      "NotificationRecent": {
        "type": "object",
        "description": "One recent notification and what happened to it.",
        "properties": {
          "event": {
            "type": "string",
            "description": "The event.",
            "enum": [
              "bounced",
              "expired",
              "rejected",
              "deferred"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the notification was made."
          },
          "recipient": {
            "type": "string",
            "description": "The recipient of the message that failed."
          },
          "webhook_status": {
            "type": "string",
            "description": "none: no webhook was set. pending: a call is due or is retried. delivered: the webhook answered 2xx. failed: mxgate gave up.",
            "enum": [
              "none",
              "pending",
              "delivered",
              "failed"
            ]
          },
          "webhook_attempts": {
            "type": "integer",
            "description": "How many webhook calls were made."
          },
          "webhook_last_error": {
            "type": [
              "string",
              "null"
            ],
            "description": "The error of the last failed webhook call. null when there was none."
          },
          "emailed": {
            "type": "boolean",
            "description": "True when the event was in an email digest."
          }
        },
        "required": [
          "event",
          "created_at",
          "recipient",
          "webhook_status",
          "webhook_attempts",
          "webhook_last_error",
          "emailed"
        ]
      },
      "NotificationSettingsView": {
        "type": "object",
        "description": "The notification settings of the account and the 20 newest notifications.",
        "properties": {
          "email": {
            "type": "boolean",
            "description": "True when an hourly digest goes to the account address."
          },
          "events": {
            "type": "array",
            "description": "The events that make a notification.",
            "items": {
              "type": "string",
              "enum": [
                "bounced",
                "expired",
                "rejected",
                "deferred"
              ]
            }
          },
          "webhook_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The URL that gets one signed call per event. null when no webhook is set.",
            "format": "uri"
          },
          "webhook_secret": {
            "type": [
              "string",
              "null"
            ],
            "description": "The secret that signs webhook calls. It starts with `whsec_`. Shown in full only in the answer that makes it; later only its first 12 characters. null when no webhook is set."
          },
          "signature": {
            "type": "string",
            "description": "How to check the signature of a webhook call."
          },
          "recent": {
            "type": "array",
            "description": "The 20 newest notifications, newest first.",
            "items": {
              "$ref": "#/components/schemas/NotificationRecent"
            }
          },
          "message": {
            "type": "string",
            "description": "Only in the answer of the PUT call that makes a new webhook secret: store the secret now."
          }
        },
        "required": [
          "email",
          "events",
          "webhook_url",
          "webhook_secret",
          "signature",
          "recent"
        ]
      },
      "NotificationSettingsRequest": {
        "type": "object",
        "description": "The settings to change. A field that is not sent stays as it is.",
        "properties": {
          "email": {
            "type": "boolean",
            "description": "true turns the hourly email digest on, false turns it off."
          },
          "events": {
            "type": "array",
            "description": "The events that make a notification. The list replaces the list that was set.",
            "items": {
              "type": "string",
              "enum": [
                "bounced",
                "expired",
                "rejected",
                "deferred"
              ]
            }
          },
          "webhook_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "A public https:// URL. null or an empty text removes the webhook. A new URL gets a new secret."
          }
        }
      },
      "WebhookTestResponse": {
        "type": "object",
        "description": "The answer to POST /v1/notifications/test.",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "True when the webhook answered with a 2xx status."
          },
          "status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The HTTP status of the webhook. null when no answer came."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "What failed. null when `ok` is true."
          },
          "message": {
            "type": "string",
            "description": "The result in one sentence."
          }
        },
        "required": [
          "ok",
          "status",
          "error",
          "message"
        ]
      },
      "WebhookEvent": {
        "type": "object",
        "description": "The JSON body that mxgate posts to your webhook URL. The call has the header `mxgate-signature`. A failed call is retried 5 times in about 8 hours.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The id of the event, for example `evt_1042`. The test call has `evt_test`."
          },
          "type": {
            "type": "string",
            "description": "The event type.",
            "enum": [
              "message.bounced",
              "message.expired",
              "message.rejected",
              "message.deferred",
              "test"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the event was made."
          },
          "data": {
            "$ref": "#/components/schemas/LogEntryView",
            "description": "The log row of the message. The test call has an object with one `message` text instead."
          }
        },
        "required": [
          "id",
          "type",
          "created_at",
          "data"
        ]
      },
      "StatusCheckView": {
        "type": "object",
        "description": "One check of the outside monitor.",
        "properties": {
          "name": {
            "type": "string",
            "description": "The name of the check, with the host and the port it looks at."
          },
          "ok": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "false after two failed runs in a row. null when no run was reported."
          },
          "detail": {
            "type": "string",
            "description": "What the last run saw, in a few words. For a failure: what is wrong."
          },
          "last_check_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the check last ran. null when there is no report yet."
          },
          "last_ok_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the check last passed. null when it never passed."
          },
          "success_rate": {
            "type": [
              "number",
              "null"
            ],
            "description": "The percent of runs that passed in the window, with two decimals. null when there is no run in the window."
          },
          "daily": {
            "type": "array",
            "description": "The percent of runs that passed on each day in `days`, in the same order. null for a day with no run.",
            "items": {
              "type": [
                "number",
                "null"
              ]
            }
          }
        },
        "required": [
          "name",
          "ok",
          "detail",
          "last_check_at",
          "last_ok_at",
          "success_rate",
          "daily"
        ]
      },
      "StatusView": {
        "type": "object",
        "description": "The health of the service, as a monitor on another machine sees it.",
        "properties": {
          "state": {
            "type": "string",
            "description": "operational: no check is down. degraded: some checks are down. outage: all checks are down. unknown: `stale` is true.",
            "enum": [
              "operational",
              "degraded",
              "outage",
              "unknown"
            ]
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the monitor sent its last report. null when there is no report yet."
          },
          "window_days": {
            "type": "integer",
            "description": "The number of days in `days`: 30."
          },
          "days": {
            "type": "array",
            "description": "The UTC days of the window as YYYY-MM-DD, oldest first.",
            "items": {
              "type": "string",
              "format": "date"
            }
          },
          "checks": {
            "type": "array",
            "description": "The checks of the monitor.",
            "items": {
              "$ref": "#/components/schemas/StatusCheckView"
            }
          },
          "stale": {
            "type": "boolean",
            "description": "True when the monitor sent no report for 10 minutes."
          }
        },
        "required": [
          "state",
          "updated_at",
          "window_days",
          "days",
          "checks",
          "stale"
        ]
      },
      "ConnectedAppView": {
        "type": "object",
        "description": "An app that the owner connected to the account through OAuth.",
        "properties": {
          "id": {
            "type": "integer",
            "description": "The id to remove the app with."
          },
          "name": {
            "type": "string",
            "description": "The name the app gave when it registered."
          },
          "client_id": {
            "type": "string",
            "description": "The OAuth client id of the app."
          },
          "connected_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the owner allowed the app."
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the app last made a call. null when it made none."
          }
        },
        "required": [
          "id",
          "name",
          "client_id",
          "connected_at",
          "last_used_at"
        ]
      },
      "ConnectedAppListResponse": {
        "type": "object",
        "description": "The answer to GET /v1/oauth/apps.",
        "properties": {
          "apps": {
            "type": "array",
            "description": "The connected apps, oldest first.",
            "items": {
              "$ref": "#/components/schemas/ConnectedAppView"
            }
          }
        },
        "required": [
          "apps"
        ]
      },
      "LoginGrantRequest": {
        "type": "object",
        "description": "The body of POST /v1/login/grant.",
        "properties": {
          "grant": {
            "type": "string",
            "description": "The one-time grant from the URL fragment after the sign-in with Google. It starts with `mxl_`."
          }
        },
        "required": [
          "grant"
        ]
      }
    }
  }
}
