{
  "openapi": "3.1.0",
  "info": {
    "title": "LuneaMail API",
    "version": "1",
    "description": "Send transactional email from templates, manage contacts and lists, and receive signed webhooks. Create an API key in Settings › API keys and send it as a Bearer token."
  },
  "servers": [
    {
      "url": "https://app.luneamail.com/api/v1"
    }
  ],
  "externalDocs": {
    "description": "API reference",
    "url": "https://luneamail.com/docs"
  },
  "security": [
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/send": {
      "post": {
        "operationId": "send",
        "summary": "Send an email",
        "description": "Sends one transactional email (a receipt, license key, password reset or alert) from a template, filled with your data. It needs no contact and doesn't add one.\n\ntemplateId is the id in the template's editor address: app.luneamail.com/templates/<templateId>.\n\nThe subject and body take the same {{variables}}. A variable used outside {{#if}} or {{#each}} is required: leave it out and the email isn't sent (MISSING_VARIABLE), so nobody gets a broken one.\n\nThe email goes out from your brand's sender name and address. Until that address's domain is verified, it's sent from LuneaMail's address with your name, and replies go to yours.\n\nUnsubscribing from marketing doesn't stop transactional email. Addresses that bounced, complained or were dropped are refused (SUPPRESSED).\n\nEach email counts toward the plan's monthly emails, like campaign email. On Pro and Team, email sent through your own SES or SMTP server doesn't.\n\nOn the Free plan the email carries a small \"Made with LuneaMail\" line.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "templateId": {
                    "type": "string",
                    "description": "The template to send."
                  },
                  "to": {
                    "type": "string",
                    "description": "One recipient email address."
                  },
                  "data": {
                    "type": "object",
                    "description": "Values for the template's {{variables}}. Any JSON: strings, numbers, booleans, arrays for {{#each}}, nested objects."
                  },
                  "subject": {
                    "type": "string",
                    "description": "Subject line, with {{variables}}. Defaults to the template's name."
                  },
                  "attachments": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "object",
                      "properties": {
                        "filename": {
                          "type": "string",
                          "maxLength": 150,
                          "description": "A plain file name, no / or \\."
                        },
                        "content": {
                          "type": "string",
                          "contentEncoding": "base64",
                          "description": "The file, base64-encoded, without a data: prefix."
                        },
                        "contentType": {
                          "type": "string",
                          "description": "MIME type. Guessed from the file name when left out."
                        }
                      },
                      "required": [
                        "filename",
                        "content"
                      ]
                    },
                    "description": "Up to 10 files, 3 MB in total after decoding. Each is { filename, content, contentType? }: content is bare base64 (no data: prefix), contentType is guessed from the file name when left out. Executables, scripts, HTML and SVG are refused."
                  }
                },
                "required": [
                  "templateId",
                  "to"
                ]
              },
              "example": {
                "templateId": "cm3p0y...",
                "to": "buyer@company.com",
                "subject": "Your {{product}} license",
                "data": {
                  "firstName": "Ana",
                  "product": "Pro",
                  "licenseKey": "ABCD-1234-EFGH",
                  "seats": 5
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "messageId": "msg_...",
                  "status": "queued"
                }
              }
            }
          },
          "400": {
            "description": "BAD_REQUEST: The body isn't valid JSON.\nINVALID_TEMPLATE: templateId is missing, isn't in this workspace, or the template is empty.\nINVALID_REQUEST: to isn't one valid email address, or subject isn't a string.\nVALIDATION_ERROR: data isn't a plain object.\nMISSING_VARIABLE: The template uses a variable the data doesn't have.\nINVALID_SUBJECT: The subject has a template syntax error.\nINVALID_ATTACHMENT: An attachment is malformed: no filename, bad base64, a data: prefix, a / in the name.\nTOO_MANY_ATTACHMENTS: More than 10 attachments.\nATTACHMENTS_TOO_LARGE: Attachments total more than 3 MB decoded.\nATTACHMENT_BLOCKED: An executable, script, HTML or SVG file, by extension or contentType.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "UNAUTHORIZED: The Authorization header is missing, malformed or holds an unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "OWNER_UNVERIFIED: The workspace owner hasn't confirmed their email address yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "CONTENT_BLOCKED: The content screen flagged the email as abusive (phishing, spoofing). The message says why.\nSUPPRESSED: The recipient bounced, complained or was dropped before.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "QUOTA_EXCEEDED: The plan's monthly emails are used up.\nRATE_LIMITED: Over the per-key rate limit. Wait for the Retry-After header's seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL_ERROR: Something failed on our side. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "SEND_FAILED: The email provider or your own server refused the email. The message has its reason.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/contacts": {
      "post": {
        "operationId": "createContact",
        "summary": "Add or update a contact",
        "description": "Adds a contact for marketing email, or updates one with the same email. Lists are added to, never removed from, and attributes merge into the ones already saved.\n\nA new contact needs consent: { confirmed: true }, your statement that the person agreed to receive your marketing. Updating an existing contact doesn't.\n\nSomeone who unsubscribed, bounced or complained is added unsubscribed and stays that way.\n\nThe plan's contact limit applies to new contacts (CONTACT_LIMIT).\n\nA new subscribed contact sends the contact.subscribed webhook.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "The contact's email address. Stored lowercase."
                  },
                  "firstName": {
                    "type": "string",
                    "description": "Up to 100 characters."
                  },
                  "lastName": {
                    "type": "string",
                    "description": "Up to 100 characters."
                  },
                  "attributes": {
                    "type": "object",
                    "description": "Up to 50 custom fields for personalisation and segments. Names start with a letter, then letters, digits, _ or -, up to 50 characters. Values: string (up to 1,000 characters), number, boolean or null."
                  },
                  "lists": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Up to 20 list ids to add the contact to. GET /lists has them."
                  },
                  "consent": {
                    "type": "object",
                    "description": "{ confirmed: true, source?, ip? }. Required for a new contact. source (up to 200 characters) and ip are kept as proof of consent."
                  }
                },
                "required": [
                  "email"
                ]
              },
              "example": {
                "email": "ana@company.com",
                "firstName": "Ana",
                "lastName": "Silva",
                "attributes": {
                  "plan": "pro",
                  "seats": 5
                },
                "lists": [
                  "cl8q1v..."
                ],
                "consent": {
                  "confirmed": true,
                  "source": "Signup form on company.com"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "created": true,
                  "contact": {
                    "id": "cm4x2k9...",
                    "email": "ana@company.com",
                    "firstName": "Ana",
                    "lastName": "Silva",
                    "subscribed": true,
                    "attributes": {
                      "plan": "pro",
                      "seats": 5
                    },
                    "lists": [
                      "cl8q1v..."
                    ],
                    "createdAt": "2026-11-07T09:14:02.511Z",
                    "updatedAt": "2026-11-07T09:14:02.511Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: A field is missing or invalid. The message names it.\nCONSENT_REQUIRED: A new contact without consent.confirmed: true.\nINVALID_LIST: A list id isn't in this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "UNAUTHORIZED: The Authorization header is missing, malformed or holds an unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "CONTACT_LIMIT: The plan's contact limit is reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: Over the per-key rate limit. Wait for the Retry-After header's seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL_ERROR: Something failed on our side. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "getContact",
        "summary": "Look up a contact",
        "description": "Returns one contact by email, with its attributes and list ids.",
        "parameters": [
          {
            "name": "email",
            "in": "query",
            "required": true,
            "description": "The contact's email address.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "contact": {
                    "id": "cm4x2k9...",
                    "email": "ana@company.com",
                    "firstName": "Ana",
                    "lastName": "Silva",
                    "subscribed": true,
                    "attributes": {
                      "plan": "pro",
                      "seats": 5
                    },
                    "lists": [
                      "cl8q1v..."
                    ],
                    "createdAt": "2026-11-07T09:14:02.511Z",
                    "updatedAt": "2026-11-07T09:14:02.511Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: No ?email= given.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "UNAUTHORIZED: The Authorization header is missing, malformed or holds an unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "NOT_FOUND: No contact with that email.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: Over the per-key rate limit. Wait for the Retry-After header's seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL_ERROR: Something failed on our side. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteContact",
        "summary": "Erase a contact",
        "description": "Erases a contact and its events, for a GDPR or CCPA deletion request. An unsubscribe or bounce record for the address is kept, so it's never emailed again.",
        "parameters": [
          {
            "name": "email",
            "in": "query",
            "required": true,
            "description": "The contact's email address.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "erased": true,
                  "note": "Any unsubscribe or bounce record for this address is kept, so it is never emailed again."
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: No ?email= given.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "UNAUTHORIZED: The Authorization header is missing, malformed or holds an unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "NOT_FOUND: No contact with that email.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: Over the per-key rate limit. Wait for the Retry-After header's seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL_ERROR: Something failed on our side. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/contacts/unsubscribe": {
      "post": {
        "operationId": "unsubscribe",
        "summary": "Unsubscribe an address",
        "description": "Opts an address out of all marketing email, for example when someone unsubscribes in your own app. It works for addresses that aren't contacts yet, so they can't be added later.\n\nA contact that was subscribed sends the contact.unsubscribed webhook. Transactional email (POST /send) still reaches them.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "The address to opt out."
                  }
                },
                "required": [
                  "email"
                ]
              },
              "example": {
                "email": "ana@company.com"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "unsubscribed": true,
                  "email": "ana@company.com",
                  "contact": true
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: The body isn't { email } with a valid address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "UNAUTHORIZED: The Authorization header is missing, malformed or holds an unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: Over the per-key rate limit. Wait for the Retry-After header's seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL_ERROR: Something failed on our side. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/lists": {
      "get": {
        "operationId": "lists",
        "summary": "List your lists",
        "description": "Returns the workspace's lists with their member counts, sorted by name. Use the ids with POST /contacts.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "lists": [
                    {
                      "id": "cl8q1v...",
                      "name": "Newsletter",
                      "members": 1240
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "UNAUTHORIZED: The Authorization header is missing, malformed or holds an unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: Over the per-key rate limit. Wait for the Retry-After header's seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL_ERROR: Something failed on our side. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "contact.subscribed": {
      "post": {
        "operationId": "contactSubscribed",
        "summary": "contact.subscribed",
        "description": "A new subscribed contact was added (in the app, by the API or an agent), or someone opted back in from the preference page.",
        "parameters": [
          {
            "name": "X-LuneaMail-Event",
            "in": "header",
            "required": true,
            "description": "The event type, such as contact.unsubscribed.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Delivery",
            "in": "header",
            "required": true,
            "description": "This delivery's id. The same on every retry, so you can skip duplicates.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Timestamp",
            "in": "header",
            "required": true,
            "description": "When this attempt was signed, in seconds since the epoch.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Signature",
            "in": "header",
            "required": true,
            "description": "v1=<hex HMAC-SHA256 of \"<timestamp>.<raw body>\" with the endpoint's secret>.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "The event id (evt_...)."
                  },
                  "type": {
                    "type": "string",
                    "const": "contact.subscribed"
                  },
                  "createdAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "workspaceId": {
                    "type": "string"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "contactId": {
                        "type": "string",
                        "description": "The contact."
                      },
                      "email": {
                        "type": "string",
                        "description": "The address."
                      },
                      "firstName": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "When known."
                      },
                      "lastName": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "When known."
                      },
                      "source": {
                        "type": "string",
                        "description": "app, api, agent or preference-page."
                      }
                    },
                    "required": []
                  }
                },
                "required": [
                  "id",
                  "type",
                  "createdAt",
                  "workspaceId",
                  "data"
                ]
              },
              "example": {
                "id": "evt_9f2c...",
                "type": "contact.subscribed",
                "createdAt": "2026-11-07T09:14:02.511Z",
                "workspaceId": "cw1a...",
                "data": {
                  "contactId": "cm4x2k9...",
                  "email": "ana@company.com",
                  "firstName": "Ana",
                  "lastName": "Silva",
                  "source": "api"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Any 2xx within 10 seconds marks the delivery as received."
          }
        }
      }
    },
    "contact.unsubscribed": {
      "post": {
        "operationId": "contactUnsubscribed",
        "summary": "contact.unsubscribed",
        "description": "Someone unsubscribed: from an email, the preference page, the API or the app.",
        "parameters": [
          {
            "name": "X-LuneaMail-Event",
            "in": "header",
            "required": true,
            "description": "The event type, such as contact.unsubscribed.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Delivery",
            "in": "header",
            "required": true,
            "description": "This delivery's id. The same on every retry, so you can skip duplicates.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Timestamp",
            "in": "header",
            "required": true,
            "description": "When this attempt was signed, in seconds since the epoch.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Signature",
            "in": "header",
            "required": true,
            "description": "v1=<hex HMAC-SHA256 of \"<timestamp>.<raw body>\" with the endpoint's secret>.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "The event id (evt_...)."
                  },
                  "type": {
                    "type": "string",
                    "const": "contact.unsubscribed"
                  },
                  "createdAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "workspaceId": {
                    "type": "string"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "contactId": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "The contact. Null when the address isn't a contact (a bounce on API email)."
                      },
                      "email": {
                        "type": "string",
                        "description": "The address."
                      },
                      "source": {
                        "type": "string",
                        "description": "Where it happened: recipient (a link in an email or the preference page), app, api or email-provider."
                      },
                      "campaignId": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "The campaign it came from, when the email provider reports one."
                      },
                      "reason": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "The provider's reason, when it gives one (bounces and complaints)."
                      }
                    },
                    "required": []
                  }
                },
                "required": [
                  "id",
                  "type",
                  "createdAt",
                  "workspaceId",
                  "data"
                ]
              },
              "example": {
                "id": "evt_9f2c...",
                "type": "contact.unsubscribed",
                "createdAt": "2026-11-07T09:14:02.511Z",
                "workspaceId": "cw1a...",
                "data": {
                  "contactId": "cm4x2k9...",
                  "email": "ana@company.com",
                  "source": "recipient"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Any 2xx within 10 seconds marks the delivery as received."
          }
        }
      }
    },
    "contact.bounced": {
      "post": {
        "operationId": "contactBounced",
        "summary": "contact.bounced",
        "description": "An address hard-bounced and won't be emailed again.",
        "parameters": [
          {
            "name": "X-LuneaMail-Event",
            "in": "header",
            "required": true,
            "description": "The event type, such as contact.unsubscribed.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Delivery",
            "in": "header",
            "required": true,
            "description": "This delivery's id. The same on every retry, so you can skip duplicates.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Timestamp",
            "in": "header",
            "required": true,
            "description": "When this attempt was signed, in seconds since the epoch.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Signature",
            "in": "header",
            "required": true,
            "description": "v1=<hex HMAC-SHA256 of \"<timestamp>.<raw body>\" with the endpoint's secret>.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "The event id (evt_...)."
                  },
                  "type": {
                    "type": "string",
                    "const": "contact.bounced"
                  },
                  "createdAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "workspaceId": {
                    "type": "string"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "contactId": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "The contact. Null when the address isn't a contact (a bounce on API email)."
                      },
                      "email": {
                        "type": "string",
                        "description": "The address."
                      },
                      "source": {
                        "type": "string",
                        "description": "Where it happened: recipient (a link in an email or the preference page), app, api or email-provider."
                      },
                      "campaignId": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "The campaign it came from, when the email provider reports one."
                      },
                      "reason": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "The provider's reason, when it gives one (bounces and complaints)."
                      }
                    },
                    "required": []
                  }
                },
                "required": [
                  "id",
                  "type",
                  "createdAt",
                  "workspaceId",
                  "data"
                ]
              },
              "example": {
                "id": "evt_9f2c...",
                "type": "contact.bounced",
                "createdAt": "2026-11-07T09:14:02.511Z",
                "workspaceId": "cw1a...",
                "data": {
                  "contactId": "cm4x2k9...",
                  "email": "ana@company.com",
                  "campaignId": "cc7d...",
                  "reason": "550 5.1.1 User unknown",
                  "source": "email-provider"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Any 2xx within 10 seconds marks the delivery as received."
          }
        }
      }
    },
    "contact.complained": {
      "post": {
        "operationId": "contactComplained",
        "summary": "contact.complained",
        "description": "Someone marked an email as spam. They won't be emailed again.",
        "parameters": [
          {
            "name": "X-LuneaMail-Event",
            "in": "header",
            "required": true,
            "description": "The event type, such as contact.unsubscribed.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Delivery",
            "in": "header",
            "required": true,
            "description": "This delivery's id. The same on every retry, so you can skip duplicates.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Timestamp",
            "in": "header",
            "required": true,
            "description": "When this attempt was signed, in seconds since the epoch.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Signature",
            "in": "header",
            "required": true,
            "description": "v1=<hex HMAC-SHA256 of \"<timestamp>.<raw body>\" with the endpoint's secret>.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "The event id (evt_...)."
                  },
                  "type": {
                    "type": "string",
                    "const": "contact.complained"
                  },
                  "createdAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "workspaceId": {
                    "type": "string"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "contactId": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "The contact. Null when the address isn't a contact (a bounce on API email)."
                      },
                      "email": {
                        "type": "string",
                        "description": "The address."
                      },
                      "source": {
                        "type": "string",
                        "description": "Where it happened: recipient (a link in an email or the preference page), app, api or email-provider."
                      },
                      "campaignId": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "The campaign it came from, when the email provider reports one."
                      },
                      "reason": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "The provider's reason, when it gives one (bounces and complaints)."
                      }
                    },
                    "required": []
                  }
                },
                "required": [
                  "id",
                  "type",
                  "createdAt",
                  "workspaceId",
                  "data"
                ]
              },
              "example": {
                "id": "evt_9f2c...",
                "type": "contact.complained",
                "createdAt": "2026-11-07T09:14:02.511Z",
                "workspaceId": "cw1a...",
                "data": {
                  "contactId": "cm4x2k9...",
                  "email": "ana@company.com",
                  "campaignId": "cc7d...",
                  "reason": null,
                  "source": "email-provider"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Any 2xx within 10 seconds marks the delivery as received."
          }
        }
      }
    },
    "campaign.sent": {
      "post": {
        "operationId": "campaignSent",
        "summary": "campaign.sent",
        "description": "A campaign started sending, now or on its schedule.",
        "parameters": [
          {
            "name": "X-LuneaMail-Event",
            "in": "header",
            "required": true,
            "description": "The event type, such as contact.unsubscribed.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Delivery",
            "in": "header",
            "required": true,
            "description": "This delivery's id. The same on every retry, so you can skip duplicates.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Timestamp",
            "in": "header",
            "required": true,
            "description": "When this attempt was signed, in seconds since the epoch.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Signature",
            "in": "header",
            "required": true,
            "description": "v1=<hex HMAC-SHA256 of \"<timestamp>.<raw body>\" with the endpoint's secret>.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "The event id (evt_...)."
                  },
                  "type": {
                    "type": "string",
                    "const": "campaign.sent"
                  },
                  "createdAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "workspaceId": {
                    "type": "string"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "campaignId": {
                        "type": "string",
                        "description": "The campaign."
                      },
                      "name": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Its name in LuneaMail."
                      },
                      "subject": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Its subject line."
                      },
                      "listId": {
                        "type": "string",
                        "description": "The list it's going to."
                      },
                      "recipients": {
                        "type": "number",
                        "description": "How many people it's going to."
                      },
                      "scheduled": {
                        "type": "boolean",
                        "description": "True when it started on a schedule."
                      }
                    },
                    "required": []
                  }
                },
                "required": [
                  "id",
                  "type",
                  "createdAt",
                  "workspaceId",
                  "data"
                ]
              },
              "example": {
                "id": "evt_9f2c...",
                "type": "campaign.sent",
                "createdAt": "2026-11-07T09:14:02.511Z",
                "workspaceId": "cw1a...",
                "data": {
                  "campaignId": "cc7d...",
                  "name": "November update",
                  "subject": "What's new in November",
                  "listId": "cl8q1v...",
                  "recipients": 1240,
                  "scheduled": false
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Any 2xx within 10 seconds marks the delivery as received."
          }
        }
      }
    },
    "campaign.completed": {
      "post": {
        "operationId": "campaignCompleted",
        "summary": "campaign.completed",
        "description": "A campaign finished sending, with its totals.",
        "parameters": [
          {
            "name": "X-LuneaMail-Event",
            "in": "header",
            "required": true,
            "description": "The event type, such as contact.unsubscribed.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Delivery",
            "in": "header",
            "required": true,
            "description": "This delivery's id. The same on every retry, so you can skip duplicates.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Timestamp",
            "in": "header",
            "required": true,
            "description": "When this attempt was signed, in seconds since the epoch.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-LuneaMail-Signature",
            "in": "header",
            "required": true,
            "description": "v1=<hex HMAC-SHA256 of \"<timestamp>.<raw body>\" with the endpoint's secret>.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "The event id (evt_...)."
                  },
                  "type": {
                    "type": "string",
                    "const": "campaign.completed"
                  },
                  "createdAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "workspaceId": {
                    "type": "string"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "campaignId": {
                        "type": "string",
                        "description": "The campaign."
                      },
                      "name": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Its name."
                      },
                      "subject": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Its subject line."
                      },
                      "status": {
                        "type": "string",
                        "description": "COMPLETED, or FAILED when nothing could be sent."
                      },
                      "sent": {
                        "type": "number",
                        "description": "Emails handed to the provider."
                      },
                      "failed": {
                        "type": "number",
                        "description": "Emails the provider refused."
                      },
                      "skipped": {
                        "type": "number",
                        "description": "Recipients skipped because they unsubscribed or were suppressed while it sent."
                      },
                      "completedAt": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "When it finished (ISO 8601)."
                      }
                    },
                    "required": []
                  }
                },
                "required": [
                  "id",
                  "type",
                  "createdAt",
                  "workspaceId",
                  "data"
                ]
              },
              "example": {
                "id": "evt_9f2c...",
                "type": "campaign.completed",
                "createdAt": "2026-11-07T09:14:02.511Z",
                "workspaceId": "cw1a...",
                "data": {
                  "campaignId": "cc7d...",
                  "name": "November update",
                  "subject": "What's new in November",
                  "status": "COMPLETED",
                  "sent": 1236,
                  "failed": 1,
                  "skipped": 3,
                  "completedAt": "2026-11-07T09:31:40.022Z"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Any 2xx within 10 seconds marks the delivery as received."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A workspace API key from Settings › API keys."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "A stable code, like MISSING_VARIABLE."
          },
          "message": {
            "type": "string",
            "description": "What went wrong, for people."
          }
        },
        "required": [
          "error",
          "message"
        ]
      }
    }
  }
}