{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/coldrig-dev/json-schema/coldrig-dev-launched-campaign-schema.json",
  "title": "LaunchedCampaign",
  "x-generated": "2026-11-04",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/coldrig-dev-openapi.yml#/components/schemas/LaunchedCampaign",
  "type": "object",
  "additionalProperties": true,
  "required": [
    "campaignId",
    "nextSteps"
  ],
  "properties": {
    "campaignId": {
      "type": "string",
      "description": "The campaign's id: the `{id}` path parameter of the other campaign calls and the MCP `campaignId` argument."
    },
    "sendWindow": {
      "$ref": "#/$defs/ResolvedSendWindow"
    },
    "timezone": {
      "type": "string",
      "description": "The IANA timezone the window is evaluated in."
    },
    "contacts": {
      "type": "integer",
      "description": "Contacts stored in the campaign, after repeated addresses were collapsed."
    },
    "suppressed": {
      "type": "integer",
      "description": "Of those, how many are on your suppression list: stored as suppressed, never scheduled."
    },
    "duplicatesRemoved": {
      "type": "integer",
      "description": "Repeated addresses in the request, collapsed to the first."
    },
    "alreadyEmailed": {
      "type": "integer",
      "description": "Contacts left out because they already got this campaign's first email from another campaign: never stored. 0 when includeAlreadyEmailed or repeatOf includes them."
    },
    "endByShortfall": {
      "type": "integer",
      "description": "Only with an endBy: about how many of the contacts this campaign will email will not get their first email before endBy on this account's current mailboxes (an estimate)."
    },
    "warning": {
      "type": "string",
      "description": "Only with an endBy, when endByShortfall is above 0 or other emails, or more people at one company than there are sending days, may still make some contacts miss endBy: one plain sentence saying so. The launch went through all the same."
    },
    "nextSteps": {
      "$ref": "#/$defs/NextSteps"
    },
    "simulation": {
      "$ref": "#/$defs/TrialSimulation"
    }
  },
  "$defs": {
    "MailboxBilling": {
      "type": "object",
      "description": "Billing projection returned on every mailbox add/remove (no silent capacity addition). `provisionedAfter` is the live mailbox count AFTER the call — reality, not the ask (a capacity-limited partial or a quoteOnly preview both report the true resulting count).\n",
      "required": [
        "provisionedAfter",
        "projectedMonthlyCents",
        "formula"
      ],
      "properties": {
        "provisionedAfter": {
          "type": "integer",
          "description": "Live set-up mailbox count after this operation."
        },
        "projectedMonthlyCents": {
          "type": "integer",
          "description": "Projected monthly: $49 a month covers the first 3 mailboxes, then $5 a month for each extra mailbox (3-mailbox minimum) — folding any checkout discount."
        },
        "formula": {
          "type": "string"
        },
        "domainEnding": {
          "type": "object",
          "description": "Only on a quoteOnly preview of an order over 10 mailboxes in the account's first billing month that buys new domains, when the platform's .co switch is on: they will end in .co unless the real call sends `dotComDomains: true`.\n",
          "required": [
            "ending",
            "newDomains",
            "feeCentsEach",
            "text"
          ],
          "properties": {
            "ending": {
              "type": "string",
              "enum": [
                "co"
              ]
            },
            "newDomains": {
              "type": "integer",
              "description": "New domains this order buys."
            },
            "feeCentsEach": {
              "type": "integer",
              "description": "The one-time fee per .com domain bought, in cents, added to the next bill."
            },
            "text": {
              "type": "string",
              "description": "The same, in one sentence to show the account owner."
            }
          }
        },
        "prepay": {
          "type": "object",
          "description": "Only on a quoteOnly preview of an order making more than 4 new domains in your first billing month, when the platform's prepaid-month switch is on: the real call must send `prepaySecondMonth: true` (else 400 `prepay_required`, nothing bought); month 2 is then charged right after the mailboxes are set up, at most `cents`.\n",
          "required": [
            "months",
            "cents",
            "text"
          ],
          "properties": {
            "months": {
              "type": "integer",
              "enum": [
                2
              ],
              "description": "The month paid up front is the second one."
            },
            "cents": {
              "type": "integer",
              "description": "Month 2 at this order's mailbox count."
            },
            "text": {
              "type": "string",
              "description": "The same, in one sentence to show the account owner."
            }
          }
        }
      }
    },
    "NextStep": {
      "type": "object",
      "required": [
        "reason",
        "kind",
        "why",
        "action",
        "waitingOn",
        "notBeforeMs",
        "effect",
        "sinceMs"
      ],
      "properties": {
        "reason": {
          "$ref": "#/$defs/NextStepReason"
        },
        "kind": {
          "type": "string",
          "enum": [
            "owed",
            "available"
          ],
          "description": "owed = the account will not progress until this happens. available = nothing is blocked; this is the next thing worth doing."
        },
        "why": {
          "type": "string",
          "description": "One customer-safe sentence. Vendor-blind: composed prose, never a raw error message."
        },
        "action": {
          "$ref": "#/$defs/NextStepAction"
        },
        "waitingOn": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "operator",
            "customer_billing",
            null
          ],
          "description": "Who must act before this can succeed. null = the agent can act right now."
        },
        "notBeforeMs": {
          "type": "integer",
          "description": "Do not repeat this call before notBeforeMs from now — the retry cadence as a machine field. 0 = now."
        },
        "effect": {
          "type": [
            "object",
            "null"
          ],
          "allOf": [
            {
              "$ref": "#/$defs/MailboxBilling"
            }
          ],
          "description": "The billing consequence of this step, or null when it changes no billable count."
        },
        "sinceMs": {
          "type": [
            "integer",
            "null"
          ],
          "description": "How long this condition has existed, when an anchor exists; null when none does."
        }
      }
    },
    "NextStepAction": {
      "type": "object",
      "description": "A DISCRIMINATED UNION on `via` — always present, so \"no action is possible\" is a stated value rather than an empty object.\n",
      "required": [
        "via"
      ],
      "properties": {
        "via": {
          "type": "string",
          "enum": [
            "mcp_tool",
            "http",
            "none"
          ]
        },
        "tool": {
          "type": "string",
          "description": "Present when via=mcp_tool. A real MCP tool name.",
          "enum": [
            "setup_infrastructure",
            "launch_campaign",
            "contact_operator",
            "ack_message",
            "configure_byo_domain"
          ]
        },
        "params": {
          "type": "object",
          "additionalProperties": true,
          "description": "Present when via=mcp_tool. Literal call arguments, complete EXCEPT the names listed in paramsToSupply."
        },
        "paramsToSupply": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Present when via=mcp_tool. Fields the caller must add: ones the platform cannot know, and on a setup_infrastructure step always physicalAddress and senderIdentity (they print in every email's footer, so they are asked of the account owner, never prefilled). Empty = send params verbatim."
        },
        "idempotencyKey": {
          "type": [
            "string",
            "null"
          ],
          "description": "Present when via=mcp_tool. The key to REUSE, or null to mint a fresh one."
        },
        "method": {
          "type": "string",
          "description": "Present when via=http."
        },
        "path": {
          "type": "string",
          "description": "Present when via=http."
        },
        "note": {
          "type": "string",
          "description": "Present when via=http or via=none — who acts / what unblocks it."
        }
      }
    },
    "NextStepReason": {
      "type": "string",
      "enum": [
        "paid_seats_unprovisioned",
        "seat_headroom_free",
        "domain_dns_incomplete",
        "ordinal_incomplete",
        "ordinal_slot_shortfall",
        "ordinal_domain_retired",
        "setup_operator_blocked",
        "setup_capacity_held",
        "large_order_payment_pending",
        "billed_quantity_drift",
        "account_frozen",
        "message_action_required",
        "mailbox_credentials_pending",
        "ready_to_launch"
      ],
      "description": "Closed union (design §7.2). paid_seats_unprovisioned = paying, nothing set up (owed). seat_headroom_free = at the account's mailbox minimum with fewer set up, the remainder cost $0 (NEVER owed). domain_dns_incomplete = a live domain's mail DNS has not come up yet. ordinal_incomplete = a requested domain ordinal never completed. ordinal_slot_shortfall = the domain landed and its mail DNS is up, but it holds fewer mailboxes than the call that created it asked for — mailbox slots complete independently, so repeating the setup call creates exactly the missing ones. ordinal_domain_retired = a committed ordinal's domain was burned by the platform itself and has not yet been automatically replaced; no self-serve call brings it back — contact Coldrig support. setup_operator_blocked = the platform stopped; only someone at Coldrig can restart it. setup_capacity_held = setup is paused while Coldrig support checks recent purchases on this account — nothing failed and nothing was charged. large_order_payment_pending = a large order waits on its payment page (owed until it is paid or expires): once it is paid, the same call repeats; nothing was bought. billed_quantity_drift = our Stripe push did not land, ours to fix. account_frozen = lifecycle freeze. With no subscription still running, re-checkout (POST /checkout) is the way out. With one still running, a second checkout is refused (it would charge twice), and the way out is the billing portal (POST /billing/portal): update the card when behind on payment, or renew the subscription when a cancellation is scheduled. An open or lost dispute (or a `disputed` billing state) bars checkout: contact Coldrig support. message_action_required = an unacked message the account will not progress past. mailbox_credentials_pending = a set-up mailbox's credentials have not reached the sending engine yet. ready_to_launch = infrastructure is up, no campaign exists yet.\n"
    },
    "NextSteps": {
      "type": "object",
      "required": [
        "status",
        "steps",
        "computedAt"
      ],
      "description": "What this account should do next, derived fresh on every response that carries it (design §7.2) — the one endpoint-agnostic self-guiding contract shared by setup_infrastructure and infrastructure_status.\n",
      "properties": {
        "status": {
          "type": "string",
          "enum": [
            "owed",
            "none_owed"
          ],
          "description": "EXPLICIT discriminator — never inferred from `steps` being empty. `none_owed` is routinely emitted WITH a non-empty `steps` array (an `available` step is not owed).\n"
        },
        "steps": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/NextStep"
          }
        },
        "computedAt": {
          "type": "integer"
        }
      }
    },
    "ResolvedSendWindow": {
      "type": "object",
      "description": "The send window that applied — the fields you sent, any you omitted filled from the default.",
      "required": [
        "startHour",
        "endHour",
        "days"
      ],
      "properties": {
        "startHour": {
          "type": "integer"
        },
        "endHour": {
          "type": "integer",
          "description": "Inclusive — the last hour a send may start in."
        },
        "days": {
          "type": "array",
          "items": {
            "type": "integer"
          },
          "description": "0 = Sunday … 6 = Saturday."
        }
      }
    },
    "TrialSimulation": {
      "type": "object",
      "description": "What a trial account's launch, add-contacts or resume call played out for the campaign before it answered. Counts what that call played, not the campaign's totals. A paid account's answer has no `simulation`.\n",
      "required": [
        "sent",
        "replies",
        "bounces",
        "complaints",
        "waiting"
      ],
      "properties": {
        "sent": {
          "type": "integer",
          "description": "Sends played out by this call."
        },
        "replies": {
          "type": "integer",
          "description": "Replies produced by this call."
        },
        "bounces": {
          "type": "integer",
          "description": "Bounces produced by this call."
        },
        "complaints": {
          "type": "integer",
          "description": "Complaints produced by this call."
        },
        "waiting": {
          "type": "integer",
          "description": "Sends of this campaign still waiting when the call answered; 0 means the whole sequence played out."
        },
        "reason": {
          "type": "string",
          "description": "Present only when `waiting` is above zero. The rest finishes by itself without a further call only for `the rest is playing out by itself`; the other reasons say why nothing more could be played.\n",
          "enum": [
            "the rest is playing out by itself",
            "no mailbox on this account can send",
            "the campaign is paused",
            "the campaign's send window has no open hour",
            "nothing more could be sent"
          ]
        }
      }
    }
  }
}