{
  "openapi": "3.1.0",
  "info": {
    "title": "Mailer",
    "version": "1.0.0",
    "description": "Routes are mounted without an /api prefix. Errors are JSON with an error_key; field validation keeps Laravel's 422 format. Prose docs: docs/api/."
  },
  "components": {
    "securitySchemes": {
      "sourceKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Per-source API key (mk_...)"
      },
      "uiToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Minted by POST /v1/workspaces/{externalId}/ui-tokens, 15 minutes"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error_key"
        ],
        "properties": {
          "error_key": {
            "type": "string"
          }
        }
      },
      "Workspace": {
        "type": "object",
        "required": [
          "external_id",
          "name",
          "timezone",
          "locale",
          "status"
        ],
        "properties": {
          "external_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "timezone": {
            "type": "string"
          },
          "locale": {
            "type": "string",
            "enum": [
              "he",
              "en"
            ]
          },
          "business_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "business_address": {
            "type": [
              "string",
              "null"
            ]
          },
          "business_contact": {
            "type": [
              "string",
              "null"
            ]
          },
          "business_registry_number": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 50,
            "pattern": "^[0-9 \\-]{1,50}$"
          },
          "tracking_enabled": {
            "type": "boolean"
          },
          "quiet_hours": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "start": {
                "type": "string"
              },
              "end": {
                "type": "string"
              }
            }
          },
          "shabbat_pause": {
            "type": "boolean"
          },
          "attribution_window_days": {
            "type": "integer",
            "minimum": 1,
            "maximum": 30
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "suspended"
            ]
          },
          "paused_reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "paused_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "description": "status, paused_reason and paused_at are owned by the service; a host cannot set them. paused_reason: bounce_rate | complaint_rate (automatic pause, hard bounces above 4 % or complaints above 0.1 % of the last 7 days' sends, min 200) | operator | null. Each pause and resume sends the always-on webhooks workspace.paused / workspace.resumed; only an operator resumes"
      },
      "Campaign": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "scheduled",
              "testing",
              "sending",
              "sent",
              "cancelled"
            ]
          },
          "category": {
            "type": "string",
            "enum": [
              "marketing"
            ]
          },
          "design_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "subject": {
            "type": "string"
          },
          "preheader": {
            "type": [
              "string",
              "null"
            ]
          },
          "from_name": {
            "type": "string"
          },
          "from_local_part": {
            "type": "string"
          },
          "sending_domain_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "reply_to": {
            "type": [
              "string",
              "null"
            ]
          },
          "recipients_count": {
            "type": "integer"
          },
          "scheduled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "started_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "cancelled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "ab_test": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/AbTest"
              },
              {
                "type": "null"
              }
            ],
            "description": "The A/B test definition (contract section 5), or null without a test"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Counts": {
        "type": "object",
        "description": "Test sends excluded. queued includes the internal sending status. unique = deliveries with at least one event of that type; total = Mailer events, or SES events for deliveries with no Mailer event of that type.",
        "properties": {
          "recipients": {
            "type": "integer"
          },
          "pending": {
            "type": "integer"
          },
          "queued": {
            "type": "integer"
          },
          "sent": {
            "type": "integer"
          },
          "skipped": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          },
          "delivered": {
            "type": "integer"
          },
          "bounced": {
            "type": "integer"
          },
          "hard_bounced": {
            "type": "integer"
          },
          "complained": {
            "type": "integer"
          },
          "opened_unique": {
            "type": "integer"
          },
          "opened_total": {
            "type": "integer"
          },
          "clicked_unique": {
            "type": "integer"
          },
          "clicked_total": {
            "type": "integer"
          },
          "unsubscribed": {
            "type": "integer"
          }
        },
        "required": [
          "recipients",
          "pending",
          "queued",
          "sent",
          "skipped",
          "failed",
          "delivered",
          "bounced",
          "hard_bounced",
          "complained",
          "opened_unique",
          "opened_total",
          "clicked_unique",
          "clicked_total",
          "unsubscribed"
        ]
      },
      "Rates": {
        "type": "object",
        "description": "round(x, 4); a zero denominator gives 0.",
        "properties": {
          "delivery_rate": {
            "type": "number"
          },
          "bounce_rate": {
            "type": "number"
          },
          "open_rate": {
            "type": "number"
          },
          "click_rate": {
            "type": "number"
          },
          "click_to_open_rate": {
            "type": "number"
          },
          "unsubscribe_rate": {
            "type": "number"
          },
          "complaint_rate": {
            "type": "number"
          }
        },
        "required": [
          "delivery_rate",
          "bounce_rate",
          "open_rate",
          "click_rate",
          "click_to_open_rate",
          "unsubscribe_rate",
          "complaint_rate"
        ]
      },
      "CampaignReport": {
        "type": "object",
        "properties": {
          "campaign_id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "scheduled",
              "testing",
              "sending",
              "sent",
              "cancelled"
            ]
          },
          "timezone": {
            "type": "string"
          },
          "delivered_estimated": {
            "type": "boolean",
            "description": "true when no delivered event exists and delivered was taken from sent"
          },
          "counts": {
            "$ref": "#/components/schemas/Counts"
          },
          "rates": {
            "$ref": "#/components/schemas/Rates"
          },
          "links": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string"
                },
                "clicked_unique": {
                  "type": "integer"
                },
                "clicked_total": {
                  "type": "integer"
                }
              },
              "required": [
                "url",
                "clicked_unique",
                "clicked_total"
              ]
            }
          },
          "variants": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CampaignReportVariant"
            },
            "description": "[] without an A/B test; one per label in order. Counts over the deliveries carrying that label: the test group, plus the remainder for the winner. delivered is estimated from sent exactly when the campaign's counts are."
          },
          "revenue": {
            "$ref": "#/components/schemas/Revenue",
            "description": "Orders attributed to this campaign, all time"
          }
        },
        "required": [
          "campaign_id",
          "status",
          "timezone",
          "delivered_estimated",
          "counts",
          "rates",
          "links",
          "variants",
          "revenue"
        ]
      },
      "TimelinePoint": {
        "type": "object",
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time",
            "description": "Bucket start, ISO 8601 with the workspace offset"
          },
          "sent": {
            "type": "integer"
          },
          "delivered": {
            "type": "integer"
          },
          "opened": {
            "type": "integer"
          },
          "clicked": {
            "type": "integer"
          },
          "unsubscribed": {
            "type": "integer"
          },
          "bounced": {
            "type": "integer"
          },
          "complained": {
            "type": "integer"
          }
        },
        "required": [
          "at",
          "sent",
          "delivered",
          "opened",
          "clicked",
          "unsubscribed",
          "bounced",
          "complained"
        ]
      },
      "Timeline": {
        "type": "object",
        "properties": {
          "bucket": {
            "type": "string",
            "enum": [
              "hour",
              "day"
            ]
          },
          "timezone": {
            "type": "string"
          },
          "points": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TimelinePoint"
            }
          }
        },
        "required": [
          "bucket",
          "timezone",
          "points"
        ]
      },
      "ReportDay": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "format": "date"
          },
          "sent": {
            "type": "integer"
          },
          "delivered": {
            "type": "integer"
          },
          "opened": {
            "type": "integer"
          },
          "clicked": {
            "type": "integer"
          },
          "unsubscribed": {
            "type": "integer"
          },
          "bounced": {
            "type": "integer"
          },
          "complained": {
            "type": "integer"
          }
        },
        "required": [
          "date",
          "sent",
          "delivered",
          "opened",
          "clicked",
          "unsubscribed",
          "bounced",
          "complained"
        ]
      },
      "SummaryDay": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "format": "date"
          },
          "sent": {
            "type": "integer"
          },
          "opened": {
            "type": "integer"
          },
          "clicked": {
            "type": "integer"
          },
          "unsubscribed": {
            "type": "integer"
          }
        },
        "required": [
          "date",
          "sent",
          "opened",
          "clicked",
          "unsubscribed"
        ]
      },
      "AutomatedReport": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "enum": [
              "marketing",
              "transactional"
            ]
          },
          "from": {
            "type": "string",
            "format": "date"
          },
          "to": {
            "type": "string",
            "format": "date"
          },
          "timezone": {
            "type": "string"
          },
          "delivered_estimated": {
            "type": "boolean"
          },
          "counts": {
            "$ref": "#/components/schemas/Counts"
          },
          "rates": {
            "$ref": "#/components/schemas/Rates"
          },
          "links": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string"
                },
                "clicked_unique": {
                  "type": "integer"
                },
                "clicked_total": {
                  "type": "integer"
                }
              },
              "required": [
                "url",
                "clicked_unique",
                "clicked_total"
              ]
            }
          },
          "days": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReportDay"
            }
          },
          "revenue": {
            "$ref": "#/components/schemas/Revenue",
            "description": "Orders attributed to this message whose order time is in from..to"
          }
        },
        "required": [
          "key",
          "name",
          "category",
          "from",
          "to",
          "timezone",
          "delivered_estimated",
          "counts",
          "rates",
          "links",
          "days",
          "revenue"
        ]
      },
      "AutomatedReportList": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "format": "date"
          },
          "to": {
            "type": "string",
            "format": "date"
          },
          "timezone": {
            "type": "string"
          },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "category": {
                  "type": "string",
                  "enum": [
                    "marketing",
                    "transactional"
                  ]
                },
                "active": {
                  "type": "boolean"
                },
                "delivered_estimated": {
                  "type": "boolean"
                },
                "counts": {
                  "$ref": "#/components/schemas/Counts"
                },
                "rates": {
                  "$ref": "#/components/schemas/Rates"
                },
                "revenue": {
                  "$ref": "#/components/schemas/Revenue",
                  "description": "Orders attributed to this message whose order time is in from..to"
                }
              },
              "required": [
                "key",
                "name",
                "category",
                "active",
                "delivered_estimated",
                "counts",
                "rates",
                "revenue"
              ]
            }
          }
        },
        "required": [
          "from",
          "to",
          "timezone",
          "data"
        ]
      },
      "SummaryReport": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "format": "date"
          },
          "to": {
            "type": "string",
            "format": "date"
          },
          "timezone": {
            "type": "string"
          },
          "delivered_estimated": {
            "type": "boolean"
          },
          "campaigns": {
            "type": "object",
            "properties": {
              "count": {
                "type": "integer",
                "description": "Campaigns that started in the range"
              },
              "counts": {
                "$ref": "#/components/schemas/Counts"
              },
              "rates": {
                "$ref": "#/components/schemas/Rates"
              }
            },
            "required": [
              "count",
              "counts",
              "rates"
            ]
          },
          "automated": {
            "type": "object",
            "properties": {
              "counts": {
                "$ref": "#/components/schemas/Counts"
              },
              "rates": {
                "$ref": "#/components/schemas/Rates"
              }
            },
            "required": [
              "counts",
              "rates"
            ]
          },
          "all": {
            "type": "object",
            "properties": {
              "counts": {
                "$ref": "#/components/schemas/Counts"
              },
              "rates": {
                "$ref": "#/components/schemas/Rates"
              }
            },
            "required": [
              "counts",
              "rates"
            ]
          },
          "revenue": {
            "$ref": "#/components/schemas/Revenue",
            "description": "Every attributed order of the workspace whose order time is in from..to"
          },
          "days": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SummaryDay"
            }
          }
        },
        "required": [
          "from",
          "to",
          "timezone",
          "delivered_estimated",
          "campaigns",
          "automated",
          "all",
          "revenue",
          "days"
        ]
      },
      "UsageCounts": {
        "type": "object",
        "properties": {
          "marketing": {
            "type": "integer"
          },
          "transactional": {
            "type": "integer"
          },
          "all": {
            "type": "integer"
          },
          "test": {
            "type": "integer"
          }
        },
        "required": [
          "marketing",
          "transactional",
          "all",
          "test"
        ]
      },
      "UsageDay": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "format": "date"
          },
          "marketing": {
            "type": "integer"
          },
          "transactional": {
            "type": "integer"
          },
          "all": {
            "type": "integer"
          },
          "test": {
            "type": "integer"
          }
        },
        "required": [
          "date",
          "marketing",
          "transactional",
          "all",
          "test"
        ]
      },
      "Usage": {
        "type": "object",
        "properties": {
          "month": {
            "type": "string",
            "description": "YYYY-MM in the workspace timezone"
          },
          "timezone": {
            "type": "string"
          },
          "totals": {
            "$ref": "#/components/schemas/UsageCounts"
          },
          "days": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UsageDay"
            },
            "description": "Every day of the month up to today (current month), the whole month (past), empty (future)"
          },
          "today": {
            "allOf": [
              {
                "$ref": "#/components/schemas/UsageDay"
              }
            ],
            "description": "Always the current local day, whatever month was asked"
          }
        },
        "required": [
          "month",
          "timezone",
          "totals",
          "days",
          "today"
        ]
      },
      "AbTestVariant": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "enum": [
              "A",
              "B",
              "C",
              "D"
            ]
          },
          "subject": {
            "type": "string",
            "description": "Mustache template"
          },
          "preheader": {
            "type": [
              "string",
              "null"
            ],
            "description": "Mustache template; null = the campaign's"
          },
          "design_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "null = the campaign's design"
          }
        }
      },
      "AbTest": {
        "type": "object",
        "properties": {
          "variants": {
            "type": "array",
            "minItems": 2,
            "maxItems": 4,
            "items": {
              "$ref": "#/components/schemas/AbTestVariant"
            }
          },
          "test_percent": {
            "type": "integer",
            "minimum": 10,
            "maximum": 50
          },
          "winner_metric": {
            "type": "string",
            "enum": [
              "open_rate",
              "click_rate",
              "manual"
            ]
          },
          "decide_after_hours": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 72
          },
          "decide_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Set when the test starts: started_at + decide_after_hours; null for manual. The decision runs at the first feeder minute after it once every test email has left"
          },
          "winner": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "A",
              "B",
              "C",
              "D",
              null
            ]
          },
          "winner_decided_by": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "open_rate",
              "click_rate",
              "host",
              null
            ]
          }
        }
      },
      "CampaignReportVariant": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "enum": [
              "A",
              "B",
              "C",
              "D"
            ]
          },
          "subject": {
            "type": "string"
          },
          "design_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "null = the campaign's design"
          },
          "winner": {
            "type": "boolean"
          },
          "counts": {
            "$ref": "#/components/schemas/Counts"
          },
          "rates": {
            "$ref": "#/components/schemas/Rates"
          }
        },
        "required": [
          "label",
          "subject",
          "design_id",
          "winner",
          "counts",
          "rates"
        ]
      },
      "DeliverabilityWarmup": {
        "type": "object",
        "description": "Warm-up of one domain (Warmup::status). Days are local calendar days in the workspace timezone; day 1 is the local date of started_at. Caps per day 1-8: 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000; from day 9 complete with no cap.",
        "required": [
          "started_at",
          "day",
          "stage",
          "daily_cap",
          "sent_today",
          "remaining_today",
          "complete"
        ],
        "properties": {
          "started_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "UTC; null before the domain is verified (then day 1 starting now)"
          },
          "day": {
            "type": "integer",
            "minimum": 1
          },
          "stage": {
            "type": "integer",
            "minimum": 1,
            "maximum": 8,
            "description": "min(day, 8)"
          },
          "daily_cap": {
            "type": [
              "integer",
              "null"
            ],
            "description": "null once complete"
          },
          "sent_today": {
            "type": "integer",
            "description": "Marketing slots reserved today (local day)"
          },
          "remaining_today": {
            "type": [
              "integer",
              "null"
            ],
            "description": "null once complete"
          },
          "complete": {
            "type": "boolean"
          }
        }
      },
      "DeliverabilityDomain": {
        "type": "object",
        "required": [
          "id",
          "domain",
          "status",
          "dkim",
          "spf",
          "mail_from",
          "dmarc",
          "last_checked_at",
          "dmarc_lost_at",
          "held_until",
          "warmup"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "domain": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "verified",
              "failed"
            ]
          },
          "dkim": {
            "type": "string",
            "enum": [
              "verified",
              "pending",
              "failed",
              "not_started"
            ],
            "description": "SES DKIM status of the last check: SUCCESS → verified, PENDING or TEMPORARY_FAILURE → pending, FAILED → failed, NOT_STARTED or never checked → not_started"
          },
          "spf": {
            "type": "string",
            "enum": [
              "verified",
              "pending",
              "failed",
              "not_started"
            ],
            "description": "Same value as mail_from (the SPF record lives on bounce.<domain>)"
          },
          "mail_from": {
            "type": "string",
            "enum": [
              "verified",
              "pending",
              "failed",
              "not_started"
            ],
            "description": "SES custom MAIL FROM (bounce.<domain>) status of the last check, mapped like dkim"
          },
          "dmarc": {
            "type": "string",
            "enum": [
              "found",
              "missing"
            ]
          },
          "last_checked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "UTC time of the last check; the statuses are as of then"
          },
          "dmarc_lost_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "UTC time DMARC was lost (kept after it is found again), or null (never had it, or never lost it)"
          },
          "held_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "UTC time held marketing rows fail (hold start + MAILER_DOMAIN_HOLD_DAYS), only while the domain is revoked or DMARC-lost; otherwise null"
          },
          "warmup": {
            "$ref": "#/components/schemas/DeliverabilityWarmup"
          }
        }
      },
      "Deliverability": {
        "type": "object",
        "required": [
          "status",
          "paused_reason",
          "paused_at",
          "window_days",
          "window_start",
          "sent_in_window",
          "min_volume",
          "bounce_rate",
          "complaint_rate",
          "thresholds",
          "account_hold",
          "domains"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "suspended"
            ]
          },
          "paused_reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "bounce_rate",
              "complaint_rate",
              "operator",
              null
            ]
          },
          "paused_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "window_days": {
            "type": "integer",
            "description": "7: the longest window. The real one starts at window_start"
          },
          "window_start": {
            "type": "string",
            "format": "date-time",
            "description": "UTC start of the window the rates cover: max(now - 7 days, the last operator resume). Shorter than 7 days after a resume"
          },
          "sent_in_window": {
            "type": "integer",
            "description": "Real (non-test) emails sent since window_start"
          },
          "min_volume": {
            "type": "integer",
            "description": "The automatic pause only judges a workspace with at least this many sends in the window (200)"
          },
          "bounce_rate": {
            "type": "number",
            "description": "Hard bounces / sent, 4 decimals, 0 when nothing was sent"
          },
          "complaint_rate": {
            "type": "number",
            "description": "Complaints / sent, 4 decimals"
          },
          "thresholds": {
            "type": "object",
            "required": [
              "bounce_rate",
              "complaint_rate"
            ],
            "properties": {
              "bounce_rate": {
                "type": "number",
                "description": "0.04"
              },
              "complaint_rate": {
                "type": "number",
                "description": "0.001"
              }
            },
            "description": "Above either (strictly) with min_volume sends, the workspace is paused"
          },
          "account_hold": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "required": [
                  "reason",
                  "code",
                  "since"
                ],
                "properties": {
                  "reason": {
                    "type": "string",
                    "enum": [
                      "bounce_rate",
                      "complaint_rate",
                      "ses_paused",
                      "unknown"
                    ]
                  },
                  "code": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "SendingPausedException",
                      "AccountSuspendedException",
                      "AccessDenied",
                      "AccessDeniedException",
                      "UnrecognizedClientException",
                      "InvalidClientTokenId",
                      "SignatureDoesNotMatch",
                      "ExpiredToken",
                      "ExpiredTokenException",
                      null
                    ],
                    "description": "The AWS error code for ses_paused, only from this list; else null"
                  },
                  "since": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time",
                    "description": "When the hold started (UTC); null if not recorded"
                  }
                }
              }
            ],
            "description": "The service-wide marketing hold (account guard), shared by every workspace; null when off. While on, all marketing waits, transactional still sends. Only an operator ends it"
          },
          "domains": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DeliverabilityDomain"
            },
            "description": "Every sending domain of the workspace, by id"
          }
        }
      },
      "Revenue": {
        "type": "array",
        "description": "Attributed revenue per currency, ordered by currency; [] when none. Only orders attributed to a click count (contract section 9).",
        "items": {
          "type": "object",
          "properties": {
            "currency": {
              "type": "string",
              "description": "ISO 4217, e.g. ILS"
            },
            "orders": {
              "type": "integer"
            },
            "value": {
              "type": "string",
              "description": "Sum with exactly 2 decimals, as a string (never a float), e.g. \"4310.50\""
            }
          },
          "required": [
            "currency",
            "orders",
            "value"
          ]
        }
      },
      "Order": {
        "type": "object",
        "properties": {
          "order_id": {
            "type": "string"
          },
          "attributed": {
            "type": "boolean"
          },
          "delivery_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "campaign_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "automated_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "The automated message's key; null when the order went to a campaign, or when the message has since been deleted (also on a repeat)"
          },
          "clicked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "order_id",
          "attributed",
          "delivery_id",
          "campaign_id",
          "automated_key",
          "clicked_at"
        ]
      },
      "CountdownUrl": {
        "type": "object",
        "required": [
          "url",
          "deadline"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "{public_url}/t/{token}.png. Public, no auth. Answers a 600x150 PNG with the days / hours / minutes left (or the ended text), Cache-Control: public, max-age=60, computed when the email is opened (not animated). A bad or tampered token answers 404 with a 1x1 transparent PNG. The URL never expires: after the deadline it shows the ended text. Put it in the design as <img src=\"{url}\" width=\"600\" alt=\"\">"
          },
          "deadline": {
            "type": "string",
            "format": "date-time",
            "description": "The deadline in UTC, e.g. 2026-11-30T22:00:00Z (seconds kept, no fraction)"
          }
        }
      }
    }
  },
  "paths": {
    "/sns": {
      "post": {
        "summary": "SES events through SNS (signature-verified; body is JSON sent as text/plain)",
        "responses": {
          "200": {
            "description": "Accepted, or ignored (unknown event type or message)"
          },
          "400": {
            "description": "Body is not JSON"
          },
          "403": {
            "description": "Bad signature, topic not in MAILER_SNS_TOPIC_ARNS, or SubscribeURL not on sns.<region>.amazonaws.com"
          }
        }
      }
    },
    "/v1/workspaces/{externalId}": {
      "put": {
        "summary": "Create or update a workspace",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "timezone": {
                    "type": "string"
                  },
                  "locale": {
                    "type": "string",
                    "enum": [
                      "he",
                      "en"
                    ],
                    "default": "he",
                    "description": "Language of the unsubscribe footer and page for this workspace"
                  },
                  "business_name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200
                  },
                  "business_address": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500
                  },
                  "business_contact": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200
                  },
                  "business_registry_number": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 50,
                    "description": "Digits, spaces and dashes only; shown in the marketing footer when set; null clears it",
                    "pattern": "^[0-9 \\-]{1,50}$"
                  },
                  "tracking_enabled": {
                    "type": "boolean",
                    "default": true
                  },
                  "quiet_hours": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "required": [
                      "start",
                      "end"
                    ],
                    "properties": {
                      "start": {
                        "type": "string",
                        "description": "HH:MM, workspace timezone"
                      },
                      "end": {
                        "type": "string",
                        "description": "HH:MM, must differ from start"
                      }
                    },
                    "description": "Quiet hours: marketing (non-test) sends due in [start, end) wait until end; wraps midnight when start > end. Both start and end required (empty object refused); null clears it; read in the workspace timezone. Transactional and test sends are never held"
                  },
                  "shabbat_pause": {
                    "type": "boolean",
                    "default": false,
                    "description": "Marketing (non-test) sends wait from Friday 16:00 to Saturday 20:00 Asia/Jerusalem (MAILER_SHABBAT_STARTS / MAILER_SHABBAT_ENDS), and the same around each holiday the operator configured. Transactional and test sends are never held"
                  },
                  "attribution_window_days": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 30,
                    "default": 5
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Workspace"
                }
              }
            }
          },
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Workspace"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors): bad quiet_hours, attribution_window_days, registry number, or null on a non-nullable field"
          }
        }
      },
      "get": {
        "summary": "Read a workspace",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Workspace"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/ui-tokens": {
      "post": {
        "summary": "Mint a UI token for a signed-in user (15 minutes)",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "external_user_id",
                  "email",
                  "abilities"
                ],
                "properties": {
                  "external_user_id": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "email": {
                    "type": "string",
                    "maxLength": 254
                  },
                  "abilities": {
                    "type": "array",
                    "items": {
                      "enum": [
                        "designs.edit",
                        "designs.test_send",
                        "campaigns.manage",
                        "automated.manage",
                        "domains.manage",
                        "reports.view"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{token, expires_at}"
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found (unknown or another source's workspace)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/suppressions": {
      "post": {
        "summary": "The host reports a withdrawn marketing consent (scope marketing, idempotent)",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "maxLength": 254
                  },
                  "occurred_at": {
                    "type": "string",
                    "description": "ISO 8601 with offset: when the person unsubscribed on the host (H7). Not more than 5 minutes in the future, not older than 30 days (422). updated_at becomes GREATEST(existing, occurred_at), created_at = occurred_at on insert. Absent: now"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{suppressed: true}"
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found (unknown or another source's workspace)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        }
      },
      "get": {
        "summary": "List the workspace's suppressions (who cannot be mailed and why)",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{data: [{email, scope, reason, created_at}], meta: {page, per_page, total}}, newest first, 50 per page"
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "use_post_search (an `email` query parameter was sent, whatever its value); page out of range keeps Laravel's validation format",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "The `email` query parameter was removed in H9a (28/10/2026): any `email` in the query is a 422 `use_post_search`. Search one address with POST .../suppressions/search. An erased row (H9a) shows `email: \"[erased]\"`, never the address."
      }
    },
    "/v1/workspaces/{externalId}/suppressions/lift": {
      "post": {
        "summary": "Lift the workspace's marketing suppression after new consent (host only, idempotent). Never lifts an all-scope suppression, a hard bounce, a complaint or an erased address unless consent_source is form or manual with a newer consent_at (H9c)",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "reason",
                  "consent_source",
                  "consent_at"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "maxLength": 254,
                    "description": "Trimmed and lower-cased before matching"
                  },
                  "reason": {
                    "type": "string",
                    "enum": [
                      "resubscribed"
                    ]
                  },
                  "consent_source": {
                    "type": "string",
                    "enum": [
                      "form",
                      "manual",
                      "import",
                      "api"
                    ]
                  },
                  "consent_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "ISO 8601, when the contact gave the new consent; not later than now + 5 minutes"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{lifted: true} when a marketing suppression was removed (and audited), {lifted: false} when there was none",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "lifted"
                  ],
                  "properties": {
                    "lifted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found (unknown or another source's workspace)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "suppression_not_liftable: the address has an all-scope suppression or a row with reason hard_bounce or complaint; nothing changed. suppression_newer: the marketing suppression was last confirmed (a repeat unsubscribe or host revoke) after consent_at; nothing changed; the body also carries suppressed_at (the row's updated_at, ISO 8601 UTC) so the host can reconcile. suppression_not_liftable wins when both apply",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "suppressed_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "Only with suppression_newer: when the marketing suppression was last confirmed (its updated_at), ISO 8601 UTC."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/suppressions/search": {
      "post": {
        "summary": "Find one address in the workspace's suppressions. The address travels in the body, never the URL",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "maxLength": 254,
                    "description": "One address, matched exactly after trimming and lower-casing (never a prefix search)"
                  },
                  "page": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{data: [{email, scope, reason, created_at}], meta: {page, per_page, total}}, newest first, 50 per page (same shape as GET suppressions)"
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found (unknown or another source's workspace)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        },
        "description": "An erased row (H9a) is found by hashing the searched address and shows `email: \"[erased]\"`, reason `erased` (marketing) or its original reason (all)."
      }
    },
    "/v1/workspaces/{externalId}/automated/{key}": {
      "post": {
        "summary": "Define an automated message and its category (host only)",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "key",
            "in": "path",
            "required": true,
            "description": "Lowercase letters, digits, _ and -, 1-100 characters; anything else is 404 not_found",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9_-]{1,100}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "category"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "category": {
                    "enum": [
                      "marketing",
                      "transactional"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "201": {
            "description": "Created (inactive)"
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found, or not_found for a bad key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/automated/{key}/send": {
      "post": {
        "summary": "Queue an automated message for one recipient (202 means queued, not sent)",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "key",
            "in": "path",
            "required": true,
            "description": "Lowercase letters, digits, _ and -, 1-100 characters; anything else is 404 not_found",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9_-]{1,100}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "maxLength": 254
                  },
                  "data": {
                    "type": "object",
                    "description": "Merge data, kept 90 days. At most 16,384 bytes of JSON, nested at most 4 levels; items: a list of at most 20 objects with a string name, image_url and url https only. Over a limit: Laravel 422 on data. See docs/api/merge-data.md"
                  },
                  "idempotency_key": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Unique per workspace across all messages; a repeat returns the first delivery"
                  },
                  "external_contact_id": {
                    "type": "string",
                    "maxLength": 100
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "{delivery_id, status}"
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found | automated_message_not_found | not_found (bad key)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "error_key automated_message_inactive | workspace_suspended | workspace_paused (marketing messages only; transactional still goes), or Laravel validation format",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/ui/domains": {
      "get": {
        "summary": "List sending domains with DNS records",
        "security": [
          {
            "uiToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Domains"
          },
          "401": {
            "description": "unauthenticated"
          },
          "403": {
            "description": "forbidden (needs domains.manage)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Add a sending domain (unique across all workspaces)",
        "security": [
          {
            "uiToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "domain"
                ],
                "properties": {
                  "domain": {
                    "type": "string",
                    "maxLength": 253,
                    "description": "Trimmed and lowercased by the server first. After that it must match ^(?!-)[a-z0-9-]{1,63}(?<!-)(\\.(?!-)[a-z0-9-]{1,63}(?<!-))+$ (so Example.COM is accepted and stored as example.com)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Domain with records"
          },
          "401": {
            "description": "unauthenticated"
          },
          "403": {
            "description": "forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "error_key domain_taken, or Laravel validation format",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/ui/domains/{id}/check": {
      "post": {
        "summary": "Check a domain now",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Domain"
          },
          "401": {
            "description": "unauthenticated"
          },
          "403": {
            "description": "forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/ui/domains/{id}": {
      "delete": {
        "summary": "Remove a domain and its SES identity",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Removed"
          },
          "401": {
            "description": "unauthenticated"
          },
          "403": {
            "description": "forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "domain_in_use (also while a draft, scheduled, testing or sending campaign uses it)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/ui/designs": {
      "get": {
        "summary": "List designs (summaries, no html)",
        "security": [
          {
            "uiToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Designs"
          },
          "401": {
            "description": "unauthenticated"
          },
          "403": {
            "description": "forbidden (needs designs.edit)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a design",
        "security": [
          {
            "uiToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "unlayer_json",
                  "html"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "unlayer_json": {
                    "type": "object"
                  },
                  "html": {
                    "type": "string",
                    "maxLength": 524288
                  },
                  "text": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Design"
          },
          "401": {
            "description": "unauthenticated"
          },
          "403": {
            "description": "forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "error_key template_syntax_error (field, line, reason) when html is not a valid template (reasons: syntax, unclosed_section, unexpected_close, unsupported_tag, too_deep, nested_same_section, too_many_tags, too_large), or Laravel validation format",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/ui/designs/{id}": {
      "get": {
        "summary": "Read a design",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Design"
          },
          "401": {
            "description": "unauthenticated"
          },
          "403": {
            "description": "forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "summary": "Update a design (all fields optional)",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "unlayer_json": {
                    "type": "object"
                  },
                  "html": {
                    "type": "string",
                    "maxLength": 524288
                  },
                  "text": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Design"
          },
          "401": {
            "description": "unauthenticated"
          },
          "403": {
            "description": "forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "error_key template_syntax_error (field, line, reason) when html is not a valid template (reasons: syntax, unclosed_section, unexpected_close, unsupported_tag, too_deep, nested_same_section, too_many_tags, too_large), or Laravel validation format",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a design",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "description": "unauthenticated"
          },
          "403": {
            "description": "forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "design_in_use (also while a draft, scheduled, testing or sending campaign uses it)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/ui/designs/{id}/preview": {
      "post": {
        "summary": "Preview a design as a real send would render it (nothing is sent or stored)",
        "description": "Renders through the same code as a send: marketing subject prefix, footer in the workspace locale, merge and escaping. The unsubscribe URL is a fake (<public_url>/u/preview) that the public page answers as an invalid token. With campaign_id the preview uses the campaign's design, subject, preheader and sender.",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [],
                "properties": {
                  "campaign_id": {
                    "type": "integer",
                    "description": "A campaign of this workspace whose design is {id}. Its subject, preheader, from_name and sender address become the defaults (category is always marketing); fields sent explicitly win. Replaces the required subject."
                  },
                  "subject": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "No CR/LF"
                  },
                  "preheader": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 255,
                    "description": "No CR/LF"
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "marketing",
                      "transactional"
                    ],
                    "default": "marketing"
                  },
                  "data": {
                    "type": "object",
                    "description": "Sample merge data"
                  },
                  "from_name": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "No CR/LF; default is the workspace name"
                  },
                  "variant": {
                    "type": "string",
                    "enum": [
                      "A",
                      "B",
                      "C",
                      "D"
                    ],
                    "description": "Needs campaign_id. An A/B variant of the campaign: its subject and preheader are used. {id} must be the campaign's design or the variant's design, else 422 design_not_found; the design rendered is {id}. Unknown label: 422 validation"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{subject, preheader, html, text, from: {name, address}}. address is null unless campaign_id is given and the campaign has a sender (local part + domain). `warnings`: list of keys, empty when a real send of the campaign would pass: `domain_not_verified` (domain missing or not verified; `from.address` stays null), `dmarc_missing` (domain verified but no DMARC), `sender_incomplete` (from_name or from_local_part blank; `from.name` then falls back to the workspace name), `subject_missing`, `design_missing` (the campaign has no design). Same checks as the send (`SendReadiness`), judged on the campaign's saved values. A plain design preview always returns `[]`. An explicit preheader: null clears the campaign's preheader."
          },
          "401": {
            "description": "unauthenticated"
          },
          "403": {
            "description": "forbidden (needs designs.edit)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "design not found in this workspace"
          },
          "422": {
            "description": "error_key business_details_missing (marketing, the workspace lacks name, address or contact) | template_syntax_error (subject or preheader is not a valid template; field, line, reason) | design_not_found (campaign_id given and {id} is not that campaign's design) | render_failed (the saved design no longer renders), or Laravel validation format (message, errors), including data over the send limits",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/ui/images": {
      "post": {
        "summary": "Upload an image for the editor (multipart, field file; jpg jpeg png gif webp, 5 MB)",
        "security": [
          {
            "uiToken": []
          }
        ],
        "responses": {
          "201": {
            "description": "{url}"
          },
          "401": {
            "description": "unauthenticated"
          },
          "403": {
            "description": "forbidden (needs designs.edit)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        }
      }
    },
    "/v1/ui/automated": {
      "get": {
        "summary": "List automated messages",
        "security": [
          {
            "uiToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Automated messages"
          },
          "401": {
            "description": "unauthenticated"
          },
          "403": {
            "description": "forbidden (needs automated.manage)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/ui/automated/{id}": {
      "patch": {
        "summary": "Configure an automated message (never its key, name or category)",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [],
                "properties": {
                  "design_id": {
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "subject": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 300,
                    "pattern": "^[^\\r\\n]*$",
                    "description": "No line breaks (CR or LF)"
                  },
                  "preheader": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 300,
                    "pattern": "^[^\\r\\n]*$",
                    "description": "No line breaks (CR or LF)"
                  },
                  "from_name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100
                  },
                  "from_local_part": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "pattern": "^[A-Za-z0-9._+-]{1,64}$"
                  },
                  "sending_domain_id": {
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "reply_to": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 254
                  },
                  "active": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Automated message"
          },
          "401": {
            "description": "unauthenticated"
          },
          "403": {
            "description": "forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "error_key automated_message_incomplete (active:true while not configured) | template_syntax_error (subject or preheader is not a valid template; field, line, reason) | domain_not_found / design_not_found (deleted between validation and the save), or Laravel validation format",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/source": {
      "get": {
        "summary": "The calling host's settings: webhook url and the opt-in events it chose",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "{slug, webhook_url, webhook_events}"
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "summary": "Choose which opt-in webhook events to receive (replaces the list)",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "webhook_events"
                ],
                "properties": {
                  "webhook_events": {
                    "type": "array",
                    "description": "Distinct values from: delivery.sent, delivery.failed, delivery.opened, delivery.clicked, campaign.completed, campaign.winner_selected, campaign.testing_started. Always-on events are refused.",
                    "items": {
                      "type": "string",
                      "enum": [
                        "delivery.sent",
                        "delivery.failed",
                        "delivery.opened",
                        "delivery.clicked",
                        "campaign.completed",
                        "campaign.winner_selected",
                        "campaign.testing_started"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{slug, webhook_url, webhook_events}"
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        }
      }
    },
    "/v1/ui/suppressions": {
      "get": {
        "summary": "List the workspace's suppressions",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{data: [{email, scope, reason, created_at}], meta: {page, per_page, total}}, newest first, 50 per page"
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (needs reports.view)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "use_post_search (an `email` query parameter was sent, whatever its value); page out of range keeps Laravel's validation format",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "The `email` query parameter was removed in H9a (28/10/2026): any `email` in the query is a 422 `use_post_search`. Search one address with POST .../suppressions/search. An erased row (H9a) shows `email: \"[erased]\"`, never the address."
      }
    },
    "/v1/ui/suppressions/search": {
      "post": {
        "summary": "Find one address in the workspace's suppressions (body, not URL)",
        "security": [
          {
            "uiToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "maxLength": 254,
                    "description": "One address, matched exactly after trimming and lower-casing (never a prefix search)"
                  },
                  "page": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{data: [{email, scope, reason, created_at}], meta: {page, per_page, total}}, newest first, 50 per page (same shape as GET suppressions)"
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (needs reports.view)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        },
        "description": "An erased row (H9a) is found by hashing the searched address and shows `email: \"[erased]\"`, reason `erased` (marketing) or its original reason (all)."
      }
    },
    "/v1/workspaces/{externalId}/campaigns": {
      "post": {
        "summary": "Create a draft campaign",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "design_id",
                  "subject",
                  "from_name",
                  "from_local_part",
                  "sending_domain_id"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "design_id": {
                    "type": "integer"
                  },
                  "subject": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "Mustache template"
                  },
                  "preheader": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 300,
                    "description": "Mustache template"
                  },
                  "from_name": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "from_local_part": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9._+-]{0,63}$"
                  },
                  "sending_domain_id": {
                    "type": "integer"
                  },
                  "reply_to": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 254
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "marketing"
                    ]
                  },
                  "external_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100,
                    "description": "Unique per workspace; a repeat returns the existing campaign with 200"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Repeat external_id: the existing campaign, unchanged",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Campaign"
                }
              }
            }
          },
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Campaign"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "design_not_found, domain_not_found, template_syntax_error (subject or preheader, same shape as designs). Laravel validation format (message, errors) for fields."
          }
        }
      },
      "get": {
        "summary": "List campaigns, newest first",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "scheduled",
                "testing",
                "sending",
                "sent",
                "cancelled"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{data: [Campaign], meta: {page, per_page, total}}, newest first, 50 per page"
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": " Laravel validation format (message, errors) for fields."
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/campaigns/{id}": {
      "get": {
        "summary": "Read a campaign",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Campaign"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found or not_found (also another workspace's id)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a draft campaign (all fields optional)",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "design_id": {
                    "type": "integer"
                  },
                  "subject": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "Mustache template"
                  },
                  "preheader": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 300,
                    "description": "Mustache template"
                  },
                  "from_name": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "from_local_part": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9._+-]{0,63}$"
                  },
                  "sending_domain_id": {
                    "type": "integer"
                  },
                  "reply_to": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 254
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "marketing"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Campaign"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found or not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "campaign_not_editable (not a draft)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "design_not_found, domain_not_found, template_syntax_error. Laravel validation format (message, errors) for fields."
          }
        }
      },
      "delete": {
        "summary": "Delete a draft campaign and its pending recipients",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found or not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "campaign_not_editable (not a draft)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/campaigns/{id}/recipients": {
      "post": {
        "summary": "Add one chunk of recipients to a draft or scheduled campaign (max 1000 per call; repeating a chunk is harmless)",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "recipients"
                ],
                "properties": {
                  "recipients": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 1000,
                    "items": {
                      "type": "object",
                      "required": [
                        "email"
                      ],
                      "properties": {
                        "email": {
                          "type": "string",
                          "maxLength": 254
                        },
                        "external_contact_id": {
                          "type": "string",
                          "maxLength": 100
                        },
                        "data": {
                          "type": "object",
                          "description": "Merge data. Same limits as automated sends (16,384 bytes, 4 levels, items at most 20). Over a limit: 422 on recipients.N.data. See docs/api/merge-data.md"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "added = new deliveries; duplicates = addresses already in the campaign or repeated in the chunk; recipients_count = campaign total",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "added": {
                      "type": "integer"
                    },
                    "duplicates": {
                      "type": "integer"
                    },
                    "recipients_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or wrong source key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found or not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "campaign_not_editable (campaign is not draft or scheduled)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "too_many_recipients (campaign would pass mailer.campaigns.max_recipients, 200000), or Laravel validation format (message, errors) for fields. The whole chunk is refused."
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/campaigns/{id}/schedule": {
      "post": {
        "summary": "Schedule a draft campaign (send now or at send_at). The scheduler starts it within a minute of scheduled_at",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "send_at": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "ISO 8601 only (YYYY-MM-DDTHH:MM, optional :SS and fraction, optional Z or ±HH:MM). Without an offset read in the workspace timezone; with one as given. Omitted, null or in the past = now. At most 90 days ahead. Other formats or phrases (tomorrow) are 422 on send_at. 404 and 409 are answered before send_at is validated"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Campaign with status scheduled and scheduled_at (UTC)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Campaign"
                }
              }
            }
          },
          "401": {
            "description": "Missing or wrong source key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found or not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "campaign_not_schedulable (status not draft)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "First failed check, in order: campaign_has_no_recipients, ab_test_too_few_recipients (ceil(recipients_count * test_percent / 100) < 2 per variant), workspace_suspended, workspace_paused, design_not_found (design deleted or unset), template_syntax_error (with field, line, reason; variant fields are variants.N.subject, variants.N.preheader, variants.N.html), domain_not_found (domain deleted or unset), domain_not_verified, dmarc_missing, business_details_missing. Field validation (send_at) keeps Laravel's 422 format",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/campaigns/{id}/cancel": {
      "post": {
        "summary": "Cancel a draft, scheduled, testing or sending campaign. Pending and queued recipients end skipped / campaign_cancelled at once; a row already being handed to SES finishes",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign with status cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Campaign"
                }
              }
            }
          },
          "401": {
            "description": "Missing or wrong source key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found or not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "campaign_not_cancellable (sent or cancelled)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/ui/campaigns": {
      "get": {
        "summary": "List campaigns (UI token, reports.view; same JSON as the host route)",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "scheduled",
                "testing",
                "sending",
                "sent",
                "cancelled"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{data: [Campaign], meta: {page, per_page, total}}, newest first, 50 per page"
          },
          "401": {
            "description": "unauthenticated"
          },
          "403": {
            "description": "forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": " Laravel validation format (message, errors) for fields."
          }
        }
      }
    },
    "/v1/ui/campaigns/{id}": {
      "get": {
        "summary": "Read a campaign (UI token, reports.view)",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Campaign"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated"
          },
          "403": {
            "description": "forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/ui/designs/{id}/test-send": {
      "post": {
        "summary": "Test-send a design to up to 5 addresses (UI token, designs.test_send)",
        "description": "A real marketing send for testing: subject starts with the advertisement prefix then the test marker ([בדיקה] or [Test]), footer and unsubscribe link included (the link suppresses nothing). No webhooks, no campaign link. Refused while the workspace is paused or suspended. The unsubscribe link in a test email is real (it suppresses marketing for that address) but sends no webhook.",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emails",
                  "subject"
                ],
                "properties": {
                  "emails": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 5,
                    "items": {
                      "type": "string",
                      "format": "email",
                      "maxLength": 254
                    },
                    "description": "1 to 5 distinct addresses"
                  },
                  "subject": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "No CR/LF"
                  },
                  "preheader": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 255
                  },
                  "from_name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 255,
                    "description": "Default: workspace name"
                  },
                  "from_local_part": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 64,
                    "description": "Default: test"
                  },
                  "sending_domain_id": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "Default: first verified domain with DMARC"
                  },
                  "reply_to": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "email"
                  },
                  "data": {
                    "type": "object",
                    "description": "Merge data (same limits as a send)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "{deliveries: [{id, email, status}]}, one per address"
          },
          "401": {
            "description": "Missing or wrong credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (needs designs.test_send; designs.edit is not enough)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation error, template_syntax_error, workspace_paused, workspace_suspended, business_details_missing, no_verified_domain, domain_not_verified, dmarc_missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "test_send_rate_limited (30 addresses per workspace per hour)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/designs/{id}/test-send": {
      "post": {
        "summary": "Test-send a design to up to 5 addresses",
        "description": "A real marketing send for testing: subject starts with the advertisement prefix then the test marker ([בדיקה] or [Test]), footer and unsubscribe link included (the link suppresses nothing). No webhooks, no campaign link. Refused while the workspace is paused or suspended. The unsubscribe link in a test email is real (it suppresses marketing for that address) but sends no webhook.",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emails",
                  "subject"
                ],
                "properties": {
                  "emails": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 5,
                    "items": {
                      "type": "string",
                      "format": "email",
                      "maxLength": 254
                    },
                    "description": "1 to 5 distinct addresses"
                  },
                  "subject": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "No CR/LF"
                  },
                  "preheader": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 255
                  },
                  "from_name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 255,
                    "description": "Default: workspace name"
                  },
                  "from_local_part": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 64,
                    "description": "Default: test"
                  },
                  "sending_domain_id": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "Default: first verified domain with DMARC"
                  },
                  "reply_to": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "email"
                  },
                  "data": {
                    "type": "object",
                    "description": "Merge data (same limits as a send)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "{deliveries: [{id, email, status}]}, one per address"
          },
          "401": {
            "description": "Missing or wrong credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation error, template_syntax_error, workspace_paused, workspace_suspended, business_details_missing, no_verified_domain, domain_not_verified, dmarc_missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "test_send_rate_limited (30 addresses per workspace per hour)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/designs/{id}/preview": {
      "post": {
        "summary": "Preview a design or a campaign's email (host key; same body and answer as the UI route)",
        "description": "Renders through the same code as a send: marketing subject prefix, footer in the workspace locale, merge and escaping. The unsubscribe URL is a fake (<public_url>/u/preview) that the public page answers as an invalid token. With campaign_id the preview uses the campaign's design, subject, preheader and sender.",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [],
                "properties": {
                  "campaign_id": {
                    "type": "integer",
                    "description": "A campaign of this workspace whose design is {id}. Its subject, preheader, from_name and sender address become the defaults (category is always marketing); fields sent explicitly win. Replaces the required subject."
                  },
                  "subject": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "No CR/LF"
                  },
                  "preheader": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 255,
                    "description": "No CR/LF"
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "marketing",
                      "transactional"
                    ],
                    "default": "marketing"
                  },
                  "data": {
                    "type": "object",
                    "description": "Sample merge data"
                  },
                  "from_name": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "No CR/LF; default is the workspace name"
                  },
                  "variant": {
                    "type": "string",
                    "enum": [
                      "A",
                      "B",
                      "C",
                      "D"
                    ],
                    "description": "Needs campaign_id. An A/B variant of the campaign: its subject and preheader are used. {id} must be the campaign's design or the variant's design, else 422 design_not_found; the design rendered is {id}. Unknown label: 422 validation"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{subject, preheader, html, text, from: {name, address}}. address is null unless campaign_id is given and the campaign has a sender (local part + domain). `warnings`: list of keys, empty when a real send of the campaign would pass: `domain_not_verified` (domain missing or not verified; `from.address` stays null), `dmarc_missing` (domain verified but no DMARC), `sender_incomplete` (from_name or from_local_part blank; `from.name` then falls back to the workspace name), `subject_missing`, `design_missing` (the campaign has no design). Same checks as the send (`SendReadiness`), judged on the campaign's saved values. A plain design preview always returns `[]`. An explicit preheader: null clears the campaign's preheader."
          },
          "403": {
            "description": "forbidden (workspace belongs to another source)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "design not found in this workspace"
          },
          "422": {
            "description": "error_key business_details_missing (marketing, the workspace lacks name, address or contact) | template_syntax_error (subject or preheader is not a valid template; field, line, reason) | design_not_found (campaign_id given and {id} is not that campaign's design) | render_failed (the saved design no longer renders), or Laravel validation format (message, errors), including data over the send limits",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/campaigns/{id}/test-send": {
      "post": {
        "summary": "Test-send a campaign's design and subject to up to 5 addresses (the campaign itself is not changed)",
        "description": "A real marketing send for testing: subject starts with the advertisement prefix then the test marker ([בדיקה] or [Test]), footer and unsubscribe link included (the link suppresses nothing). No webhooks, no campaign link. Refused while the workspace is paused or suspended. The unsubscribe link in a test email is real (it suppresses marketing for that address) but sends no webhook.",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emails"
                ],
                "properties": {
                  "emails": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 5,
                    "items": {
                      "type": "string",
                      "format": "email",
                      "maxLength": 254
                    },
                    "description": "1 to 5 distinct addresses"
                  },
                  "data": {
                    "type": "object",
                    "description": "Merge data (same limits as a send)"
                  },
                  "variant": {
                    "type": "string",
                    "enum": [
                      "A",
                      "B",
                      "C",
                      "D"
                    ],
                    "description": "An A/B variant of this campaign: its subject, preheader and design are sent instead of the campaign's. Unknown label: 422 validation"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "{deliveries: [{id, email, status}]}, one per address"
          },
          "401": {
            "description": "Missing or wrong credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Validation error, template_syntax_error, design_not_found, campaign_incomplete, workspace_paused, workspace_suspended, business_details_missing, no_verified_domain, domain_not_verified, dmarc_missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "test_send_rate_limited (30 addresses per workspace per hour)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/deliveries/{id}": {
      "get": {
        "summary": "Read one delivery with its events",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{id, campaign_id, automated_key, variant, is_test, email, external_contact_id, category, status, skip_reason, failure_reason, scheduled_for, sent_at, events: [{type, occurred_at, url, bounce_type}]}. status is pending|queued|sent|skipped|failed (an internal sending claim reads as queued)"
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/ui/deliveries/{id}": {
      "get": {
        "summary": "Read one delivery with its events (UI token, reports.view)",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{id, campaign_id, automated_key, variant, is_test, email, external_contact_id, category, status, skip_reason, failure_reason, scheduled_for, sent_at, events: [{type, occurred_at, url, bounce_type}]}. status is pending|queued|sent|skipped|failed (an internal sending claim reads as queued)"
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/campaigns/{id}/deliveries": {
      "get": {
        "summary": "Recipient activity of a campaign",
        "description": "Real sends only (test sends are left out). Ordered by id, 50 per page. The response carries email addresses: filter by status and activity only, never put an address in a URL.",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "queued",
                "sent",
                "skipped",
                "failed"
              ]
            }
          },
          {
            "name": "activity",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "opened",
                "clicked",
                "unsubscribed",
                "bounced",
                "complained"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{data: [{id, email, external_contact_id, variant, status, skip_reason, failure_reason, sent_at, delivered, opened, clicked, unsubscribed, bounced, complained}], meta: {page, per_page, total}}"
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        }
      }
    },
    "/v1/ui/campaigns/{id}/deliveries": {
      "get": {
        "summary": "Recipient activity of a campaign (UI token, reports.view)",
        "description": "Real sends only (test sends are left out). Ordered by id, 50 per page. The response carries email addresses: filter by status and activity only, never put an address in a URL.",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "queued",
                "sent",
                "skipped",
                "failed"
              ]
            }
          },
          {
            "name": "activity",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "opened",
                "clicked",
                "unsubscribed",
                "bounced",
                "complained"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{data: [{id, email, external_contact_id, variant, status, skip_reason, failure_reason, sent_at, delivered, opened, clicked, unsubscribed, bounced, complained}], meta: {page, per_page, total}}"
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/domains": {
      "get": {
        "summary": "List the workspace's sending domains with DNS records (read-only; same list as GET /v1/ui/domains)",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Domains: id, domain, status, dmarc_found, verified_at, dmarc_lost_at, held_until, records"
          },
          "401": {
            "description": "unauthenticated"
          },
          "404": {
            "description": "workspace_not_found (unknown, or owned by another source)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/campaigns/{id}/report": {
      "get": {
        "summary": "Campaign report: counts, rates, links",
        "description": "Counts exclude test sends. unique = deliveries with at least one event of that type (Mailer or SES); total = Mailer events, plus SES events only for deliveries with no Mailer event of that type. Open rates are inflated by Apple Mail Privacy Protection and bots. delivered is estimated from sent when no delivered event exists (delivered_estimated). links are grouped by the original URL, top 100 by clicked_unique.",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignReport"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/ui/campaigns/{id}/report": {
      "get": {
        "summary": "Campaign report (UI token, reports.view)",
        "description": "Counts exclude test sends. unique = deliveries with at least one event of that type (Mailer or SES); total = Mailer events, plus SES events only for deliveries with no Mailer event of that type. Open rates are inflated by Apple Mail Privacy Protection and bots. delivered is estimated from sent when no delivered event exists (delivered_estimated). links are grouped by the original URL, top 100 by clicked_unique.",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignReport"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (token lacks reports.view)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/campaigns/{id}/timeline": {
      "get": {
        "summary": "Campaign timeline",
        "description": "Points per hour or day in the workspace timezone, empty buckets included, from started_at to the earlier of now and started_at + 7 days (hour) / 60 days (day). sent by sent_at; delivered, unsubscribed, bounced, complained by event time; opened/clicked = deliveries whose first open/click fell in the bucket. Not started: points []. Test sends excluded.",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "bucket",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "hour",
                "day"
              ],
              "default": "hour"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign timeline",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Timeline"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        }
      }
    },
    "/v1/ui/campaigns/{id}/timeline": {
      "get": {
        "summary": "Campaign timeline (UI token, reports.view)",
        "description": "Points per hour or day in the workspace timezone, empty buckets included, from started_at to the earlier of now and started_at + 7 days (hour) / 60 days (day). sent by sent_at; delivered, unsubscribed, bounced, complained by event time; opened/clicked = deliveries whose first open/click fell in the bucket. Not started: points []. Test sends excluded.",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "bucket",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "hour",
                "day"
              ],
              "default": "hour"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign timeline",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Timeline"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (token lacks reports.view)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/automated/{key}/report": {
      "get": {
        "summary": "Automated message report",
        "description": "Report for one automated message key (an automation step). from/to are whole days in the workspace timezone (default the last 30 days including today, max 366). Counts cover deliveries created in the range; days[] buckets sends by sent_at and events by their own time. Counts exclude test sends; same counting rules as the campaign report.",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "key",
            "in": "path",
            "required": true,
            "description": "Automated message key (a-z0-9_-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "YYYY-MM-DD in the workspace timezone. Default: 29 days before `to`",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "YYYY-MM-DD in the workspace timezone, on or after `from`, at most 366 days inclusive. Default: today",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Automated message report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomatedReport"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "automated_message_not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        }
      }
    },
    "/v1/ui/automated/{key}/report": {
      "get": {
        "summary": "Automated message report (UI token, reports.view)",
        "description": "Report for one automated message key (an automation step). from/to are whole days in the workspace timezone (default the last 30 days including today, max 366). Counts cover deliveries created in the range; days[] buckets sends by sent_at and events by their own time. Counts exclude test sends; same counting rules as the campaign report.",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "description": "Automated message key (a-z0-9_-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "YYYY-MM-DD in the workspace timezone. Default: 29 days before `to`",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "YYYY-MM-DD in the workspace timezone, on or after `from`, at most 366 days inclusive. Default: today",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Automated message report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomatedReport"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (token lacks reports.view)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "automated_message_not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/reports/automated": {
      "get": {
        "summary": "All automated message reports",
        "description": "Every automated message with counts and rates in one call (a whole flow canvas), ordered by key. from/to are whole days in the workspace timezone (default the last 30 days including today, max 366). Counts cover deliveries created in the range; days[] buckets sends by sent_at and events by their own time. Counts exclude test sends; same counting rules as the campaign report.",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "YYYY-MM-DD in the workspace timezone. Default: 29 days before `to`",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "YYYY-MM-DD in the workspace timezone, on or after `from`, at most 366 days inclusive. Default: today",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "All automated message reports",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomatedReportList"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        }
      }
    },
    "/v1/ui/reports/automated": {
      "get": {
        "summary": "All automated message reports (UI token, reports.view)",
        "description": "Every automated message with counts and rates in one call (a whole flow canvas), ordered by key. from/to are whole days in the workspace timezone (default the last 30 days including today, max 366). Counts cover deliveries created in the range; days[] buckets sends by sent_at and events by their own time. Counts exclude test sends; same counting rules as the campaign report.",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "YYYY-MM-DD in the workspace timezone. Default: 29 days before `to`",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "YYYY-MM-DD in the workspace timezone, on or after `from`, at most 366 days inclusive. Default: today",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "All automated message reports",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomatedReportList"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (token lacks reports.view)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/reports/summary": {
      "get": {
        "summary": "Workspace summary report",
        "description": "Dashboard totals split into campaigns (deliveries with a campaign) and automated (all other real deliveries); campaigns.count = campaigns that started in the range. from/to are whole days in the workspace timezone (default the last 30 days including today, max 366). Counts cover deliveries created in the range; days[] buckets sends by sent_at and events by their own time. Counts exclude test sends; same counting rules as the campaign report.",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "YYYY-MM-DD in the workspace timezone. Default: 29 days before `to`",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "YYYY-MM-DD in the workspace timezone, on or after `from`, at most 366 days inclusive. Default: today",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Workspace summary report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SummaryReport"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        }
      }
    },
    "/v1/ui/reports/summary": {
      "get": {
        "summary": "Workspace summary report (UI token, reports.view)",
        "description": "Dashboard totals split into campaigns (deliveries with a campaign) and automated (all other real deliveries); campaigns.count = campaigns that started in the range. from/to are whole days in the workspace timezone (default the last 30 days including today, max 366). Counts cover deliveries created in the range; days[] buckets sends by sent_at and events by their own time. Counts exclude test sends; same counting rules as the campaign report.",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "YYYY-MM-DD in the workspace timezone. Default: 29 days before `to`",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "YYYY-MM-DD in the workspace timezone, on or after `from`, at most 366 days inclusive. Default: today",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Workspace summary report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SummaryReport"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (token lacks reports.view)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/usage": {
      "get": {
        "summary": "Emails sent per day and month",
        "description": "For host billing and plan limits. Counts deliveries with status sent by sent_at, per local day in the workspace timezone. marketing and transactional exclude test sends; all = marketing + transactional (no test); test = test sends, counted apart. days lists every day of the month up to today (whole month for a past month, empty for a future one); today is always the current local day.",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "month",
            "in": "query",
            "required": false,
            "description": "YYYY-MM in the workspace timezone, year 2000-2100. Default (also for an empty value): the current local month",
            "schema": {
              "type": "string",
              "pattern": "^(20\\d{2}|2100)-(0[1-9]|1[0-2])$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Usage for one month: totals, days and today, counted by sent_at in the workspace timezone",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found (no workspace with this externalId for the calling source)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors.month): month not YYYY-MM, year outside 2000-2100, or an array. An empty month= is treated as absent"
          }
        }
      }
    },
    "/v1/ui/usage": {
      "get": {
        "summary": "Emails sent per day and month (UI token, reports.view)",
        "description": "For host billing and plan limits. Counts deliveries with status sent by sent_at, per local day in the workspace timezone. marketing and transactional exclude test sends; all = marketing + transactional (no test); test = test sends, counted apart. days lists every day of the month up to today (whole month for a past month, empty for a future one); today is always the current local day.",
        "security": [
          {
            "uiToken": []
          }
        ],
        "parameters": [
          {
            "name": "month",
            "in": "query",
            "required": false,
            "description": "YYYY-MM in the workspace timezone, year 2000-2100. Default (also for an empty value): the current local month",
            "schema": {
              "type": "string",
              "pattern": "^(20\\d{2}|2100)-(0[1-9]|1[0-2])$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Usage for one month: totals, days and today, counted by sent_at in the workspace timezone",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (token lacks reports.view)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors.month): month not YYYY-MM, year outside 2000-2100, or an array. An empty month= is treated as absent"
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/campaigns/{id}/ab-test": {
      "put": {
        "summary": "Define or replace the A/B test of a draft campaign",
        "description": "Replaces every variant in one transaction. Labels are given in order A, B, C, D. Running the test (split at start, testing, decide, winner) is in POST .../ab-test/winner and docs/api/ab-tests.md.",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "variants",
                  "test_percent",
                  "winner_metric"
                ],
                "properties": {
                  "variants": {
                    "type": "array",
                    "minItems": 2,
                    "maxItems": 4,
                    "items": {
                      "type": "object",
                      "required": [
                        "subject"
                      ],
                      "properties": {
                        "subject": {
                          "type": "string",
                          "maxLength": 255,
                          "description": "Mustache template"
                        },
                        "preheader": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "maxLength": 300,
                          "description": "Mustache template; null = the campaign's"
                        },
                        "design_id": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "description": "A design of this workspace; null = the campaign's"
                        }
                      }
                    }
                  },
                  "test_percent": {
                    "type": "integer",
                    "minimum": 10,
                    "maximum": 50,
                    "description": "Share of recipients in the test, split equally between variants"
                  },
                  "winner_metric": {
                    "type": "string",
                    "enum": [
                      "open_rate",
                      "click_rate",
                      "manual"
                    ]
                  },
                  "decide_after_hours": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "maximum": 72,
                    "description": "Required unless winner_metric is manual"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Campaign with ab_test",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Campaign"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found or not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "campaign_not_editable (not a draft)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "ab_test_needs_tracking (winner_metric open_rate or click_rate while the workspace has tracking_enabled false; manual is accepted), design_not_found (a variant design not in this workspace), template_syntax_error (field variants.N.subject or variants.N.preheader, with line and reason). Laravel validation format (message, errors) for fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Remove the A/B test of a draft campaign",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign with ab_test null",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Campaign"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found or not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "campaign_not_editable (not a draft)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/campaigns/{id}/ab-test/winner": {
      "post": {
        "summary": "Pick the A/B winner (host)",
        "description": "Only while the campaign is testing, whatever winner_metric (overrides an automatic decision that has not happened yet). The campaign becomes sending; the recipients not in the test get the winner. Webhook campaign.winner_selected with decided_by host.",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric id",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "variant"
                ],
                "properties": {
                  "variant": {
                    "type": "string",
                    "enum": [
                      "A",
                      "B",
                      "C",
                      "D"
                    ],
                    "description": "An existing label of this campaign's test"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Campaign (status sending, ab_test.winner, ab_test.winner_decided_by host)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Campaign"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found or not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "campaign_not_testing (not testing: draft, scheduled, sending, finished, already decided, or no A/B test); answered before the body is validated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors) for variant: missing, or a label this campaign does not have",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/deliverability": {
      "get": {
        "summary": "Deliverability: domain health, rates, warm-up, pause state",
        "description": "Domain health (stored SES statuses of the last check, DMARC), hard-bounce and complaint rates against the pause thresholds, per-domain warm-up, the workspace pause state and the service-wide account hold. Reads stored state only: no SES or DNS call. Rates cover window_start to now (7 days, or less after an operator resume), non-test sends.",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deliverability of the workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Deliverability"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found (no workspace with this externalId for the calling source)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/ui/deliverability": {
      "get": {
        "summary": "Deliverability (UI token, domains.manage)",
        "description": "Domain health (stored SES statuses of the last check, DMARC), hard-bounce and complaint rates against the pause thresholds, per-domain warm-up, the workspace pause state and the service-wide account hold. Reads stored state only: no SES or DNS call. Rates cover window_start to now (7 days, or less after an operator resume), non-test sends.",
        "security": [
          {
            "uiToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Deliverability of the workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Deliverability"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (token lacks domains.manage)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/orders": {
      "post": {
        "summary": "Report an order; the Mailer attributes it to the contact's last click within attribution_window_days (idempotent per order_id). No email or contact id is stored",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "order_id",
                  "value"
                ],
                "properties": {
                  "order_id": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "The host's order id, a JSON string (send numeric ids as \"1001\", not 1001); unique per workspace. A repeat answers 200 with the first result and changes nothing"
                  },
                  "email": {
                    "type": "string",
                    "maxLength": 254,
                    "description": "Required without external_contact_id. Trimmed and lower-cased before matching; never stored",
                    "format": "email"
                  },
                  "external_contact_id": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "Required without email. When given, it decides the match (the email is then not used); never stored"
                  },
                  "value": {
                    "oneOf": [
                      {
                        "type": "string",
                        "pattern": "^\\d{1,10}(\\.\\d{1,2})?$"
                      },
                      {
                        "type": "number",
                        "minimum": 0,
                        "maximum": 9999999999.99,
                        "multipleOf": 0.01
                      }
                    ],
                    "description": "Decimal >= 0, at most 2 decimals, at most 9999999999.99. A string is safest (\"120.50\")"
                  },
                  "currency": {
                    "type": "string",
                    "pattern": "^[A-Z]{3}$",
                    "description": "Three uppercase letters, default ILS. Checked by shape only, not against the ISO 4217 list"
                  },
                  "occurred_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "ISO 8601, default now. Without an offset it is read in the workspace timezone (like campaign send_at). Not later than now + 5 minutes"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Stored (attributed or not)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                }
              }
            }
          },
          "200": {
            "description": "Repeat of an order_id already stored: the first result, nothing changed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found (unknown or another source's workspace)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors)"
          }
        }
      }
    },
    "/v1/ui/countdown-urls": {
      "post": {
        "summary": "Create a signed countdown image URL (/t/{token}.png) for a design. No contact, address or workspace in the token",
        "security": [
          {
            "uiToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "deadline"
                ],
                "properties": {
                  "deadline": {
                    "type": "string",
                    "format": "date-time",
                    "maxLength": 40,
                    "description": "Strict ISO 8601 with an offset or Z (2026-12-01T00:00:00+02:00; seconds and fraction optional), after now and before now + 365 days. Without an offset, or relative words (\"tomorrow Z\"): 422",
                    "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(:\\d{2}(\\.\\d+)?)?(Z|[+-]\\d{2}:\\d{2})$"
                  },
                  "locale": {
                    "type": "string",
                    "enum": [
                      "he",
                      "en"
                    ],
                    "description": "Labels language; default the workspace locale. he: ימים / שעות / דקות, ended המבצע הסתיים; en: days / hours / minutes, ended Offer ended"
                  },
                  "theme": {
                    "type": "object",
                    "additionalProperties": false,
                    "properties": {
                      "background": {
                        "type": "string",
                        "pattern": "^#[0-9A-Fa-f]{6}$",
                        "description": "Default #111827"
                      },
                      "color": {
                        "type": "string",
                        "pattern": "^#[0-9A-Fa-f]{6}$",
                        "description": "Text colour, default #FFFFFF"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Signed URL; nothing is stored (the deadline, locale and colours live in the signed token)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CountdownUrl"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated"
          },
          "403": {
            "description": "forbidden (needs designs.edit)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors): deadline, locale, theme, theme.background, theme.color"
          },
          "429": {
            "description": "countdown_url_rate_limited: more than 60 URLs a minute from this API key (host route) or this UI token (/v1/ui). Only successful creates count. Retry-After gives the seconds to wait",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/countdown-urls": {
      "post": {
        "summary": "Create a signed countdown image URL (/t/{token}.png) for a design. No contact, address or workspace in the token",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "deadline"
                ],
                "properties": {
                  "deadline": {
                    "type": "string",
                    "format": "date-time",
                    "maxLength": 40,
                    "description": "Strict ISO 8601 with an offset or Z (2026-12-01T00:00:00+02:00; seconds and fraction optional), after now and before now + 365 days. Without an offset, or relative words (\"tomorrow Z\"): 422",
                    "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(:\\d{2}(\\.\\d+)?)?(Z|[+-]\\d{2}:\\d{2})$"
                  },
                  "locale": {
                    "type": "string",
                    "enum": [
                      "he",
                      "en"
                    ],
                    "description": "Labels language; default the workspace locale. he: ימים / שעות / דקות, ended המבצע הסתיים; en: days / hours / minutes, ended Offer ended"
                  },
                  "theme": {
                    "type": "object",
                    "additionalProperties": false,
                    "properties": {
                      "background": {
                        "type": "string",
                        "pattern": "^#[0-9A-Fa-f]{6}$",
                        "description": "Default #111827"
                      },
                      "color": {
                        "type": "string",
                        "pattern": "^#[0-9A-Fa-f]{6}$",
                        "description": "Text colour, default #FFFFFF"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Signed URL; nothing is stored (the deadline, locale and colours live in the signed token)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CountdownUrl"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found (unknown or another source's workspace)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors): deadline, locale, theme, theme.background, theme.color"
          },
          "429": {
            "description": "countdown_url_rate_limited: more than 60 URLs a minute from this API key (host route) or this UI token (/v1/ui). Only successful creates count. Retry-After gives the seconds to wait",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workspaces/{externalId}/erasures": {
      "post": {
        "summary": "Right to erasure for one address in the workspace (host key only, idempotent, audited by keyed hash)",
        "description": "Skips the address's pending/queued deliveries (skip_reason erased), anonymises its delivery history, purges it from stored webhook payloads, and turns its suppressions into hash-only rows so it stays blocked for marketing forever (never liftable; an all-scope row keeps blocking every category). A repeat call changes nothing and returns zero counts. Every call appends an audit row (workspace, keyed hash, counts, time). No UI-token route. Docs: docs/api/erasures.md The address hash is keyed with MAILER_HASH_KEY.",
        "security": [
          {
            "sourceKey": []
          }
        ],
        "parameters": [
          {
            "name": "externalId",
            "in": "path",
            "required": true,
            "description": "The host's id for the workspace (A-Za-z0-9._:-, 1-100)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "maxLength": 254,
                    "description": "Trimmed and lower-cased before matching"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{erased: true, counts: {...}} rows changed per table by this call",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "erased",
                    "counts"
                  ],
                  "properties": {
                    "erased": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "counts": {
                      "type": "object",
                      "required": [
                        "deliveries",
                        "deliveries_skipped",
                        "in_flight",
                        "delivery_events",
                        "outbound_webhooks",
                        "suppressions"
                      ],
                      "properties": {
                        "deliveries": {
                          "type": "integer",
                          "description": "Delivery rows anonymised (address replaced by <hash>+<id>@erased.invalid; merge_data, test_payload, external_contact_id nulled; the address in failure_reason becomes [erased]; an idempotency_key containing it becomes erased:<id>; tracked link queries cut). The deliveries APIs then show email \"[erased]\""
                        },
                        "deliveries_skipped": {
                          "type": "integer",
                          "description": "pending/queued deliveries moved to skipped, skip_reason erased"
                        },
                        "in_flight": {
                          "type": "integer",
                          "description": "Deliveries that were sending (in flight): anonymised now and again by a delayed second pass (FinishErasure, about 20 minutes later)"
                        },
                        "delivery_events": {
                          "type": "integer",
                          "description": "Event URLs whose query and fragment were cut"
                        },
                        "outbound_webhooks": {
                          "type": "integer",
                          "description": "Stored webhook payloads (any status) about one of the address's deliveries (data.delivery_id) or mentioning it: mentions become <hash>@erased.invalid, data.email the placeholder, data.external_contact_id null"
                        },
                        "suppressions": {
                          "type": "integer",
                          "description": "Suppression rows made hash-only, inserted (reason erased) or deleted as duplicates"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "workspace_not_found (unknown or another source's workspace)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Laravel validation format (message, errors): email missing, not an address, or over 254"
          }
        }
      }
    }
  }
}