{
  "openapi": "3.1.0",
  "info": {
    "title": "Mailcheer API",
    "version": "1.0.0",
    "summary": "Transactional email, subscribers, suppression lists and campaigns.",
    "description": "Mailcheer's public API.\n\nIt covers two uses: sending individual (transactional) emails from your application, and driving your lists and campaigns — from code or from an AI agent, through the MCP server.\n\n## Connected in three minutes\n\n1. **Create a key** under Settings → API in your workspace. It starts with `mch_live_` and carries the permissions you choose.\n2. **Call `GET /api/v1/me`.** It returns the workspace, the key's permissions, the remaining quota, the **verified** domains and the registered senders. That is what tells you which address to write from: without it, your first send fails on `from`.\n3. **Send** with `POST /api/v1/emails`.\n\n```bash\ncurl https://mailcheer.com/api/v1/emails \\\n  -H \"Authorization: Bearer mch_live_…\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"from\": \"Your brand <hello@yourdomain.com>\",\n    \"to\": \"customer@example.com\",\n    \"subject\": \"Your September invoice\",\n    \"html\": \"<p>Here it is.</p>\"\n  }'\n```\n\n## What applies to every call\n\n- **Base address** `https://mailcheer.com`; every path starts with `/api/v1`. Bodies and responses in JSON (`Content-Type: application/json`).\n- **Authentication** `Authorization: Bearer mch_live_…`, or the `X-Api-Key` header if your gateway swallows the first.\n- **Rate limit** 600 requests per minute per key. Beyond that: 429, with `Retry-After` in seconds.\n- **Errors** always the same shape — read `error.code` (or `name`), never `message`, which is written for a human and may change.\n- **Language** messages follow your `Accept-Language` header: **English by default, French if you ask for it** — and only the first preference is read. The `code` never changes language.\n- **One workspace per key.** Nothing crosses from one workspace to another.\n\n## Coming from Resend\n\nThe fields and the response of `POST /api/v1/emails` are modeled on theirs: `reply_to`, `tags` as a `{name, value}` array, and an error that also carries `statusCode` / `message` / `name`. A migration comes down to changing the base address and the key. One difference to know: the response is **202**, not 200 — the message is accepted, not delivered yet.\n\n## From an AI agent (MCP)\n\nThe MCP server is at `https://mailcheer.com/api/mcp` — JSON-RPC over **POST**, Streamable HTTP transport, sessionless: every request carries the key in the same `Authorization` header. It opens no stream (a `GET` answers 405, which clients know how to read). The tools call this same API with your key: same permissions, same quota, same refusals.\n\n## Five rules apply to every send, without exception\n\n1. `from` must be on a domain verified in the workspace;\n2. an address on the suppression list is refused, with its reason — and the whole call fails, never a silent partial send;\n3. your plan's monthly quota counts these sends the same as campaigns;\n4. a bounce or complaint rate that is too high suspends sending;\n5. a subscriber added through the API receives a confirmation, unless `double_opt_in: false` is explicit — and then you bear responsibility for the consent.\n\n## Webhooks — Mailcheer calls you\nThe API answers when you ask it; a webhook tells you without being asked. Subscribe an address of your own (`POST /api/v1/webhooks`, or Settings → API) and Mailcheer posts JSON to it on every event you choose.\n\n**Verify the signature, always.** Every call carries the header `Mailcheer-Signature: t=<unix timestamp>,v1=<hex>` where `v1` is the HMAC-SHA256 of `\"<t>.<raw body>\"` signed with the subscription's secret. Compare in constant time, and reject a `t` older than 300 seconds — without that second check, an intercepted call stays replayable forever.\n\n```js\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nfunction valid(secret, header, rawBody) {\n  const [t, v1] = [/t=(\\d+)/.exec(header)?.[1], /v1=([a-f0-9]+)/.exec(header)?.[1]];\n  if (!t || !v1) return false;\n  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;\n  const expected = createHmac(\"sha256\", secret).update(`${t}.${rawBody}`).digest(\"hex\");\n  return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));\n}\n```\n\n**Answer 2xx in under fifteen seconds.** Everything else is a failure: Mailcheer retries four times (30 s, 2 min, 10 min, 1 h) then gives up and logs it. Do your work in the background and answer straight away — a long job is indistinguishable from an outage.\n\n**The same event can arrive twice** (a retry after a lost response). `id` is stable from one attempt to the next: keep it and ignore duplicates.\n\nBodies all have the same shape: `{ \"id\": \"evt_…\", \"type\": \"…\", \"created_at\": \"…\", \"data\": { … } }`.",
    "contact": {
      "name": "Mailcheer",
      "url": "https://mailcheer.com/en/docs/api",
      "email": "contact@mailcheer.com"
    },
    "license": {
      "name": "Mailcheer Terms of Sale",
      "url": "https://mailcheer.com/en/cgv"
    }
  },
  "servers": [
    {
      "url": "https://mailcheer.com",
      "description": "Production. There is no separate test environment: try it on a workspace of your own, with an address of your own. Every path below already includes the `/api/v1` prefix."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "apiKeyHeader": []
    }
  ],
  "tags": [
    {
      "name": "Emails",
      "description": "Individual sends and their status."
    },
    {
      "name": "Subscribers",
      "description": "The people signed up to your lists."
    },
    {
      "name": "Suppression",
      "description": "Addresses that will receive nothing further."
    },
    {
      "name": "Campaigns",
      "description": "Drafts, sending and statistics."
    },
    {
      "name": "Workspace",
      "description": "Who the key belongs to."
    },
    {
      "name": "Webhooks",
      "description": "Mailcheer calls YOUR service when something happens."
    }
  ],
  "paths": {
    "/api/v1": {
      "get": {
        "operationId": "getApiMap",
        "tags": ["Workspace"],
        "summary": "The API map",
        "description": "No key required: lists the endpoints, the permissions and the address of the documentation.",
        "security": [],
        "responses": {
          "200": {
            "description": "The map.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Carte"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/me": {
      "get": {
        "operationId": "getAccount",
        "tags": ["Workspace"],
        "summary": "Who this key belongs to",
        "description": "Workspace, the key's permissions, plan, emails left this month, sending domains and senders. Call this first: it is what tells you which address to write from. No particular permission is required.",
        "responses": {
          "200": {
            "description": "The state of the workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "429": {
            "$ref": "#/components/responses/TropDAppels"
          }
        }
      }
    },
    "/api/v1/emails": {
      "post": {
        "operationId": "sendEmail",
        "tags": ["Emails"],
        "summary": "Send an email",
        "description": "Permission required: `emails:send`.\n\nAnswers **202**: Amazon has accepted the message, it is not delivered yet. Delivery is confirmed a few seconds later with `GET /api/v1/emails/{id}`.\n\nUse the `Idempotency-Key` header: replaying the same call returns the same response without a second send.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailRequest"
              },
              "examples": {
                "simple": {
                  "summary": "One recipient",
                  "value": {
                    "from": "Your brand <hello@yourdomain.com>",
                    "to": "customer@example.com",
                    "subject": "Your September invoice",
                    "html": "<p>Here it is, attached to this message.</p>"
                  }
                },
                "complet": {
                  "summary": "Several recipients, reply-to and tags",
                  "value": {
                    "from": "hello@yourdomain.com",
                    "to": ["marie@example.com", "paul@example.com"],
                    "subject": "Your order is ready",
                    "html": "<p>It is waiting for you.</p>",
                    "text": "It is waiting for you.",
                    "replyTo": "support@yourdomain.com",
                    "headers": {
                      "X-Entity-Ref-ID": "order-4192"
                    },
                    "tags": {
                      "type": "order"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted by our sending provider.",
            "headers": {
              "Idempotent-Replay": {
                "description": "`true` if the response is that of an identical call already handled.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Email"
                },
                "examples": {
                  "accepte": {
                    "summary": "Accepted — read it again a few seconds later",
                    "value": {
                      "id": "eml_5c1a9",
                      "object": "email",
                      "from": "hello@yourdomain.com",
                      "to": ["customer@example.com"],
                      "subject": "Your September invoice",
                      "created_at": "2026-09-18T09:41:12.000Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "402": {
            "$ref": "#/components/responses/QuotaAtteint"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "409": {
            "$ref": "#/components/responses/Conflit"
          },
          "422": {
            "$ref": "#/components/responses/Refuse"
          },
          "429": {
            "$ref": "#/components/responses/TropDAppels"
          },
          "502": {
            "$ref": "#/components/responses/EnvoiEchoue"
          }
        }
      }
    },
    "/api/v1/emails/{id}": {
      "get": {
        "operationId": "getEmail",
        "tags": ["Emails"],
        "summary": "Status of an email",
        "description": "Permission required: `emails:send`. The status changes after the send, as our provider reports back.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The email.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      }
    },
    "/api/v1/subscribers": {
      "get": {
        "operationId": "listSubscribers",
        "tags": ["Subscribers"],
        "summary": "List subscribers",
        "description": "Permission required: `subscribers:read`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "subscribed",
                "unsubscribed",
                "bounced",
                "complained"
              ]
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Search on address, first name or last name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The requested page.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Liste"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Subscriber"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          }
        }
      },
      "post": {
        "operationId": "upsertSubscriber",
        "tags": ["Subscribers"],
        "summary": "Add or update a subscriber",
        "description": "Permission required: `subscribers:write`.\n\nBy default the person enters `pending` and receives a confirmation email. `double_opt_in: false` subscribes them straight away — to be used only if consent was collected elsewhere, and the caller then bears responsibility for it.\n\nAn address on the suppression list, unsubscribed, or returned as a hard bounce is refused.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubscriberRequest"
              },
              "examples": {
                "simple": {
                  "summary": "An address and a first name",
                  "value": {
                    "email": "marie@example.com",
                    "firstName": "Marie"
                  }
                },
                "complet": {
                  "summary": "With tags and custom fields",
                  "value": {
                    "email": "marie@example.com",
                    "firstName": "Marie",
                    "lastName": "Dupont",
                    "tags": ["customers", "paris-store"],
                    "fields": {
                      "city": "Ajaccio"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriberResult"
                }
              }
            }
          },
          "200": {
            "description": "The subscriber already existed and has been updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriberResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "409": {
            "$ref": "#/components/responses/Conflit"
          },
          "422": {
            "$ref": "#/components/responses/Refuse"
          }
        }
      }
    },
    "/api/v1/subscribers/{email}": {
      "get": {
        "operationId": "getSubscriber",
        "tags": ["Subscribers"],
        "summary": "One subscriber record",
        "description": "Permission required: `subscribers:read`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/EmailPath"
          }
        ],
        "responses": {
          "200": {
            "description": "The record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subscriber"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      },
      "delete": {
        "operationId": "unsubscribeSubscriber",
        "tags": ["Subscribers"],
        "summary": "Unsubscribe",
        "description": "Permission required: `subscribers:write`.\n\n**Does not erase the record**: the person moves to `unsubscribed` and their address enters the suppression list. Erasing the trace would bring the address back at the next file import.",
        "parameters": [
          {
            "$ref": "#/components/parameters/EmailPath"
          }
        ],
        "responses": {
          "200": {
            "description": "Unsubscribed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "subscriber"
                    },
                    "email": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "const": "unsubscribed"
                    },
                    "suppressed": {
                      "type": "boolean"
                    },
                    "deleted": {
                      "type": "boolean",
                      "const": false,
                      "description": "Always `false`: the record is kept."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      }
    },
    "/api/v1/suppression": {
      "get": {
        "operationId": "listSuppressions",
        "tags": ["Suppression"],
        "summary": "The suppressed addresses",
        "description": "Permission required: `subscribers:read`. The `scope` field distinguishes addresses suppressed for this workspace (`organization`) from those suppressed platform-wide (`platform`).",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "name": "email",
            "in": "query",
            "description": "Check one specific address.",
            "schema": {
              "type": "string",
              "format": "email"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The requested page.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Liste"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Suppression"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          }
        }
      },
      "post": {
        "operationId": "createSuppression",
        "tags": ["Suppression"],
        "summary": "Suppress an address",
        "description": "Permission required: `subscribers:write`. The matching subscriber, if there is one, moves to `unsubscribed` in the same operation.\n\n**No endpoint removes an address from this list**, and that is deliberate: it is the one action that suspends a sending capability. It is done by hand, in your Mailcheer workspace.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email"],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "reason": {
                    "type": "string",
                    "enum": ["unsubscribe", "bounce", "complaint", "manual"],
                    "default": "manual"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Suppressed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Suppression"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "422": {
            "$ref": "#/components/responses/Refuse"
          }
        }
      }
    },
    "/api/v1/campaigns": {
      "get": {
        "operationId": "listCampaigns",
        "tags": ["Campaigns"],
        "summary": "List campaigns",
        "description": "Permission required: `campaigns:read`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["draft", "scheduled", "sending", "sent", "archived"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The requested page.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Liste"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Campaign"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          }
        }
      },
      "post": {
        "operationId": "createCampaign",
        "tags": ["Campaigns"],
        "summary": "Create a draft",
        "description": "Permission required: `campaigns:write`. **Nothing goes out**: sending is a second action, deliberately kept separate.\n\nContent is written either in blocks (`content`) or in plain text (`text`): a blank line separates two paragraphs, `# ` at the start of a line makes a heading.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CampaignRequest"
              },
              "examples": {
                "texte": {
                  "summary": "A letter written in plain text",
                  "value": {
                    "name": "September newsletter",
                    "subject": "What we changed this month",
                    "preheader": "Three new things, and one we owed you.",
                    "from": "hello@yourdomain.com",
                    "text": "# Three new things\n\nHere is what changed this month.\n\nEnjoy the read."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Draft created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "object": {
                      "type": "string",
                      "const": "campaign"
                    },
                    "name": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "const": "draft"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          },
          "422": {
            "$ref": "#/components/responses/Refuse"
          }
        }
      }
    },
    "/api/v1/campaigns/{id}": {
      "get": {
        "operationId": "getCampaign",
        "tags": ["Campaigns"],
        "summary": "Status and statistics",
        "description": "Permission required: `campaigns:read`. Rates are calculated on delivered messages, not on recipients: a dead address must not pull down the open rate of those who did receive it.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The campaign.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Campaign"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      }
    },
    "/api/v1/campaigns/{id}/stats": {
      "get": {
        "operationId": "getCampaignStats",
        "tags": ["Campaigns"],
        "summary": "The full report",
        "description": "Permission required: `campaigns:read`. Everything the Mailcheer campaign view shows, so you can display it in your own software: counters, rates (shares between 0 and 1, `null` as long as nothing has gone out), the curve of the first 48 hours in eight six-hour windows (empty if the campaign has not gone out), what people read on (top five rows, shares between 0 and 1, `proxiedShare` = the share of opens coming from a privacy relay such as Apple Mail or Gmail), links from most to least clicked, and the HTML as rendered at send time (empty string otherwise). Open and click rates are calculated on delivered messages; delivery and bounce rates on recipients.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignStats"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      }
    },
    "/api/v1/campaigns/{id}/send": {
      "post": {
        "operationId": "sendCampaign",
        "tags": ["Campaigns"],
        "summary": "Start the send",
        "description": "Permission required: `campaigns:write`. **Irreversible** (except `dry_run`).\n\n⚠️ **Without a body, the campaign goes to ALL subscribers of its audience with status `subscribed`** (the whole list, or the segment chosen in the interface) — minus those on the suppression list, recomputed at the last moment.\n\n**`to` restricts the send to a list of addresses.** It can only narrow: the letter goes to the requested addresses that are active subscribers of the audience, never to an address on the suppression list. The response carries `audience`: who is kept, who is excluded, and why. An empty list writes to nobody (422); a campaign with an A/B test refuses `to` (the winning version would go out later to the whole audience).\n\n**`dry_run: true` runs every check and queues nothing**: a `200` response with `would_send`, `blocked_reason` and the recipient count. That is the number to show BEFORE confirming. Other fields are ignored, as before — except look-alikes of `dry_run` (`dryRun`, `dry-run`, `test`, `simulate`…) and of `to` (`To`, `recipients`, `emails`, `to_emails`, `destinataires`, `audience`…), refused with 422: ignored, the former would send for real, the latter would send to the whole list.\n\nThe call freezes the recipient list and fills the queue. Messages then go out at the rate our provider allows: the response says `sending` and a `queued` count, never `sent`. Follow the progress with `GET /api/v1/campaigns/{id}`.\n\nA campaign already `sending` or `sent` returns 409: we do not re-send, we duplicate. An insufficient monthly quota, a workspace with damaged reputation, no sender, or an empty list return 422 with the reason.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "to": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 50000,
                    "description": "Optional. Restricts the send to these addresses — only those that are active subscribers of the audience, never an address on the suppression list. Omitted: the whole audience. Empty (`[]`): nobody, the send is refused."
                  },
                  "dry_run": {
                    "type": "boolean",
                    "description": "`true`: check and count everything, queue nothing."
                  }
                }
              },
              "examples": {
                "groupe": {
                  "summary": "Write to a group",
                  "value": {
                    "to": ["claire@example.com", "marc@example.com"]
                  }
                },
                "simulation": {
                  "summary": "Preview first",
                  "value": {
                    "to": ["claire@example.com", "marc@example.com"],
                    "dry_run": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dry run (`dry_run`): nothing was queued.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "object": {
                      "type": "string",
                      "const": "send_preview"
                    },
                    "dry_run": {
                      "type": "boolean",
                      "const": true
                    },
                    "would_send": {
                      "type": "boolean",
                      "description": "Would the real send, started now, go out?"
                    },
                    "blocked_reason": {
                      "type": ["string", "null"],
                      "description": "If not, the exact message the real send would return."
                    },
                    "recipients": {
                      "type": ["integer", "null"],
                      "description": "How many would be queued; `null` if the refusal comes before counting."
                    },
                    "audience": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/Audience"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "The per-address breakdown, when `to` is given."
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Recipients frozen and queued.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "object": {
                      "type": "string",
                      "const": "campaign"
                    },
                    "status": {
                      "type": "string",
                      "const": "sending"
                    },
                    "queued": {
                      "type": "integer",
                      "description": "The number of recipients actually frozen in the queue."
                    },
                    "audience": {
                      "$ref": "#/components/schemas/Audience",
                      "description": "Present when the send was restricted by `to`."
                    },
                    "note": {
                      "type": "string",
                      "description": "What is left to happen, in plain words. Written for a human; do not write logic against it."
                    }
                  }
                },
                "examples": {
                  "file": {
                    "summary": "Queued",
                    "value": {
                      "id": "cmp_8d2",
                      "object": "campaign",
                      "status": "sending",
                      "queued": 3182,
                      "note": "3182 recipient(s) queued. Messages then go out at the rate our sending provider allows; follow the progress with GET /v1/campaigns/cmp_8d2."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          },
          "409": {
            "$ref": "#/components/responses/Conflit"
          },
          "422": {
            "$ref": "#/components/responses/Refuse"
          }
        }
      }
    },
    "/api/v1/audience": {
      "post": {
        "operationId": "previewAudience",
        "tags": ["Campaigns"],
        "summary": "Who would receive, without creating a campaign",
        "description": "Permission required: `subscribers:read`. **Read-only**: nothing is created, nothing is sent.\n\nReturns what a campaign sent right now to the whole list would reach — restricted to `to` if given, with the same per-address breakdown as the send. To display \"12 will receive, 3 are excluded\" while someone chooses who to write to, without creating a draft.\n\nCampaign-specific checks (subject, content, quota, segment) remain those of `dry_run` on `POST /api/v1/campaigns/{id}/send`.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "to": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 50000,
                    "description": "Optional. Restricts the send to these addresses — only those that are active subscribers of the audience, never an address on the suppression list. Omitted: the whole audience. Empty (`[]`): nobody, the send is refused."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The recipient count, and the per-address breakdown if `to` is given.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "audience"
                    },
                    "recipients": {
                      "type": "integer"
                    },
                    "audience": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/Audience"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "422": {
            "$ref": "#/components/responses/Refuse"
          }
        }
      }
    },
    "/api/v1/webhooks": {
      "get": {
        "operationId": "listWebhooks",
        "tags": ["Webhooks"],
        "summary": "Your subscriptions",
        "description": "Permission required: `webhooks:read`. Secrets do not appear in the list — read the subscription itself to get one.",
        "responses": {
          "200": {
            "description": "The workspace's subscriptions.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Liste"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Webhook"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          }
        }
      },
      "post": {
        "operationId": "createWebhook",
        "tags": ["Webhooks"],
        "summary": "Create a subscription",
        "description": "Permission required: `webhooks:write`. The response carries the `secret` — store it like a password, it is what verifies every call.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "422": {
            "$ref": "#/components/responses/Refuse"
          }
        }
      }
    },
    "/api/v1/webhooks/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The subscription's identifier.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "operationId": "getWebhook",
        "tags": ["Webhooks"],
        "summary": "One subscription, secret included",
        "description": "Permission required: `webhooks:read`. The secret is readable, unlike an API key: it gives access to nothing on our side, and a service being redeployed needs it to verify signatures.",
        "responses": {
          "200": {
            "description": "The subscription.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      },
      "patch": {
        "operationId": "updateWebhook",
        "tags": ["Webhooks"],
        "summary": "Update a subscription",
        "description": "Permission required: `webhooks:write`. Pass `enabled: false` to suspend without losing the log.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string",
                      "enum": [
                        "email.delivered",
                        "email.opened",
                        "email.clicked",
                        "email.bounced",
                        "email.complained",
                        "subscriber.created",
                        "subscriber.unsubscribed"
                      ]
                    }
                  },
                  "enabled": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          },
          "422": {
            "$ref": "#/components/responses/Refuse"
          }
        }
      },
      "delete": {
        "operationId": "deleteWebhook",
        "tags": ["Webhooks"],
        "summary": "Delete a subscription",
        "description": "Permission required: `webhooks:write`. The delivery log goes with it.",
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "webhook"
                    },
                    "id": {
                      "type": "string"
                    },
                    "deleted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      }
    },
    "/api/v1/webhooks/{id}/test": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The subscription's identifier.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "operationId": "testWebhook",
        "tags": ["Webhooks"],
        "summary": "Send a test event",
        "description": "Permission required: `webhooks:write`. Same signature, same headers, same log as a real event — and your service's response is AWAITED, so you know straight away. The body carries `\"data\": { \"test\": true }`.",
        "responses": {
          "200": {
            "description": "The test was attempted — read `status`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "webhook_test"
                    },
                    "delivery_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": ["delivered", "pending", "failed"]
                    },
                    "attempts": {
                      "type": "integer"
                    },
                    "response_status": {
                      "type": ["integer", "null"]
                    },
                    "error": {
                      "type": ["string", "null"]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      }
    },
    "/api/v1/webhooks/{id}/deliveries": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The subscription's identifier.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "operationId": "listWebhookDeliveries",
        "tags": ["Webhooks"],
        "summary": "The delivery log",
        "description": "Permission required: `webhooks:read`. This is what answers “I am not receiving anything”: you see the attempt, the status code your service returned and the start of its response.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "The most recent deliveries.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Liste"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/WebhookDelivery"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      }
    },
    "/api/v1/webhooks/{id}/deliveries/{deliveryId}/replay": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The subscription's identifier.",
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "deliveryId",
          "in": "path",
          "required": true,
          "description": "The identifier of the delivery to replay.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "operationId": "replayWebhookDelivery",
        "tags": ["Webhooks"],
        "summary": "Replay a delivery",
        "description": "Permission required: `webhooks:write`. The replayed body is the original one, byte for byte — so the same event `id`. If your service had in fact received it, it will recognize a duplicate.",
        "responses": {
          "200": {
            "description": "Replayed — read `status`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "webhook_delivery"
                    },
                    "id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": ["delivered", "pending", "failed"]
                    },
                    "attempts": {
                      "type": "integer"
                    },
                    "response_status": {
                      "type": ["integer", "null"]
                    },
                    "error": {
                      "type": ["string", "null"]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key for the workspace, created under Settings → API in your Mailcheer workspace. Shape: `mch_live_…`.\n\n`Authorization: Bearer mch_live_…`\n\nThe key carries permissions (`scopes`): every operation states which one it requires, and `GET /api/v1/me` states the ones yours carries. A key belongs to ONE workspace — there is no cross-workspace call."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "The same key, in a dedicated header. Strictly equivalent to the Bearer form: some enterprise clients and gateways do not let `Authorization` through. Do not send both."
      }
    },
    "parameters": {
      "Limit": {
        "name": "limit",
        "in": "query",
        "description": "Number of items per page, from 1 to 100.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 50
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "description": "The `next_cursor` from the previous response.",
        "schema": {
          "type": "string"
        }
      },
      "EmailPath": {
        "name": "email",
        "in": "path",
        "required": true,
        "description": "The address, URL-encoded.",
        "schema": {
          "type": "string",
          "format": "email"
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "description": "A unique value per send. Replaying the same call returns the same response, without a second send. The same key with a different body returns 409.",
        "schema": {
          "type": "string",
          "maxLength": 200
        }
      }
    },
    "schemas": {
      "EmailRequest": {
        "type": "object",
        "required": ["from", "to", "subject"],
        "description": "The body of `POST /api/v1/emails`.\n\n`from`, `to` and `subject` are required, and **at least one of `html` or `text`** — an empty body is refused with a 422.\n\nAttachments go in `attachments`, in Resend's format.\n\nUnknown keys are silently ignored: a body written for Resend passes through as is.",
        "properties": {
          "from": {
            "type": "string",
            "minLength": 3,
            "maxLength": 320,
            "description": "**Required.** An address on a domain verified in the workspace — otherwise 422 `unverified_from_domain`, which hands you the list of domains you can use. `GET /api/v1/me` gives it too, before you try. Both forms are accepted: the bare address, or “Name <address@domain.com>” so the name shows in the inbox.",
            "examples": [
              "Your brand <hello@yourdomain.com>",
              "hello@yourdomain.com"
            ]
          },
          "to": {
            "type": ["string", "array"],
            "description": "**Required.** One recipient, or an array of 1 to 50 recipients. Beyond 50, split into several calls — or go through a campaign. Each entry accepts the form “Name <address@domain.com>” as well as the bare address, and is 3 to 320 characters long.\n\n⚠️ **Recipients see one another**: they go out in the same message, not as blind copies. For each to receive their own, make one call per person.",
            "oneOf": [
              {
                "title": "A single recipient",
                "type": "string",
                "minLength": 3,
                "maxLength": 320,
                "examples": [
                  "customer@example.com",
                  "Marie Dupont <marie@example.com>"
                ]
              },
              {
                "title": "Several recipients",
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 3,
                  "maxLength": 320
                },
                "minItems": 1,
                "maxItems": 50,
                "examples": [["marie@example.com", "paul@example.com"]]
              }
            ],
            "examples": [
              "customer@example.com",
              ["marie@example.com", "paul@example.com"]
            ]
          },
          "subject": {
            "type": "string",
            "minLength": 1,
            "maxLength": 998,
            "description": "**Required.** The message's subject. The 998-character ceiling is the email standard's; in practice an inbox shows 40 to 60.",
            "examples": ["Your September invoice"]
          },
          "html": {
            "type": "string",
            "maxLength": 400000,
            "description": "The body in HTML. `html` or `text`: at least one of the two, both if you can — a message with no text version is marked down by filters.",
            "examples": ["<p>Here it is, attached to this message.</p>"]
          },
          "text": {
            "type": "string",
            "maxLength": 400000,
            "description": "The body in plain text, for clients that do not display HTML.",
            "examples": ["Here it is, attached to this message."]
          },
          "cc": {
            "type": ["string", "array"],
            "oneOf": [
              {
                "title": "One address",
                "type": "string",
                "minLength": 3,
                "maxLength": 320,
                "examples": ["accounting@example.com"]
              },
              {
                "title": "Several addresses",
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 3,
                  "maxLength": 320
                },
                "minItems": 1,
                "maxItems": 50,
                "examples": [
                  ["accounting@example.com", "management@example.com"]
                ]
              }
            ],
            "description": "Visible copy. One address or an array of 1 to 50. Recipients in copy see and are seen. They count against the quota and go through the same checks as `to` — suppression list included.",
            "examples": ["accounting@example.com"]
          },
          "bcc": {
            "type": ["string", "array"],
            "oneOf": [
              {
                "title": "One address",
                "type": "string",
                "minLength": 3,
                "maxLength": 320,
                "examples": ["archive@example.com"]
              },
              {
                "title": "Several addresses",
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 3,
                  "maxLength": 320
                },
                "minItems": 1,
                "maxItems": 50,
                "examples": [["archive@example.com", "copy@example.com"]]
              }
            ],
            "description": "Blind copy. One address or an array of 1 to 50. It appears in no header of the received message, including when the send carries attachments — it stays in the envelope. Same quota and same checks as `to`.",
            "examples": ["archive@example.com"]
          },
          "replyTo": {
            "type": ["string", "array"],
            "description": "The address recipients will reply to, when it differs from `from`. One, or an array of 1 to 10. Same form as `to`: the bare address or “Name <address>”.",
            "oneOf": [
              {
                "title": "One address",
                "type": "string",
                "minLength": 3,
                "maxLength": 320,
                "examples": ["support@yourdomain.com"]
              },
              {
                "title": "Several addresses",
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 3,
                  "maxLength": 320
                },
                "minItems": 1,
                "maxItems": 10,
                "examples": [
                  ["support@yourdomain.com", "accounting@yourdomain.com"]
                ]
              }
            ],
            "examples": ["support@yourdomain.com"]
          },
          "reply_to": {
            "type": ["string", "array"],
            "description": "Alias of `replyTo`, for clients coming from Resend. The address recipients will reply to, when it differs from `from`. One, or an array of 1 to 10. Same form as `to`: the bare address or “Name <address>”.\n\nFill in only one of the two.",
            "oneOf": [
              {
                "title": "One address",
                "type": "string",
                "minLength": 3,
                "maxLength": 320,
                "examples": ["support@yourdomain.com"]
              },
              {
                "title": "Several addresses",
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 3,
                  "maxLength": 320
                },
                "minItems": 1,
                "maxItems": 10,
                "examples": [
                  ["support@yourdomain.com", "accounting@yourdomain.com"]
                ]
              }
            ],
            "examples": ["support@yourdomain.com"]
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Free-form headers, as name → value pairs. The ones that carry the message's identity (From, To, Reply-To, List-Unsubscribe…) are ignored: they are ours to answer for.",
            "examples": [
              {
                "X-Entity-Ref-ID": "order-4192"
              }
            ]
          },
          "tags": {
            "type": ["object", "array"],
            "description": "Free-form tags for your own tracking; they come back in delivery notifications and on `GET /api/v1/emails/{id}`. Two ways to write them: an object of key → value, or an array of `{name, value}` as with Resend (10 entries at most in that second form).\n\nNames and values are **rewritten, not refused**: any character outside `A-Z a-z 0-9 _ -` becomes `_`, and beyond 256 characters the value is cut. An accent or a space therefore passes without error, but not unchanged.",
            "oneOf": [
              {
                "title": "Key/value object",
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "examples": [
                  {
                    "type": "order",
                    "channel": "shop"
                  }
                ]
              },
              {
                "title": "Array of {name, value} (Resend form)",
                "type": "array",
                "items": {
                  "type": "object",
                  "required": ["name", "value"],
                  "properties": {
                    "name": {
                      "type": "string",
                      "examples": ["type"]
                    },
                    "value": {
                      "type": "string",
                      "examples": ["order"]
                    }
                  }
                },
                "maxItems": 10,
                "examples": [
                  [
                    {
                      "name": "type",
                      "value": "order"
                    }
                  ]
                ]
              }
            ],
            "examples": [
              {
                "type": "order"
              }
            ]
          },
          "unsubscribe_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "The one-click unsubscribe address. Mailcheer then sets `List-Unsubscribe` AND `List-Unsubscribe-Post` — the two always go together, and without the second Gmail shows no button. Gmail and Yahoo have expected this header since February 2024 on any send to people who did not write to you first; without it, the only way out you offer is the Spam button, and it is your sending domain's reputation that pays. Must be https. The header itself is still rejected inside `headers`: Mailcheer writes it, you only provide the target.",
            "example": "https://yourdomain.com/unsubscribe/abc123"
          },
          "attachments": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["filename", "content"],
              "properties": {
                "filename": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 255,
                  "description": "The file name as it will appear in the inbox. Line breaks and path separators are stripped from it.",
                  "examples": ["quote-2026-09.pdf"]
                },
                "content": {
                  "type": "string",
                  "description": "The file, encoded in **base64**. That is the only accepted form.",
                  "examples": ["JVBERi0xLjQKJcfsj6IK…"]
                },
                "content_type": {
                  "type": "string",
                  "maxLength": 127,
                  "description": "The MIME type. Optional: inferred from the extension when missing (`.pdf` → `application/pdf`), falling back to `application/octet-stream`.",
                  "examples": ["application/pdf"]
                },
                "contentType": {
                  "type": "string",
                  "maxLength": 127,
                  "description": "Alias of `content_type`."
                }
              }
            },
            "maxItems": 20,
            "description": "Attachments, in Resend's format: `filename` + `content` in base64. Twenty at most.\n\n**The limit is Amazon SES's: 40 MB per message once encoded**, which is roughly 30 MB of actual files (base64 adds a third). Beyond that the call is refused with a 422 naming the file, its size and the size reached — no message goes out without its attachment.\n\nRejected, and always out loud: extensions inbox providers reject (`.exe`, `.bat`, `.js`, `.vbs`…), a `content` that is not valid base64, and Resend's `path` field — our server will not fetch a file from an address you choose; encode it.\n\nWithout an attachment, nothing changes in the way the message goes out.",
            "examples": [
              [
                {
                  "filename": "quote-2026-09.pdf",
                  "content": "JVBERi0xLjQKJcfsj6IK…",
                  "content_type": "application/pdf"
                }
              ]
            ]
          }
        }
      },
      "Email": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The identifier to pass back to `GET /api/v1/emails/{id}` to learn about delivery."
          },
          "object": {
            "type": "string",
            "const": "email"
          },
          "from": {
            "type": "string"
          },
          "to": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Always an array in the response, even if you passed a single address."
          },
          "subject": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EmailDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Email"
          },
          {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "queued",
                  "sent",
                  "delivered",
                  "bounced",
                  "complained",
                  "failed"
                ],
                "description": "`queued` on acceptance, then `sent`, `delivered`, `bounced`, `complained` or `failed` as our provider reports back. Expect a few seconds before `delivered`."
              },
              "error": {
                "type": ["string", "null"],
                "description": "The reason for the refusal, when `status` is `bounced`, `complained` or `failed`. `null` otherwise."
              },
              "tags": {
                "type": ["object", "null"],
                "description": "The send's tags, after forbidden characters were rewritten.",
                "additionalProperties": {
                  "type": "string"
                }
              },
              "sent_at": {
                "type": ["string", "null"],
                "format": "date-time"
              },
              "delivered_at": {
                "type": ["string", "null"],
                "format": "date-time"
              }
            }
          }
        ]
      },
      "SubscriberRequest": {
        "type": "object",
        "required": ["email"],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 320,
            "description": "**Required.** Lower-cased on save.",
            "examples": ["marie@example.com"]
          },
          "firstName": {
            "type": "string",
            "maxLength": 120,
            "examples": ["Marie"]
          },
          "lastName": {
            "type": "string",
            "maxLength": 120,
            "examples": ["Dupont"]
          },
          "prenom": {
            "type": "string",
            "maxLength": 120,
            "examples": ["Marie"],
            "description": "Alias of `firstName`. Fill in only one of the two."
          },
          "nom": {
            "type": "string",
            "maxLength": 120,
            "examples": ["Dupont"],
            "description": "Alias of `lastName`. Fill in only one of the two."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 60
            },
            "description": "Workspace tags. The ones that do not exist yet are created.",
            "maxItems": 20,
            "examples": [["customers", "paris-store"]]
          },
          "fields": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "maxLength": 500
            },
            "description": "Custom fields, by their technical key. Unknown keys create nothing: they come back in `ignored_fields`.",
            "examples": [
              {
                "city": "Ajaccio"
              }
            ]
          },
          "double_opt_in": {
            "type": "boolean",
            "default": true,
            "description": "`false` subscribes straight away, with no confirmation email. The caller then bears responsibility for the consent.",
            "examples": [true]
          }
        }
      },
      "Subscriber": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "type": "string",
            "const": "subscriber"
          },
          "email": {
            "type": "string"
          },
          "first_name": {
            "type": ["string", "null"]
          },
          "last_name": {
            "type": ["string", "null"]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "subscribed",
              "unsubscribed",
              "bounced",
              "complained"
            ]
          },
          "source": {
            "type": ["string", "null"]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "fields": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "confirmed_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "unsubscribed_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SubscriberResult": {
        "type": "object",
        "properties": {
          "subscriber": {
            "$ref": "#/components/schemas/Subscriber"
          },
          "updated": {
            "type": "boolean",
            "description": "`true` if the subscriber already existed."
          },
          "confirmation_sent": {
            "type": "boolean",
            "description": "`true` if a confirmation email actually went out."
          },
          "ignored_fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Keys of `fields` unknown to this workspace, written nowhere."
          }
        }
      },
      "Suppression": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "const": "suppression"
          },
          "email": {
            "type": "string"
          },
          "reason": {
            "type": "string",
            "enum": ["unsubscribe", "bounce", "complaint", "manual"]
          },
          "scope": {
            "type": "string",
            "enum": ["organization", "platform"]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CampaignRequest": {
        "type": "object",
        "required": ["name"],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 160,
            "description": "**Required.** Internal name, never seen by recipients.",
            "examples": ["September newsletter"]
          },
          "subject": {
            "type": "string",
            "maxLength": 400,
            "description": "The subject. It may stay empty on a draft, but the send will refuse it.",
            "examples": ["What we changed this month"]
          },
          "preheader": {
            "type": "string",
            "maxLength": 400,
            "description": "Preview shown after the subject in the inbox.",
            "examples": ["Three new things, and one we owed you."]
          },
          "kind": {
            "type": "string",
            "maxLength": 40,
            "default": "newsletter",
            "description": "Your own classification label. Free text, no imposed value.",
            "examples": ["newsletter"]
          },
          "from": {
            "type": "string",
            "maxLength": 320,
            "description": "The address of a sender **already registered** in the workspace — not any address on a verified domain, unlike an individual send. Unknown: 404, with the list of available addresses. `GET /api/v1/me` gives them in `senders`.",
            "examples": ["hello@yourdomain.com"]
          },
          "sender_id": {
            "type": "string",
            "maxLength": 60,
            "description": "The sender's identifier, if you know it (`senders[].id` from `GET /api/v1/me`). Takes precedence over `from`.\n\nWith neither `from` nor `sender_id`, Mailcheer takes the workspace's default sender; failing that, if a single sender exists, that one. If several exist with no default, the campaign is created **without a sender** and it is the send that will fail: designate one.",
            "examples": ["snd_71a"]
          },
          "text": {
            "type": "string",
            "maxLength": 200000,
            "description": "The content in plain text — the form to prefer from code or from an agent. A blank line separates two paragraphs, `# ` at the start of a line makes a heading, `## ` a subheading. Ignored if `content` is provided.",
            "examples": [
              "# Three new things\n\nHere is what changed this month.\n\nEnjoy the read."
            ]
          },
          "content": {
            "type": "object",
            "description": "The content in blocks, the editor's full form. Write this only if you are reusing the content of an existing campaign: to compose, `text` does the same job without your having to fabricate block identifiers.",
            "properties": {
              "preheader": {
                "type": "string"
              },
              "blocks": {
                "type": "array",
                "description": "Editor blocks, each with its `id` and its `type` (`heading`, `text`, …).",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        },
        "description": "The body of `POST /api/v1/campaigns`. Only `name` is required — but a draft with no subject and no content cannot be sent. Give the content through `text` (plain) or through `content` (blocks), never both."
      },
      "Campaign": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "type": "string",
            "const": "campaign"
          },
          "name": {
            "type": "string"
          },
          "subject": {
            "type": ["string", "null"]
          },
          "kind": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": ["draft", "scheduled", "sending", "sent", "archived"]
          },
          "from": {
            "type": ["string", "null"]
          },
          "scheduled_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "sent_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "schedule_error": {
            "type": ["string", "null"],
            "description": "Why a scheduled send did not go out."
          },
          "stats": {
            "type": "object",
            "properties": {
              "recipients": {
                "type": "integer"
              },
              "sent": {
                "type": "integer"
              },
              "delivered": {
                "type": "integer"
              },
              "opened": {
                "type": "integer"
              },
              "clicked": {
                "type": "integer"
              },
              "bounced": {
                "type": "integer"
              },
              "complained": {
                "type": "integer"
              },
              "open_rate": {
                "type": ["number", "null"]
              },
              "click_rate": {
                "type": ["number", "null"]
              }
            }
          }
        }
      },
      "CampaignStats": {
        "type": "object",
        "properties": {
          "campaign": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "subject": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "draft",
                  "scheduled",
                  "sending",
                  "testing",
                  "sent",
                  "archived"
                ]
              },
              "kind": {
                "type": "string"
              },
              "fromName": {
                "type": "string"
              },
              "fromEmail": {
                "type": "string"
              },
              "sentAt": {
                "type": ["string", "null"],
                "format": "date-time"
              },
              "scheduledAt": {
                "type": ["string", "null"],
                "format": "date-time"
              },
              "updatedAt": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "counts": {
            "type": "object",
            "properties": {
              "recipients": {
                "type": "integer"
              },
              "delivered": {
                "type": "integer"
              },
              "opened": {
                "type": "integer"
              },
              "clicked": {
                "type": "integer"
              },
              "bounced": {
                "type": "integer"
              },
              "complained": {
                "type": "integer"
              },
              "unsubscribed": {
                "type": "integer"
              }
            }
          },
          "rates": {
            "type": "object",
            "description": "Shares between 0 and 1; `null` as long as the denominator is empty.",
            "properties": {
              "delivered": {
                "type": ["number", "null"],
                "minimum": 0,
                "maximum": 1
              },
              "opened": {
                "type": ["number", "null"],
                "minimum": 0,
                "maximum": 1
              },
              "clicked": {
                "type": ["number", "null"],
                "minimum": 0,
                "maximum": 1
              },
              "bounced": {
                "type": ["number", "null"],
                "minimum": 0,
                "maximum": 1
              }
            }
          },
          "timeline": {
            "type": "array",
            "description": "Eight six-hour windows from the send, `+0h` to `+42h`. Empty if the campaign has not gone out.",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": "string",
                  "example": "+6h"
                },
                "opened": {
                  "type": "integer"
                },
                "clicked": {
                  "type": "integer"
                }
              }
            }
          },
          "audience": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer",
                "description": "Opens whose device is known."
              },
              "proxiedShare": {
                "type": ["number", "null"],
                "minimum": 0,
                "maximum": 1,
                "description": "Share of opens coming from a privacy relay (Apple Mail, Gmail): received, not necessarily read."
              },
              "device": {
                "type": "array",
                "description": "Five rows at most, from the most to the least frequent.",
                "items": {
                  "type": "object",
                  "properties": {
                    "label": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "share": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    }
                  }
                }
              },
              "os": {
                "type": "array",
                "description": "Five rows at most, from the most to the least frequent.",
                "items": {
                  "type": "object",
                  "properties": {
                    "label": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "share": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    }
                  }
                }
              },
              "client": {
                "type": "array",
                "description": "Five rows at most, from the most to the least frequent.",
                "items": {
                  "type": "object",
                  "properties": {
                    "label": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "share": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    }
                  }
                }
              }
            }
          },
          "links": {
            "type": "array",
            "description": "From most to least clicked.",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string"
                },
                "clicks": {
                  "type": "integer"
                }
              }
            }
          },
          "html": {
            "type": "string",
            "description": "The HTML as rendered at send time; empty string as long as it does not exist."
          }
        }
      },
      "Audience": {
        "type": "object",
        "description": "The breakdown of a send restricted by `to`. The count always adds up: `requested = duplicates + retained + excluded`.",
        "properties": {
          "requested": {
            "type": "integer",
            "description": "Entries received in `to`, as sent."
          },
          "duplicates": {
            "type": "integer",
            "description": "Entries repeating an address already seen (case and spaces ignored)."
          },
          "retained": {
            "type": "integer",
            "description": "Addresses that receive (or would receive) the campaign."
          },
          "excluded": {
            "type": "integer",
            "description": "Excluded addresses."
          },
          "reasons": {
            "type": "object",
            "description": "Excluded addresses per reason; every reason is present, even at zero.",
            "properties": {
              "invalid": {
                "type": "integer",
                "description": "Not an email address."
              },
              "not_in_list": {
                "type": "integer",
                "description": "No subscriber of the workspace has this address."
              },
              "pending": {
                "type": "integer",
                "description": "Sign-up not confirmed yet (double opt-in)."
              },
              "unsubscribed": {
                "type": "integer"
              },
              "bounced": {
                "type": "integer"
              },
              "complained": {
                "type": "integer"
              },
              "suppressed": {
                "type": "integer",
                "description": "Active subscriber, but the address is on the suppression list."
              },
              "outside_segment": {
                "type": "integer",
                "description": "Active subscriber, outside the segment the campaign targets."
              }
            }
          },
          "excluded_addresses": {
            "type": "array",
            "description": "Every excluded address, with its reason.",
            "items": {
              "type": "object",
              "properties": {
                "email": {
                  "type": "string"
                },
                "reason": {
                  "type": "string",
                  "enum": [
                    "invalid",
                    "not_in_list",
                    "pending",
                    "unsubscribed",
                    "bounced",
                    "complained",
                    "suppressed",
                    "outside_segment"
                  ]
                }
              }
            }
          }
        }
      },
      "Account": {
        "type": "object",
        "description": "What the key gives access to, and what is left to use. The first call to make: it gives the verified domains and the registered senders — that is, the only addresses your sends will pass with.",
        "properties": {
          "object": {
            "type": "string",
            "const": "account"
          },
          "organization": {
            "type": "object",
            "description": "The workspace the key belongs to. `null` if the workspace has been deleted.",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "slug": {
                "type": "string"
              }
            }
          },
          "key": {
            "type": "object",
            "description": "The key used for this call. Its secret is never returned.",
            "properties": {
              "name": {
                "type": "string",
                "description": "The name you gave it."
              },
              "scopes": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "emails:send",
                    "subscribers:read",
                    "subscribers:write",
                    "campaigns:read",
                    "campaigns:write"
                  ]
                },
                "description": "The permissions of THIS key. A call outside this list returns 403."
              }
            }
          },
          "plan": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "emails_per_month": {
                "type": ["integer", "null"],
                "description": "`null` = unlimited."
              }
            }
          },
          "usage": {
            "type": "object",
            "properties": {
              "period": {
                "type": "string",
                "description": "The current month, `YYYY-MM`.",
                "examples": ["2026-09"]
              },
              "emails_sent": {
                "type": "integer",
                "description": "This month's sends, API and campaigns together."
              },
              "emails_remaining": {
                "type": ["integer", "null"],
                "description": "`null` = unlimited. At 0, sends return 402."
              }
            }
          },
          "subscribers": {
            "type": "integer",
            "description": "Subscribers with status `subscribed` — the ones a campaign would reach."
          },
          "sending_domains": {
            "type": "array",
            "description": "The workspace's domains. Only those with `verified: true` can serve as `from`.",
            "items": {
              "type": "object",
              "properties": {
                "domain": {
                  "type": "string",
                  "examples": ["yourdomain.com"]
                },
                "verified": {
                  "type": "boolean"
                }
              }
            }
          },
          "senders": {
            "type": "array",
            "description": "The registered senders. A CAMPAIGN can only go out from one of them; an individual email, on the other hand, accepts any address on a verified domain.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "To pass as `sender_id` when creating a campaign."
                },
                "from": {
                  "type": "string",
                  "format": "email"
                },
                "name": {
                  "type": ["string", "null"],
                  "description": "The name shown in the inbox."
                },
                "default": {
                  "type": "boolean",
                  "description": "The one a campaign takes if you designate none."
                }
              }
            }
          }
        },
        "examples": [
          {
            "object": "account",
            "organization": {
              "id": "org_3f9",
              "name": "Your brand",
              "slug": "your-brand"
            },
            "key": {
              "name": "Production",
              "scopes": ["emails:send", "subscribers:read"]
            },
            "plan": {
              "id": "pro",
              "name": "Pro",
              "emails_per_month": 50000
            },
            "usage": {
              "period": "2026-09",
              "emails_sent": 1240,
              "emails_remaining": 48760
            },
            "subscribers": 3182,
            "sending_domains": [
              {
                "domain": "yourdomain.com",
                "verified": true
              }
            ],
            "senders": [
              {
                "id": "snd_71a",
                "from": "hello@yourdomain.com",
                "name": "Your brand",
                "default": true
              }
            ]
          }
        ]
      },
      "Liste": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "has_more": {
            "type": "boolean",
            "description": "One more page after this one."
          },
          "next_cursor": {
            "type": ["string", "null"],
            "description": "To pass back as `?cursor=` for the next page. `null` on the last page."
          }
        },
        "description": "The envelope of every list. To walk through it: as long as `has_more` is `true`, call the same address again with `?cursor=<next_cursor>`. The order is stable; a page returns at most `limit` items (50 by default, 100 at most)."
      },
      "Erreur": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "missing_api_key",
                  "invalid_api_key",
                  "revoked_api_key",
                  "insufficient_scope",
                  "rate_limit_exceeded",
                  "validation_error",
                  "unverified_from_domain",
                  "suppressed_recipient",
                  "quota_exceeded",
                  "reputation_blocked",
                  "not_found",
                  "conflict",
                  "idempotency_key_reused",
                  "send_failed",
                  "internal_error"
                ],
                "description": "Write your code against this field, never against `message`."
              },
              "message": {
                "type": "string",
                "description": "Written to be read by a human; may be rephrased without notice."
              },
              "details": {
                "type": "object",
                "description": "What you need to fix things without guessing: `issues` (the offending field and why) on a validation error, `verifiedDomains` on a refused domain, `available` on an unknown sender.",
                "additionalProperties": true
              }
            }
          },
          "statusCode": {
            "type": "integer",
            "examples": [422]
          },
          "message": {
            "type": "string",
            "description": "The same text as `error.message` — written for a human."
          },
          "name": {
            "type": "string",
            "enum": [
              "missing_api_key",
              "invalid_api_key",
              "revoked_api_key",
              "insufficient_scope",
              "rate_limit_exceeded",
              "validation_error",
              "unverified_from_domain",
              "suppressed_recipient",
              "quota_exceeded",
              "reputation_blocked",
              "not_found",
              "conflict",
              "idempotency_key_reused",
              "send_failed",
              "internal_error"
            ],
            "description": "The same value as `error.code`. Write your logic against it."
          }
        },
        "description": "Two readings of the same content. `error` is Mailcheer's shape, structured. `statusCode`, `message` and `name` follow Resend's shape, so that code written against that API shows a correct message without being rewritten. `name` carries the same value as `error.code`, and `message` the same text as `error.message`.",
        "examples": [
          {
            "error": {
              "code": "unverified_from_domain",
              "message": "Domain “example.com” is not verified in this workspace. Verified domains: yourdomain.com.",
              "details": {
                "from": "hello@example.com",
                "verifiedDomains": ["yourdomain.com"]
              }
            },
            "statusCode": 422,
            "message": "Domain “example.com” is not verified in this workspace. Verified domains: yourdomain.com.",
            "name": "unverified_from_domain"
          }
        ]
      },
      "Carte": {
        "type": "object",
        "description": "The API's table of contents, readable without a key.",
        "properties": {
          "name": {
            "type": "string",
            "examples": ["Mailcheer API"]
          },
          "version": {
            "type": "string",
            "examples": ["1"]
          },
          "documentation": {
            "type": "string",
            "format": "uri"
          },
          "openapi": {
            "type": "string",
            "format": "uri",
            "description": "This document."
          },
          "mcp": {
            "type": "string",
            "format": "uri",
            "description": "The MCP server's address."
          },
          "authentication": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string"
              },
              "header": {
                "type": "string"
              },
              "where": {
                "type": "string"
              }
            }
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "emails:send",
                "subscribers:read",
                "subscribers:write",
                "campaigns:read",
                "campaigns:write"
              ]
            },
            "description": "Every permission a key can carry."
          },
          "rate_limit": {
            "type": "string",
            "examples": ["600 requests per minute per key"]
          },
          "endpoints": {
            "type": "array",
            "description": "Each endpoint and the permission it requires.",
            "items": {
              "type": "object",
              "properties": {
                "method": {
                  "type": "string"
                },
                "path": {
                  "type": "string"
                },
                "scope": {
                  "type": "string",
                  "description": "`—` if no particular permission is required."
                }
              }
            }
          }
        }
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "const": "webhook"
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "The name you give it."
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "email.delivered",
                "email.opened",
                "email.clicked",
                "email.bounced",
                "email.complained",
                "subscriber.created",
                "subscriber.unsubscribed"
              ]
            }
          },
          "enabled": {
            "type": "boolean"
          },
          "secret": {
            "type": "string",
            "description": "The signing secret (`whsec_…`). Returned on creation, when reading one specific subscription, and after a secret rotation — **never in the list**, which we log in full without a second thought."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookRequest": {
        "type": "object",
        "required": ["name", "url", "events"],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 80,
            "example": "My CRM"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "`https` required, and never a private-network address.",
            "example": "https://your-service.com/mailcheer/events"
          },
          "events": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string",
              "enum": [
                "email.delivered",
                "email.opened",
                "email.clicked",
                "email.bounced",
                "email.complained",
                "subscriber.created",
                "subscriber.unsubscribed"
              ]
            }
          }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "const": "webhook_delivery"
          },
          "id": {
            "type": "string"
          },
          "webhook_id": {
            "type": "string"
          },
          "event_id": {
            "type": "string",
            "description": "Stable across attempts: it is the deduplication key."
          },
          "event_type": {
            "type": "string",
            "enum": [
              "email.delivered",
              "email.opened",
              "email.clicked",
              "email.bounced",
              "email.complained",
              "subscriber.created",
              "subscriber.unsubscribed"
            ]
          },
          "status": {
            "type": "string",
            "enum": ["pending", "delivered", "failed"]
          },
          "attempts": {
            "type": "integer"
          },
          "response_status": {
            "type": ["integer", "null"],
            "description": "The HTTP status code your service returned."
          },
          "response_body": {
            "type": ["string", "null"],
            "description": "The first 500 characters of its response."
          },
          "error": {
            "type": ["string", "null"]
          },
          "next_attempt_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "delivered_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    },
    "responses": {
      "NonAuthentifie": {
        "description": "Key missing, unknown or revoked.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      },
      "Interdit": {
        "description": "The key does not have the requested permission, or the workspace's sends are suspended.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      },
      "Introuvable": {
        "description": "The object does not exist in this workspace.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      },
      "Conflit": {
        "description": "Incompatible state, or idempotency key reused with a different body.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      },
      "Refuse": {
        "description": "Malformed field, unverified domain, or recipient on the suppression list.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      },
      "QuotaAtteint": {
        "description": "The plan's monthly quota has been reached.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      },
      "TropDAppels": {
        "description": "More than 600 requests per minute for this key.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Limit": {
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      },
      "EnvoiEchoue": {
        "description": "Our sending provider refused the message.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "The written documentation, with per-language examples.",
    "url": "https://mailcheer.com/en/docs/api"
  }
}
