{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/coldrig-dev/json-schema/coldrig-dev-launch-campaign-input-schema.json",
  "title": "LaunchCampaignInput",
  "x-generated": "2026-11-04",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/coldrig-dev-openapi.yml#/components/schemas/LaunchCampaignInput",
  "type": "object",
  "required": [
    "name",
    "offer",
    "leads",
    "sequence"
  ],
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
    },
    "offer": {
      "type": "string",
      "minLength": 1,
      "maxLength": 2000
    },
    "leads": {
      "type": "array",
      "minItems": 1,
      "maxItems": 5000,
      "items": {
        "$ref": "#/$defs/LeadInput"
      }
    },
    "sequence": {
      "type": "array",
      "minItems": 1,
      "maxItems": 10,
      "items": {
        "$ref": "#/$defs/SequenceStepInput"
      }
    },
    "timezone": {
      "type": "string",
      "default": "UTC"
    },
    "sendWindow": {
      "type": "object",
      "description": "Evaluated in `timezone`. Any field you omit gets the recommended default below — except on a test-mode account, where omitted fields are open every hour of every day until it upgrades and they take the recommended default. The decision is yours; include 0 and 6 in `days` to send on weekends. Setting exactly one of startHour/endHour combines it with the recommended default for the other (08/16, including on a test-mode account, since that value re-resolves to the same default on upgrade) — if that combination would wrap the window past midnight (start > end), the launch is refused; set both explicitly, including for a deliberate overnight window such as {startHour:22, endHour:6}.\n",
      "properties": {
        "startHour": {
          "type": "integer",
          "minimum": 0,
          "maximum": 23
        },
        "endHour": {
          "type": "integer",
          "minimum": 0,
          "maximum": 23,
          "description": "Inclusive — the last hour a send may start in."
        },
        "days": {
          "type": "array",
          "items": {
            "type": "integer",
            "minimum": 0,
            "maximum": 6
          },
          "minItems": 1,
          "maxItems": 7,
          "description": "0 = Sunday … 6 = Saturday."
        }
      },
      "default": {
        "startHour": 8,
        "endHour": 16,
        "days": [
          1,
          2,
          3,
          4,
          5
        ]
      }
    },
    "stopOnReply": {
      "type": "boolean",
      "default": true
    },
    "endBy": {
      "type": "string",
      "format": "date-time",
      "description": "Optional fixed end date — an ISO 8601 instant with a zone. From then on nothing more in this campaign sends: every step still waiting is skipped and recorded as a `failed` event with reason `campaign_ended`. Paid accounts only: a trial account that sends one is refused 400. Must be later than now and later than every step's notBefore, or the launch is refused 400.\n"
    },
    "listSource": {
      "type": "string",
      "maxLength": 500,
      "description": "Where the lead list came from, in a sentence. REQUIRED on a crowdfunding account (a missing or \"unknown\" source is refused 400); optional elsewhere."
    },
    "previewOnly": {
      "type": "boolean",
      "description": "Optional. true = a dry run — every launch check runs and the rendered steps for the first three leads come back with the counts a launch would report; nothing is created or sent."
    },
    "repeatOf": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200,
      "description": "Optional. Send it only to repeat a campaign on purpose: the existingCampaignId that a 409 duplicate_campaign refusal named. Accepted once, and only while it names the most recent campaign of this account with the same contacts and the same subjects and bodies as written; any other value is refused and nothing is created."
    },
    "includeAlreadyEmailed": {
      "type": "boolean",
      "description": "Optional. true also stores and emails the contacts who already got this campaign's first email from another campaign of this account; without it they are skipped and counted in alreadyEmailed."
    }
  },
  "$defs": {
    "LeadAttributes": {
      "type": "object",
      "additionalProperties": false,
      "description": "Optional audience details for this contact. Recorded for your account only, so results can be broken down by audience; never shared. A missing field reads \"unknown\".\n",
      "properties": {
        "role": {
          "type": "string",
          "minLength": 1,
          "maxLength": 120,
          "description": "One line, e.g. \"Head of RevOps\"."
        },
        "seniority": {
          "type": "string",
          "enum": [
            "c_level",
            "vp",
            "director",
            "manager",
            "individual",
            "founder",
            "other"
          ]
        },
        "industry": {
          "type": "string",
          "minLength": 1,
          "maxLength": 120
        },
        "companySize": {
          "type": "string",
          "enum": [
            "1-10",
            "11-50",
            "51-200",
            "201-1000",
            "1001-5000",
            "5001+"
          ]
        },
        "trigger": {
          "type": "string",
          "minLength": 1,
          "maxLength": 120,
          "description": "Why now, e.g. \"raised a Series A\"."
        },
        "timezone": {
          "type": "string",
          "minLength": 1,
          "maxLength": 100,
          "description": "IANA time zone, e.g. \"America/Chicago\"; refused 400 otherwise. The local send hour is recorded in it (the campaign's zone otherwise)."
        }
      }
    },
    "LeadInput": {
      "type": "object",
      "required": [
        "email"
      ],
      "properties": {
        "email": {
          "type": "string",
          "format": "email",
          "description": "Trimmed and lower-cased before it is checked or stored."
        },
        "firstName": {
          "type": "string",
          "maxLength": 200,
          "default": "",
          "description": "Optional. A launch whose subject or body uses {{firstName}} is refused 400, naming how many contacts have none; no fallback word is invented. One line of plain text: a line break, tab or control character is refused (400) naming this field. Right-to-left text and its direction marks are accepted."
        },
        "company": {
          "type": "string",
          "maxLength": 200,
          "default": "",
          "description": "One line of plain text: a line break, tab or control character is refused (400) naming this field. Right-to-left text and its direction marks are accepted."
        },
        "vars": {
          "type": "object",
          "maxProperties": 50,
          "additionalProperties": {
            "type": "string",
            "maxLength": 2000
          },
          "description": "Optional per-lead values for your own {{key}} tokens: word-keys (letters, digits, underscore; up to 64 characters) to one-line plain-text strings (no line breaks, tabs or control characters). Right-to-left text and its direction marks are accepted. A {{key}} used in any subject or body must be present and non-empty on EVERY lead, or the launch is refused 400 naming the token and how many leads lack it. A key may not be firstName or company. A value is inserted once as plain text; a value that itself contains {{...}} reaches the recipient as that literal text.\n"
        },
        "attributes": {
          "$ref": "#/$defs/LeadAttributes"
        }
      }
    },
    "SequenceStepInput": {
      "type": "object",
      "required": [
        "step",
        "body",
        "delayDays"
      ],
      "properties": {
        "step": {
          "type": "integer",
          "minimum": 1
        },
        "subject": {
          "type": "string",
          "minLength": 1,
          "maxLength": 300,
          "description": "Required on step 1, and the first email's subject may not start with \"Re:\" (400). A later step (step > 1) may omit it to keep step 1's subject line (recommended) — it resolves at launch to \"Re: \" + step 1's subject (unchanged if that already starts with \"Re: \"). Set one explicitly on a later step to change only its subject line: every later step is still sent In-Reply-To step 1, though some mail clients display a changed subject as a separate conversation. One line of plain text: a line break, tab or control character is refused (400) naming this field. Right-to-left text and its direction marks are accepted.\n"
        },
        "body": {
          "type": "string",
          "minLength": 1,
          "maxLength": 20000
        },
        "delayDays": {
          "type": "integer",
          "minimum": 0,
          "maximum": 60
        },
        "notBefore": {
          "type": "string",
          "format": "date-time",
          "description": "Optional fixed date this step is held until — an ISO 8601 instant with a zone (e.g. \"2026-11-03T09:00:00-05:00\" or \"...Z\"). The step goes out at the later of notBefore and delayDays after the previous step actually sent, still inside sendWindow. Paid accounts only: a trial account that sends one is refused 400 (use delayDays).\n"
        }
      }
    }
  }
}