{
  "openapi": "3.1.0",
  "info": {
    "title": "MeowMail API",
    "version": "1.0.0",
    "summary": "Free disposable email API. Receive-only, no accounts, TTL-expiring inboxes.",
    "description": "Create throwaway inboxes and read the mail they receive.\n\nBuilt for automated testing of signup and verification flows, and for privacy tooling.\n\n**Inboxes are public.** There is no authorization on inbox contents: anyone who knows the address can read it. Never use MeowMail for anything you would mind a stranger reading, and never for password resets on accounts that matter.\n\n**Mail is ephemeral.** Emails are permanently deleted roughly one hour after arrival.\n\n**Receive-only.** There is no send endpoint and never will be in this API version.",
    "termsOfService": "https://docs.meowmail.in/fair-use",
    "contact": {
      "name": "MeowMail",
      "url": "https://docs.meowmail.in"
    },
    "license": {
      "name": "Free to use under the fair-use policy",
      "url": "https://docs.meowmail.in/fair-use"
    }
  },
  "servers": [
    {
      "url": "https://api.meowmail.in",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Meta",
      "description": "Discovery, spec, and health"
    },
    {
      "name": "Keys",
      "description": "Free self-serve API keys"
    },
    {
      "name": "Domains",
      "description": "Domains that accept mail"
    },
    {
      "name": "Inboxes",
      "description": "Create inboxes and read their mail"
    },
    {
      "name": "Emails",
      "description": "Individual emails and attachments"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "apiKeyHeader": []
    },
    {}
  ],
  "paths": {
    "/api/v1": {
      "get": {
        "tags": [
          "Meta"
        ],
        "operationId": "getServiceInfo",
        "summary": "Service discovery",
        "description": "Endpoint index, docs links, and the current email TTL.",
        "responses": {
          "200": {
            "description": "Service information",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/openapi.json": {
      "get": {
        "tags": [
          "Meta"
        ],
        "operationId": "getOpenApiSpec",
        "summary": "This document",
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 specification",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "description": "The OpenAPI 3.1 document for this API. Outside the developer quota. Import it into Postman or Insomnia, or generate a typed client from it."
      }
    },
    "/api/v1/health": {
      "get": {
        "tags": [
          "Meta"
        ],
        "operationId": "getHealth",
        "summary": "Health check",
        "responses": {
          "200": {
            "description": "Service health",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "status": {
                          "type": "string",
                          "examples": [
                            "ok"
                          ]
                        },
                        "database": {
                          "type": "string",
                          "enum": [
                            "connected",
                            "disconnected"
                          ]
                        },
                        "smtp": {
                          "type": "string",
                          "enum": [
                            "running",
                            "stopped"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "description": "Liveness and dependency check: a single `SELECT 1` against the database plus the SMTP listener state. Outside the developer quota, so a monitoring system can poll it without burning the fair-use budget of everyone sharing its egress IP."
      }
    },
    "/api/v1/keys": {
      "post": {
        "tags": [
          "Keys"
        ],
        "operationId": "createApiKey",
        "summary": "Create a free API key",
        "description": "No signup, no password, no email verification. The raw key is returned exactly once and cannot be recovered.\n\nA key raises your rate limits and is required for long-polling (`wait`).\n\nLimited to 3 keys per IP per 24 hours.",
        "security": [
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "label"
                ],
                "properties": {
                  "label": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "Short name for this integration. Helps us help you.",
                    "examples": [
                      "acme-e2e-tests"
                    ]
                  },
                  "contact_email": {
                    "type": "string",
                    "maxLength": 254,
                    "description": "Optional and unverified. Used only to warn you about breaking changes. Not a real inbox we can mail today — we have no outbound email."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Key created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "api_key": {
                          "type": "string",
                          "examples": [
                            "mm_kQ8fT2xLp0aZ7nVc4WmJhRb1"
                          ]
                        },
                        "key_prefix": {
                          "type": "string"
                        },
                        "label": {
                          "type": "string"
                        },
                        "tier": {
                          "type": "string",
                          "examples": [
                            "free"
                          ]
                        },
                        "limits": {
                          "$ref": "#/components/schemas/Limits"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "warning": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/me": {
      "get": {
        "tags": [
          "Keys"
        ],
        "operationId": "getIdentity",
        "summary": "Who am I, and what have I used",
        "description": "Reports your tier, limits, and current usage. Reading your quota does not consume it.",
        "responses": {
          "200": {
            "description": "Caller identity",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "kind": {
                          "type": "string",
                          "enum": [
                            "anonymous",
                            "key"
                          ]
                        },
                        "tier": {
                          "type": "string",
                          "enum": [
                            "anonymous",
                            "free"
                          ]
                        },
                        "key_prefix": {
                          "type": "string"
                        },
                        "label": {
                          "type": "string"
                        },
                        "limits": {
                          "$ref": "#/components/schemas/Limits"
                        },
                        "usage": {
                          "type": "object",
                          "properties": {
                            "requests_this_minute": {
                              "type": "integer"
                            },
                            "requests_today": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v1/domains": {
      "get": {
        "tags": [
          "Domains"
        ],
        "operationId": "listDomains",
        "summary": "List domains that accept mail",
        "description": "Call this rather than hardcoding a domain — the set changes.",
        "responses": {
          "200": {
            "description": "Active domains",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "domain": {
                            "type": "string",
                            "examples": [
                              "meowmail.in"
                            ]
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "count": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/inboxes": {
      "post": {
        "tags": [
          "Inboxes"
        ],
        "operationId": "createInbox",
        "summary": "Generate an inbox address",
        "description": "Inboxes are implicit: nothing is stored until mail arrives, and any address at an active domain already works. This endpoint exists so you don't have to reimplement our address format, and so the address you get back is guaranteed to be empty right now.\n\nOmit `local_part` for a random address. Omit `domain` and you get a default — prefer passing it explicitly from `GET /api/v1/domains`.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string",
                    "examples": [
                      "meowmail.in"
                    ]
                  },
                  "local_part": {
                    "type": "string",
                    "maxLength": 64,
                    "pattern": "^[a-z0-9][a-z0-9._+\\-]{0,63}$",
                    "description": "Optional. Lowercased. Random if omitted."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Inbox address",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Inbox"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/inboxes/{address}/emails": {
      "get": {
        "tags": [
          "Inboxes"
        ],
        "operationId": "listEmails",
        "summary": "List emails, optionally waiting for new mail",
        "description": "Newest first by default.\n\nPass `since` with the id of the newest email you have already seen to get only newer ones. **With `since`, results are returned oldest-first** so that advancing your cursor to the last item can never skip mail.\n\nPass `wait` (needs an API key) to hold the connection until mail arrives, up to 30 seconds. This is strongly preferred over a tight polling loop: it costs us less and returns to you faster.\n\nBodies are not included — fetch `GET /api/v1/emails/{id}` for those.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Address"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "Return only emails newer than this id. Flips ordering to oldest-first.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "before",
            "in": "query",
            "description": "Return only emails older than this id. Pages backwards.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "wait",
            "in": "query",
            "description": "Seconds to hold the connection if the result would be empty. Requires an API key. Set your HTTP client timeout above this value.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 30,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Email summaries. An empty array means no mail matched (including after a `wait` timed out).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EmailSummary"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "address": {
                          "type": "string"
                        },
                        "count": {
                          "type": "integer"
                        },
                        "next_since": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Feed straight back as `since` on your next poll."
                        },
                        "next_before": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Feed back as `before` to page into older mail."
                        },
                        "waited": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/KeyRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "Inboxes"
        ],
        "operationId": "purgeInbox",
        "summary": "Delete every email in an inbox",
        "description": "Irreversible. Useful between test cases.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Address"
          }
        ],
        "responses": {
          "200": {
            "description": "Purge result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "address": {
                          "type": "string"
                        },
                        "deleted": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/emails/{id}": {
      "get": {
        "tags": [
          "Emails"
        ],
        "operationId": "getEmail",
        "summary": "Get one email with bodies and attachment metadata",
        "parameters": [
          {
            "$ref": "#/components/parameters/EmailId"
          }
        ],
        "responses": {
          "200": {
            "description": "Full email",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/EmailDetail"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Bodies, headers, and attachment metadata for one email.\n\n`html_body` is sanitized server-side but should still be rendered in a sandboxed iframe. `text_body` and `html_body` are each `null` when the message carried no such part — a message with only an HTML part is common, so always check both.\n\nReturns 404 once the email expires, which is roughly an hour after it arrived."
      },
      "delete": {
        "tags": [
          "Emails"
        ],
        "operationId": "deleteEmail",
        "summary": "Delete one email",
        "parameters": [
          {
            "$ref": "#/components/parameters/EmailId"
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Permanently deletes one email, ahead of its TTL. Irreversible, and there are no backups. Returns 204 with no body."
      }
    },
    "/api/v1/emails/{id}/attachments": {
      "get": {
        "tags": [
          "Emails"
        ],
        "operationId": "listAttachments",
        "summary": "List attachment metadata",
        "description": "`GET /api/v1/emails/{id}` already inlines this. Use this endpoint when you only need the list.",
        "parameters": [
          {
            "$ref": "#/components/parameters/EmailId"
          }
        ],
        "responses": {
          "200": {
            "description": "Attachments",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Attachment"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "email_id": {
                          "type": "string"
                        },
                        "count": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/emails/{id}/attachments/{attachment_id}": {
      "get": {
        "tags": [
          "Emails"
        ],
        "operationId": "downloadAttachment",
        "summary": "Download attachment bytes",
        "description": "Returns raw binary, not JSON. Attachments over 5 MB are never stored, so they will not appear here.",
        "parameters": [
          {
            "$ref": "#/components/parameters/EmailId"
          },
          {
            "name": "attachment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Raw file bytes",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "`Authorization: Bearer mm_...`. Omit entirely to use the anonymous tier."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Alternative to the Authorization header."
      }
    },
    "parameters": {
      "Address": {
        "name": "address",
        "in": "path",
        "required": true,
        "description": "Full inbox address, URL-encoded. Case-insensitive.",
        "schema": {
          "type": "string",
          "examples": [
            "abc123@meowmail.in"
          ]
        }
      },
      "EmailId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Email id (UUIDv7 — sorts chronologically, which is what makes the cursors work).",
        "schema": {
          "type": "string"
        }
      }
    },
    "schemas": {
      "Limits": {
        "type": "object",
        "properties": {
          "requests_per_minute": {
            "type": "integer"
          },
          "requests_per_day": {
            "type": "integer"
          },
          "concurrent_waits": {
            "type": "integer",
            "description": "Simultaneous long-poll requests allowed. 0 for anonymous callers."
          }
        }
      },
      "Inbox": {
        "type": "object",
        "properties": {
          "address": {
            "type": "string",
            "examples": [
              "abc123@meowmail.in"
            ]
          },
          "local_part": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "email_ttl_seconds": {
            "type": "integer",
            "description": "How long an email survives after arrival."
          }
        }
      },
      "EmailSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "sender": {
            "type": [
              "string",
              "null"
            ],
            "description": "The From header as received. Unverified — no SPF/DKIM checks. Do not trust it."
          },
          "mailed_by": {
            "type": [
              "string",
              "null"
            ],
            "description": "Domain of the SMTP envelope sender."
          },
          "subject": {
            "type": [
              "string",
              "null"
            ]
          },
          "has_attachments": {
            "type": "boolean"
          },
          "received_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EmailDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/EmailSummary"
          },
          {
            "type": "object",
            "properties": {
              "text_body": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "html_body": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Scripts, event handlers, and embeddable tags are stripped server-side. Still render it in a sandboxed iframe — regex HTML filtering is defence in depth, not a guarantee."
              },
              "attachments": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Attachment"
                }
              }
            }
          }
        ]
      },
      "Attachment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "filename": {
            "type": "string"
          },
          "content_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "size_bytes": {
            "type": "integer"
          },
          "download_url": {
            "type": "string"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "unauthorized",
                  "key_required",
                  "not_found",
                  "rate_limited",
                  "quota_exceeded",
                  "too_many_waiters",
                  "internal_error"
                ],
                "description": "Stable machine-readable code. Branch on this, not on `message`."
              },
              "message": {
                "type": "string",
                "description": "Human-readable. May be reworded at any time."
              },
              "docs": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Malformed request",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "A key was presented but is invalid or revoked. Omit the header entirely for anonymous access.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such resource. Most often the email expired.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit or daily quota exceeded, or too many long-polls already in flight for this key. Honour `retry-after`.",
        "headers": {
          "retry-after": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds to wait."
          },
          "ratelimit-limit": {
            "schema": {
              "type": "integer"
            }
          },
          "ratelimit-remaining": {
            "schema": {
              "type": "integer"
            }
          },
          "ratelimit-reset": {
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "KeyRequired": {
        "description": "This feature requires an API key. Create a free one with POST /api/v1/keys.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
