{
  "name": "Campaign Commander",
  "description": "Political operations platform. One organization credential, bound to the organization that owns it, drives the MCP server. Connect to MCP with OAuth 2.1 (the supported self-service flow) or an existing API key in a compatible secret store; the versioned REST Agent API is API-key-only and an OAuth access token is not a direct versioned-API credential.",
  "canonical": "https://www.campaigncommanderapp.com",
  "documents": {
    "docs": "https://www.campaigncommanderapp.com/docs/api",
    "openapi": "https://www.campaigncommanderapp.com/openapi.json",
    "llmsTxt": "https://www.campaigncommanderapp.com/llms.txt",
    "llmsFullTxt": "https://www.campaigncommanderapp.com/llms-full.txt",
    "manifest": "https://www.campaigncommanderapp.com/docs/agent-manifest.json"
  },
  "protocol": {
    "type": "mcp",
    "transport": "streamable-http",
    "endpoint": "https://www.campaigncommanderapp.com/api/mcp",
    "stateless": true,
    "authentication": [
      "api-key",
      "oauth2"
    ]
  },
  "connection": {
    "version": 1,
    "resolveRelativeTo": "the URL from which this document was retrieved",
    "preferredMcpAuthentication": "oauth2",
    "paths": {
      "bootstrap": "/llms.txt",
      "manifest": "/docs/agent-manifest.json",
      "mcp": "/api/mcp",
      "protectedResourceMetadata": "/.well-known/oauth-protected-resource",
      "authorizationServerMetadata": "/.well-known/oauth-authorization-server"
    },
    "publicClient": {
      "token_endpoint_auth_method": "none",
      "pkce": "S256"
    },
    "verify": [
      "initialize",
      "tools/list",
      "get_capabilities"
    ]
  },
  "rest": {
    "baseUrl": "https://www.campaigncommanderapp.com/api/v1",
    "authentication": [
      "api-key"
    ]
  },
  "capabilities": {
    "tools": 134,
    "operations": 154,
    "workflows": 18
  },
  "scopes": [
    {
      "scope": "email:read",
      "allows": "Read email campaigns and drafts"
    },
    {
      "scope": "email:draft",
      "allows": "Create and edit email drafts and their audiences"
    },
    {
      "scope": "email:test",
      "allows": "Send one email test"
    },
    {
      "scope": "email:send",
      "allows": "Send and schedule live email"
    },
    {
      "scope": "email:transactional",
      "allows": "Send one-off transactional email and manage its templates"
    },
    {
      "scope": "agent:runs",
      "allows": "Create and update this credential's work runs"
    },
    {
      "scope": "lists:read",
      "allows": "Read, preview, and resolve saved audiences"
    },
    {
      "scope": "lists:write",
      "allows": "Create, edit, and change membership of saved lists"
    },
    {
      "scope": "contacts:read",
      "allows": "Read and search contacts, and read the contact-field registry"
    },
    {
      "scope": "contacts:write",
      "allows": "Create and update contacts, tags, channel consent, and contact-field definitions"
    },
    {
      "scope": "sms:read",
      "allows": "Read text campaigns, recipients, and outcomes"
    },
    {
      "scope": "sms:draft",
      "allows": "Create and edit text drafts and preview audiences"
    },
    {
      "scope": "sms:test",
      "allows": "Send one text test"
    },
    {
      "scope": "sms:send",
      "allows": "Send and schedule live text"
    },
    {
      "scope": "voice:read",
      "allows": "Read voice campaigns, calls, numbers, and dialer sessions"
    },
    {
      "scope": "voice:draft",
      "allows": "Create voice drafts, upload assets, and open dialer sessions"
    },
    {
      "scope": "voice:send",
      "allows": "Place calls and advance dialer sessions"
    },
    {
      "scope": "calls:read",
      "allows": "List the prepared calling queues you may work, and your own active or paused shift"
    },
    {
      "scope": "calls:operate",
      "allows": "Save your own operator phone in the member calling workspace"
    },
    {
      "scope": "mail:read",
      "allows": "Read physical-mail campaigns and preview audiences"
    },
    {
      "scope": "mail:draft",
      "allows": "Create physical-mail drafts"
    },
    {
      "scope": "mail:send",
      "allows": "Launch a physical-mail campaign"
    },
    {
      "scope": "email:verify",
      "allows": "Quote and run email verification"
    },
    {
      "scope": "address:verify",
      "allows": "Quote and run postal address verification"
    },
    {
      "scope": "settings:read",
      "allows": "Read sender and organization settings"
    },
    {
      "scope": "settings:write",
      "allows": "Save organization settings"
    },
    {
      "scope": "profile:read",
      "allows": "Read your own personal profile and preferences"
    },
    {
      "scope": "profile:write",
      "allows": "Save your own personal profile and preferences"
    },
    {
      "scope": "suppression:read",
      "allows": "Read do-not-contact records"
    },
    {
      "scope": "suppression:write",
      "allows": "Record or override do-not-contact records"
    },
    {
      "scope": "logs:read",
      "allows": "Read delivery events and the audit log"
    },
    {
      "scope": "conversions:write",
      "allows": "Record a conversion"
    },
    {
      "scope": "intake:write",
      "allows": "Accept a website form submission as a contact"
    },
    {
      "scope": "exports:read",
      "allows": "Export contacts"
    },
    {
      "scope": "inbox:read",
      "allows": "Read inbox conversations for reply triage"
    },
    {
      "scope": "registration:read",
      "allows": "Read texting brand registration status and preview a would-be filing"
    },
    {
      "scope": "registration:prepare",
      "allows": "Read campaign-registration readiness and run the free campaign website-compliance preflight"
    },
    {
      "scope": "registration:submit",
      "allows": "Request approval for and submit a paid texting brand registration"
    },
    {
      "scope": "billing:read",
      "allows": "Read organization funds, ledger transactions and saved Auto Top-Up settings"
    },
    {
      "scope": "billing:write",
      "allows": "Configure Auto Top-Up against an already-authorized saved card; enabling permits future automatic charges"
    },
    {
      "scope": "team:read",
      "allows": "Read the organization team roster and its current pending invitations"
    },
    {
      "scope": "team:manage",
      "allows": "Invite, re-queue, and cancel team invitations, and change, suspend, reactivate, or remove an ordinary (non-owner) team member"
    }
  ],
  "tools": [
    {
      "name": "get_capabilities",
      "description": "Read the scopes granted to this API key and the Agent API actions those scopes allow. Call this first when you are unsure what this key can do. It changes no business state (the read records one routine audit row).",
      "scopes": [],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {},
        "examples": [
          {}
        ]
      }
    },
    {
      "name": "list_contacts",
      "description": "Search contacts in this organization by name, email, or phone, or look one up by an exact email or phone. With q, tag, or neither, returns a page of contacts, sorted by sort_by/sort_order (created_at, first_name, last_name, email, or phone) and optionally filtered to those that have an email, phone, or address on file. A presence filter means only that the field is not NULL in the database; a blank value still counts, and it is not a claim that the value is verified, deliverable, or opted in. An exact email or phone returns one contact and does not apply q, tag, sort_by, sort_order, filter_type, page, or limit; supplied enum selectors are still validated. Omit selectors for defaults rather than passing an empty string. When the database does not provide an exact count, total and hasMore are null (unknown), not proof the list has ended. Does not return mailing addresses.",
      "scopes": [
        "contacts:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "q": {
            "type": "string",
            "description": "Search name, email, or phone. At most 100 characters."
          },
          "email": {
            "type": "string",
            "description": "Exact email address. Returns one contact, or an error when none matches."
          },
          "phone": {
            "type": "string",
            "description": "Exact U.S. phone number. It is normalized before lookup."
          },
          "tag": {
            "type": "string",
            "description": "Only contacts that have this tag."
          },
          "sort_by": {
            "type": "string",
            "enum": [
              "created_at",
              "first_name",
              "last_name",
              "email",
              "phone"
            ],
            "description": "Sort field for the paginated list. Defaults to created_at."
          },
          "sort_order": {
            "type": "string",
            "enum": [
              "asc",
              "desc"
            ],
            "description": "Sort direction for the paginated list. Defaults to desc."
          },
          "filter_type": {
            "type": "string",
            "enum": [
              "all",
              "email",
              "phone",
              "address"
            ],
            "description": "Keep only contacts whose named field is present (not NULL). all, the default, applies no filter."
          },
          "page": {
            "type": "integer",
            "minimum": 1,
            "description": "Page number, starting at 1."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50,
            "description": "Page size. At most 50."
          }
        },
        "examples": [
          {
            "q": "ada",
            "sort_by": "created_at",
            "sort_order": "desc",
            "filter_type": "email",
            "page": 1,
            "limit": 25
          }
        ]
      }
    },
    {
      "name": "get_contact",
      "description": "Read one contact in this organization by id. Returns an error when the contact is not in this organization. Does not return a mailing address.",
      "scopes": [
        "contacts:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "contact_id"
        ],
        "properties": {
          "contact_id": {
            "type": "string",
            "description": "Contact id."
          }
        },
        "examples": [
          {
            "contact_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "list_segments",
      "description": "List saved audiences (lists) in this organization, including name, id, and contact count. Use a returned id when a later tool asks for a segment. Does not return the people on the list.",
      "scopes": [
        "lists:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {},
        "examples": [
          {}
        ]
      }
    },
    {
      "name": "create_email_draft",
      "description": "Create an unsent email draft in this organization. This does not send, schedule, or charge. Choose recipient_type all for every eligible contact, segment with segment_id or segment_ids for saved lists, selected with selected_contact_ids, or campaign_engagement with source_campaign_id and engagement. Scheduling and sending are separate tools and are not in this skeleton.",
      "scopes": [
        "email:draft"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "name",
          "subject",
          "message"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Draft name. At most 160 characters."
          },
          "subject": {
            "type": "string",
            "description": "Email subject. At most 255 characters."
          },
          "message": {
            "type": "string",
            "description": "Email body. HTML is accepted."
          },
          "recipient_type": {
            "type": "string",
            "enum": [
              "all",
              "segment",
              "selected",
              "campaign_engagement"
            ],
            "description": "Who the draft is addressed to. Defaults to all when omitted."
          },
          "segment_id": {
            "type": "string",
            "description": "One saved list id. Use with recipient_type segment."
          },
          "segment_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Saved list ids to combine. Use with recipient_type segment."
          },
          "selected_contact_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Contact ids. Use with recipient_type selected."
          },
          "source_campaign_id": {
            "type": "string",
            "description": "Earlier email campaign id. Use with recipient_type campaign_engagement."
          },
          "engagement": {
            "type": "string",
            "enum": [
              "opened",
              "clicked"
            ],
            "description": "Which engagement to keep. Use with recipient_type campaign_engagement."
          },
          "first_name_fallback": {
            "type": [
              "string",
              "null"
            ],
            "description": "Greeting used when a contact has no first name and the message has no inline {first_name|...} fallback. Trimmed and capped at 60 characters. Omit to use Friend. Null or blank stores Friend. An inline tag still wins."
          }
        },
        "examples": [
          {
            "name": "Saturday reminder",
            "subject": "Doors open at 9",
            "message": "<p>Bring a clipboard.</p>",
            "recipient_type": "all"
          }
        ]
      }
    },
    {
      "name": "create_contact",
      "description": "Add one contact to this organization. Provide at least a name, email, phone, or mailing address. A duplicate email returns the existing contact instead of creating a second one. A do-not-email address is refused unless override_suppression is set with a note. Mailing address fields are the four postal columns only; verification fields cannot be set. This does not send anything.",
      "scopes": [
        "contacts:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "first_name": {
            "type": "string",
            "description": "First name. At most 120 characters."
          },
          "last_name": {
            "type": "string",
            "description": "Last name. At most 120 characters."
          },
          "email": {
            "type": "string",
            "description": "Email address. Normalized to lower case."
          },
          "phone": {
            "type": "string",
            "description": "U.S. phone number. Normalized to E.164."
          },
          "address": {
            "type": [
              "string",
              "null"
            ],
            "description": "Street address. Trimmed; a partial address is allowed."
          },
          "city": {
            "type": [
              "string",
              "null"
            ],
            "description": "City. Trimmed."
          },
          "state": {
            "type": [
              "string",
              "null"
            ],
            "description": "State. Trimmed and upper-cased."
          },
          "zip": {
            "type": [
              "string",
              "null"
            ],
            "description": "ZIP code as text. A leading zero is kept."
          },
          "notes": {
            "type": "string",
            "description": "Free-form notes. At most 5,000 characters."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Up to 25 tags."
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {
              "type": [
                "string",
                "number"
              ]
            },
            "description": "Flat custom-field values: a map of field name to a text or number value. Names are trimmed to 100 characters and values to 2,000; a number is stored as text. Nested objects and arrays are not stored. This sets values on a contact; it does not define or type fields."
          },
          "override_suppression": {
            "type": "boolean",
            "description": "Only for an overridable do-not-email entry. Requires override_note."
          },
          "override_note": {
            "type": "string",
            "description": "Why a suppression is being overridden. Required with override_suppression."
          }
        },
        "examples": [
          {
            "first_name": "Ada",
            "last_name": "Lovelace",
            "email": "ada@example.org",
            "tags": [
              "volunteer"
            ],
            "custom_fields": {
              "precinct": "12"
            }
          }
        ]
      }
    },
    {
      "name": "update_contact",
      "description": "Change fields on one contact in this organization by id. Only the fields you pass are changed. The mailing address fields are the four postal columns only; verification fields cannot be set. Returns not found when the contact is not in this organization. This does not send anything.",
      "scopes": [
        "contacts:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "contact_id"
        ],
        "properties": {
          "contact_id": {
            "type": "string",
            "description": "Contact id."
          },
          "first_name": {
            "type": "string",
            "description": "New first name."
          },
          "last_name": {
            "type": "string",
            "description": "New last name."
          },
          "email": {
            "type": "string",
            "description": "New email address."
          },
          "phone": {
            "type": "string",
            "description": "New U.S. phone number."
          },
          "address": {
            "type": [
              "string",
              "null"
            ],
            "description": "New street address. Pass null or an empty string to clear it."
          },
          "city": {
            "type": [
              "string",
              "null"
            ],
            "description": "New city. Pass null or an empty string to clear it."
          },
          "state": {
            "type": [
              "string",
              "null"
            ],
            "description": "New state. Trimmed and upper-cased."
          },
          "zip": {
            "type": [
              "string",
              "null"
            ],
            "description": "New ZIP code as text. A leading zero is kept."
          },
          "notes": {
            "type": "string",
            "description": "New notes."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Replacement tag list, up to 25."
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {
              "type": [
                "string",
                "number"
              ]
            },
            "description": "Flat custom-field values: a map of field name to a text or number value. Names are trimmed to 100 characters and values to 2,000; a number is stored as text. Omitted leaves the stored values unchanged; an empty object clears them all. This replaces the whole map, not one field, and does not define or type fields."
          }
        },
        "examples": [
          {
            "contact_id": "11111111-1111-4111-8111-111111111111",
            "notes": "Prefers texts"
          }
        ]
      }
    },
    {
      "name": "add_contact_tags",
      "description": "Add up to 25 tags to one contact in this organization by id. Existing tags are kept. Suppression and system verdict tags cannot be set this way.",
      "scopes": [
        "contacts:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "contact_id",
          "tags"
        ],
        "properties": {
          "contact_id": {
            "type": "string",
            "description": "Contact id."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags to add, up to 25."
          }
        },
        "examples": [
          {
            "contact_id": "11111111-1111-4111-8111-111111111111",
            "tags": [
              "door-knocked"
            ]
          }
        ]
      }
    },
    {
      "name": "remove_contact_tags",
      "description": "Remove up to 25 tags from one contact in this organization by id. Only the named tags are removed; the rest of the tag list is kept. Removing a tag the contact does not have is a no-op. Suppression and system verdict tags cannot be changed this way. This removes ordinary tags only; it does not replace the whole tag list.",
      "scopes": [
        "contacts:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "contact_id",
          "tags"
        ],
        "properties": {
          "contact_id": {
            "type": "string",
            "description": "Contact id."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags to remove, up to 25."
          }
        },
        "examples": [
          {
            "contact_id": "11111111-1111-4111-8111-111111111111",
            "tags": [
              "volunteers"
            ]
          }
        ]
      }
    },
    {
      "name": "list_suppressions",
      "description": "List do-not-contact entries (email, SMS, voice) for this organization, newest first. Read-only. It does not return mailing addresses.",
      "scopes": [
        "suppression:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "channel": {
            "type": "string",
            "enum": [
              "email",
              "sms",
              "voice",
              "all"
            ],
            "description": "Filter by stored channel. Omit to return records across every channel; 'all' returns only all-channel records."
          },
          "page": {
            "type": "integer",
            "minimum": 1,
            "description": "Page number, starting at 1."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Page size. At most 100."
          }
        },
        "examples": [
          {
            "channel": "email",
            "page": 1,
            "limit": 50
          }
        ]
      }
    },
    {
      "name": "add_suppression",
      "description": "Record an email and/or phone do-not-contact entry for this organization using the same ledger the dashboard uses. Provide an email, a phone, or both. This does not send anything.",
      "scopes": [
        "suppression:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "description": "Email to suppress."
          },
          "phone": {
            "type": "string",
            "description": "U.S. phone number to suppress."
          },
          "reason": {
            "type": "string",
            "enum": [
              "manual",
              "recipient_blocks_sender",
              "unsubscribe",
              "legal_request"
            ],
            "description": "Why this entry exists. One of the listed values; any other string is rejected."
          },
          "notes": {
            "type": "string",
            "description": "Note recorded on the entry. Required, 3 to 1,000 characters."
          }
        },
        "required": [
          "reason",
          "notes"
        ],
        "examples": [
          {
            "email": "person@example.org",
            "reason": "recipient_blocks_sender",
            "notes": "Asked to stop by phone"
          }
        ]
      }
    },
    {
      "name": "preview_email_audience",
      "description": "Non-mutating check for a would-be email campaign: recipient count, estimated cost, and blockers. Call this after a draft is agreed and before asking to send. It does not create, schedule, or send anything and never charges.",
      "scopes": [
        "email:draft"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "name",
          "subject",
          "message"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Draft name."
          },
          "subject": {
            "type": "string",
            "description": "Email subject."
          },
          "message": {
            "type": "string",
            "description": "Email body. HTML is accepted."
          },
          "recipient_type": {
            "type": "string",
            "enum": [
              "all",
              "segment",
              "selected",
              "campaign_engagement"
            ],
            "description": "Audience kind. Defaults to all."
          },
          "segment_id": {
            "type": "string",
            "description": "One saved list id."
          },
          "segment_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Saved list ids to combine."
          },
          "selected_contact_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Contact ids for recipient_type selected."
          },
          "source_campaign_id": {
            "type": "string",
            "description": "Earlier email campaign id for campaign_engagement."
          },
          "engagement": {
            "type": "string",
            "enum": [
              "opened",
              "clicked"
            ],
            "description": "Engagement to keep for campaign_engagement."
          }
        },
        "examples": [
          {
            "name": "Saturday reminder",
            "subject": "Doors open at 9",
            "message": "<p>Bring a clipboard.</p>",
            "recipient_type": "all"
          }
        ]
      }
    },
    {
      "name": "get_email_draft",
      "description": "Read one email campaign in this organization by id, including status and scheduling state. Returns not found for another organization's campaign.",
      "scopes": [
        "email:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "Email campaign id."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "update_email_draft",
      "description": "Revise an unsent, unscheduled email draft in this organization. Only draft campaigns can be changed. Recipient selection and paid dispatch stay separate tools. This does not send or charge.",
      "scopes": [
        "email:draft"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "Email campaign id."
          },
          "name": {
            "type": "string",
            "description": "New draft name."
          },
          "subject": {
            "type": "string",
            "description": "New subject."
          },
          "message": {
            "type": "string",
            "description": "New body. HTML is accepted."
          },
          "from_name": {
            "type": "string",
            "description": "New From name."
          },
          "from_email": {
            "type": "string",
            "description": "New From email."
          },
          "reply_to_email": {
            "type": "string",
            "description": "New Reply-To email."
          },
          "first_name_fallback": {
            "type": [
              "string",
              "null"
            ],
            "description": "Greeting used when a contact has no first name and the message has no inline {first_name|...} fallback. Trimmed and capped at 60 characters. Omit to leave the saved value. Null or blank clears it back to Friend. An inline tag still wins."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "subject": "Doors open at 10"
          }
        ]
      }
    },
    {
      "name": "send_email_campaign",
      "description": "Send a prepared email campaign in this organization and charge organization balance. This spends money and cannot be undone. It requires confirm true and a unique idempotency_key. Billing, suppression, consent, and do-not-contact rules still apply. Prefer preview_email_audience first.",
      "scopes": [
        "email:send"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "Email campaign id."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to send and spend."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not send twice."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "confirm": true,
            "idempotency_key": "saturday-reminder-2026"
          }
        ]
      }
    },
    {
      "name": "create_segment",
      "description": "Save a new audience (list) in this organization and return its id and matched contact count. The filters use the same language as the dashboard, so preview_list first when the count matters. The saved audience is what a draft selects when you set its recipients. It creates nothing else, sends nothing, and never charges.",
      "scopes": [
        "lists:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "List name, at most 120 characters."
          },
          "description": {
            "type": "string",
            "description": "Optional note about who is on this list, at most 1,000 characters."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Match contacts that carry every one of these list tags."
          },
          "tags_any": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Match contacts that carry at least one of these tags."
          },
          "state": {
            "type": "string",
            "description": "Only contacts in this state, by two-letter or full name."
          },
          "has_email": {
            "type": "boolean",
            "description": "When true, only contacts with an email address."
          },
          "has_phone": {
            "type": "boolean",
            "description": "When true, only contacts with a phone number."
          },
          "has_address": {
            "type": "boolean",
            "description": "When true, only contacts with a mailing address on file."
          },
          "exclude_do_not_contact": {
            "type": "boolean",
            "description": "When true, drop contacts marked do not contact."
          }
        },
        "examples": [
          {
            "name": "Polk volunteers",
            "tags": [
              "volunteer"
            ],
            "exclude_do_not_contact": true
          }
        ]
      }
    },
    {
      "name": "get_segment",
      "description": "Read one saved audience (list) in this organization by id, including its name, description, filters, and contact count. Returns not found for another organization's list. It does not return the people on the list and changes no business state (the read records one routine audit row).",
      "scopes": [
        "lists:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "segment_id"
        ],
        "properties": {
          "segment_id": {
            "type": "string",
            "description": "Saved audience (list) id."
          }
        },
        "examples": [
          {
            "segment_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "preview_list",
      "description": "Count how many contacts in this organization a saved-audience definition would match, using the same filter language as the dashboard. Non-mutating: it creates no list, changes no business state (the read records one routine audit row), sends nothing, and never charges. Use it to size an audience before saving or sending.",
      "scopes": [
        "lists:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Match contacts that carry every one of these list tags."
          },
          "tags_any": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Match contacts that carry at least one of these tags."
          },
          "state": {
            "type": "string",
            "description": "Only contacts in this state, by two-letter or full name."
          },
          "has_email": {
            "type": "boolean",
            "description": "When true, only contacts with an email address."
          },
          "has_phone": {
            "type": "boolean",
            "description": "When true, only contacts with a phone number."
          },
          "has_address": {
            "type": "boolean",
            "description": "When true, only contacts with a mailing address on file."
          },
          "exclude_do_not_contact": {
            "type": "boolean",
            "description": "When true, drop contacts marked do not contact."
          }
        },
        "examples": [
          {
            "tags": [
              "volunteer"
            ],
            "exclude_do_not_contact": true
          }
        ]
      }
    },
    {
      "name": "change_segment_members",
      "description": "Add or remove contacts on a saved static membership list in this organization by list id. Only a list whose sole filter is tags can change, and up to 100 contacts may change per call. It does not create or delete the list, does not return contacts, and does not send anything.",
      "scopes": [
        "lists:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "segment_id",
          "action",
          "contact_ids"
        ],
        "properties": {
          "segment_id": {
            "type": "string",
            "description": "Saved audience (list) id."
          },
          "action": {
            "type": "string",
            "enum": [
              "add",
              "remove"
            ],
            "description": "Add the contacts to the list, or remove them from it."
          },
          "contact_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Contact ids to add or remove, at most 100."
          }
        },
        "examples": [
          {
            "segment_id": "11111111-1111-4111-8111-111111111111",
            "action": "add",
            "contact_ids": [
              "22222222-2222-4222-8222-222222222222"
            ]
          }
        ]
      }
    },
    {
      "name": "resolve_list",
      "description": "Turn a name, slug, or id an operator typed into this organization's saved audience ids and counts, returning one exact match or a short list of candidates to choose from. Read-only. It does not create a list and does not return the people on it.",
      "scopes": [
        "lists:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "query"
        ],
        "properties": {
          "query": {
            "type": "string",
            "description": "The list name, slug, or id to resolve, at most 120 characters."
          }
        },
        "examples": [
          {
            "query": "donors"
          }
        ]
      }
    },
    {
      "name": "get_segment_usage",
      "description": "Report which campaigns reference a saved audience in this organization so it is not deleted while in use, broken down by email, SMS, mail, and voice with a total. Read-only. It does not delete, rename, or edit the list.",
      "scopes": [
        "lists:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "segment_id"
        ],
        "properties": {
          "segment_id": {
            "type": "string",
            "description": "Saved audience (list) id."
          }
        },
        "examples": [
          {
            "segment_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "set_contact_consent",
      "description": "Record or change one contact's email, SMS, or do-not-contact consent state in this organization by contact id. Set at least one of the flags. It does not send anything, does not create a contact, and does not touch other contacts.",
      "scopes": [
        "contacts:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "contact_id"
        ],
        "properties": {
          "contact_id": {
            "type": "string",
            "description": "Contact id."
          },
          "email_opt_out": {
            "type": "boolean",
            "description": "True stops email to this contact; false clears that block."
          },
          "sms_opt_out": {
            "type": "boolean",
            "description": "True stops SMS to this contact; false clears that block."
          },
          "do_not_contact": {
            "type": "boolean",
            "description": "True marks the contact do not contact on every channel."
          }
        },
        "examples": [
          {
            "contact_id": "11111111-1111-4111-8111-111111111111",
            "sms_opt_out": true
          }
        ]
      }
    },
    {
      "name": "restore_suppression",
      "description": "Withdraw a suppression override recorded through the API so a do-not-contact entry is enforced again in this organization. It does not delete the entry, does not clear consent or spam-trap blocks, and does not send anything.",
      "scopes": [
        "suppression:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "suppression_id"
        ],
        "properties": {
          "suppression_id": {
            "type": "string",
            "description": "Do-not-contact entry id."
          }
        },
        "examples": [
          {
            "suppression_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "override_suppression",
      "description": "Bypass one email do-not-contact entry in this organization with a required note so the address may be contacted again. Consent blocks and spam-trap entries are refused. It does not delete the entry and does not send anything.",
      "scopes": [
        "suppression:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "suppression_id",
          "note"
        ],
        "properties": {
          "suppression_id": {
            "type": "string",
            "description": "Do-not-contact entry id."
          },
          "note": {
            "type": "string",
            "description": "Why this entry is being bypassed, at least 3 and at most 1,000 characters."
          }
        },
        "examples": [
          {
            "suppression_id": "11111111-1111-4111-8111-111111111111",
            "note": "Recipient asked to be re-added"
          }
        ]
      }
    },
    {
      "name": "list_email_campaigns",
      "description": "List this organization's email campaigns, newest first, 20 per page by default and at most 100, with status, type, recipient count, and sent, delivered, open, click, bounce, and unsubscribe totals, plus pagination and metricDefinitions. opened_count is unique recipients with any recorded open, including Apple Mail Privacy Protection prefetches and machine scanners; it is not total open hits. Read-only. It does not return message bodies or the recipient roster; read one campaign with get_email_draft or its delivery log with list_email_recipients.",
      "scopes": [
        "email:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "page": {
            "type": "integer",
            "minimum": 1,
            "description": "Page number, starting at 1."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Campaigns per page, default 20. At most 100."
          }
        },
        "examples": [
          {},
          {
            "limit": 5
          },
          {
            "page": 2,
            "limit": 20
          }
        ]
      }
    },
    {
      "name": "test_email_campaign",
      "description": "Send a prepared email campaign to the API key owner's mailbox, a named active organization member, or a verified test recipient, as a real test delivery. This spends money and cannot be undone, so it requires confirm true and a unique idempotency_key. It does not send to the full audience; use send_email_campaign for that.",
      "scopes": [
        "email:test"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "Email campaign id."
          },
          "to": {
            "type": "string",
            "description": "Optional mailbox to test to. Defaults to the key owner's address. Must be the owner, an active organization member, or a verified test recipient."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees the test send spends money."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not send the test twice."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "to": "ada@example.org",
            "confirm": true,
            "idempotency_key": "email-test-saturday-01"
          }
        ]
      }
    },
    {
      "name": "list_test_recipients",
      "description": "List email addresses this organization has allowed to receive test sends, with pending or verified status. Read-only. It does not send a message and does not return confirmation links.",
      "scopes": [
        "email:test"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {},
        "examples": [
          {}
        ]
      }
    },
    {
      "name": "add_test_recipient",
      "description": "Add an email address that may receive test sends for this organization after that address confirms a verification link. A verified address is returned without another email. A pending address is not emailed again within ten minutes. This sends one verification email. It does not send a campaign or charge the organization.",
      "scopes": [
        "email:test"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "description": "Address to allow after it confirms the verification link."
          }
        },
        "examples": [
          {
            "email": "ada@example.org"
          }
        ]
      }
    },
    {
      "name": "remove_test_recipient",
      "description": "Remove an address from this organization's test-recipient list. Later test sends to that address are refused unless it is the key owner or an active member. This does not send a message or change the organization balance.",
      "scopes": [
        "email:test"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "recipient_id"
        ],
        "properties": {
          "recipient_id": {
            "type": "string",
            "description": "Test recipient id returned by list_test_recipients."
          }
        },
        "examples": [
          {
            "recipient_id": "33333333-3333-4333-8333-333333333333"
          }
        ]
      }
    },
    {
      "name": "schedule_email_campaign",
      "description": "Queue a prepared email draft to go out at a chosen future time. This commits a paid send and cannot be undone, so it requires confirm true and a unique idempotency_key. It does not send immediately. Note the two money events differ: any spend approval is used up now, when the campaign is queued, and is not available for another send afterwards; the balance hold is placed later, when the send job runs.",
      "scopes": [
        "email:send"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id",
          "scheduled_at",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "Email campaign id."
          },
          "scheduled_at": {
            "type": "string",
            "description": "When to send, in ISO 8601 (for example 2026-10-11T18:30:00-04:00). Must be in the future."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to schedule a paid send."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not schedule twice."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "scheduled_at": "2026-10-11T18:30:00-04:00",
            "confirm": true,
            "idempotency_key": "email-schedule-saturday-01"
          }
        ]
      }
    },
    {
      "name": "unschedule_email_campaign",
      "description": "Cancel a scheduled email campaign before the send job claims it and return it to an editable draft. It cannot be recalled once the send starts, so it requires confirm true and a unique idempotency_key. It does not delete the draft and does not recall any email already sent.",
      "scopes": [
        "email:send"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "Email campaign id."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to cancel the scheduled send."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not act twice."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "confirm": true,
            "idempotency_key": "email-unschedule-saturday-01"
          }
        ]
      }
    },
    {
      "name": "list_email_recipients",
      "description": "List per-recipient delivery status for one email campaign in this organization, including sent, delivered, open, click, bounce, and unsubscribe times plus the charge for each. Read-only and paginated. It does not return the campaign's message content. It does return the audience selectors as segment ids.",
      "scopes": [
        "logs:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "Email campaign id."
          },
          "page": {
            "type": "integer",
            "minimum": 1,
            "description": "Page number, starting at 1."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 250,
            "description": "Page size. At most 250."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "page": 1,
            "limit": 100
          }
        ]
      }
    },
    {
      "name": "list_email_events",
      "description": "List delivery events for one email campaign in this organization, newest first, including sends, bounces, and complaints with their event and bounce types. Read-only and paginated. It does not return the recipient roster or the campaign's message content.",
      "scopes": [
        "logs:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "Email campaign id."
          },
          "page": {
            "type": "integer",
            "minimum": 1,
            "description": "Page number, starting at 1."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 250,
            "description": "Page size. At most 250."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "page": 1,
            "limit": 100
          }
        ]
      }
    },
    {
      "name": "set_email_audience",
      "description": "Replace a draft email campaign's saved audience and recipient snapshot, choosing all, segment, selected, or campaign_engagement targeting. Only an unsent draft you own can change, and segments or contacts are checked for suppressions. It changes who would receive the campaign; it does not send, schedule, or charge.",
      "scopes": [
        "email:draft"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id",
          "recipient_type"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "Email campaign id."
          },
          "recipient_type": {
            "type": "string",
            "enum": [
              "all",
              "segment",
              "selected",
              "campaign_engagement"
            ],
            "description": "Who the draft is addressed to."
          },
          "segment_id": {
            "type": "string",
            "description": "One saved list id. Use with recipient_type segment."
          },
          "segment_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Saved list ids to combine. Use with recipient_type segment."
          },
          "selected_contact_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Contact ids. Use with recipient_type selected."
          },
          "source_campaign_id": {
            "type": "string",
            "description": "Earlier email campaign id. Use with recipient_type campaign_engagement."
          },
          "engagement": {
            "type": "string",
            "enum": [
              "opened",
              "clicked"
            ],
            "description": "Which engagement to keep. Use with recipient_type campaign_engagement."
          },
          "exclude_campaign_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Earlier campaign ids whose recipients to skip."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "recipient_type": "segment",
            "segment_ids": [
              "22222222-2222-4222-8222-222222222222"
            ]
          }
        ]
      }
    },
    {
      "name": "refresh_email_recipients",
      "description": "Re-resolve a draft email campaign's recipient snapshot from its already-saved audience after contacts change, removing duplicates and re-applying suppressions. Only an unsent draft you own with a saved audience can refresh. It does not change the audience itself and does not send or charge.",
      "scopes": [
        "email:draft"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "Email campaign id."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "get_email",
      "description": "Read one sent email message in this organization by id, including its delivery status, recipients, subject, and event timestamps. Returns not found for another organization's message. It does not return the message body or other channels' messages.",
      "scopes": [
        "email:transactional"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "email_id"
        ],
        "properties": {
          "email_id": {
            "type": "string",
            "description": "Sent email message id."
          }
        },
        "examples": [
          {
            "email_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "list_emails",
      "description": "List sent email messages in this organization, newest first, optionally filtered by delivery status or recipient address. Read-only. It does not return draft campaigns or campaign audience lists.",
      "scopes": [
        "email:transactional"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "reserved",
              "sending",
              "sent",
              "delivered",
              "opened",
              "clicked",
              "bounced",
              "complained",
              "failed",
              "unknown"
            ],
            "description": "Only messages with this delivery status."
          },
          "to": {
            "type": "string",
            "description": "Only messages sent to this recipient address."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 200,
            "description": "Page size. At most 200."
          }
        },
        "examples": [
          {
            "status": "delivered",
            "limit": 50
          }
        ]
      }
    },
    {
      "name": "list_sms_campaigns",
      "description": "List this organization's SMS and MMS campaigns with copy, status, and progress counts, newest first, optionally filtered by status or channel. Read-only. It does not return recipient phone numbers or per-recipient delivery results.",
      "scopes": [
        "sms:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "scheduled",
              "sending",
              "completed",
              "paused",
              "failed",
              "cancelled"
            ],
            "description": "Only campaigns in this status."
          },
          "type": {
            "type": "string",
            "enum": [
              "sms",
              "mms"
            ],
            "description": "Only campaigns of this channel."
          },
          "page": {
            "type": "integer",
            "minimum": 1,
            "description": "Page number, starting at 1."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Campaigns per page. At most 100."
          }
        },
        "examples": [
          {
            "status": "draft",
            "page": 1,
            "limit": 25
          }
        ]
      }
    },
    {
      "name": "get_sms_campaign",
      "description": "Read one SMS or MMS campaign in this organization by id, including stored copy, status, sender-prepared counts, and scheduling state. Does not return recipient phone numbers, delivery events, or failure breakdowns.",
      "scopes": [
        "sms:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "SMS or MMS campaign id."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "create_sms_draft",
      "description": "Create an unsent SMS or MMS draft in this organization from a message and an audience choice. Set type mms and supply an image to make it a picture message; the image is stored as a publicly readable asset, but this does not send, schedule, or charge. Change the audience later with set_sms_audience while the campaign is still an unscheduled draft. Sending and scheduling are separate paid tools.",
      "scopes": [
        "sms:draft"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "name",
          "message"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Draft name. At most 120 characters."
          },
          "message": {
            "type": "string",
            "description": "SMS body text. At most 1,600 characters."
          },
          "type": {
            "type": "string",
            "enum": [
              "sms",
              "mms"
            ],
            "description": "sms for a plain text draft, or mms for a picture message. Defaults to sms; defaults to mms when an image is supplied."
          },
          "image_base64": {
            "type": "string",
            "description": "Base64-encoded image bytes for an mms draft: a JPG, PNG, or GIF at most 1 MB decoded. Requires image_content_type and image_filename. It is stored as a publicly readable asset but sends nothing and charges nothing."
          },
          "image_content_type": {
            "type": "string",
            "enum": [
              "image/jpeg",
              "image/png",
              "image/gif"
            ],
            "description": "The image's MIME type. Required with image_base64."
          },
          "image_filename": {
            "type": "string",
            "description": "File name for the uploaded image. Required with image_base64."
          },
          "recipient_type": {
            "type": "string",
            "enum": [
              "all",
              "segment"
            ],
            "description": "Who receives the draft. Defaults to all eligible contacts when omitted."
          },
          "segment_id": {
            "type": "string",
            "description": "One saved list id. Use with recipient_type segment."
          },
          "segment_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Saved list ids to combine. Use with recipient_type segment."
          }
        },
        "examples": [
          {
            "name": "Saturday reminder",
            "message": "Doors open at 9. Reply STOP to opt out.",
            "recipient_type": "all"
          },
          {
            "name": "Weekend picture",
            "message": "Doors open at 9. Reply STOP to opt out.",
            "type": "mms",
            "image_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP4z8DwHwAFAAH/VscvDQAAAABJRU5ErkJggg==",
            "image_content_type": "image/png",
            "image_filename": "doors.png",
            "recipient_type": "all"
          }
        ]
      }
    },
    {
      "name": "update_sms_draft",
      "description": "Revise the name, message, or channel of an unsent draft in this organization. It changes draft copy only. It does not change the audience (use set_sms_audience), schedule, send, or charge. Media depends on what you pass: editing text or name alone keeps an MMS draft's current image, supplying type mms with a new image replaces it (stored as a publicly readable asset), and setting type sms removes it.",
      "scopes": [
        "sms:draft"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "SMS or MMS campaign id."
          },
          "name": {
            "type": "string",
            "description": "New draft name. At most 120 characters."
          },
          "message": {
            "type": "string",
            "description": "New SMS body text. At most 1,600 characters."
          },
          "type": {
            "type": "string",
            "enum": [
              "sms",
              "mms"
            ],
            "description": "New channel. sms removes an MMS image; mms keeps or replaces it. Omit to keep the current channel."
          },
          "image_base64": {
            "type": "string",
            "description": "Base64-encoded replacement image bytes for an mms draft: a JPG, PNG, or GIF at most 1 MB decoded. Requires image_content_type and image_filename."
          },
          "image_content_type": {
            "type": "string",
            "enum": [
              "image/jpeg",
              "image/png",
              "image/gif"
            ],
            "description": "The image's MIME type. Required with image_base64."
          },
          "image_filename": {
            "type": "string",
            "description": "File name for the uploaded image. Required with image_base64."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "message": "Doors open at 10. Reply STOP to opt out."
          }
        ]
      }
    },
    {
      "name": "set_sms_audience",
      "description": "Replace an unscheduled SMS or MMS draft's saved audience and recipient snapshot, choosing all, segment, or custom targeting. Only a draft that has not started delivery can change. It recalculates the recipient count and cost estimate and does not send, schedule, charge, or move organization balance. A sent or scheduled campaign stays locked.",
      "scopes": [
        "sms:draft"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id",
          "recipient_type"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "SMS or MMS campaign id."
          },
          "recipient_type": {
            "type": "string",
            "enum": [
              "all",
              "segment",
              "custom"
            ],
            "description": "Who the draft is addressed to."
          },
          "segment_id": {
            "type": "string",
            "description": "One saved list id. Use with recipient_type segment."
          },
          "segment_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Saved list ids to combine. Use with recipient_type segment."
          },
          "filters": {
            "type": "object",
            "description": "Custom audience filters. Use with recipient_type custom. Keys match the text draft API.",
            "properties": {
              "tags": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Match contacts that carry every one of these tags."
              },
              "state": {
                "type": "string",
                "description": "Only contacts in this state."
              },
              "hasPhone": {
                "type": "boolean",
                "description": "When true, only contacts with a phone number."
              },
              "excludeDNC": {
                "type": "boolean",
                "description": "When true, drop contacts marked do not contact."
              }
            }
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "recipient_type": "segment",
            "segment_ids": [
              "22222222-2222-4222-8222-222222222222"
            ]
          }
        ]
      }
    },
    {
      "name": "preview_sms_audience",
      "description": "Non-mutating preflight for a would-be SMS or MMS campaign: eligible recipient count, estimated cost, sender readiness, and blockers. It does not create, upload media, schedule, or send anything, and never charges or holds money. Prefer this before asking to send.",
      "scopes": [
        "sms:draft"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "name",
          "message"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Draft name."
          },
          "message": {
            "type": "string",
            "description": "SMS body text."
          },
          "type": {
            "type": "string",
            "enum": [
              "sms",
              "mms"
            ],
            "description": "Channel to estimate. Defaults to sms."
          },
          "recipient_type": {
            "type": "string",
            "enum": [
              "all",
              "segment"
            ],
            "description": "Audience kind. Defaults to all eligible contacts when omitted."
          },
          "segment_id": {
            "type": "string",
            "description": "One saved list id. Use with recipient_type segment."
          },
          "segment_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Saved list ids to combine. Use with recipient_type segment."
          },
          "has_media": {
            "type": "boolean",
            "description": "Set true to estimate an MMS draft's picture-message cost without uploading an image."
          }
        },
        "examples": [
          {
            "name": "Saturday reminder",
            "message": "Doors open at 9. Reply STOP to opt out.",
            "recipient_type": "all"
          }
        ]
      }
    },
    {
      "name": "test_sms_campaign",
      "description": "Send the stored SMS or MMS creative once to the API key holder's verified personal phone and charge organization balance. This spends money; it does not text the campaign audience or change the draft. Requires confirm true and a unique idempotency_key.",
      "scopes": [
        "sms:test"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "SMS or MMS campaign id."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to spend and send."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not act twice."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "confirm": true,
            "idempotency_key": "sms-test-2026-0001"
          }
        ]
      }
    },
    {
      "name": "send_sms_campaign",
      "description": "Start a prepared SMS or MMS draft in this organization and dispatch it to its recipients, charging organization balance. This spends money and cannot be undone; it requires confirm true and a unique idempotency_key. It does not create or edit the draft.",
      "scopes": [
        "sms:send"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "SMS or MMS campaign id."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to spend and send."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not act twice."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "confirm": true,
            "idempotency_key": "sms-send-2026-0001"
          }
        ]
      }
    },
    {
      "name": "schedule_sms_campaign",
      "description": "Queue an SMS or MMS draft in this organization to dispatch at a future time. It does not text anyone now; it requires confirm true and a unique idempotency_key. Note the two money events differ: any spend approval is used up now, when the campaign is queued, and is not available for another send afterwards; the balance hold is placed later, when the send job runs. Send and edit are separate tools.",
      "scopes": [
        "sms:send"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id",
          "scheduled_at",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "SMS or MMS campaign id."
          },
          "scheduled_at": {
            "type": "string",
            "description": "Future send time as an ISO-8601 date-time."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to spend and send."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not act twice."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "scheduled_at": "2026-11-03T14:00:00.000Z",
            "confirm": true,
            "idempotency_key": "sms-schedule-2026-0001"
          }
        ]
      }
    },
    {
      "name": "unschedule_sms_campaign",
      "description": "Return a scheduled SMS or MMS campaign in this organization to an editable draft before its send time. This cancels the queued dispatch; it does not send, and requires confirm true and a unique idempotency_key. Nothing is refunded: no balance hold was placed when it was scheduled, and the spend approval used to schedule it is not returned, so it cannot be reused for another send.",
      "scopes": [
        "sms:send"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "SMS or MMS campaign id."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to spend and send."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not act twice."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "confirm": true,
            "idempotency_key": "sms-unschedule-2026-0001"
          }
        ]
      }
    },
    {
      "name": "list_sms_recipients",
      "description": "List a page of recipient delivery outcomes for one SMS or MMS campaign in this organization, optionally filtered by status. Does not return phone numbers or message identifiers. Requires both sms:read and logs:read.",
      "scopes": [
        "sms:read",
        "logs:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "SMS or MMS campaign id."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "sending",
              "sent",
              "delivered",
              "failed",
              "skipped"
            ],
            "description": "Only recipients in this status."
          },
          "page": {
            "type": "integer",
            "minimum": 1,
            "description": "Page number, starting at 1."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 250,
            "description": "Recipients per page. At most 250."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "status": "failed",
            "page": 1,
            "limit": 100
          }
        ]
      }
    },
    {
      "name": "list_sms_events",
      "description": "List a page of delivery events for one SMS or MMS campaign in this organization from the durable send ledger, optionally filtered by status. Does not return raw recipient phone numbers or carrier payloads. Requires both sms:read and logs:read.",
      "scopes": [
        "sms:read",
        "logs:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "SMS or MMS campaign id."
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "sending",
              "sent",
              "delivered",
              "failed"
            ],
            "description": "Only events in this status."
          },
          "page": {
            "type": "integer",
            "minimum": 1,
            "description": "Page number, starting at 1."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 250,
            "description": "Events per page. At most 250."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "status": "delivered",
            "page": 1,
            "limit": 100
          }
        ]
      }
    },
    {
      "name": "get_sms_outcomes",
      "description": "Summarize delivery outcomes for one SMS or MMS campaign in this organization: sent, delivered, failed, skipped, and failure counts by reason. Does not return recipient phone numbers or individual message records. Requires both sms:read and logs:read.",
      "scopes": [
        "sms:read",
        "logs:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "SMS or MMS campaign id."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "get_sms_readiness",
      "description": "Report what is missing before this organization may send SMS or MMS right now, including sender readiness and balance. Read-only: it does not send, schedule, upload media, or change anything, and it returns no carrier account identifiers.",
      "scopes": [
        "sms:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {},
        "examples": [
          {}
        ]
      }
    },
    {
      "name": "list_mail_campaigns",
      "description": "List physical-mail campaigns for this organization, newest first, with status, piece counts, and cost fields. Call it to find a campaign id for get_mail_campaign. It does not return artwork, mailing addresses, or recipient data.",
      "scopes": [
        "mail:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "queued",
              "processing",
              "mailed",
              "paused",
              "completed",
              "failed",
              "canceled",
              "needs_attention"
            ],
            "description": "Only campaigns in this status. Omit for every status."
          },
          "page": {
            "type": "integer",
            "minimum": 1,
            "description": "Page number, starting at 1."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Page size. At most 100."
          }
        },
        "examples": [
          {
            "status": "queued",
            "page": 1,
            "limit": 25
          }
        ]
      }
    },
    {
      "name": "get_mail_campaign",
      "description": "Read one physical-mail campaign by id, including its frozen return identity, recipient statistics, and recent piece progress. Returns not found for another organization's campaign. It does not return artwork files, mailing addresses, or recipient data.",
      "scopes": [
        "mail:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "Physical-mail campaign id."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "create_mail_draft",
      "description": "Create a held physical-mail campaign draft in this organization from prepared artwork supplied here as base64. It validates the artwork and the audience and reserves the quoted balance, so it spends money and requires confirm true and a unique idempotency_key. Nothing is printed or mailed and no final amount is charged, so the draft can still be changed or cancelled. Size and cost it first with preview_mail_audience.",
      "scopes": [
        "mail:draft"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "name",
          "mail_type",
          "front_creative_base64",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Draft name. At most 160 characters."
          },
          "mail_type": {
            "type": "string",
            "enum": [
              "postcard",
              "letter"
            ],
            "description": "Kind of printed piece."
          },
          "postcard_size": {
            "type": "string",
            "enum": [
              "4x6",
              "6x9",
              "6x11"
            ],
            "description": "Postcard size. Required for a postcard; do not pass one for a letter, since a letter carrying a postcard size is rejected."
          },
          "front_creative_base64": {
            "type": "string",
            "description": "Front artwork as base64-encoded PDF or PNG. Required."
          },
          "back_creative_base64": {
            "type": "string",
            "description": "Back artwork as base64-encoded PDF or PNG. Required for a postcard and optional for a letter."
          },
          "notes": {
            "type": "string",
            "description": "Free-form internal notes."
          },
          "segment_id": {
            "type": "string",
            "description": "Saved-list id to mail. Omit to mail every eligible contact."
          },
          "include_partial_addresses": {
            "type": "boolean",
            "description": "Include unverified addresses. Defaults to false, mailing verified addresses only."
          },
          "one_mail_per_address": {
            "type": "boolean",
            "description": "Send at most one piece per household. Defaults to false."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not act twice."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to reserve the balance for this draft."
          }
        },
        "examples": [
          {
            "name": "Ada fall postcard",
            "mail_type": "postcard",
            "postcard_size": "6x9",
            "front_creative_base64": "JVBERi0xLjQKJcTl",
            "segment_id": "22222222-2222-4222-8222-222222222222",
            "one_mail_per_address": true,
            "idempotency_key": "ada-postcard-2026-0001",
            "confirm": true
          }
        ]
      }
    },
    {
      "name": "preview_mail_audience",
      "description": "Non-mutating preflight for a physical-mail campaign: eligible recipient count, cost estimate, and blockers. Call it after the mail options are agreed and before launching. It does not create a campaign, reserve funds, upload artwork, or submit anything for printing.",
      "scopes": [
        "mail:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "mail_type"
        ],
        "properties": {
          "mail_type": {
            "type": "string",
            "enum": [
              "postcard",
              "letter"
            ],
            "description": "Kind of printed piece."
          },
          "postcard_size": {
            "type": "string",
            "enum": [
              "4x6",
              "6x9",
              "6x11"
            ],
            "description": "Postcard size. Required for a postcard; do not pass one for a letter, since a letter carrying a postcard size is rejected."
          },
          "recipient_type": {
            "type": "string",
            "enum": [
              "all",
              "segment"
            ],
            "description": "Audience kind. Required."
          },
          "segment_id": {
            "type": "string",
            "description": "Saved-list id. Required when recipient_type is segment."
          },
          "include_partial_addresses": {
            "type": "boolean",
            "description": "Include unverified addresses in the estimate. Defaults to false."
          },
          "one_mail_per_address": {
            "type": "boolean",
            "description": "Count at most one piece per household. Defaults to false."
          }
        },
        "examples": [
          {
            "mail_type": "postcard",
            "postcard_size": "6x9",
            "recipient_type": "all"
          }
        ]
      }
    },
    {
      "name": "launch_mail_campaign",
      "description": "Print and dispatch a held physical-mail campaign and charge the organization balance. This spends money and cannot be undone: a printed piece cannot be recalled once it is submitted. It requires confirm true and a unique idempotency_key, accepts no audience or artwork changes, and is limited to a queued or needs_attention campaign already held in this organization; a plain draft is the one state it cannot start.",
      "scopes": [
        "mail:send"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "Held physical-mail campaign id."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to spend and send."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not act twice."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "confirm": true,
            "idempotency_key": "ada-mail-launch-0001"
          }
        ]
      }
    },
    {
      "name": "list_voice_campaigns",
      "description": "List voice and voicemail campaigns for this organization, newest first, with status and cost fields. Call it to find a campaign id for get_voice_campaign. It does not return recipient phone numbers, audio URLs, or provider identifiers.",
      "scopes": [
        "voice:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "call",
              "voicemail"
            ],
            "description": "Only campaigns of this kind. Omit for both."
          },
          "page": {
            "type": "integer",
            "minimum": 1,
            "description": "Page number, starting at 1."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Page size. At most 100."
          }
        },
        "examples": [
          {
            "type": "voicemail",
            "page": 1,
            "limit": 25
          }
        ]
      }
    },
    {
      "name": "get_voice_campaign",
      "description": "Read one voice or voicemail campaign by id with its name, status, message, and aggregate per-recipient call-status counts. Returns not found for another organization's campaign. It does not return recipient phone numbers or provider identifiers.",
      "scopes": [
        "voice:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "Voice or voicemail campaign id."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "create_voice_draft",
      "description": "Create a local voice or voicemail campaign draft. A one-to-one call draft needs a message script; a voicemail draft needs an uploaded audio asset token with its duration. It does not place calls, reserve funds, schedule delivery, or send anything.",
      "scopes": [
        "voice:draft"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "name",
          "type"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Draft name. At most 160 characters."
          },
          "type": {
            "type": "string",
            "enum": [
              "call",
              "voicemail"
            ],
            "description": "Kind of voice campaign."
          },
          "message": {
            "type": "string",
            "description": "Read-aloud script. Required for a one-to-one call draft."
          },
          "cc_number_id": {
            "type": "string",
            "description": "Caller ID to use. Omit for the organization default."
          },
          "recipient_type": {
            "type": "string",
            "enum": [
              "all",
              "segment",
              "custom"
            ],
            "description": "Audience kind. Required."
          },
          "segment_id": {
            "type": "string",
            "description": "Saved-list id. Required when recipient_type is segment."
          },
          "contact_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Contact ids. Required when recipient_type is custom."
          },
          "audio_asset_token": {
            "type": "string",
            "description": "Voicemail asset token obtained from the audio upload endpoint. Required for a voicemail draft."
          },
          "audio_duration": {
            "type": "integer",
            "minimum": 1,
            "maximum": 300,
            "description": "Audio length in seconds. Required for a voicemail draft."
          },
          "audio_url": {
            "type": "string",
            "description": "HTTPS audio URL. Prefer audio_asset_token."
          }
        },
        "examples": [
          {
            "name": "Ada voicemail draft",
            "type": "voicemail",
            "recipient_type": "all",
            "audio_asset_token": "11111111-1111-4111-8111-111111111111",
            "audio_duration": 30
          }
        ]
      }
    },
    {
      "name": "launch_voice_campaign",
      "description": "Launch a prepared voicemail campaign and charge the organization balance as calls connect. This spends money and dispatches calls to recipients; it requires confirm true and a unique idempotency_key. It launches only voicemail campaigns that already have audio, not one-to-one calls, and it holds to quiet hours, consent, and balance rules.",
      "scopes": [
        "voice:send"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "Voicemail campaign id that has audio."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to spend and send."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not act twice."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "confirm": true,
            "idempotency_key": "ada-voicemail-launch-0001"
          }
        ]
      }
    },
    {
      "name": "list_voice_recipients",
      "description": "List paginated per-recipient outcomes for one voice campaign, including call status, cost, and timestamps. Call it to review progress before or after a launch. It does not return phone numbers, email addresses, audio URLs, or provider identifiers.",
      "scopes": [
        "voice:read",
        "logs:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "Voice or voicemail campaign id."
          },
          "call_status": {
            "type": "string",
            "enum": [
              "pending",
              "dialing",
              "completed",
              "failed",
              "skipped"
            ],
            "description": "Only recipients in this call status. Omit for every recipient."
          },
          "page": {
            "type": "integer",
            "minimum": 1,
            "description": "Page number, starting at 1."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 250,
            "description": "Page size. At most 250."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "call_status": "completed",
            "limit": 100
          }
        ]
      }
    },
    {
      "name": "start_voice_call",
      "description": "Place one supervised one-to-one call that bridges the key owner's already-verified operator phone to a single contact, and charge the organization balance as the call connects. This spends money and cannot be un-rung; it requires confirm true and a unique idempotency_key. The agent cannot set the bridge destination, and no autonomous voice or text-to-speech is placed.",
      "scopes": [
        "voice:send"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "contact_id",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "contact_id": {
            "type": "string",
            "description": "Contact to call."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to spend and send."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not act twice."
          }
        },
        "examples": [
          {
            "contact_id": "33333333-3333-4333-8333-333333333333",
            "confirm": true,
            "idempotency_key": "ada-voice-call-0001"
          }
        ]
      }
    },
    {
      "name": "get_voice_call",
      "description": "Read one voice call session by id, sanitized to omit call-control identifiers. Returns not found for another organization's call. It does not place, change, or end a call.",
      "scopes": [
        "voice:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "call_id"
        ],
        "properties": {
          "call_id": {
            "type": "string",
            "description": "Voice call session id."
          }
        },
        "examples": [
          {
            "call_id": "44444444-4444-4444-8444-444444444444"
          }
        ]
      }
    },
    {
      "name": "list_voice_numbers",
      "description": "List the voice-enabled caller IDs this organization may place calls from, marking the default one. Use a returned id as a campaign caller ID. It does not buy, release, or change numbers and does not return connection identifiers.",
      "scopes": [
        "voice:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {},
        "examples": [
          {}
        ]
      }
    },
    {
      "name": "upload_voice_asset",
      "description": "Store a prepared MP3 or WAV voicemail audio supplied here as base64 and return the asset token that create_voice_draft requires. It does not place calls, create a campaign, or charge anything. Audio must actually be MP3 or WAV and at most ten megabytes.",
      "scopes": [
        "voice:draft"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "audio_base64"
        ],
        "properties": {
          "audio_base64": {
            "type": "string",
            "description": "MP3 or WAV audio as base64."
          },
          "content_type": {
            "type": "string",
            "enum": [
              "audio/mpeg",
              "audio/mp3",
              "audio/wav",
              "audio/x-wav"
            ],
            "description": "Declared audio type. Defaults to audio/mpeg. The contents must match it."
          }
        },
        "examples": [
          {
            "audio_base64": "SUQzBAAAAAAAI1RT",
            "content_type": "audio/mpeg"
          }
        ]
      }
    },
    {
      "name": "list_dialer_sessions",
      "description": "List this operator's recent power-dialer sessions, newest first. Each session is an operator-first queue. On a conference session the operator is dialled once and stays live; on a legacy_bridge session, used when conference admission is not granted, the operator is dialled again for every contact. It does not dial a recipient and does not start any call.",
      "scopes": [
        "voice:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "page": {
            "type": "integer",
            "minimum": 1,
            "description": "Page number, starting at 1."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Page size. At most 100."
          }
        },
        "examples": [
          {
            "page": 1,
            "limit": 25
          }
        ]
      }
    },
    {
      "name": "get_dialer_session",
      "description": "Read one power-dialer session by id: queue position, the current contact, and session state. Call it to decide whether to advance to the next contact. It does not start a call or change the queue.",
      "scopes": [
        "voice:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "session_id"
        ],
        "properties": {
          "session_id": {
            "type": "string",
            "description": "Power-dialer session id."
          }
        },
        "examples": [
          {
            "session_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "advance_dialer_session",
      "description": "Hang up the current contact and announce the next one in an operator-first power-dialer session, charging the session meter while the operator stays connected. This spends money and dispatches a call; it requires confirm true and a unique idempotency_key. On a conference session the operator is already bridged and is not dialled again. On a legacy_bridge session, used when conference admission is not granted, the operator is dialled again on each Next.",
      "scopes": [
        "voice:send"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "session_id",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "session_id": {
            "type": "string",
            "description": "Power-dialer session to advance."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to spend and send."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not act twice."
          }
        },
        "examples": [
          {
            "session_id": "11111111-1111-4111-8111-111111111111",
            "confirm": true,
            "idempotency_key": "ada-dialer-next-0001"
          }
        ]
      }
    },
    {
      "name": "get_active_call",
      "description": "Read the voice call this organization has live right now: who is on it, its state, and how long it has been running. Read-only. It does not dial, join, transfer, or hang up any call.",
      "scopes": [
        "voice:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {},
        "examples": [
          {}
        ]
      }
    },
    {
      "name": "start_dialer_session",
      "description": "Open or resume this operator's power-dialer session for a call campaign. On a conference session the operator is dialled once and stays live while each contact is announced in turn, so there is no manual dial; on a legacy_bridge session, used when conference admission is not granted, the operator is dialled again for every contact. This only opens or resumes the queue and does not dial any contact yet. The bridge destination is the verified personal phone already configured in Account settings and cannot be set here.",
      "scopes": [
        "voice:draft"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "Call campaign id whose contacts form the queue. Voicemail campaigns are not accepted here."
          },
          "cc_number_id": {
            "type": "string",
            "description": "Optional caller id to present. Omit to use the organization default."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "verify_emails",
      "description": "Charge the organization balance and launch an email verification run over every contact or one saved audience, marking each address deliverable or undeliverable. This spends money and starts a background run; it does not send any mail. It requires confirm true and a unique idempotency_key.",
      "scopes": [
        "email:verify"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "recipient_type": {
            "type": "string",
            "enum": [
              "all",
              "segment"
            ],
            "description": "Scope of the run. all checks every contact; segment limits it to saved audiences."
          },
          "segment_id": {
            "type": "string",
            "description": "One saved audience id. Use with recipient_type segment."
          },
          "segment_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Multiple saved audience ids. Use with recipient_type segment."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to spend and send."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not act twice."
          }
        },
        "examples": [
          {
            "recipient_type": "all",
            "confirm": true,
            "idempotency_key": "clean-list-2026-run-01"
          }
        ]
      }
    },
    {
      "name": "verify_addresses",
      "description": "Charge the organization balance and launch an address check over the organization mailing audience, flagging each address as deliverable or undeliverable. This spends money and starts a background run; it does not print or mail anything. It requires confirm true and a unique idempotency_key.",
      "scopes": [
        "address:verify"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to spend and send."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not act twice."
          }
        },
        "examples": [
          {
            "confirm": true,
            "idempotency_key": "clean-addresses-2026-run-01"
          }
        ]
      }
    },
    {
      "name": "list_email_templates",
      "description": "List the saved transactional email templates for this organization, with each template id, name, and subject. Use a returned id with get_email_template or send_transactional_email. It does not return template bodies or send anything.",
      "scopes": [
        "email:transactional"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {},
        "examples": [
          {}
        ]
      }
    },
    {
      "name": "get_email_template",
      "description": "Read one saved transactional email template by id, including its name, subject, and stored markup. Returns not found when the template is not in this organization. It does not send anything or change the template.",
      "scopes": [
        "email:transactional"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "template_id"
        ],
        "properties": {
          "template_id": {
            "type": "string",
            "description": "Transactional email template id."
          }
        },
        "examples": [
          {
            "template_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "create_email_template",
      "description": "Create a saved transactional email template for this organization, the same record the templates page edits. Send a name and subject, plus html and/or text; a template must carry at least one body. Subject and bodies may use double curly brace placeholders, filled from variables on a send. A name already used in this organization is refused. This does not send any email and does not charge the balance; use send_transactional_email to send.",
      "scopes": [
        "email:transactional"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "name",
          "subject"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Template name, 1 to 80 characters."
          },
          "subject": {
            "type": "string",
            "description": "Subject line. May contain double curly brace placeholders."
          },
          "html": {
            "type": "string",
            "description": "HTML body. Provide html, text, or both."
          },
          "text": {
            "type": "string",
            "description": "Plain-text body. Provide html, text, or both."
          }
        },
        "examples": [
          {
            "name": "Shift confirmation",
            "subject": "Your shift on {{date}}",
            "html": "<p>See you at {{time}}.</p>",
            "text": "See you at {{time}}."
          }
        ]
      }
    },
    {
      "name": "update_email_template",
      "description": "Change a saved transactional email template in this organization. Only the fields you pass are changed; an omitted name, subject, html, or text keeps its stored value. Send an empty string for html or text to clear that body, or a new value to replace it. An update that carries no fields is refused, as is a name already used by another template here. This does not send any email and does not charge the balance.",
      "scopes": [
        "email:transactional"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "template_id"
        ],
        "properties": {
          "template_id": {
            "type": "string",
            "description": "Transactional email template id."
          },
          "name": {
            "type": "string",
            "description": "New template name, 1 to 80 characters. Omit to keep the stored name."
          },
          "subject": {
            "type": "string",
            "description": "New subject line. Omit to keep the stored subject."
          },
          "html": {
            "type": "string",
            "description": "New HTML body, or an empty string to clear it. Omit to keep the stored value."
          },
          "text": {
            "type": "string",
            "description": "New plain-text body, or an empty string to clear it. Omit to keep the stored value."
          }
        },
        "examples": [
          {
            "template_id": "11111111-1111-4111-8111-111111111111",
            "subject": "Your shift on {{date}} (updated)"
          }
        ]
      }
    },
    {
      "name": "delete_email_template",
      "description": "Delete one saved transactional email template from this organization. This cannot be undone and removes only that template; it does not send, recall, or charge anything, and already-sent messages keep their content. Returns not found for another organization's template or an unknown id.",
      "scopes": [
        "email:transactional"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "template_id"
        ],
        "properties": {
          "template_id": {
            "type": "string",
            "description": "Transactional email template id."
          }
        },
        "examples": [
          {
            "template_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "send_transactional_email",
      "description": "Charge the organization balance and send one transactional email to a single address, optionally rendering a saved template with variables. This spends money and cannot be undone. Suppression and consent rules still apply. It requires confirm true and a unique idempotency_key, and it is not for bulk campaign mail.",
      "scopes": [
        "email:transactional"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "to",
          "subject",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "to": {
            "type": "string",
            "description": "Recipient email address."
          },
          "subject": {
            "type": "string",
            "description": "Subject line. No line breaks."
          },
          "html": {
            "type": "string",
            "description": "HTML body. Provide html, text, or a template_id."
          },
          "text": {
            "type": "string",
            "description": "Plain-text body."
          },
          "from": {
            "type": "string",
            "description": "From address. Defaults to the organization's verified From."
          },
          "reply_to": {
            "type": "string",
            "description": "Reply-To address. Defaults to the organization reply-to."
          },
          "template_id": {
            "type": "string",
            "description": "Saved template id to render when html or text is omitted."
          },
          "tracking": {
            "type": "boolean",
            "description": "Turn open and click tracking on for this message. Defaults to false."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to spend and send."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not act twice."
          }
        },
        "examples": [
          {
            "to": "volunteer@example.org",
            "subject": "Your shift confirmation",
            "html": "<p>Thanks for signing up.</p>",
            "confirm": true,
            "idempotency_key": "shift-confirm-2026-0001"
          }
        ]
      }
    },
    {
      "name": "list_transactional_emails",
      "description": "List recent transactional emails for this organization, newest first, with id, recipient, subject, status, and timestamps. Filter by status or recipient address. It does not return message bodies and does not send anything.",
      "scopes": [
        "email:transactional"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "reserved",
              "sending",
              "sent",
              "delivered",
              "opened",
              "clicked",
              "bounced",
              "complained",
              "failed",
              "unknown"
            ],
            "description": "Only messages in this delivery state."
          },
          "to": {
            "type": "string",
            "description": "Only messages to this exact recipient address."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "How many messages to return. At most 100."
          }
        },
        "examples": [
          {
            "status": "delivered",
            "limit": 50
          }
        ]
      }
    },
    {
      "name": "get_transactional_email",
      "description": "Read one transactional email by id, including its recipients, status, and delivery timestamps. Returns not found when the message is not in this organization. It is read-only and does not resend or change anything.",
      "scopes": [
        "email:transactional"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "message_id"
        ],
        "properties": {
          "message_id": {
            "type": "string",
            "description": "Transactional email id."
          }
        },
        "examples": [
          {
            "message_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "list_audit_log",
      "description": "Page through this organization's audit log, newest first: credential actions plus every paid or bulk action from any source (dashboard, API, MCP, cron, import), such as list cleaning, email or address verification, campaign launches, number and 10DLC purchases, balance holds and imports. Paid entries carry source, outcome (started, succeeded, partial, failed, blocked), estimated and actual cost in cents, the target list or segment ids and names, counts, start and end time, and the request id. Filter by action, source, outcome, actor, API key, request id or date. Use it to answer who did what and when. It never returns campaign message bodies or contact data, and it changes no business state (the read records one routine audit row).",
      "scopes": [
        "logs:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "page": {
            "type": "integer",
            "minimum": 1,
            "description": "Page number, starting at 1."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 250,
            "description": "Events per page. At most 250."
          },
          "action": {
            "type": "string",
            "description": "Only this action, such as email_verification.run. End with * for a prefix."
          },
          "source": {
            "type": "string",
            "enum": [
              "ui",
              "api",
              "mcp",
              "cron",
              "import",
              "system"
            ],
            "description": "Only entries from this source."
          },
          "outcome": {
            "type": "string",
            "enum": [
              "started",
              "succeeded",
              "partial",
              "failed",
              "blocked"
            ],
            "description": "Only paid or bulk actions with this outcome."
          },
          "actor_user_id": {
            "type": "string",
            "description": "Only entries by this user id."
          },
          "key_id": {
            "type": "string",
            "description": "Only entries made with this API key id."
          },
          "request_id": {
            "type": "string",
            "description": "Only entries from this request id."
          },
          "paid_only": {
            "type": "boolean",
            "description": "true returns only paid and bulk actions."
          },
          "since": {
            "type": "string",
            "description": "ISO 8601 start time (inclusive)."
          },
          "until": {
            "type": "string",
            "description": "ISO 8601 end time (exclusive)."
          }
        },
        "examples": [
          {
            "page": 1,
            "limit": 100
          },
          {
            "paid_only": true,
            "outcome": "blocked",
            "since": "2026-10-15T00:00:00Z"
          }
        ]
      }
    },
    {
      "name": "list_inbox_conversations",
      "description": "List inbox conversations for this organization, newest activity first, with id, channel, subject, status, and a short preview. Filter by status or channel. It returns contact display names only, never email addresses, phone numbers, or media, and it does not send a reply.",
      "scopes": [
        "inbox:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "active",
              "closed"
            ],
            "description": "Only conversations in this state."
          },
          "channel": {
            "type": "string",
            "enum": [
              "sms",
              "email"
            ],
            "description": "Only conversations on this channel."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "How many conversations to return. At most 100."
          }
        },
        "examples": [
          {
            "channel": "sms",
            "status": "active",
            "limit": 50
          }
        ]
      }
    },
    {
      "name": "get_inbox_conversation",
      "description": "Read one inbox conversation by id, including its recent message history and delivery state. Returns not found when the conversation is not in this organization. It is for read-only triage and does not send, assign, or close anything.",
      "scopes": [
        "inbox:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "conversation_id"
        ],
        "properties": {
          "conversation_id": {
            "type": "string",
            "description": "Inbox conversation id."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "How many recent messages to include. At most 100."
          }
        },
        "examples": [
          {
            "conversation_id": "11111111-1111-4111-8111-111111111111",
            "limit": 100
          }
        ]
      }
    },
    {
      "name": "export_contacts",
      "description": "Read a page of this organization's contact records as JSON, including consent, verification, and tag fields, with a total count and a has-more flag. Page through because the full list can be large. It returns records inline and does not produce a downloadable file.",
      "scopes": [
        "exports:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "page": {
            "type": "integer",
            "minimum": 1,
            "description": "Page number, starting at 1."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10000,
            "description": "Records per page. At most 10000."
          }
        },
        "examples": [
          {
            "page": 1,
            "limit": 1000
          }
        ]
      }
    },
    {
      "name": "get_communications_settings",
      "description": "Read this organization's sending identity and channel settings: from name and address, reply-to, sending-domain authentication state, setup status, and compliance details. It is redacted and read-only and does not change or verify a domain.",
      "scopes": [
        "settings:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {},
        "examples": [
          {}
        ]
      }
    },
    {
      "name": "get_organization_settings",
      "description": "Read this organization's identity and compliance settings: committee name, paid-for-by line, mailing address, From name and address, Reply-To, entity type, EIN, legal name, and website, together with setup completion and the exact missing fields. Verification tokens and provider identifiers are never returned. Read-only; it changes no business state (the read records one routine audit row) and registers nothing.",
      "scopes": [
        "settings:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {},
        "examples": [
          {}
        ]
      }
    },
    {
      "name": "update_organization_settings",
      "description": "Save this organization's identity and compliance settings, exactly like the Account, Organization settings form. It replaces the whole form: every required field below must be sent, and these optional fields are cleared when you omit them: street2, optional contact, EIN, and the default phone. The legal name defaults to the committee name when omitted, and the website falls back to the optional contact when omitted, matching the dashboard. Send a Campaign Verify token only to change it; a saved token mask leaves the stored token in place. The caller must be an active owner or admin. This does not send, register, or charge anything.",
      "scopes": [
        "settings:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "committee_name",
          "authorization_line",
          "physical_address_street",
          "physical_address_city",
          "physical_address_state",
          "physical_address_zip",
          "email_from_name",
          "email_from_address",
          "reply_to_email",
          "entity_type"
        ],
        "properties": {
          "committee_name": {
            "type": "string",
            "description": "Committee or campaign name."
          },
          "authorization_line": {
            "type": "string",
            "description": "Paid-for-by / authorized-by line."
          },
          "physical_address_street": {
            "type": "string",
            "description": "Mailing street address."
          },
          "physical_address_street2": {
            "type": "string",
            "description": "Mailing address line 2. Cleared when omitted."
          },
          "physical_address_city": {
            "type": "string",
            "description": "Mailing city."
          },
          "physical_address_state": {
            "type": "string",
            "description": "Mailing state."
          },
          "physical_address_zip": {
            "type": "string",
            "description": "Mailing ZIP."
          },
          "optional_contact": {
            "type": "string",
            "description": "Optional public contact. Cleared when omitted."
          },
          "reply_to_email": {
            "type": "string",
            "description": "Reply-To email address."
          },
          "email_from_name": {
            "type": "string",
            "description": "From name for email."
          },
          "email_from_address": {
            "type": "string",
            "description": "From email address."
          },
          "entity_type": {
            "type": "string",
            "enum": [
              "candidate",
              "pac",
              "party",
              "other_527",
              "501c3",
              "501c4",
              "501c5",
              "501c6"
            ],
            "description": "Organization type. Any other value is rejected."
          },
          "ein": {
            "type": "string",
            "description": "Optional EIN. Cleared when omitted."
          },
          "legal_business_name": {
            "type": "string",
            "description": "Optional legal name. Defaults to the committee name when omitted."
          },
          "website": {
            "type": "string",
            "description": "Optional website. When omitted it falls back to the optional contact, matching the dashboard. Send an empty string to clear it without changing the optional contact."
          },
          "default_campaign_phone_id": {
            "type": "string",
            "description": "Optional default sending number id from this organization. Another organization's number is rejected. Cleared when omitted."
          },
          "campaign_verify_token": {
            "type": "string",
            "description": "Optional Campaign Verify token for political texting. A saved mask leaves the stored token unchanged."
          },
          "campaign_verify_email_token": {
            "type": "string",
            "description": "Optional Campaign Verify token for email. A saved mask leaves the stored token unchanged."
          }
        },
        "examples": [
          {
            "committee_name": "Friends of Ada",
            "authorization_line": "Paid for by Friends of Ada",
            "physical_address_street": "1 Main St",
            "physical_address_city": "Polk City",
            "physical_address_state": "FL",
            "physical_address_zip": "33868",
            "email_from_name": "Friends of Ada",
            "email_from_address": "send@example.org",
            "reply_to_email": "reply@example.org",
            "entity_type": "candidate"
          }
        ]
      }
    },
    {
      "name": "add_conversion",
      "description": "Record a conversion event against a campaign in this organization, carrying an external id, an optional value in cents, and the time it occurred. Repeating the same external id returns the existing event instead of a duplicate. It does not send, charge, or edit the campaign.",
      "scopes": [
        "conversions:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id",
          "event",
          "external_id"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "Campaign the conversion belongs to."
          },
          "event": {
            "type": "string",
            "description": "Event name, such as donation or signup. Lowercase letters, digits, hyphens, underscores."
          },
          "external_id": {
            "type": "string",
            "description": "Your unique id for this event, used to deduplicate retries."
          },
          "value_cents": {
            "type": "integer",
            "minimum": 0,
            "description": "Optional event value in whole cents."
          },
          "occurred_at": {
            "type": "string",
            "description": "Optional ISO 8601 timestamp for when the event happened."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "event": "donation",
            "external_id": "donation-0001",
            "value_cents": 2500
          }
        ]
      }
    },
    {
      "name": "list_agent_runs",
      "description": "List durable work runs started by this exact credential in this organization, newest first, with id, status, objective, and timestamps. Use a returned id with get_agent_run. It does not start, update, or delete a run.",
      "scopes": [
        "agent:runs"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "How many runs to return. At most 100."
          }
        },
        "examples": [
          {
            "limit": 50
          }
        ]
      }
    },
    {
      "name": "get_agent_run",
      "description": "Read one durable work run by id, including its status, objective, summary, and timestamps. Returns not found when the run does not belong to this credential and organization. It is read-only and does not update the run.",
      "scopes": [
        "agent:runs"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "run_id"
        ],
        "properties": {
          "run_id": {
            "type": "string",
            "description": "Agent run id."
          }
        },
        "examples": [
          {
            "run_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "get_texting_brand_registration",
      "description": "Read this organization's stored texting brand-registration status, the authoritative one-time brand filing fee, and whether the credential is the active, fully-set-up organization owner (role-and-setup eligibility only, not spending authority). It reports the locally stored filing state, not a live carrier reconciliation, and never returns carrier identifiers, raw payloads, verification tokens, or a full EIN. It has no business effect: it registers nothing, charges nothing, creates no profile, and assigns no number. Paid brand submission is available through submit_texting_brand_registration (POST /api/v1/registration/brand) when the credential holds registration:submit and the active organization owner has completed setup; the owner-and-setup flag here is not a credential grant. One routine API audit row is recorded.",
      "scopes": [
        "registration:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {},
        "examples": [
          {}
        ]
      }
    },
    {
      "name": "preview_texting_brand_registration",
      "description": "Validate would-be texting brand-registration details against this organization's saved settings, reporting the required fixes and the authoritative brand filing fee, exactly like the dashboard's business-information step. Omitted fields fall back to the saved value or the credential owner's own name, email, and phone; an empty string clears a value. It reserves nothing, completes no setup, and guarantees no carrier acceptance. The `ready` flag means the packet passed field validation only, not that the organization is already filed, permitted, or funded. It has no business effect: it registers nothing, charges nothing, and creates nothing; one routine API audit row is recorded. Paid submission is available through submit_texting_brand_registration (POST /api/v1/registration/brand).",
      "scopes": [
        "registration:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "legal_business_name": {
            "type": "string",
            "description": "Legal business name. Omit to use the saved value."
          },
          "ein": {
            "type": "string",
            "description": "Nine-digit EIN, such as 12-3456789. Omit to use the saved value."
          },
          "street_address": {
            "type": "string",
            "description": "Street address. Omit to use the saved value."
          },
          "street_address2": {
            "type": "string",
            "description": "Street address line 2. Omit to use the saved value."
          },
          "city": {
            "type": "string",
            "description": "City. Omit to use the saved value."
          },
          "state": {
            "type": "string",
            "description": "Two-letter state abbreviation. Omit to use the saved value."
          },
          "postal_code": {
            "type": "string",
            "description": "ZIP code. Omit to use the saved value."
          },
          "website": {
            "type": "string",
            "description": "Public website. Send an empty string to clear it; omit to use the saved value."
          },
          "authorized_rep_name": {
            "type": "string",
            "description": "Authorized representative name. Omit to use the credential owner's name."
          },
          "authorized_rep_email": {
            "type": "string",
            "description": "Authorized representative email. Omit to use the credential owner's email."
          },
          "authorized_rep_phone": {
            "type": "string",
            "description": "Authorized representative phone. Omit to use the credential owner's phone."
          }
        },
        "examples": [
          {}
        ]
      }
    },
    {
      "name": "request_texting_brand_approval",
      "description": "Create a pending spend-approval request for this organization's 10DLC brand filing, for a key that requires an operator to approve each filing. It builds the exact carrier packet the submission will file (the same fields and fallbacks as submit_texting_brand_registration) so the operator approves that exact packet. It charges nothing, reserves nothing, files nothing, and cannot approve itself: the account owner approves the pending request in Account Settings → Agent approvals, then pass the returned approval id to submit_texting_brand_registration as approval_id. Only the active organization owner may request it. Requires scope registration:submit and a unique idempotency_key.",
      "scopes": [
        "registration:submit"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "idempotency_key"
        ],
        "properties": {
          "legal_business_name": {
            "type": "string",
            "description": "Legal business name. Omit to use the saved value."
          },
          "ein": {
            "type": "string",
            "description": "Nine-digit EIN, such as 12-3456789. Omit to use the saved value."
          },
          "street_address": {
            "type": "string",
            "description": "Street address. Omit to use the saved value."
          },
          "street_address2": {
            "type": "string",
            "description": "Street address line 2. Omit to use the saved value."
          },
          "city": {
            "type": "string",
            "description": "City. Omit to use the saved value."
          },
          "state": {
            "type": "string",
            "description": "Two-letter state abbreviation. Omit to use the saved value."
          },
          "postal_code": {
            "type": "string",
            "description": "ZIP code. Omit to use the saved value."
          },
          "website": {
            "type": "string",
            "description": "Public website. Send an empty string to clear it; omit to use the saved value."
          },
          "authorized_rep_name": {
            "type": "string",
            "description": "Authorized representative name. Omit to use the credential owner's name."
          },
          "authorized_rep_email": {
            "type": "string",
            "description": "Authorized representative email. Omit to use the credential owner's email."
          },
          "authorized_rep_phone": {
            "type": "string",
            "description": "Authorized representative phone. Omit to use the credential owner's phone."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not act twice."
          }
        },
        "examples": [
          {
            "idempotency_key": "brand-approval-2026-01"
          }
        ]
      }
    },
    {
      "name": "submit_texting_brand_registration",
      "description": "File this organization's 10DLC brand registration and charge the non-refundable one-time brand filing fee. Only the active, fully-set-up organization owner may file, and the fee comes from the server, never the caller. Omitted fields fall back to the saved value or the credential owner's own name, email, and phone; an empty string clears a value. It spends money and cannot be undone: it requires confirm true, a unique idempotency_key, and scope registration:submit. A repeat with the same fields and key is already on file and charges nothing; an uncertain provider outcome is never retried blindly. If this key requires an approval, request it first with the same fields and pass the returned id as approval_id.",
      "scopes": [
        "registration:submit"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "legal_business_name": {
            "type": "string",
            "description": "Legal business name. Omit to use the saved value."
          },
          "ein": {
            "type": "string",
            "description": "Nine-digit EIN, such as 12-3456789. Omit to use the saved value."
          },
          "street_address": {
            "type": "string",
            "description": "Street address. Omit to use the saved value."
          },
          "street_address2": {
            "type": "string",
            "description": "Street address line 2. Omit to use the saved value."
          },
          "city": {
            "type": "string",
            "description": "City. Omit to use the saved value."
          },
          "state": {
            "type": "string",
            "description": "Two-letter state abbreviation. Omit to use the saved value."
          },
          "postal_code": {
            "type": "string",
            "description": "ZIP code. Omit to use the saved value."
          },
          "website": {
            "type": "string",
            "description": "Public website. Send an empty string to clear it; omit to use the saved value."
          },
          "authorized_rep_name": {
            "type": "string",
            "description": "Authorized representative name. Omit to use the credential owner's name."
          },
          "authorized_rep_email": {
            "type": "string",
            "description": "Authorized representative email. Omit to use the credential owner's email."
          },
          "authorized_rep_phone": {
            "type": "string",
            "description": "Authorized representative phone. Omit to use the credential owner's phone."
          },
          "approval_id": {
            "type": "string",
            "description": "Optional spend approval id for a key that requires an operator to approve each filing."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to spend and send."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not act twice."
          }
        },
        "examples": [
          {
            "confirm": true,
            "idempotency_key": "brand-file-2026-01"
          }
        ]
      }
    },
    {
      "name": "get_sending_domain",
      "description": "Read this organization's sending-domain authentication state and the exact DNS records it requires, together with whether the provider identity is verified. For an owned domain it reads the provider directly; a domain this organization does not own is reported as not linked without asking the provider, so it never discloses another organization's domain. Read-only; it claims nothing and changes no business state.",
      "scopes": [
        "settings:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "domain": {
            "type": "string",
            "description": "Optional domain to read. Omit to use the saved campaign domain."
          },
          "profile": {
            "type": "string",
            "enum": [
              "campaign",
              "transactional",
              "registry"
            ],
            "description": "Which saved profile to read. Defaults to campaign."
          }
        },
        "examples": [
          {},
          {
            "domain": "example.com",
            "profile": "campaign"
          }
        ]
      }
    },
    {
      "name": "preview_sending_domain",
      "description": "Check whether an ensure, verify, or recover action for a sending domain would be eligible, without claiming the domain, calling the provider, or saving anything. It reports the proposed domain, the blockers that would stop it, and the side effects of the matching configuration call. It never inspects or discloses an unattached provider identity. Read-only.",
      "scopes": [
        "settings:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "domain": {
            "type": "string",
            "description": "Optional domain to preview."
          },
          "from_email": {
            "type": "string",
            "description": "Optional From address to preview."
          },
          "profile": {
            "type": "string",
            "enum": [
              "campaign",
              "transactional",
              "registry"
            ],
            "description": "Which profile the action would use. Defaults to campaign."
          }
        },
        "examples": [
          {},
          {
            "domain": "example.com",
            "from_email": "chair@example.com"
          }
        ]
      }
    },
    {
      "name": "configure_sending_domain",
      "description": "Authenticate a sending domain for this organization: ensure creates the identity and returns the records to publish, verify rechecks an existing identity, and recover resumes a partially completed configuration. Only an active owner or admin in the credential's organization may run it, and it works during an incomplete setup so the From domain can be finished. It never sends a message, charges a balance, turns on a channel, or changes a price, rate, pause, or account state.",
      "scopes": [
        "settings:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "action"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "ensure",
              "verify",
              "recover"
            ],
            "description": "ensure creates and returns records, verify rechecks, recover resumes."
          },
          "profile": {
            "type": "string",
            "enum": [
              "campaign",
              "transactional",
              "registry"
            ],
            "description": "Which saved profile to configure. Defaults to campaign."
          },
          "domain": {
            "type": "string",
            "description": "Optional domain. Defaults to the saved From domain."
          },
          "from_email": {
            "type": "string",
            "description": "Optional From address on that domain. Defaults to the saved From address."
          }
        },
        "examples": [
          {
            "action": "ensure",
            "from_email": "chair@example.com"
          }
        ]
      }
    },
    {
      "name": "list_my_calling_campaigns",
      "description": "List the prepared calling queues this member may work in the credential's organization, plus this member's OWN active or paused shift session id per queue, if any. Returns the organization's launched calling campaigns with a pending queue; the list is organization-wide, not an individual assignment, and only your own shift ids are personal. A queue's pending_count of null means the count could not be read (unavailable), not zero. `truncated` reports honestly whether more campaigns could exist than were read, and `shiftsTruncated` reports honestly whether your own shift page was capped, so a null session_id cannot be read as proof you have no open shift. It reads no balance, sender identity, contact/audience inventory, or management advice, changes no business state (the read records one routine audit row), and calls nothing.",
      "scopes": [
        "calls:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {},
        "examples": [
          {}
        ]
      }
    },
    {
      "name": "update_my_calling_phone",
      "description": "Save the credential owner's OWN operator phone. Accepts only a `phone` value; a caller-supplied user or organization selector is refused. The number is normalized but is NOT verified, NOT provisioned, and NOT rung, and this does not place a call or move any money. It returns only whether a phone was set and its last four digits, never your full profile.",
      "scopes": [
        "calls:operate"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "phone"
        ],
        "properties": {
          "phone": {
            "type": "string",
            "description": "Your operator phone as a U.S. number. It is normalized but not verified."
          }
        },
        "examples": [
          {
            "phone": "+18635550101"
          }
        ]
      }
    },
    {
      "name": "get_personal_settings",
      "description": "Read the credential owner's OWN personal profile and preferences: full name, personal phone (with its normalized form), time zone, country, default sender name and addresses, SMS signature, quiet hours, opt-in preferences, and notification preferences. The effect is user-wide: these settings belong to the person across every organization they belong to, not to one organization. It returns only the owner's own row, never another user's, and never a role, administrator flag, balance, credential, or secret. It changes no business state (the read records one routine audit row) and sends nothing.",
      "scopes": [
        "profile:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {},
        "examples": [
          {}
        ]
      }
    },
    {
      "name": "update_personal_settings",
      "description": "Change the credential owner's OWN personal profile and preferences, the same records the Account, About You / Communication / Consent / Notification forms edit. The effect is user-wide: these settings belong to the person across every organization they belong to. Only the fields you pass are changed; an omitted field keeps its stored value, and an empty string or null clears a string field. A saved phone is normalized but is NOT verified, provisioned, or rung. Notification preferences govern FUTURE alerts only; saving them does not send, schedule, or charge anything, and the low-balance threshold is an alert threshold, not a spending limit or Auto Top-Up authority. It cannot change your email/login identity, password, MFA, verification flags, role, membership, organization selection, administrator flags, balance, spending authorization, or API credentials.",
      "scopes": [
        "profile:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "full_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your full name. Omit to keep it; send an empty string or null to clear it."
          },
          "personal_phone": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your personal U.S. phone number. It is normalized but not verified. Omit to keep it; send an empty string or null to clear it."
          },
          "timezone": {
            "type": [
              "string",
              "null"
            ],
            "description": "IANA time zone, such as America/New_York. Omit to keep it; send an empty string or null to clear it."
          },
          "country": {
            "type": [
              "string",
              "null"
            ],
            "description": "Two-letter country code, such as US. Omit to keep it; send an empty string or null to clear it."
          },
          "default_from_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Default sender (From) name. Omit to keep it; send an empty string or null to clear it."
          },
          "default_from_email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Default sender (From) email address. Omit to keep it; send an empty string or null to clear it."
          },
          "default_reply_to_email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Default Reply-To email address. Omit to keep it; send an empty string or null to clear it."
          },
          "default_sms_signature": {
            "type": [
              "string",
              "null"
            ],
            "description": "Default SMS signature. Omit to keep it; send an empty string or null to clear it."
          },
          "quiet_hours_enabled": {
            "type": "boolean",
            "description": "Whether quiet hours are on. Omit to keep it."
          },
          "quiet_hours_start": {
            "type": [
              "string",
              "null"
            ],
            "description": "Quiet-hours start time, such as 21:00. Omit to keep it; send an empty string or null to clear it."
          },
          "quiet_hours_end": {
            "type": [
              "string",
              "null"
            ],
            "description": "Quiet-hours end time, such as 09:00. Omit to keep it; send an empty string or null to clear it."
          },
          "opt_in_methods": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "Saved opt-in methods. This is a saved preference, not recipient-consent evidence. Omit to keep the stored list; send an empty list or null to clear it."
          },
          "opt_in_description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Saved opt-in description. Omit to keep it; send an empty string or null to clear it."
          },
          "billing_contact_email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Billing contact email address. Omit to keep it; send an empty string or null to clear it."
          },
          "low_balance_threshold_cents": {
            "type": "integer",
            "minimum": 0,
            "description": "Low-balance ALERT threshold in whole cents. This is not a spending limit. Omit to keep it; 0 is a real value, not an omission."
          },
          "usage_notifications_enabled": {
            "type": "boolean",
            "description": "Whether usage notifications are on. Omit to keep it."
          },
          "daily_spend_alerts_enabled": {
            "type": "boolean",
            "description": "Whether daily-spend alerts are on. Omit to keep it."
          }
        },
        "examples": [
          {
            "full_name": "Ada Lovelace",
            "personal_phone": "+18635550101",
            "timezone": "America/New_York",
            "billing_contact_email": "billing@example.org"
          }
        ]
      }
    },
    {
      "name": "get_texting_campaign_registration",
      "description": "Read this organization's stored texting campaign (10DLC) registration readiness: the current filing status, rejection and retry notes, the frozen filed packet, the resumable draft, the persisted free site-check history, the authoritative campaign review fee, and whether setup is complete. Owner or admin only, matching the dashboard's campaign surface. It reports the locally stored state, not a live carrier reconciliation, and it never returns carrier identifiers, raw provider payloads, verification tokens, or the raw registration row, while your own brand names, URLs, and copy are preserved verbatim. already_filed means a carrier campaign reference is recorded, not approval or delivery, and a missing reference does not prove no filing occurred; confirmation_required or submitting mean do not submit another filing, and none of these flags establish whether a fee was charged, so a missing carrier identity is never permission to submit. Paid campaign filing is available through submit_texting_campaign_registration when the credential holds registration:submit. It has no business effect: it files nothing, charges nothing, creates no profile, and assigns no number; one routine API audit row is recorded. The free website preflight is check_texting_campaign_site.",
      "scopes": [
        "registration:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {},
        "examples": [
          {}
        ]
      }
    },
    {
      "name": "check_texting_campaign_site",
      "description": "Run the free website-compliance preflight for this organization's campaign: validate the opt-in, privacy, and terms URLs, fetch the live public pages, and return the same carrier checklist the paid submit enforces, then append one immutable history row stamped with this organization and the check time. Owner or admin only, and the organization must be fully set up. This is a readiness check, not a filing: it charges nothing, files nothing with any carrier, creates no profile, and assigns no number. It does make outbound reads of your public pages and appends history, so a repeat is a new timestamped record rather than a no-op. A failed history write is surfaced as an error and is never presented as durable evidence. The URLs must be public https pages on your own domain; platform or reseller hosts are refused. Requires scope registration:prepare.",
      "scopes": [
        "registration:prepare"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "opt_in_url",
          "privacy_policy_url",
          "terms_url"
        ],
        "properties": {
          "opt_in_url": {
            "type": "string",
            "description": "Public https URL of the SMS opt-in form on your campaign website."
          },
          "privacy_policy_url": {
            "type": "string",
            "description": "Public https URL of your privacy policy."
          },
          "terms_url": {
            "type": "string",
            "description": "Public https URL of your terms of service."
          },
          "opt_in_screenshot_url": {
            "type": "string",
            "description": "Optional public https URL of a screenshot of the opt-in form."
          }
        },
        "examples": [
          {
            "opt_in_url": "https://friends.example.org/sms",
            "privacy_policy_url": "https://friends.example.org/privacy",
            "terms_url": "https://friends.example.org/terms"
          }
        ]
      }
    },
    {
      "name": "save_texting_campaign_draft",
      "description": "Save this organization's texting campaign compliance draft: the opt-in form URL, privacy policy URL, terms URL, and optional opt-in screenshot URL, opt-in description, and HELP reply contact. This is the same draft the paid filing later reads. Owner or admin only, matching the dashboard's campaign surface, and the organization must be fully set up. A field you send is written; an omitted field keeps its stored value, and an empty string clears a saved value. A supplied HELP contact must be a valid email address, and an empty one never erases the stored shared inbox. It is free: it charges nothing, files nothing with any carrier, creates no profile, and assigns no number, and it records one routine API audit row. The free preflight is check_texting_campaign_site and the paid filing is submit_texting_campaign_registration. Requires scope registration:prepare.",
      "scopes": [
        "registration:prepare"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "opt_in_url": {
            "type": "string",
            "description": "Public https URL of the SMS opt-in form on your campaign website. An empty string clears the saved value."
          },
          "privacy_policy_url": {
            "type": "string",
            "description": "Public https URL of your privacy policy. An empty string clears the saved value."
          },
          "terms_url": {
            "type": "string",
            "description": "Public https URL of your terms of service. An empty string clears the saved value."
          },
          "opt_in_screenshot_url": {
            "type": "string",
            "description": "Optional public https URL of a screenshot of the opt-in form. An empty string clears the saved value."
          },
          "opt_in_description": {
            "type": "string",
            "description": "Optional description of how a person opts in. It must name your opt-in form URL, privacy URL, terms URL, STOP, and HELP, or it is refused. An empty string clears the saved value."
          },
          "help_email": {
            "type": "string",
            "description": "Optional HELP reply contact email address. A saved inbox is never erased by an empty value."
          }
        },
        "examples": [
          {
            "opt_in_url": "https://friends.example.org/sms",
            "privacy_policy_url": "https://friends.example.org/privacy",
            "terms_url": "https://friends.example.org/terms"
          }
        ]
      }
    },
    {
      "name": "submit_texting_campaign_registration",
      "description": "File this organization's 10DLC campaign registration and charge the non-refundable review fee for this submission. Owner or admin only, and the organization must be fully set up. The fee comes from the server, never the caller, and the packet is built from the saved compliance draft. It requires compliance_acknowledged true, fee_acknowledged true, confirm true, a unique idempotency_key, and scope registration:submit. Before any fee is charged it validates the draft, fetches the three filed public pages, and runs the same carrier checklist as check_texting_campaign_site. When the business registration is not yet approved it saves the compliance draft as queued and charges nothing. A repeat with the same idempotency_key is already on file and charges nothing; an uncertain carrier outcome is never retried blindly. Optionally pass help_email to set the HELP reply contact, campaign_verify_token when a political organization needs one, and approval_id when this key requires an operator to approve the filing first.",
      "scopes": [
        "registration:submit"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "compliance_acknowledged",
          "fee_acknowledged",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "compliance_acknowledged": {
            "type": "boolean",
            "description": "Must be true: acknowledges the messaging compliance requirements of the filing."
          },
          "fee_acknowledged": {
            "type": "boolean",
            "description": "Must be true: acknowledges the non-refundable review fee charged for this submission."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to spend and send."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not act twice."
          },
          "help_email": {
            "type": "string",
            "description": "Optional HELP reply contact email address for this filing. Omit to use the saved one."
          },
          "campaign_verify_token": {
            "type": "string",
            "description": "Optional Campaign Verify token for a political organization that needs one. Omit to use the saved token."
          },
          "approval_id": {
            "type": "string",
            "description": "Optional spend approval id for a key that requires an operator to approve each filing."
          }
        },
        "examples": [
          {
            "compliance_acknowledged": true,
            "fee_acknowledged": true,
            "confirm": true,
            "idempotency_key": "campaign-file-2026-01"
          }
        ]
      }
    },
    {
      "name": "get_billing_balance",
      "description": "Read this organization's real funds: availableBalanceCents is the actual spendable balance (active holds already deducted), and outstandingHoldsCents is the total currently reserved but not yet settled or voided. A failed read is an error, never a fabricated zero. It is read-only, charges nothing, and does not create a payment profile.",
      "scopes": [
        "billing:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {},
        "examples": [
          {}
        ]
      }
    },
    {
      "name": "list_billing_transactions",
      "description": "Page through this organization's ledger, newest first, with each entry's public id, type, amount in whole cents, the running balance after it, its description, and its timestamp, plus an exact total and a has-more flag. It returns no processor identifiers, idempotency keys, metadata, or another member's private settings, and it changes no business or billing state, though each read records one routine API audit row.",
      "scopes": [
        "billing:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "offset": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000,
            "description": "How many newest entries to skip. Defaults to 0."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "How many entries to return. At most 100."
          }
        },
        "examples": [
          {
            "offset": 0,
            "limit": 25
          }
        ]
      }
    },
    {
      "name": "get_auto_topup_settings",
      "description": "Read this organization's saved automatic-replenishment configuration: whether it is enabled, the trigger balance, the top-up amount, the daily caps and cooldown, and the saved payment method's brand, last 4 digits and expiry. It never returns a card number, card identifier, or secret, and it changes no business or billing state, though each read records one routine API audit row.",
      "scopes": [
        "billing:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {},
        "examples": [
          {}
        ]
      }
    },
    {
      "name": "update_auto_topup_settings",
      "description": "Save this organization's Auto Top-Up configuration, exactly like Account, Auto Top-Up. Sending enabled true authorizes FUTURE automatic off-session charges against the already-saved, already-authorized card according to the threshold, amount, daily caps and cooldown; it does NOT charge now, add or remove a card, or deposit funds. Enabling requires a card already on file; without one this is refused. The trigger balance is $5–$1,000 (500–100000 cents) and the top-up amount is $10–$1,000 (1000–100000 cents), both whole cents. Saving the same configuration twice is the same as saving it once. The caller must be an active organization owner or admin.",
      "scopes": [
        "billing:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "enabled",
          "threshold_cents",
          "amount_cents"
        ],
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Whether to enable automatic replenishment. Enabling authorizes future automatic charges against the saved card."
          },
          "threshold_cents": {
            "type": "integer",
            "minimum": 500,
            "maximum": 100000,
            "description": "Trigger balance in whole cents. Replenishes when the available balance drops below this."
          },
          "amount_cents": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 100000,
            "description": "Top-up amount in whole cents."
          }
        },
        "examples": [
          {
            "enabled": true,
            "threshold_cents": 2500,
            "amount_cents": 10000
          }
        ]
      }
    },
    {
      "name": "preview_contact_import",
      "description": "Parse and preview a CSV/XLSX contact or suppression import before anything is committed. Returns the per-tab classification, exact row counts, unresolved metadata decisions, global-suppression matches, and a signed preview token. Requires contacts:read. Writes preview evidence only, changes no contacts, and charges nothing. Send the same files back to commit_contact_import with the token.",
      "scopes": [
        "contacts:read"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "files"
        ],
        "properties": {
          "files": {
            "type": "array",
            "description": "One or more CSV or XLSX files, each as base64 in content_base64. An MCP call IS a deployed function request, so the files travel base64-encoded inside one JSON body bounded by that function's 4.5 MB limit: the aggregate decoded size is capped near 2.6 MiB and a single file larger than that is refused. A whole-file upload too large to inline can instead be sent in chunks with stage_contact_import_upload and referenced by its staging id. No file path or URL is accepted.",
            "items": {
              "type": "object",
              "required": [
                "content_base64"
              ],
              "additionalProperties": false,
              "properties": {
                "filename": {
                  "type": "string",
                  "description": "File name, for provenance. Defaults to import-N.csv."
                },
                "content_base64": {
                  "type": "string",
                  "description": "The file bytes, base64-encoded. A data: URL prefix is stripped."
                }
              }
            }
          },
          "plan": {
            "type": "array",
            "description": "Per-tab decisions from a prior preview. Each entry pins a tab (by key, or by name for older previews) to role contacts/suppression/ignore, chooses suppression channels, and maps columns. Omit to accept the automatic classification.",
            "items": {
              "type": "object",
              "required": [
                "name",
                "role"
              ],
              "additionalProperties": false,
              "properties": {
                "name": {
                  "type": "string",
                  "description": "Tab name the plan applies to."
                },
                "key": {
                  "type": "string",
                  "description": "Stable tab key within the upload, when one was returned by preview."
                },
                "role": {
                  "type": "string",
                  "enum": [
                    "contacts",
                    "suppression",
                    "ignore"
                  ],
                  "description": "What the tab is."
                },
                "channels": {
                  "type": "object",
                  "additionalProperties": false,
                  "description": "Suppression channels when role is suppression.",
                  "properties": {
                    "email": {
                      "type": "boolean"
                    },
                    "sms": {
                      "type": "boolean"
                    },
                    "voice": {
                      "type": "boolean"
                    }
                  }
                },
                "mapping": {
                  "type": "object",
                  "additionalProperties": true,
                  "description": "Column header per field. A null value deselects a column; custom maps a header to a custom-field key.",
                  "properties": {
                    "email": {
                      "type": "string"
                    },
                    "phone": {
                      "type": "string"
                    },
                    "firstName": {
                      "type": "string"
                    },
                    "lastName": {
                      "type": "string"
                    },
                    "address": {
                      "type": "string"
                    },
                    "city": {
                      "type": "string"
                    },
                    "state": {
                      "type": "string"
                    },
                    "zip": {
                      "type": "string"
                    },
                    "tags": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    },
                    "custom": {
                      "type": "object",
                      "additionalProperties": true,
                      "description": "Header to custom-field key, or null to ignore."
                    }
                  }
                }
              }
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags to apply to every imported contact."
          },
          "token": {
            "type": "string",
            "description": "A prior preview token, to reuse the frozen classification instead of re-running the classifier."
          },
          "review_choices": {
            "type": "object",
            "additionalProperties": true,
            "description": "Answers for the metadata classifier's uncertain decisions, keyed '<evidenceId>'. Each value is 'suppress' or 'none'. Commit is refused while any decision is unresolved."
          },
          "sms_consent_attested": {
            "type": "boolean",
            "description": "Operator attests the file carries SMS consent for the phone numbers it imports."
          },
          "voice_consent_attested": {
            "type": "boolean",
            "description": "Operator attests the file carries voice-call consent for the phone numbers it imports."
          },
          "clean_emails": {
            "type": "boolean",
            "description": "Ask to clean imported emails. Requires the email:verify scope. The commit saves contacts and does NOT clean or charge inline: cleaning exactly this import's contacts is a separate paid step, clean_import_cohort, keyed by the cohortId the commit returns. Requesting it here records the intent and reports it as requested and not performed."
          },
          "clean_addresses": {
            "type": "boolean",
            "description": "Ask to clean imported addresses. Requires the address:verify scope. The commit saves contacts and does NOT clean or charge inline: cleaning exactly this import's contacts is a separate paid step, clean_import_cohort, keyed by the cohortId the commit returns. Requesting it here records the intent and reports it as requested and not performed."
          }
        },
        "examples": [
          {
            "files": [
              {
                "filename": "voters.csv",
                "content_base64": "RW1haWwsUGhvbmUKYWRhQGV4YW1wbGUub3JnLDg2MzU1NTAxMDAK"
              }
            ],
            "tags": [
              "canvass"
            ]
          }
        ]
      }
    },
    {
      "name": "commit_contact_import",
      "description": "Commit a previously previewed import. Requires contacts:write; cleaned emails or addresses additionally require email:verify or address:verify and are refused before any contact is written if missing. The commit saves contacts and charges nothing, even when cleaning is requested: cleaning exactly this import's contacts runs as a SEPARATE paid step, clean_import_cohort, keyed by the cohortId the response carries. The response's `cleaning` block reports the requested channels as requested and NOT performed by this commit. Re-sending an unchanged commit with the same preview token replays the stored receipt without importing twice. Returns the persisted counts, the resulting job id for readback, and the cohortId when the commit was preview-backed.",
      "scopes": [
        "contacts:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "files",
          "token"
        ],
        "properties": {
          "files": {
            "type": "array",
            "description": "One or more CSV or XLSX files, each as base64 in content_base64. An MCP call IS a deployed function request, so the files travel base64-encoded inside one JSON body bounded by that function's 4.5 MB limit: the aggregate decoded size is capped near 2.6 MiB and a single file larger than that is refused. A whole-file upload too large to inline can instead be sent in chunks with stage_contact_import_upload and referenced by its staging id. No file path or URL is accepted.",
            "items": {
              "type": "object",
              "required": [
                "content_base64"
              ],
              "additionalProperties": false,
              "properties": {
                "filename": {
                  "type": "string",
                  "description": "File name, for provenance. Defaults to import-N.csv."
                },
                "content_base64": {
                  "type": "string",
                  "description": "The file bytes, base64-encoded. A data: URL prefix is stripped."
                }
              }
            }
          },
          "token": {
            "type": "string",
            "description": "The preview token returned by preview_contact_import."
          },
          "plan": {
            "type": "array",
            "description": "Per-tab decisions from a prior preview. Each entry pins a tab (by key, or by name for older previews) to role contacts/suppression/ignore, chooses suppression channels, and maps columns. Omit to accept the automatic classification.",
            "items": {
              "type": "object",
              "required": [
                "name",
                "role"
              ],
              "additionalProperties": false,
              "properties": {
                "name": {
                  "type": "string",
                  "description": "Tab name the plan applies to."
                },
                "key": {
                  "type": "string",
                  "description": "Stable tab key within the upload, when one was returned by preview."
                },
                "role": {
                  "type": "string",
                  "enum": [
                    "contacts",
                    "suppression",
                    "ignore"
                  ],
                  "description": "What the tab is."
                },
                "channels": {
                  "type": "object",
                  "additionalProperties": false,
                  "description": "Suppression channels when role is suppression.",
                  "properties": {
                    "email": {
                      "type": "boolean"
                    },
                    "sms": {
                      "type": "boolean"
                    },
                    "voice": {
                      "type": "boolean"
                    }
                  }
                },
                "mapping": {
                  "type": "object",
                  "additionalProperties": true,
                  "description": "Column header per field. A null value deselects a column; custom maps a header to a custom-field key.",
                  "properties": {
                    "email": {
                      "type": "string"
                    },
                    "phone": {
                      "type": "string"
                    },
                    "firstName": {
                      "type": "string"
                    },
                    "lastName": {
                      "type": "string"
                    },
                    "address": {
                      "type": "string"
                    },
                    "city": {
                      "type": "string"
                    },
                    "state": {
                      "type": "string"
                    },
                    "zip": {
                      "type": "string"
                    },
                    "tags": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    },
                    "custom": {
                      "type": "object",
                      "additionalProperties": true,
                      "description": "Header to custom-field key, or null to ignore."
                    }
                  }
                }
              }
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags to apply to every imported contact. Must match the preview."
          },
          "review_choices": {
            "type": "object",
            "additionalProperties": true,
            "description": "Answers for the metadata classifier's uncertain decisions, keyed '<evidenceId>'. Each value is 'suppress' or 'none'. Commit is refused while any decision is unresolved."
          },
          "sms_consent_attested": {
            "type": "boolean",
            "description": "Operator attests the file carries SMS consent for the phone numbers it imports."
          },
          "voice_consent_attested": {
            "type": "boolean",
            "description": "Operator attests the file carries voice-call consent for the phone numbers it imports."
          },
          "clean_emails": {
            "type": "boolean",
            "description": "Ask to clean imported emails. Requires the email:verify scope. The commit saves contacts and does NOT clean or charge inline: cleaning exactly this import's contacts is a separate paid step, clean_import_cohort, keyed by the cohortId the commit returns. Requesting it here records the intent and reports it as requested and not performed."
          },
          "clean_addresses": {
            "type": "boolean",
            "description": "Ask to clean imported addresses. Requires the address:verify scope. The commit saves contacts and does NOT clean or charge inline: cleaning exactly this import's contacts is a separate paid step, clean_import_cohort, keyed by the cohortId the commit returns. Requesting it here records the intent and reports it as requested and not performed."
          }
        },
        "examples": [
          {
            "files": [
              {
                "content_base64": "RW1haWwKYWRhQGV4YW1wbGUub3JnCg=="
              }
            ],
            "token": "eyJ...signature",
            "tags": [
              "canvass"
            ]
          }
        ]
      }
    },
    {
      "name": "list_contact_imports",
      "description": "List this organization's persisted imports, newest first, with counts per import. Requires contacts:read. Returns a bounded page and an honest continuation; it does not return per-row outcomes.",
      "scopes": [
        "contacts:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Page size. At most 100."
          },
          "offset": {
            "type": "integer",
            "minimum": 0,
            "description": "Rows to skip, from a prior next_offset."
          }
        },
        "examples": [
          {
            "limit": 25
          }
        ]
      }
    },
    {
      "name": "get_contact_import_outcomes",
      "description": "Read the per-row outcomes of one past import by job id, newest evidence first. Requires contacts:read. Returns a bounded page with a keyset cursor and an honest continuation; category filters to create, merge, suppress, or skip. Missing tables or a foreign import are errors, not empty success.",
      "scopes": [
        "contacts:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "import_id"
        ],
        "properties": {
          "import_id": {
            "type": "string",
            "description": "The job id returned by commit_contact_import or list_contact_imports."
          },
          "category": {
            "type": "string",
            "enum": [
              "create",
              "merge",
              "suppress",
              "skip"
            ],
            "description": "Only rows of this kind."
          },
          "cursor": {
            "type": "string",
            "description": "Keyset cursor from a prior next_cursor."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 500,
            "description": "Page size. At most 500."
          }
        },
        "examples": [
          {
            "import_id": "11111111-1111-4111-8111-111111111111",
            "category": "create",
            "limit": 200
          }
        ]
      }
    },
    {
      "name": "get_import_cleaning_cohort",
      "description": "Read the safe metadata of one frozen import identity cohort by its id (returned as cohortId by commit_contact_import when the commit was preview-backed). Requires contacts:read. Returns the cohort's id, preview and job references, snapshot version/fingerprint, the requested cleaning channels, the CAPTURED UNIQUE identity counts, a capture state and whether cohort-scoped paid cleaning is available. It never returns the raw email/address keys, never a billable-unit or pending-check count, and never a cleaning execution status. A foreign or absent cohort is 404; a missing schema is an error, not empty success.",
      "scopes": [
        "contacts:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "cohort_id"
        ],
        "properties": {
          "cohort_id": {
            "type": "string",
            "description": "The cohortId returned by commit_contact_import."
          }
        },
        "examples": [
          {
            "cohort_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "stage_contact_import_upload",
      "description": "Begin or extend a staged upload for a contact file too large to send inline in one call. Requires contacts:write. Send the file in chunks: the first call omits staging_id and creates the staged upload, every later call sends the returned staging_id with the next chunk, and final true marks the last chunk. Stores bytes only: nothing is parsed, no contact is written and nothing is charged. The returned staging_id is the durable reference a later import commit uses for this file.",
      "scopes": [
        "contacts:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "filename",
          "content_base64"
        ],
        "properties": {
          "filename": {
            "type": "string",
            "description": "File name for the whole upload, for provenance. Required on the first chunk."
          },
          "content_base64": {
            "type": "string",
            "description": "This chunk's bytes, base64-encoded. A data: URL prefix is stripped. At most 10 MB decoded per chunk."
          },
          "staging_id": {
            "type": "string",
            "description": "The staging id returned by the first chunk. Omit on the first call."
          },
          "final": {
            "type": "boolean",
            "description": "Set true on the last chunk of the file."
          }
        },
        "examples": [
          {
            "filename": "voters.csv",
            "content_base64": "RW1haWwKYWRhQGV4YW1wbGUub3JnCg==",
            "final": true
          }
        ]
      }
    },
    {
      "name": "clean_import_cohort",
      "description": "Clean exactly one frozen import identity cohort and charge organization balance. Requires email:verify and address:verify. It acts only on the contacts that match the cohort's captured email keys and complete-address hashes, never on the whole organization, and never accepts a caller-supplied key list. This spends money and cannot be undone, so it requires confirm true and a unique idempotency_key. The credential organization is the one that owns the API key. Nothing is charged when no cohort identity needs cleaning.",
      "scopes": [
        "email:verify",
        "address:verify"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "cohort_id",
          "channels",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "cohort_id": {
            "type": "string",
            "description": "The cohortId returned by commit_contact_import."
          },
          "channels": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "email",
                "address"
              ]
            },
            "description": "Which identities to clean. At least one of email or address."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to spend."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not spend twice."
          },
          "approval_id": {
            "type": "string",
            "description": "Optional id of an approved operator spend approval for this exact cohort."
          }
        },
        "examples": [
          {
            "cohort_id": "11111111-1111-4111-8111-111111111111",
            "channels": [
              "email"
            ],
            "confirm": true,
            "idempotency_key": "clean-cohort-2026-0001"
          }
        ]
      }
    },
    {
      "name": "record_form_submission",
      "description": "Record one website form submission as a contact in this organization, using the same consent and suppression engine the website endpoint uses. Provide an email, a phone number, or both, plus the source name. A checked opt-in box needs its verbatim consent text; an unchecked box withholds that channel. A dated opt-out, a STOP, or a live suppression is never cleared, even by a checked box. The contact change and its receipt commit in one serialized transaction, so reusing the same external_id makes a concurrent or sequential duplicate replay the first accepted receipt as a duplicate without a second mutation, and a same-key conflict is never overwritten. A receipt is the historical outcome of the first accepted write, not proof that the stored contact still matches this request (later legitimate edits can change it); an interrupted keyed retry is not auto-resent, so read the receipt for that external_id first. With no external_id there is no dedupe or replay and a repeated submission is a separate operation. No message is sent and no charge is made.",
      "scopes": [
        "intake:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "source"
        ],
        "properties": {
          "email": {
            "type": "string",
            "description": "Email address. Stored lowercased. Provide this, a phone, or both."
          },
          "phone": {
            "type": "string",
            "description": "U.S. phone number. Stored as E.164. Provide this, an email, or both."
          },
          "first_name": {
            "type": "string",
            "description": "First name, up to 80 characters. Empty leaves the saved value."
          },
          "last_name": {
            "type": "string",
            "description": "Last name, up to 80 characters. Empty leaves the saved value."
          },
          "street": {
            "type": "string",
            "description": "Street address, up to 200 characters. Empty leaves the saved value."
          },
          "city": {
            "type": "string",
            "description": "City, up to 80 characters. Empty leaves the saved value."
          },
          "state": {
            "type": "string",
            "description": "State. A two-letter value is stored uppercased. Empty leaves the saved value."
          },
          "zip": {
            "type": "string",
            "description": "Postal code, up to 10 characters. Use postal_code when you only have that name."
          },
          "postal_code": {
            "type": "string",
            "description": "Postal code alias accepted when zip is omitted."
          },
          "source": {
            "type": "string",
            "description": "Short form name such as volunteer, membership, contact, newsletter, or event. Letters, numbers, underscore, hyphen, up to 40."
          },
          "email_opt_in": {
            "type": "boolean",
            "description": "Whether the email box was checked. Omitted means false. A real false is preserved."
          },
          "sms_opt_in": {
            "type": "boolean",
            "description": "Whether the text box was checked. Omitted means false. A real false is preserved."
          },
          "email_consent_text": {
            "type": "string",
            "description": "The email checkbox label, verbatim, 3 to 2000 characters. Required when email_opt_in is true."
          },
          "sms_consent_text": {
            "type": "string",
            "description": "The text checkbox label, verbatim, 3 to 2000 characters. Required when sms_opt_in is true."
          },
          "opt_in_ip_address": {
            "type": "string",
            "description": "IPv4 or IPv6 text of the consenting device. Stored when this submission records text consent."
          },
          "opt_in_attestation": {
            "type": "boolean",
            "description": "Operator attestation for the recorded text consent. Omitted defaults to true when text consent is recorded."
          },
          "locale": {
            "type": "string",
            "description": "Language tag such as en or es-US."
          },
          "submitted_at": {
            "type": "string",
            "description": "ISO-8601 time of the submission, not in the future. Used as the consent and confirmation time. Omitted means the server time."
          },
          "external_id": {
            "type": "string",
            "description": "Stable id for this submission, up to 128 characters. Reusing it replays the FIRST accepted submission as a duplicate instead of a second write; the contact and its receipt commit atomically, so a concurrent duplicate never re-mutates the contact. An interrupted keyed retry is not auto-resent; read the receipt for this external_id first."
          },
          "attributes": {
            "type": "object",
            "additionalProperties": {
              "type": [
                "string",
                "number",
                "boolean"
              ]
            },
            "description": "Up to 40 extra fields from the form, as a map of field name to a text, number, or boolean value. Reserved keys (email, phone, consent and opt-out fields, form_source, locale, opt_in_*) are ignored by the intake core. Names are trimmed to 40 characters and values to 500."
          }
        },
        "examples": [
          {
            "email": "ada@example.org",
            "phone": "863-555-0148",
            "first_name": "Ada",
            "last_name": "Lovelace",
            "source": "volunteer",
            "email_opt_in": true,
            "email_consent_text": "Send me email updates from the campaign.",
            "external_id": "volunteer-form-100",
            "attributes": {
              "shift": "saturday",
              "student": true,
              "year": 2026
            }
          }
        ]
      }
    },
    {
      "name": "get_form_submission",
      "description": "Read the persisted receipt of a past form submission by the external_id that was sent, inside this organization. Use this to recover after an interrupted submission: a created/updated result is the historical outcome of the first accepted write and an identity_conflict receipt names both contacts that were left unchanged. The receipt does not prove the stored contact still matches any later request, so look the contact up directly when current values matter. A missing receipt is a 404 error, not a success. Read-only: it changes nothing.",
      "scopes": [
        "intake:write"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "external_id"
        ],
        "properties": {
          "external_id": {
            "type": "string",
            "description": "The external_id sent with the original submission, up to 128 characters."
          }
        },
        "examples": [
          {
            "external_id": "volunteer-form-100"
          }
        ]
      }
    },
    {
      "name": "list_team_members",
      "description": "Read the authorized organization's team roster: each member's id, email, display name, canonical role and membership status, membership timestamps where the schema supports them, and whether the member is the protected organization owner. The organization is the one that owns this credential; no argument selects it. Authorization is canonical: role and status come from that organization's membership row, so a member whose own selected organization is elsewhere is still listed here with THIS organization's role. Removed canonical members are excluded and are never restored from a stale profile pointer. Reading requires the credential's membership and user account to be ACTIVE; a read-only organization state such as a locked organization stays readable, but a suspended or removed membership does not. This is a bounded, read-only page; total is the true number of distinct members, not one page's length. It returns no balances, no platform-administrator flags, no secrets, no tokens, and no other organization's membership. It changes no business state; the read records one routine API audit row. This tool itself does not invite, remove, or change anyone; those are the separate team:manage tools (create_team_invitation, resend_team_invitation, cancel_team_invitation, change_team_member_role, suspend_team_member, reactivate_team_member, remove_team_member).",
      "scopes": [
        "team:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "page": {
            "type": "integer",
            "minimum": 1,
            "description": "Page number, starting at 1."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Roster page size. At most 100."
          }
        },
        "examples": [
          {
            "page": 1,
            "limit": 50
          }
        ]
      }
    },
    {
      "name": "list_team_invitations",
      "description": "Read the authorized organization's CURRENT pending invitations: id, email, role, created_at, and expires_at for invitations that are not accepted, not cancelled, not dismissed, and not expired, newest first. The organization is the one that owns this credential; no argument selects it. Reading requires the credential's membership and user account to be ACTIVE; a read-only organization state such as a locked organization stays readable, but a suspended or removed membership does not. It does NOT return an invitation token, a copy-link value, or the full history of past or accepted invitations, and it does not send or resend anything. It changes no business state; the read records one routine API audit row. A bounded, read-only page; total is the true count, not one page's length.",
      "scopes": [
        "team:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "page": {
            "type": "integer",
            "minimum": 1,
            "description": "Page number, starting at 1."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Invitation page size. At most 100."
          }
        },
        "examples": [
          {
            "page": 1,
            "limit": 50
          }
        ]
      }
    },
    {
      "name": "list_contact_fields",
      "description": "Read the authorized organization's contact-field registry: the fields that have an explicit key/label/type definition, unioned with the keys that already appear in contacts' stored custom values and whose type is inferred from those values. Each field is returned as key (verbatim, including spaces and case), label, and type. This is the SAME registry the app's contact field settings use, so a key it lists can be filtered on; it does not return contact values, contact ids, or the contacts that carry a field. It is a bounded, read-only result: the full registry with a truthful total, not a silently truncated page. It changes no business state (the read records one routine API audit row). It does not define, convert, or remove any field.",
      "scopes": [
        "contacts:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "additionalProperties": false,
        "properties": {},
        "examples": [
          {}
        ]
      }
    },
    {
      "name": "upsert_contact_field",
      "description": "Save an explicit key/label/type definition for one contact field in the authorized organization, keyed by the field key. Use this to pin a field's type (for example to make a field a number or a date) so it can be filtered as that type. The key is preserved verbatim; the label is stored trimmed. The stored row is read back and returned. This is a metadata save: it does NOT convert, clear, or repair any contact's existing value, and it does NOT change a stored value's type. Existing values that no longer match the new type keep their stored text; the app reports a type change when a filter no longer matches. A repeated identical save settles the row on that key/label/type; it spends nothing and sends nothing.",
      "scopes": [
        "contacts:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "key",
          "label",
          "type"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Field key, at most 120 characters. Preserved verbatim, including spaces and case."
          },
          "label": {
            "type": "string",
            "description": "Human-readable label, at most 120 characters. Stored trimmed."
          },
          "type": {
            "type": "string",
            "enum": [
              "text",
              "number",
              "date",
              "boolean"
            ],
            "description": "The field's type. One of the listed values; any other string is rejected."
          }
        },
        "examples": [
          {
            "key": "precinct",
            "label": "Precinct",
            "type": "number"
          }
        ]
      }
    },
    {
      "name": "create_team_invitation",
      "description": "Invite one person to the credential's organization as an admin, member, or volunteer, using the same invitation workflow the dashboard uses. The acting organization and the inviter come only from the credential; the body accepts only an email and role. A person who already has a membership of any status is refused and should be reactivated instead; a person with a live pending invitation returns a conflict. The response exposes only safe invitation metadata and a truthful `queued` acknowledgment: it means a source email event was written, NOT that the email was sent or delivered. It creates no organization, grants no role, moves no money, and never returns an invite token or a whole user row.",
      "scopes": [
        "team:manage"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "email",
          "role"
        ],
        "properties": {
          "email": {
            "type": "string",
            "description": "Invitee email address. Normalized to lower case."
          },
          "role": {
            "type": "string",
            "enum": [
              "admin",
              "member",
              "volunteer"
            ],
            "description": "The role the invitee will hold if they accept."
          }
        },
        "examples": [
          {
            "email": "person@example.org",
            "role": "member"
          }
        ]
      }
    },
    {
      "name": "resend_team_invitation",
      "description": "Re-queue the invitation email for a still-pending invitation in the credential's organization. This is NOT idempotent: there is no request-key store and each call is a new email request, exactly like the dashboard's re-queue action. It exposes no token, sends no email itself, and moves no money. If the outcome cannot be confirmed, read the current invitation state rather than retrying automatically.",
      "scopes": [
        "team:manage"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "invitation_id"
        ],
        "properties": {
          "invitation_id": {
            "type": "string",
            "description": "Team invitation id (UUID)."
          }
        },
        "examples": [
          {
            "invitation_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "cancel_team_invitation",
      "description": "Cancel a still-pending invitation in the credential's organization. A person can be invited again afterwards. Cancelling an invitation that is already accepted, cancelled, or dismissed is refused. It sends nothing and moves no money.",
      "scopes": [
        "team:manage"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "invitation_id"
        ],
        "properties": {
          "invitation_id": {
            "type": "string",
            "description": "Team invitation id (UUID)."
          }
        },
        "examples": [
          {
            "invitation_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "change_team_member_role",
      "description": "Change an ordinary member's role between admin and member in the credential's organization. The target is the member id in the argument, never a body selector. Promotion to owner is refused, the organization owner cannot be changed, and you cannot change your own role. The result is read back from the live membership; a readback that cannot be established is reported as uncertain, not as success or a proven no-write.",
      "scopes": [
        "team:manage"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "member_id",
          "role"
        ],
        "properties": {
          "member_id": {
            "type": "string",
            "description": "Team member user id (UUID)."
          },
          "role": {
            "type": "string",
            "enum": [
              "admin",
              "member"
            ],
            "description": "The member's new role."
          }
        },
        "examples": [
          {
            "member_id": "22222222-2222-4222-8222-222222222222",
            "role": "admin"
          }
        ]
      }
    },
    {
      "name": "suspend_team_member",
      "description": "Suspend an ordinary active member in the credential's organization, dropping them to read-only. The organization owner cannot be suspended, you cannot suspend yourself, and the last active admin cannot be suspended. An optional note is recorded on the audit trail. The result is read back from the live membership.",
      "scopes": [
        "team:manage"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "member_id"
        ],
        "properties": {
          "member_id": {
            "type": "string",
            "description": "Team member user id (UUID)."
          },
          "note": {
            "type": "string",
            "description": "Optional note recorded on the team audit trail."
          }
        },
        "examples": [
          {
            "member_id": "22222222-2222-4222-8222-222222222222",
            "note": "On leave"
          }
        ]
      }
    },
    {
      "name": "reactivate_team_member",
      "description": "Reactivate a suspended member in the credential's organization. The organization owner cannot be acted on and you cannot reactivate yourself. A member who is not suspended is refused. An optional note is recorded on the audit trail. The result is read back from the live membership.",
      "scopes": [
        "team:manage"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "member_id"
        ],
        "properties": {
          "member_id": {
            "type": "string",
            "description": "Team member user id (UUID)."
          },
          "note": {
            "type": "string",
            "description": "Optional note recorded on the team audit trail."
          }
        },
        "examples": [
          {
            "member_id": "22222222-2222-4222-8222-222222222222"
          }
        ]
      }
    },
    {
      "name": "remove_team_member",
      "description": "Remove an ordinary member from the credential's organization. This ends their membership; it cannot be undone through this surface. The organization owner cannot be removed, you cannot remove yourself, and the last active admin cannot be removed. An optional note is recorded on the audit trail. This removes a membership only: it does not delete the person's account, transfer data, or move money.",
      "scopes": [
        "team:manage"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "member_id"
        ],
        "properties": {
          "member_id": {
            "type": "string",
            "description": "Team member user id (UUID)."
          },
          "note": {
            "type": "string",
            "description": "Optional note recorded on the team audit trail."
          }
        },
        "examples": [
          {
            "member_id": "22222222-2222-4222-8222-222222222222"
          }
        ]
      }
    },
    {
      "name": "search_phone_numbers",
      "description": "Search the carrier marketplace for phone numbers this organization can buy. Lists toll-free inventory, or local inventory in a given area code or state, with the one-time setup price and the monthly rental for each number. Use it to find a phone_number to pass to purchase_phone_number. It is read-only: it reserves no funds, orders nothing, and assigns nothing, and it records one routine audit row.",
      "scopes": [
        "settings:read"
      ],
      "readOnly": true,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "properties": {
          "area_code": {
            "type": "string",
            "description": "US area code (3 digits) for a local search. Ignored for a toll-free search."
          },
          "state": {
            "type": "string",
            "description": "Two-letter US state for a local search. Ignored for a toll-free search."
          },
          "type": {
            "type": "string",
            "enum": [
              "toll-free",
              "local"
            ],
            "description": "Which inventory to search. Defaults to toll-free."
          }
        },
        "examples": [
          {
            "type": "toll-free"
          }
        ]
      }
    },
    {
      "name": "purchase_phone_number",
      "description": "Buy one phone number (toll-free or local) for this organization and charge the one-time setup fee from the organization balance. This spends money and cannot be undone, so it requires confirm true and a unique idempotency_key, and phone_number must be a number returned by search_phone_numbers. A retry with the same idempotency_key reuses one reservation and never orders a second number. Local numbers are automatically attached to the organization's current 10DLC campaign when one is approved. The monthly rental is billed later by the rental job. Pass approval_id only when an operator has approved this exact purchase.",
      "scopes": [
        "settings:write"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "phone_number",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "phone_number": {
            "type": "string",
            "description": "The phone number to buy, in E.164 form, exactly as returned by search_phone_numbers."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to spend."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not buy twice."
          },
          "approval_id": {
            "type": "string",
            "description": "Optional operator approval id for this exact purchase."
          }
        },
        "examples": [
          {
            "phone_number": "+18005550100",
            "confirm": true,
            "idempotency_key": "phone-purchase-2026"
          }
        ]
      }
    },
    {
      "name": "set_default_phone_number",
      "description": "Set one active number as the organization's default caller id. It changes settings only: it moves no money and orders nothing, and it does not release or reassign any other number. Returns the id it set. Use list_voice_numbers to find a number_id.",
      "scopes": [
        "settings:write"
      ],
      "readOnly": false,
      "destructive": false,
      "requiresConfirmation": false,
      "inputSchema": {
        "type": "object",
        "required": [
          "number_id"
        ],
        "properties": {
          "number_id": {
            "type": "string",
            "description": "The id of the active number to make the default."
          }
        },
        "examples": [
          {
            "number_id": "11111111-1111-4111-8111-111111111111"
          }
        ]
      }
    },
    {
      "name": "test_voice_recording",
      "description": "Play one robocall draft's saved recording to the API key owner's registered phone. The draft is not launched and the call is not a campaign recipient. This spends money and cannot be undone, so it requires confirm true and a unique idempotency_key. Pass approval_id only when an operator has approved this exact test.",
      "scopes": [
        "voice:send"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "campaign_id",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "campaign_id": {
            "type": "string",
            "description": "The robocall draft whose recording to play."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to spend."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not place a second test call."
          },
          "approval_id": {
            "type": "string",
            "description": "Optional operator approval id for this exact test."
          }
        },
        "examples": [
          {
            "campaign_id": "11111111-1111-4111-8111-111111111111",
            "confirm": true,
            "idempotency_key": "recording-test-2026"
          }
        ]
      }
    },
    {
      "name": "test_robocall",
      "description": "Place one standalone robocall of an organization-owned recording to the API key owner's registered personal phone. It never attaches to a campaign. This spends money and cannot be undone, so it requires confirm true and a unique idempotency_key. Pass approval_id only when an operator has approved this exact test.",
      "scopes": [
        "voice:send"
      ],
      "readOnly": false,
      "destructive": true,
      "requiresConfirmation": true,
      "inputSchema": {
        "type": "object",
        "required": [
          "recording_id",
          "confirm",
          "idempotency_key"
        ],
        "properties": {
          "recording_id": {
            "type": "string",
            "description": "The id of the saved recording to play."
          },
          "confirm": {
            "type": "boolean",
            "description": "Must be true. Set only after the user agrees to spend."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Unique 16 to 128 character key so a retry does not place a second test call."
          },
          "approval_id": {
            "type": "string",
            "description": "Optional operator approval id for this exact test."
          }
        },
        "examples": [
          {
            "recording_id": "11111111-1111-4111-8111-111111111111",
            "confirm": true,
            "idempotency_key": "robocall-test-2026"
          }
        ]
      }
    }
  ],
  "workflows": [
    {
      "id": "billing-read",
      "title": "Billing: inspect funds and transaction history",
      "summary": "Read funds, ledger history and existing Auto Top-Up configuration without initiating a payment or changing settings.",
      "preconditions": [
        "An organization-bound credential with billing:read and the app's owner/admin financial access."
      ],
      "requiredScopes": [
        "billing:read"
      ],
      "operations": [
        "getBillingBalance",
        "listBillingTransactions",
        "getAutoTopUpSettings"
      ],
      "steps": [
        {
          "tool": "get_billing_balance",
          "purpose": "Read available funds and outstanding reservations in whole USD cents. Holds are already deducted from the available balance; do not subtract them again.",
          "preconditions": [
            "billing:read."
          ],
          "outputs": [
            "Available balance, outstanding holds, currency and observation time. These are separate reads, not an atomic snapshot."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "list_billing_transactions",
          "purpose": "Read the requested ledger page. When collecting the full history, advance offset by rows actually returned and continue until hasMore is false; do not mistake a server-capped page for the whole ledger.",
          "preconditions": [
            "billing:read.",
            "Use a bounded limit and an offset beginning at zero for a new traversal."
          ],
          "outputs": [
            "Public ledger entries and exact pagination total/hasMore. Entries can change between requests; deduplicate by public entry id during a traversal."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "get_auto_topup_settings",
          "purpose": "Inspect current replenishment settings, caps, cooldown and saved-card summary. An absent settings row is not permission to add a card or charge one.",
          "preconditions": [
            "billing:read."
          ],
          "outputs": [
            "Saved configuration and limited payment-method summary, or null when no configuration exists."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        }
      ],
      "choices": [],
      "gaps": [
        "gap.billing-and-autotopup"
      ]
    },
    {
      "id": "billing-configure",
      "title": "Billing: configure existing Auto Top-Up",
      "summary": "Change an explicitly requested replenishment configuration, then read the persisted settings back. This is not a deposit or card-entry workflow.",
      "preconditions": [
        "The user has authorized this configuration change, not merely requested a balance check.",
        "An active organization owner/admin credential with billing:read and billing:write.",
        "Enabling requires an already-saved, already-authorized card and permits future automatic charges. Incomplete setup does not block this configuration control."
      ],
      "requiredScopes": [
        "billing:read",
        "billing:write"
      ],
      "operations": [
        "getAutoTopUpSettings",
        "updateAutoTopUpSettings"
      ],
      "steps": [
        {
          "tool": "get_auto_topup_settings",
          "purpose": "Read the existing configuration before choosing the complete requested enabled, threshold_cents and amount_cents values. Do not infer an authorization to enable automatic payments from this read.",
          "preconditions": [
            "billing:read."
          ],
          "outputs": [
            "Current settings, saved-card summary and existing guardrails, or null."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "update_auto_topup_settings",
          "purpose": "Save the requested enabled flag, trigger balance and replenishment amount. Preserve the existing card, authorization metadata, caps and cooldown.",
          "preconditions": [
            "billing:write.",
            "Explicit complete configuration: enabled boolean; threshold_cents 500–100000; amount_cents 1000–100000; amounts are whole cents.",
            "An already-authorized saved card when enabling."
          ],
          "outputs": [
            "The saved configuration. Read it back and compare all three requested settings before reporting success."
          ],
          "readback": "get_auto_topup_settings",
          "sideEffects": [
            "Updates configuration and records a routine audit row. Enabling permits future automatic charges; this call does not collect payment, add a card or deposit funds."
          ],
          "retry": "Read back after an uncertain result. The same configuration is idempotent, but do not overwrite a newer user change blindly.",
          "branchOf": null
        }
      ],
      "choices": [],
      "gaps": [
        "gap.billing-and-autotopup"
      ]
    },
    {
      "id": "connect",
      "title": "Connect and discover capabilities",
      "summary": "Authenticate, then read exactly what this credential may do before doing anything else. Nothing here changes data.",
      "preconditions": [
        "An organization API key (Authorization: Bearer cc_live_...) or an OAuth 2.1 access token issued by this server.",
        "The key's issuer is an active owner or admin of the organization."
      ],
      "requiredScopes": [],
      "operations": [
        "getAgentCapabilities",
        "getAgentOpenApiDocument"
      ],
      "steps": [
        {
          "tool": "get_capabilities",
          "purpose": "Read the granted scopes, the organization the credential acts in, and whether setup is complete. This is the only tool a credential may call before setup is finished.",
          "preconditions": [
            "A valid credential."
          ],
          "outputs": [
            "Granted scopes.",
            "Organization binding.",
            "Setup completion and the questions to ask the operator."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        }
      ],
      "choices": [],
      "gaps": [
        "gap.team-credentials"
      ]
    },
    {
      "id": "setup-repair",
      "title": "Inspect and repair organization setup",
      "summary": "Read the sender and organization settings, show the exact missing fields, and save a complete packet so sends are accepted.",
      "preconditions": [
        "Owner or admin role for the save step.",
        "The settings:read scope to read them and settings:write to save."
      ],
      "requiredScopes": [
        "settings:read",
        "settings:write"
      ],
      "operations": [
        "getOrganizationSettings",
        "getCommunicationSettings",
        "getSendingDomain",
        "previewSendingDomain",
        "configureSendingDomain",
        "updateOrganizationSettings"
      ],
      "steps": [
        {
          "tool": "get_organization_settings",
          "purpose": "Read the committee identity, mailing address, sending identity, entity metadata, and the exact missing fields.",
          "preconditions": [
            "settings:read."
          ],
          "outputs": [
            "Saved settings.",
            "A setup.complete flag and the missing field list."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "get_communications_settings",
          "purpose": "Read the sender from name, from address, reply-to, the public compliance block, and the sending domain's authentication state with the records it requires.",
          "preconditions": [
            "settings:read."
          ],
          "outputs": [
            "Sender and compliance summary.",
            "The sending domain's state and, for an owned domain, the exact DNS records to publish."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "preview_sending_domain",
          "purpose": "Check whether authenticating the From domain would be eligible before making any change.",
          "preconditions": [
            "settings:read."
          ],
          "outputs": [
            "The proposed domain, an eligibility flag, the blockers, and the side effects of configuring it."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "get_sending_domain",
          "purpose": "Read the sending domain's authentication state and the exact DNS records that must be published.",
          "preconditions": [
            "settings:read."
          ],
          "outputs": [
            "Ownership, verification state, and the required DNS records."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "configure_sending_domain",
          "purpose": "Create, recheck, or resume the sending domain so its DNS records can be published and verified.",
          "preconditions": [
            "settings:write.",
            "Owner or admin.",
            "Action confirmed with the operator."
          ],
          "outputs": [
            "The updated domain state and the records to publish."
          ],
          "readback": "get_sending_domain",
          "sideEffects": [
            "Creates or rechecks a domain identity. Does not send, charge, or turn on a channel."
          ],
          "retry": "Re-running ensure or verify is safe: it rechecks the domain rather than creating a duplicate identity.",
          "branchOf": null
        },
        {
          "tool": "update_organization_settings",
          "purpose": "Save the whole settings form once the operator has supplied the missing values.",
          "preconditions": [
            "settings:write.",
            "Owner or admin.",
            "Values confirmed with the operator."
          ],
          "outputs": [
            "The saved settings and the recomputed setup completion."
          ],
          "readback": "get_organization_settings",
          "sideEffects": [
            "Replaces the organization settings form. Does not send, register, or charge."
          ],
          "retry": "Re-sending the identical form is safe; it overwrites with the same values and does not send.",
          "branchOf": null
        }
      ],
      "choices": [],
      "gaps": [
        "gap.domain-configuration",
        "gap.phone-defaults"
      ]
    },
    {
      "id": "personal-profile",
      "title": "Personal profile and preferences",
      "summary": "Read and change the credential owner's OWN personal profile and preferences: name, personal phone, time zone, country, sender defaults, SMS signature, quiet hours, opt-in preferences, and notification preferences. The effect is user-wide: these settings belong to the person across every organization they belong to.",
      "preconditions": [
        "The profile:read scope to read and profile:write to save.",
        "The credential owner is an active owner or admin (current eligibility)."
      ],
      "requiredScopes": [
        "profile:read",
        "profile:write"
      ],
      "operations": [
        "getPersonalSettings",
        "updatePersonalSettings"
      ],
      "steps": [
        {
          "tool": "get_personal_settings",
          "purpose": "Read the owner's own personal profile and preferences, including the normalized phone readback. Returns only the owner's own row, never another user's, and never a role, administrator flag, balance, credential, or secret.",
          "preconditions": [
            "profile:read."
          ],
          "outputs": [
            "The saved personal profile.",
            "The normalized personal phone readback."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "update_personal_settings",
          "purpose": "Save a personal profile/preference patch once the operator has confirmed the values. Only the fields passed are changed; an omitted field is kept.",
          "preconditions": [
            "profile:write.",
            "Owner or admin.",
            "Values confirmed with the operator."
          ],
          "outputs": [
            "The saved personal profile and the list of changed fields."
          ],
          "readback": "get_personal_settings",
          "sideEffects": [
            "Updates the owner's own personal row. A saved phone is normalized but NOT verified, provisioned, or rung. Notification preferences govern future alerts only; saving them does not send, schedule, or charge."
          ],
          "retry": "Re-sending the identical patch is safe; it writes the same values and does not send.",
          "branchOf": null
        }
      ],
      "choices": [],
      "gaps": [
        "gap.member-credential-parity"
      ]
    },
    {
      "id": "audience",
      "title": "Build and confirm an audience",
      "summary": "Find or create contacts, resolve a natural-language audience to a saved list, size it, and snapshot its membership. Everything here is free and reversible.",
      "preconditions": [
        "contacts:read and lists:read to read; contacts:write and lists:write to change."
      ],
      "requiredScopes": [
        "lists:read",
        "lists:write",
        "contacts:read"
      ],
      "operations": [
        "listContacts",
        "getContact",
        "createContact",
        "updateContact",
        "addContactTags",
        "resolveList",
        "listLists",
        "createList",
        "getList",
        "previewList",
        "addListMembers",
        "removeListMembers",
        "getListUsage"
      ],
      "steps": [
        {
          "tool": "resolve_list",
          "purpose": "Turn a plain-language audience ('my donors') into one saved list, or a set of candidates to confirm.",
          "preconditions": [
            "lists:read."
          ],
          "outputs": [
            "A single resolved list, or needs_confirmation with candidates."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "preview_list",
          "purpose": "Count how many contacts a saved-audience definition would match without saving it.",
          "preconditions": [
            "lists:read."
          ],
          "outputs": [
            "A contact count, never the contacts themselves."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "create_segment",
          "purpose": "Save the audience as a reusable list once its filters are agreed.",
          "preconditions": [
            "lists:write.",
            "The filters confirmed with the operator."
          ],
          "outputs": [
            "The new list id and its matched contact count."
          ],
          "readback": "get_segment",
          "sideEffects": [
            "Creates one saved list. Sends nothing and charges nothing."
          ],
          "retry": "Repeating creates a second list. Check get_segment first if a retry is uncertain.",
          "branchOf": null
        },
        {
          "tool": "get_segment",
          "purpose": "Read a saved list by id, including its filters and current contact count.",
          "preconditions": [
            "lists:read."
          ],
          "outputs": [
            "The stored list record."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "change_segment_members",
          "purpose": "Add or remove contacts on a static tag list.",
          "preconditions": [
            "lists:write.",
            "A static list whose only filter is tags.",
            "At most 100 contact ids per call."
          ],
          "outputs": [
            "The number of members added or removed."
          ],
          "readback": "get_segment",
          "sideEffects": [
            "Adds or removes the list's tags on those contacts. Sends nothing and charges nothing."
          ],
          "retry": "Adding an already-present member is a no-op; the call reports the change it made.",
          "branchOf": null
        },
        {
          "tool": "list_contacts",
          "purpose": "Search, sort, and filter contacts by name, email, or phone to build or check a list.",
          "preconditions": [
            "contacts:read."
          ],
          "outputs": [
            "A page of contacts, ordered by sort_by (created_at, first_name, last_name, email, phone) and sort_order (asc, desc), optionally kept to those that have an email, phone, or address on file with filter_type.",
            "An exact email or phone returns one contact and ignores q, tag, sort_by, sort_order, filter_type, page, and limit.",
            "A presence filter means the field is not NULL in the database, not that it is verified, deliverable, or opted in."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        }
      ],
      "choices": [],
      "gaps": [
        "gap.campaign-registration"
      ]
    },
    {
      "id": "email",
      "title": "Email: draft, test, send, report",
      "summary": "Build an email piece, verify the audience and cost, send one proof, send or schedule the live campaign, then read recipients and delivery events back.",
      "preconditions": [
        "email:read, email:draft, and email:test for the draft and proof; email:send for the live send and schedule.",
        "Setup complete (sender identity).",
        "An agreed audience, message, and spend."
      ],
      "requiredScopes": [
        "email:read",
        "email:draft",
        "email:test",
        "email:send",
        "logs:read"
      ],
      "operations": [
        "listEmailCampaigns",
        "createEmailCampaign",
        "updateEmailCampaign",
        "replaceEmailAudience",
        "previewEmailCampaign",
        "listTestRecipients",
        "addTestRecipient",
        "removeTestRecipient",
        "sendEmailTest",
        "sendEmailCampaign",
        "scheduleEmailCampaign",
        "listEmailRecipients",
        "listEmailEvents",
        "getEmailCampaign"
      ],
      "steps": [
        {
          "tool": "create_email_draft",
          "purpose": "Create an unsent email draft with a name, subject, body, and a recipient selection.",
          "preconditions": [
            "email:draft."
          ],
          "outputs": [
            "The draft campaign id."
          ],
          "readback": "get_email_draft",
          "sideEffects": [
            "Creates one unsent draft. Sends nothing and charges nothing."
          ],
          "retry": "Repeating creates a second draft. Read the campaign back first if a retry is uncertain.",
          "branchOf": null
        },
        {
          "tool": "set_email_audience",
          "purpose": "Set or replace the draft's audience and rebuild its recipient snapshot.",
          "preconditions": [
            "email:draft.",
            "An unscheduled draft."
          ],
          "outputs": [
            "The updated audience and recipient count."
          ],
          "readback": "get_email_draft",
          "sideEffects": [
            "Changes the draft's recipient snapshot only."
          ],
          "retry": "Re-applying the same audience selection is safe, but a segment is re-resolved at call time: the same saved selection can match a changed contact set, so read the draft back to confirm the current recipient count rather than assuming the previous set.",
          "branchOf": null
        },
        {
          "tool": "preview_email_audience",
          "purpose": "Size the would-be campaign and read its estimated cost and blockers before spending.",
          "preconditions": [
            "email:draft."
          ],
          "outputs": [
            "Recipient count.",
            "Estimated cost.",
            "Blockers."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row; never charges."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "list_test_recipients",
          "purpose": "See which extra addresses may receive a test send, and whether each one is still pending or already verified.",
          "preconditions": [
            "email:test."
          ],
          "outputs": [
            "Each address and its pending or verified status."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "add_test_recipient",
          "purpose": "Allow one more address to receive test sends after that address confirms the verification link.",
          "preconditions": [
            "email:test."
          ],
          "outputs": [
            "The address and whether a verification email was sent."
          ],
          "sideEffects": [
            "Sends one verification email when the address is new or the previous link is old enough to replace. Does not send a campaign and does not charge."
          ],
          "retry": "Repeating a verified address sends nothing. A pending address is not emailed again within ten minutes.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "remove_test_recipient",
          "purpose": "Take an address off the test-recipient list.",
          "preconditions": [
            "email:test."
          ],
          "outputs": [
            "Confirmation that the address was removed."
          ],
          "sideEffects": [
            "The address can no longer receive test sends unless it is the owner or an active member."
          ],
          "retry": "Removing an address that is already gone returns not found. Read the list again before retrying.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "test_email_campaign",
          "purpose": "Send one proof to the credential owner, an active member, or a verified test recipient.",
          "preconditions": [
            "email:test.",
            "Recipients are the key owner's address, active members of the same organization, or verified test recipients."
          ],
          "outputs": [
            "Per-recipient test results."
          ],
          "sideEffects": [
            "Sends real email to those test addresses and bills each test on its own."
          ],
          "retry": "Every proof is paid: it needs confirm true and a fresh idempotency_key even for a single recipient, because the runner gates the whole tool and not a recipient count. Reuse the same key so a repeat does not send a second proof.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "send_email_campaign",
          "branchOf": "email-delivery",
          "purpose": "Send the prepared campaign now and spend organization balance.",
          "preconditions": [
            "email:send.",
            "The operator has agreed to the audience, message, and cost.",
            "confirm true and a fresh idempotency_key.",
            "An unscheduled draft."
          ],
          "outputs": [
            "The accepted send result: the campaign moves to sending/queued. Per-recipient delivery and the final charge are NOT final at this point."
          ],
          "readback": "get_email_draft",
          "sideEffects": [
            "Queues real delivery and spends organization balance as sends are accepted, not necessarily in one charge. Delivery cannot be recalled."
          ],
          "retry": "Do not retry blindly. The call returns an accepted send, not a finished delivery. Read the campaign status back, then read list_email_events for the queued/sent/delivered evidence before treating it as done; reuse the same idempotency_key so a repeat does not send a second time."
        },
        {
          "tool": "schedule_email_campaign",
          "branchOf": "email-delivery",
          "purpose": "Queue the prepared campaign for a future send at a chosen ISO-8601 time.",
          "preconditions": [
            "email:send.",
            "A draft owned by this organization.",
            "A time at least 5 minutes ahead and no more than 366 days out."
          ],
          "outputs": [
            "The scheduled campaign with its scheduled_at (queued, not sent)."
          ],
          "readback": "get_email_draft",
          "sideEffects": [
            "Does not deliver now and places no balance hold on the queue. Two money events differ: the spend approval is consumed now, when the campaign is queued, and the balance hold is placed later, when the send job runs."
          ],
          "retry": "A retry of the same schedule with the same idempotency_key returns the scheduled campaign and does not move the time."
        },
        {
          "tool": "list_email_recipients",
          "purpose": "Read the per-recipient delivery rows for a campaign.",
          "preconditions": [
            "logs:read."
          ],
          "outputs": [
            "Recipient rows with status and timestamps."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "list_email_events",
          "purpose": "Read delivery events (queued, sent, delivered, bounced, opened, clicked) for a campaign. Use this as the delivery evidence after a send.",
          "preconditions": [
            "logs:read."
          ],
          "outputs": [
            "Delivery events with status."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        }
      ],
      "choices": [
        {
          "id": "email-delivery",
          "decision": "Deliver now or schedule for later?",
          "options": [
            "send_email_campaign",
            "schedule_email_campaign"
          ],
          "guidance": "Take exactly one. `send_email_campaign` starts delivery immediately; `schedule_email_campaign` queues delivery for a future time. They are not sequential steps: an agent must never call both for the same draft in one run."
        }
      ],
      "gaps": []
    },
    {
      "id": "sms",
      "title": "SMS: readiness, draft, send, report",
      "summary": "Check texting readiness, draft a text or MMS, preview the audience, send one proof, send or schedule, then read recipients, events, and outcomes.",
      "preconditions": [
        "sms:read, sms:draft, and sms:test for the draft and proof; sms:send for the live send and schedule.",
        "Texting readiness satisfied (brand registered and a sending number assigned)."
      ],
      "requiredScopes": [
        "sms:read",
        "sms:draft",
        "sms:test",
        "sms:send",
        "logs:read"
      ],
      "operations": [
        "getSmsReadiness",
        "createSmsCampaign",
        "updateSmsCampaign",
        "replaceSmsAudience",
        "previewSmsCampaign",
        "sendSmsTest",
        "sendSmsCampaign",
        "scheduleSmsCampaign",
        "listSmsRecipients",
        "listSmsEvents",
        "getSmsOutcomes",
        "getSmsCampaign",
        "listSmsCampaigns"
      ],
      "steps": [
        {
          "tool": "get_sms_readiness",
          "purpose": "Check whether the organization can text: registration state and an assigned sending number.",
          "preconditions": [
            "sms:read."
          ],
          "outputs": [
            "A readiness verdict with the reasons it is not ready."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "create_sms_draft",
          "purpose": "Create an unsent text or MMS draft with its message and audience. Set type mms and pass an image to make a picture message; the image is stored as a publicly readable asset.",
          "preconditions": [
            "sms:draft."
          ],
          "outputs": [
            "The draft campaign id."
          ],
          "readback": "get_sms_campaign",
          "sideEffects": [
            "Creates one unsent draft. An MMS image is stored as a publicly readable asset. Sends nothing and charges nothing."
          ],
          "retry": "Repeating creates a second draft. Read it back first if a retry is uncertain.",
          "branchOf": null
        },
        {
          "tool": "set_sms_audience",
          "purpose": "Replace an unscheduled text draft's audience and rebuild its recipient snapshot and cost estimate.",
          "preconditions": [
            "sms:draft.",
            "An unscheduled draft."
          ],
          "outputs": [
            "The updated audience, recipient count, and cost estimate."
          ],
          "readback": "get_sms_campaign",
          "sideEffects": [
            "Changes the draft's recipient snapshot and estimate only. Sends nothing and does not move organization balance."
          ],
          "retry": "Re-applying the same audience selection is safe, but a list is re-resolved at call time: the same saved selection can match a changed contact set, so read the draft back to confirm the current recipient count rather than assuming the previous set.",
          "branchOf": null
        },
        {
          "tool": "update_sms_draft",
          "purpose": "Revise an unsent draft's name, message, or channel. On an MMS draft a text-only edit keeps the current image, a new image replaces it, and setting type sms removes it.",
          "preconditions": [
            "sms:draft."
          ],
          "outputs": [
            "The updated draft."
          ],
          "readback": "get_sms_campaign",
          "sideEffects": [
            "Updates one unsent draft. A replacement image is stored as a publicly readable asset. Sends nothing and charges nothing."
          ],
          "retry": "Repeating revises the same draft. Read it back first if a retry is uncertain.",
          "branchOf": null
        },
        {
          "tool": "preview_sms_audience",
          "purpose": "Size the would-be text and read its estimated cost before spending.",
          "preconditions": [
            "sms:draft."
          ],
          "outputs": [
            "Recipient count and estimated cost."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row; never charges."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "test_sms_campaign",
          "purpose": "Send one proof text to the credential owner or up to five same-organization addresses.",
          "preconditions": [
            "sms:test.",
            "Same recipient rules as the email proof."
          ],
          "outputs": [
            "Per-recipient test results."
          ],
          "sideEffects": [
            "Sends real texts to those test addresses and bills each."
          ],
          "retry": "Every proof is paid: it needs confirm true and a fresh idempotency_key even for a single recipient, because the runner gates the whole tool and not a recipient count. Reuse the same key so a repeat does not send a second proof.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "send_sms_campaign",
          "branchOf": "sms-delivery",
          "purpose": "Dispatch the prepared text now and spend organization balance.",
          "preconditions": [
            "sms:send.",
            "The operator has agreed to the audience, message, and cost.",
            "confirm true and a fresh idempotency_key."
          ],
          "outputs": [
            "The accepted dispatch result: the campaign moves to sending/queued. Per-recipient delivery and the final charge are NOT final at this point."
          ],
          "readback": "get_sms_campaign",
          "sideEffects": [
            "Queues real dispatch and spends organization balance as sends are accepted, not necessarily in one charge. Delivery cannot be recalled."
          ],
          "retry": "Do not retry blindly. The call returns an accepted dispatch, not a finished delivery. Read the campaign status back, then read list_sms_events and get_sms_outcomes for the sent/delivered/failed evidence before treating it as done; reuse the same idempotency_key so a repeat does not send a second time."
        },
        {
          "tool": "schedule_sms_campaign",
          "branchOf": "sms-delivery",
          "purpose": "Queue the prepared text for a future dispatch at a chosen ISO-8601 time.",
          "preconditions": [
            "sms:send.",
            "A draft owned by this organization."
          ],
          "outputs": [
            "The scheduled campaign with its scheduled_at (queued, not sent)."
          ],
          "readback": "get_sms_campaign",
          "sideEffects": [
            "Does not dispatch now and places no balance hold on the queue. Two money events differ: the spend approval is consumed now, when the campaign is queued, and the balance hold is placed later, when the send job runs."
          ],
          "retry": "A retry of the same schedule with the same idempotency_key returns the scheduled campaign and does not move the time."
        },
        {
          "tool": "list_sms_recipients",
          "purpose": "Read the per-recipient delivery rows for a text campaign.",
          "preconditions": [
            "sms:read and logs:read."
          ],
          "outputs": [
            "Recipient rows with status."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "list_sms_events",
          "purpose": "Read delivery events for a text campaign.",
          "preconditions": [
            "sms:read and logs:read."
          ],
          "outputs": [
            "Delivery events with status."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "get_sms_outcomes",
          "purpose": "Read the delivery outcome summary (delivered, undelivered, failed) for a text campaign.",
          "preconditions": [
            "sms:read and logs:read."
          ],
          "outputs": [
            "Outcome counts."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        }
      ],
      "choices": [
        {
          "id": "sms-delivery",
          "decision": "Deliver now or schedule for later?",
          "options": [
            "send_sms_campaign",
            "schedule_sms_campaign"
          ],
          "guidance": "Take exactly one. `send_sms_campaign` dispatches immediately; `schedule_sms_campaign` queues dispatch for a future time. They are not sequential steps: an agent must never call both for the same draft in one run."
        }
      ],
      "gaps": [
        "gap.campaign-registration"
      ]
    },
    {
      "id": "transactional",
      "title": "Transactional email and templates",
      "summary": "Manage reusable transactional templates, then send a one-off system email. Templates are a save, not a send; the send is a confirm-gated paid action.",
      "preconditions": [
        "email:transactional.",
        "A verified sending domain for the From address."
      ],
      "requiredScopes": [
        "email:transactional"
      ],
      "operations": [
        "listTransactionalTemplates",
        "createTransactionalTemplate",
        "updateTransactionalTemplate",
        "getTransactionalTemplate",
        "sendTransactionalEmail",
        "getTransactionalEmail",
        "listTransactionalEmails"
      ],
      "steps": [
        {
          "tool": "list_email_templates",
          "purpose": "List the organization's saved transactional templates.",
          "preconditions": [
            "email:transactional."
          ],
          "outputs": [
            "Template summaries with ids."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "create_email_template",
          "purpose": "Save a new transactional template with a subject and at least one body.",
          "preconditions": [
            "email:transactional.",
            "A unique name within the organization."
          ],
          "outputs": [
            "The new template id."
          ],
          "readback": "get_email_template",
          "sideEffects": [
            "Creates one saved template. Sends nothing and charges nothing."
          ],
          "retry": "A duplicate name is refused with 409; a repeat creates no second row.",
          "branchOf": null
        },
        {
          "tool": "update_email_template",
          "purpose": "Change a template's subject or body. Only the fields you pass change.",
          "preconditions": [
            "email:transactional."
          ],
          "outputs": [
            "The updated template."
          ],
          "readback": "get_email_template",
          "sideEffects": [
            "Updates one saved template. Sends nothing and charges nothing."
          ],
          "retry": "Repeating the same values is safe; an empty update is refused with 400.",
          "branchOf": null
        },
        {
          "tool": "send_transactional_email",
          "purpose": "Send one transactional email using a template or inline subject and body.",
          "preconditions": [
            "email:transactional.",
            "A From address on a verified domain.",
            "The recipient confirmed with the operator.",
            "confirm true and a fresh idempotency_key."
          ],
          "outputs": [
            "The send result and log id."
          ],
          "readback": "get_transactional_email",
          "sideEffects": [
            "Sends real email and charges the organization balance."
          ],
          "retry": "Do not retry blindly. If the outcome is uncertain, read the log row back; reuse the same idempotency_key so a repeat is a no-op.",
          "branchOf": null
        },
        {
          "tool": "get_transactional_email",
          "purpose": "Read one transactional email's log row by id.",
          "preconditions": [
            "email:transactional."
          ],
          "outputs": [
            "The stored log row."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        }
      ],
      "choices": [],
      "gaps": []
    },
    {
      "id": "brand-registration",
      "title": "Texting brand registration: preview, optional approval, submit, status",
      "summary": "Read the stored brand filing state, preview a would-be packet, optionally route it through the configured approval policy, submit the paid filing, and read the status back.",
      "preconditions": [
        "registration:read to preview and read status; registration:submit to submit.",
        "Owner role and complete setup for submission.",
        "An agreed one-time brand filing fee."
      ],
      "requiredScopes": [
        "registration:read",
        "registration:submit"
      ],
      "operations": [
        "getBrandRegistration",
        "previewBrandRegistration",
        "submitBrandRegistration",
        "requestSpendApproval"
      ],
      "steps": [
        {
          "tool": "get_texting_brand_registration",
          "purpose": "Read the stored brand filing state, the authoritative fee, and whether the credential is the active, fully-set-up owner.",
          "preconditions": [
            "registration:read."
          ],
          "outputs": [
            "Filing state, fee, and eligibility that means role-and-setup only (not spending authority)."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "preview_texting_brand_registration",
          "purpose": "Validate would-be brand details against the saved settings and return the required fixes and the fee.",
          "preconditions": [
            "registration:read."
          ],
          "outputs": [
            "A ready flag that means field validation passed only, the missing fields, and the fee."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row; files nothing and charges nothing."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "request_texting_brand_approval",
          "purpose": "When this key has a configured spend limit, request authorization for the filing instead of submitting directly. This is the existing configured policy, not a new consent gate.",
          "preconditions": [
            "registration:submit.",
            "The key has a standing spend limit set by its owner."
          ],
          "outputs": [
            "An approval id to pass to the submit step once someone approves."
          ],
          "sideEffects": [
            "Records an approval request. Files nothing and charges nothing."
          ],
          "retry": "A new request supersedes nothing; read the approval list back if unsure.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "submit_texting_brand_registration",
          "purpose": "Submit the paid brand filing built from the saved identity and the credential owner's representative fallbacks.",
          "preconditions": [
            "registration:submit.",
            "Owner role and complete setup.",
            "A valid preview.",
            "confirm true, a fresh idempotency_key, and the fee acknowledgement."
          ],
          "outputs": [
            "The filing result and the charged fee."
          ],
          "readback": "get_texting_brand_registration",
          "sideEffects": [
            "Charges the non-refundable one-time brand fee and files with the carrier. Cannot be recalled."
          ],
          "retry": "Do not retry blindly. If the carrier outcome is uncertain, read the status back and reconcile; reuse the same idempotency_key so a replay does not double-charge or double-file.",
          "branchOf": null
        }
      ],
      "choices": [],
      "gaps": []
    },
    {
      "id": "campaign-registration-readiness",
      "title": "Texting campaign registration: read readiness and run the free site preflight",
      "summary": "Read the stored campaign-registration state, save a compliance draft, and run the free website-compliance preflight. Paid campaign filing is available through submit_texting_campaign_registration.",
      "preconditions": [
        "registration:read to read readiness; registration:prepare to run the free site preflight.",
        "Owner or admin role: the dashboard's campaign surface is owner/admin only, and the versioned endpoints match that boundary.",
        "An organization-bound credential. Reading readiness works while setup is incomplete; the site preflight requires a complete setup."
      ],
      "requiredScopes": [
        "registration:read",
        "registration:prepare"
      ],
      "operations": [
        "getCampaignRegistration",
        "checkCampaignSite"
      ],
      "steps": [
        {
          "tool": "get_texting_campaign_registration",
          "purpose": "Read the current filing status, rejection and retry notes, the frozen filed packet, the resumable draft, and the persisted free site-check history.",
          "preconditions": [
            "registration:read."
          ],
          "outputs": [
            "The locally stored filing state and whether the credential can resume Step 2.",
            "The frozen filed packet and the current draft (your own brand names, URLs, and copy).",
            "The append-only site-check history with timestamps.",
            "The filing flags are local only: already_filed means a carrier campaign reference is recorded, not approval or delivery, and a missing reference is not proof the carrier received nothing; confirmation_required or submitting mean do not submit another filing; none of these flags establish whether a fee was charged, so a missing carrier identity is never permission to submit."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "check_texting_campaign_site",
          "purpose": "Validate the opt-in, privacy, and terms URLs against the live public pages and return the same carrier checklist the paid submit enforces, recording one timestamped history row.",
          "preconditions": [
            "registration:prepare.",
            "Public https opt-in, privacy, and terms URLs on the organization's own domain; a platform or reseller host is refused.",
            "A complete organization setup."
          ],
          "outputs": [
            "Whether the site passes, the failing labels, and the per-page checks.",
            "One immutable history row with the authenticated organization and the check time."
          ],
          "readback": "get_texting_campaign_registration",
          "sideEffects": [
            "Fetches the organization's public pages and appends one append-only history row. Charges nothing, files nothing with any carrier, and creates no profile or number."
          ],
          "retry": "Not idempotent: each call appends a new timestamped history row. Safe to repeat with the same URLs; a failed append is returned as an error, never as durable evidence.",
          "branchOf": null
        }
      ],
      "choices": [],
      "gaps": [
        "gap.campaign-registration"
      ]
    },
    {
      "id": "mail",
      "title": "Physical mail: draft, preview, launch",
      "summary": "Create a physical-mail piece with base64 artwork, size the audience, launch the campaign, and read its status. Mail is the one channel that cannot be recalled.",
      "preconditions": [
        "mail:read to preview; mail:draft to create; mail:send to launch.",
        "An agreed audience, artwork, and spend."
      ],
      "requiredScopes": [
        "mail:read",
        "mail:draft",
        "mail:send"
      ],
      "operations": [
        "listMailCampaigns",
        "getMailCampaign",
        "createMailCampaign",
        "previewMailCampaign",
        "launchMailCampaign"
      ],
      "steps": [
        {
          "tool": "create_mail_draft",
          "purpose": "Create a physical-mail draft, assembling the base64 artwork into the upload the endpoint expects. Confirm-gated.",
          "preconditions": [
            "mail:draft.",
            "confirm true and a fresh idempotency_key."
          ],
          "outputs": [
            "The draft campaign id."
          ],
          "readback": "get_mail_campaign",
          "sideEffects": [
            "Creates one draft with its artwork. Prints nothing and charges nothing yet."
          ],
          "retry": "Reuse the same idempotency_key so a retry does not create a second draft.",
          "branchOf": null
        },
        {
          "tool": "preview_mail_audience",
          "purpose": "Size the physical-mail audience and read the estimated cost.",
          "preconditions": [
            "mail:read."
          ],
          "outputs": [
            "Recipient count and estimated cost."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "launch_mail_campaign",
          "purpose": "Launch the physical-mail campaign and charge the organization balance.",
          "preconditions": [
            "mail:send.",
            "The operator has agreed to the audience, artwork, and cost.",
            "confirm true and a fresh idempotency_key."
          ],
          "outputs": [
            "The launch result and charged amount."
          ],
          "readback": "get_mail_campaign",
          "sideEffects": [
            "Spends organization balance and hands the pieces to print and mail. Mail cannot be recalled."
          ],
          "retry": "Do not retry blindly. The retry window for an uncertain piece is the print vendor's 24-hour idempotency key; past it a retry prints a second piece. Stop and ask a person.",
          "branchOf": null
        },
        {
          "tool": "get_mail_campaign",
          "purpose": "Read one physical-mail campaign by id, including its status.",
          "preconditions": [
            "mail:read."
          ],
          "outputs": [
            "The stored campaign record."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        }
      ],
      "choices": [],
      "gaps": []
    },
    {
      "id": "voice",
      "title": "Voice and the power dialer",
      "summary": "Prepare voice assets and campaigns, place calls, and run the operator-first power dialer. Readiness (a voice-enabled number and the operator's personal phone) is required before the first call.",
      "preconditions": [
        "voice:read, voice:draft, and voice:send as needed.",
        "Readiness satisfied: a voice-enabled number, a resolvable connection, and the operator's personal phone set in account settings."
      ],
      "requiredScopes": [
        "voice:read",
        "voice:draft",
        "voice:send"
      ],
      "operations": [
        "listVoiceNumbers",
        "uploadVoiceAsset",
        "createVoiceCampaign",
        "launchVoiceCampaign",
        "getVoiceCampaign",
        "listVoiceCampaigns",
        "listVoiceRecipients",
        "placeVoiceCall",
        "getVoiceCall",
        "getActiveVoiceCall",
        "testVoiceRobocallDraft",
        "testVoiceRobocall",
        "openPowerDialerSession",
        "getPowerDialerSession",
        "dialNextPowerDialerContact",
        "listPowerDialerSessions"
      ],
      "steps": [
        {
          "tool": "list_voice_numbers",
          "purpose": "List the organization's voice-enabled numbers to confirm one is available.",
          "preconditions": [
            "voice:read."
          ],
          "outputs": [
            "Voice-capable numbers."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "upload_voice_asset",
          "purpose": "Upload a base64 voicemail audio asset for a campaign.",
          "preconditions": [
            "voice:draft.",
            "Audio no larger than the documented upload limit."
          ],
          "outputs": [
            "The stored asset reference."
          ],
          "readback": "get_voice_campaign",
          "sideEffects": [
            "Stores one audio asset. Places no call and charges nothing."
          ],
          "retry": "Re-uploading the same asset is safe; it stores another copy.",
          "branchOf": null
        },
        {
          "tool": "create_voice_draft",
          "purpose": "Create an unsent voice (voicemail) campaign with its audience and asset.",
          "preconditions": [
            "voice:draft."
          ],
          "outputs": [
            "The draft campaign id."
          ],
          "readback": "get_voice_campaign",
          "sideEffects": [
            "Creates one unsent campaign. Places no call and charges nothing."
          ],
          "retry": "Repeating creates a second draft. Read it back first if a retry is uncertain.",
          "branchOf": null
        },
        {
          "tool": "launch_voice_campaign",
          "purpose": "Launch the voicemail campaign and charge the organization balance.",
          "preconditions": [
            "voice:send.",
            "The operator has agreed to the audience and cost.",
            "confirm true and a fresh idempotency_key."
          ],
          "outputs": [
            "The launch result and charged amount."
          ],
          "readback": "get_voice_campaign",
          "sideEffects": [
            "Spends organization balance and starts real voicemail delivery."
          ],
          "retry": "Do not retry blindly. If the outcome is uncertain, read the campaign back; reuse the same idempotency_key.",
          "branchOf": null
        },
        {
          "tool": "start_voice_call",
          "purpose": "Place a single voice call to one contact.",
          "preconditions": [
            "voice:send.",
            "The contact confirmed.",
            "confirm true and a fresh idempotency_key."
          ],
          "outputs": [
            "The call session id."
          ],
          "readback": "get_voice_call",
          "sideEffects": [
            "Places one real call and bills the connected minute."
          ],
          "retry": "Do not retry blindly. Read the active call back before placing another.",
          "branchOf": null
        },
        {
          "tool": "start_dialer_session",
          "purpose": "Open or resume a power dialer session for a campaign. Calls stay operator-first.",
          "preconditions": [
            "voice:draft.",
            "A campaign id.",
            "Readiness satisfied."
          ],
          "outputs": [
            "The session id."
          ],
          "readback": "get_dialer_session",
          "sideEffects": [
            "Dials the operator once and keeps the agent leg up. Bills 6 cents per operator-connected started minute."
          ],
          "retry": "Do not retry blindly. Read the session list back to find the existing session.",
          "branchOf": null
        },
        {
          "tool": "advance_dialer_session",
          "purpose": "Advance the dialer to the next contact against the live operator leg.",
          "preconditions": [
            "voice:send.",
            "A live session.",
            "confirm true and a fresh idempotency_key."
          ],
          "outputs": [
            "The next contact, or that the session is finished."
          ],
          "readback": "get_dialer_session",
          "sideEffects": [
            "Hangs up on the current contact, keeps the operator leg, and dials the next contact."
          ],
          "retry": "Do not retry blindly. Read the session back; a repeat can dial a second contact.",
          "branchOf": null
        },
        {
          "tool": "get_active_call",
          "purpose": "Read the currently active call for this organization.",
          "preconditions": [
            "voice:read."
          ],
          "outputs": [
            "The active call, or nothing when none is live."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        }
      ],
      "choices": [],
      "gaps": [
        "gap.phone-defaults"
      ]
    },
    {
      "id": "contact-import",
      "title": "Import a contact or suppression file",
      "summary": "Upload one or more CSV/XLSX workbooks, preview what will change, answer every uncertain decision, commit, and read the persisted counts and per-row outcomes back. Nothing is imported by parsing a file alone; the commit is the mutation.",
      "preconditions": [
        "contacts:read to preview and read back; contacts:write to commit and to stage a large upload.",
        "Optional list cleaning needs email:verify and address:verify, but is NOT performed by the import: it runs as a separate cohort-scoped step (clean_import_cohort) keyed by the cohortId the commit returns, and the organization-wide verification tools are never run off a file's permission."
      ],
      "requiredScopes": [
        "contacts:read",
        "contacts:write",
        "email:verify",
        "address:verify"
      ],
      "operations": [
        "previewContactImport",
        "commitContactImport",
        "listContactImports",
        "getContactImportOutcomes",
        "getImportCleaningCohort",
        "stageContactImportUpload",
        "cleanImportCohort"
      ],
      "steps": [
        {
          "tool": "stage_contact_import_upload",
          "purpose": "Send a contact file that is too large to inline in one call, in bounded chunks. The first chunk omits staging_id and returns one; every later chunk sends that id, and final true marks the last chunk. The staging id is the reference a later import commit uses for the file.",
          "preconditions": [
            "contacts:write.",
            "The file bytes as base64, up to 10 MB decoded per chunk."
          ],
          "outputs": [
            "A staging id, the 0-based chunk index just written, the bytes received so far and whether the upload is complete."
          ],
          "sideEffects": [
            "Stores the chunk bytes for this credential's organization. Changes no contact, parses nothing and charges nothing."
          ],
          "retry": "Safe to read back with the returned staging id; re-sending an entire chunk would append it a second time, so reconcile by the returned chunk index before repeating any call.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "preview_contact_import",
          "purpose": "Parse and classify the uploaded files and return the exact counts, unresolved decisions, suppression matches and a signed previewToken. Send the per-tab plan, list tags and reviewChoices to keep the operator's decisions.",
          "preconditions": [
            "contacts:read.",
            "The CSV/XLSX files as base64; no file path or URL is accepted."
          ],
          "outputs": [
            "A classified preview, the row counts, unresolved decision ids, and a previewToken."
          ],
          "sideEffects": [
            "Writes preview evidence for this credential's organization. Changes no contacts. Charges nothing."
          ],
          "retry": "Safe to call again; each preview supersedes the last. Read the counts back before committing.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "commit_contact_import",
          "purpose": "Commit the previewed import with the previewToken and the same files and options. Resolve every uncertain decision first.",
          "preconditions": [
            "contacts:write.",
            "A fresh previewToken.",
            "No unresolved decisions.",
            "email:verify / address:verify when cleaning is requested."
          ],
          "outputs": [
            "The persisted counts, the job id, and the cohortId when the commit was preview-backed. When cleaning was requested, a `cleaning` block reporting the requested channels as requested and NOT performed by the commit."
          ],
          "readback": "get_contact_import_outcomes",
          "sideEffects": [
            "Creates, merges and suppresses contacts and writes the import job and per-row evidence. It NEVER charges and NEVER cleans inline: no list cleaning runs, and no organization-wide verification is triggered."
          ],
          "retry": "Do not retry blindly. Re-sending the unchanged commit with the same previewToken replays the stored receipt without importing twice; a changed file or mapping needs a new preview. Cleaning runs only through clean_import_cohort on the returned cohortId; never run a whole-organization verification on the strength of this file's permission.",
          "branchOf": null
        },
        {
          "tool": "list_contact_imports",
          "purpose": "List past imports for this organization, newest first, to find a job to inspect or confirm one landed.",
          "preconditions": [
            "contacts:read."
          ],
          "outputs": [
            "A bounded page of imports with counts and an honest continuation."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "get_contact_import_outcomes",
          "purpose": "Read the persisted per-row outcomes of one import, optionally filtered to one kind, with a keyset continuation.",
          "preconditions": [
            "contacts:read.",
            "A job id from commit_contact_import or list_contact_imports."
          ],
          "outputs": [
            "A bounded page of outcome rows with a next_cursor and has_more."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again; advance by the returned cursor, never assume a short page is the end.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "get_import_cleaning_cohort",
          "purpose": "Read back the frozen import identity cohort captured with a preview-backed commit, using the cohortId the commit returned.",
          "preconditions": [
            "contacts:read.",
            "A cohortId returned by commit_contact_import (preview-backed commits only)."
          ],
          "outputs": [
            "The cohort's safe metadata: preview and job references, snapshot version/fingerprint, requested cleaning channels, the CAPTURED UNIQUE identity counts, a capture state and whether cohort-scoped paid cleaning is available."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again; a foreign or absent cohort is 404. It reports capture and availability only; it never reports or runs any cleaning.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "clean_import_cohort",
          "purpose": "Clean exactly one frozen import identity cohort and charge organization balance. It acts only on the contacts matching the cohort's captured email keys and complete-address hashes, never on the whole organization and never on a caller-supplied key list.",
          "preconditions": [
            "email:verify and address:verify.",
            "A cohortId returned by a preview-backed commit_contact_import.",
            "confirm true and a unique idempotency_key."
          ],
          "outputs": [
            "Per requested channel: whether cleaning was requested and performed, the counts, and the approved maximum. Nothing is charged when no cohort identity needs cleaning."
          ],
          "sideEffects": [
            "Spends organization balance and updates the matched contacts' verification verdicts. It cannot be undone."
          ],
          "retry": "Do not retry blindly after an uncertain provider result: read the cohort back and the contacts' verification state first. A retry that finds no identity needing cleaning charges nothing and returns the unused approval.",
          "readback": null,
          "branchOf": null
        }
      ],
      "choices": [],
      "gaps": [
        "gap.import-list-cleaning",
        "gap.import-large-files"
      ]
    },
    {
      "id": "form-intake",
      "title": "Website form intake: record a submission and read the receipt back",
      "summary": "Record a website form submission as a contact through the same consent and suppression engine the website endpoint uses, then read the persisted receipt back by the external id. Nothing is sent and nothing is charged.",
      "preconditions": [
        "An organization-bound credential with intake:write and the app's owner/admin operational access.",
        "An active organization. Intake is a contact write that works before organization onboarding is complete, matching the legacy website endpoint."
      ],
      "requiredScopes": [
        "intake:write"
      ],
      "operations": [
        "recordFormSubmission",
        "getFormSubmission"
      ],
      "steps": [
        {
          "tool": "record_form_submission",
          "purpose": "Create or update one contact from a form submission. Provide an email, a phone, or both, and a source; a checked opt-in box needs its verbatim consent text, and an unchecked box withholds that channel.",
          "preconditions": [
            "intake:write.",
            "An email, a phone, or both, and a source."
          ],
          "outputs": [
            "The contact id, the created/updated/duplicate result, the applied list tag, and each channel verdict.",
            "An identity_conflict refusal naming both contacts when the email and the phone already belong to two different contacts."
          ],
          "readback": "get_form_submission",
          "sideEffects": [
            "Creates or updates one contact and records one routine audit row. Applies source-derived list tags and channel consent. Sends nothing and charges nothing. A dated opt-out, a STOP, or a live suppression is never cleared."
          ],
          "retry": "Reuse the same external_id on a retry so a COMPLETED submission replays the stored receipt as a duplicate rather than a second write. The adapter commits the contact and its receipt in one serialized transaction, so a same-key concurrent duplicate yields a single mutation and the first winner's receipt, and no receipt is written without its contact. A stored receipt records the outcome of the first accepted write, not proof the stored contact still matches this request, because later legitimate edits can change the contact. An interrupted (transport-ambiguous) keyed retry is not auto-resent: read the stored receipt for this external_id first. With no external_id there is no dedupe or replay; a repeated unkeyed submission is a separate operation.",
          "branchOf": null
        },
        {
          "tool": "get_form_submission",
          "purpose": "Read the persisted receipt of a past submission by the external id that was sent, to confirm the outcome or recover after an interrupted attempt.",
          "preconditions": [
            "intake:write.",
            "The external_id sent with the original submission."
          ],
          "outputs": [
            "The stored created/updated/duplicate receipt, or the stored identity_conflict receipt naming both unchanged contacts."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again. The receipt is the historical outcome of the first accepted write. A keyed submission writes its receipt with its contact in the same transaction, so an interrupted keyed attempt is resolved by reading this receipt: a present receipt proves the commit landed, and its absence after a failed request means no mutation was committed. The receipt does not show whether a later legitimate edit changed the contact, so look the contact up directly when that matters.",
          "readback": null,
          "branchOf": null
        }
      ],
      "choices": [],
      "gaps": [
        "gap.form-intake-atomicity"
      ]
    },
    {
      "id": "team-roster",
      "title": "Team: read the roster and current pending invitations",
      "summary": "Read the authorized organization's team roster and its CURRENT pending invitations without making changes. To invite, re-queue, or cancel an invitation, or to manage an ordinary member, use the team-management workflow.",
      "preconditions": [
        "An organization-bound credential with team:read whose issuing owner/admin is an ACTIVE member of the organization.",
        "A read-only organization state (for example a locked organization) stays readable; a suspended or removed credential membership does not."
      ],
      "requiredScopes": [
        "team:read"
      ],
      "operations": [
        "listTeamMembers",
        "listTeamInvitations"
      ],
      "steps": [
        {
          "tool": "list_team_members",
          "purpose": "Read a bounded page of the roster: id, email, display name, canonical role and membership status, membership timestamps, and the protected-owner flag. Advance by the returned page while hasMore is true; total is the true number of members.",
          "preconditions": [
            "team:read.",
            "Owner or admin role on an active membership."
          ],
          "outputs": [
            "A page of members with an honest pagination block.",
            "Each member's role/status come from THIS organization's membership, so a member whose own selected organization is elsewhere is still shown with this organization's role."
          ],
          "sideEffects": [
            "No business-state change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "list_team_invitations",
          "purpose": "Read a bounded page of the CURRENT pending invitations (not accepted, not cancelled, not dismissed, not expired), newest first: id, email, role, created_at, expires_at.",
          "preconditions": [
            "team:read.",
            "Owner or admin role on an active membership."
          ],
          "outputs": [
            "A page of pending invitations with an honest pagination block.",
            "No invitation token or copy-link value, and no history of past or accepted invitations."
          ],
          "sideEffects": [
            "No business-state change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        }
      ],
      "choices": [],
      "gaps": [
        "gap.member-management"
      ]
    },
    {
      "id": "contact-fields",
      "title": "Contact fields: discover the registry and pin a field's type",
      "summary": "Read the organization's contact-field registry and, when a field should be typed, save an explicit key/label/type definition and read it back. A definition save changes field metadata only; it does not convert, clear, or repair any stored value.",
      "preconditions": [
        "An organization-bound credential with contacts:read to list fields and contacts:write to save a definition.",
        "The organization is the one that owns the credential; no argument selects it."
      ],
      "requiredScopes": [
        "contacts:read",
        "contacts:write"
      ],
      "operations": [
        "listContactFields",
        "upsertContactField"
      ],
      "steps": [
        {
          "tool": "list_contact_fields",
          "purpose": "Read the registry: the explicit definitions unioned with the types inferred from stored contact values, keyed by their exact field keys.",
          "preconditions": [
            "contacts:read."
          ],
          "outputs": [
            "The full bounded registry with a truthful total.",
            "Keys are verbatim, including spaces and case; each field carries key, label, and type."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "upsert_contact_field",
          "purpose": "Save an explicit key/label/type definition for one field so it can be filtered as that type.",
          "preconditions": [
            "contacts:write.",
            "An agreed field key, label, and supported type (text, number, date, boolean)."
          ],
          "outputs": [
            "The stored definition read back in field.",
            "confirmed is false when the save committed but the stored row could not be read back; that is not a refusal."
          ],
          "readback": "list_contact_fields",
          "sideEffects": [
            "Writes one field-definition row and records a routine audit row. It does NOT convert, clear, or repair any contact's stored value and does not change a stored value's type."
          ],
          "retry": "A repeated identical save settles the same key/label/type and is safe; do not replay an unknown (unconfirmed transport) outcome blindly; read the registry back first.",
          "branchOf": null
        }
      ],
      "choices": [],
      "gaps": [
        "gap.contact-field-typed-values"
      ]
    },
    {
      "id": "team-management",
      "title": "Team: read the roster and manage invitations and ordinary members",
      "summary": "Read the authorized organization's roster and current pending invitations, then invite, re-queue, or cancel an invitation, or change, suspend, reactivate, or remove an ordinary (non-owner) member, with each management action read back from the live state. This is ordinary owner/admin authority inside the credential's organization; the organization owner is never a target.",
      "preconditions": [
        "An organization-bound credential holding BOTH team:read and team:manage, whose issuing owner/admin is an ACTIVE member of the organization.",
        "Confirm each management action with the operator before it runs; none is money-moving or irreversible, so no confirm/idempotency argument is required."
      ],
      "requiredScopes": [
        "team:read",
        "team:manage"
      ],
      "operations": [
        "listTeamMembers",
        "listTeamInvitations",
        "createTeamInvitation",
        "resendTeamInvitation",
        "cancelTeamInvitation",
        "changeTeamMemberRole",
        "suspendTeamMember",
        "reactivateTeamMember",
        "removeTeamMember"
      ],
      "steps": [
        {
          "tool": "list_team_members",
          "purpose": "Read the current roster (id, email, display name, canonical role/status, protected-owner flag) before changing anything.",
          "preconditions": [
            "team:read."
          ],
          "outputs": [
            "A bounded page of members with an honest pagination block; total is the true member count."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "list_team_invitations",
          "purpose": "Read the CURRENT pending invitations (id, email, role, created_at, expires_at) to find the one to re-queue or cancel.",
          "preconditions": [
            "team:read."
          ],
          "outputs": [
            "A bounded page of pending invitations; no invite token or copy-link value is ever returned."
          ],
          "sideEffects": [
            "No data change beyond the routine audit row."
          ],
          "retry": "Read-only. Safe to call again.",
          "readback": null,
          "branchOf": null
        },
        {
          "tool": "create_team_invitation",
          "purpose": "Invite one person by email as admin, member, or volunteer. Only email and role are accepted; a person who already has a membership must be reactivated instead.",
          "preconditions": [
            "team:manage.",
            "An agreed email and role."
          ],
          "outputs": [
            "Safe invitation metadata and a `queued` acknowledgment: a source email event was written, which is NOT proof the email was sent or delivered."
          ],
          "readback": "list_team_invitations",
          "sideEffects": [
            "Creates one pending invitation and queues a source email event. Creates no organization, grants no role, and moves no money."
          ],
          "retry": "Read the pending invitations back after an uncertain result. A repeated invite for the same live pending invitation returns a conflict rather than a second one.",
          "branchOf": null
        },
        {
          "tool": "resend_team_invitation",
          "purpose": "Re-queue the invitation email for a still-pending invitation.",
          "preconditions": [
            "team:manage.",
            "A still-pending invitation id."
          ],
          "outputs": [
            "The re-queue acknowledgment; the email itself is not sent by this call."
          ],
          "readback": "list_team_invitations",
          "sideEffects": [
            "Queues another source email event for the same pending invitation. Sends no email itself and moves no money."
          ],
          "retry": "This is NOT idempotent: there is no request-key store, so each call queues the email again as a new request. Read the invitation state back instead of retrying blindly.",
          "branchOf": null
        },
        {
          "tool": "cancel_team_invitation",
          "purpose": "Cancel a still-pending invitation so the person can be invited again.",
          "preconditions": [
            "team:manage.",
            "A still-pending invitation id."
          ],
          "outputs": [
            "The cancellation result; the pending invitation is no longer listed."
          ],
          "readback": "list_team_invitations",
          "sideEffects": [
            "Marks the invitation cancelled. Sends nothing and moves no money."
          ],
          "retry": "Cancelling an already accepted, cancelled, or dismissed invitation is refused. Read the invitations back before re-inviting.",
          "branchOf": null
        },
        {
          "tool": "change_team_member_role",
          "purpose": "Change an ordinary member's role between admin and member.",
          "preconditions": [
            "team:manage.",
            "A non-owner member id and the new role."
          ],
          "outputs": [
            "The live membership read back (id, email, role, membership_status); an unconfirmable readback is reported uncertain, not as success."
          ],
          "readback": "list_team_members",
          "sideEffects": [
            "Changes one ordinary member's role. Promotion to owner, changing the owner, and changing your own role are refused. Sends nothing and moves no money."
          ],
          "retry": "Read the roster back after an uncertain result before repeating; a repeat settles the same role.",
          "branchOf": null
        },
        {
          "tool": "suspend_team_member",
          "purpose": "Suspend an ordinary active member, dropping them to read-only.",
          "preconditions": [
            "team:manage.",
            "A non-owner active member id."
          ],
          "outputs": [
            "The live membership read back with its membership_status."
          ],
          "readback": "list_team_members",
          "sideEffects": [
            "Suspends one ordinary membership. The owner cannot be suspended, you cannot suspend yourself, and the last active admin cannot be suspended. Sends nothing and moves no money."
          ],
          "retry": "Read the roster back; a repeat against an already-suspended member is refused.",
          "branchOf": null
        },
        {
          "tool": "reactivate_team_member",
          "purpose": "Reactivate a suspended member.",
          "preconditions": [
            "team:manage.",
            "A suspended, non-owner member id."
          ],
          "outputs": [
            "The live membership read back with its restored membership_status."
          ],
          "readback": "list_team_members",
          "sideEffects": [
            "Reactivates one suspended membership. The owner cannot be acted on and you cannot reactivate yourself. Sends nothing and moves no money."
          ],
          "retry": "Read the roster back; a member who is not suspended is refused, so a repeat is safe and reports the refusal.",
          "branchOf": null
        },
        {
          "tool": "remove_team_member",
          "purpose": "Remove an ordinary member, ending their membership. This cannot be undone through this surface.",
          "preconditions": [
            "team:manage.",
            "A non-owner member id, confirmed with the operator."
          ],
          "outputs": [
            "The removal result; removed members are no longer listed."
          ],
          "readback": "list_team_members",
          "sideEffects": [
            "Ends one ordinary membership. It does not delete the account, transfer data, or move money. The owner cannot be removed, you cannot remove yourself, and the last active admin cannot be removed."
          ],
          "retry": "Do not treat removal as reversible here. Read the roster back to confirm the outcome; re-inviting the person starts a fresh invitation.",
          "branchOf": null
        }
      ],
      "choices": [],
      "gaps": [
        "gap.member-management"
      ]
    }
  ],
  "gaps": [
    {
      "id": "gap.campaign-registration",
      "title": "Campaign (texting) registration",
      "detail": "Reading the stored campaign-registration readiness and running the free website-compliance preflight are available through get_texting_campaign_registration and check_texting_campaign_site. Paid campaign filing is available through submit_texting_campaign_registration (POST /api/v1/registration/campaign/submit) when the credential holds registration:submit and the active organization owner has completed setup. An uncertain carrier outcome is never retried blindly."
    },
    {
      "id": "gap.member-management",
      "title": "Member and team management",
      "detail": "An authorized owner/admin credential can read the organization's roster and its CURRENT pending invitations through list_team_members and list_team_invitations (scope team:read, owner/admin only), and can invite, re-queue, and cancel a team invitation and change, suspend, reactivate, or remove an ordinary (non-owner) member through a narrow set of team:manage tools and /api/v1/team operations. The management surface is ordinary organization authority only: the organization owner cannot be acted on, self-actions are refused, promotion to owner is refused, the last active admin cannot be suspended or removed, and platform is_admin alone is not authority. An invitation write queues a source email event, which is NOT proof of delivery. Two gaps remain: an invitee's own identity-plane actions (accepting, previewing, or dismissing their own invitation) and the credential lifecycle (creating, listing, rotating, or revoking API keys and OAuth connected apps) still have no tool and stay in the signed-in product. Do not present invitee-side acceptance or credential management as a runnable step."
    },
    {
      "id": "gap.team-credentials",
      "title": "Credentials and connected apps",
      "detail": "Creating, listing, rotating, or revoking API keys and OAuth connected apps has no tool. A key is minted and revoked by a signed-in owner in Account Settings."
    },
    {
      "id": "gap.domain-configuration",
      "title": "Sending domain and DNS configuration",
      "detail": "Reading an organization's sending-domain authentication state, previewing a configuration, and creating, rechecking, or resuming the domain identity are available through get_sending_domain, preview_sending_domain, and configure_sending_domain and the versioned /api/v1/communications/sending-domain operations. The Agent API returns the exact DNS records an owned domain requires. Publishing those records at the customer's own DNS host stays with the customer, and provider-hosted DNS changes, domain deletion or detach, and custom MAIL FROM are out of scope. Do not present MAIL FROM as configured."
    },
    {
      "id": "gap.billing-and-autotopup",
      "title": "Funding and payment-method management",
      "detail": "Balance and ledger reads and existing Auto Top-Up configuration are available through billing tools. Adding or removing a payment method, initiating a deposit, and recording an offline payment have no tool. Card entry stays in the secure payment interface, never in a chat or tool argument."
    },
    {
      "id": "gap.calling-execution",
      "title": "Calling execution, media, and reach",
      "detail": "A member's connected agent can read the prepared calling queues it may work and save the member's own operator phone (calls:read and calls:operate). Placing, advancing, media, or billing a call, buying or provisioning a number, and the full personal-settings, key-management, and account portals remain unavailable to a member credential and stay in the signed-in product. Do not present starting, advancing, or billing a call as a runnable step for a member connection."
    },
    {
      "id": "gap.phone-defaults",
      "title": "Phone numbers and defaults",
      "detail": "Purchasing a toll-free or local number and setting the organization's default campaign phone are available through purchase_phone_number and set_default_phone_number. Local numbers are automatically attached to the organization's current 10DLC campaign when one is approved."
    },
    {
      "id": "gap.import-list-cleaning",
      "title": "Import list cleaning",
      "detail": "A commit never cleans and never charges. Cleaning exactly the contacts a single import saved is a SEPARATE, bounded paid step: clean_import_cohort acts on the cohortId a preview-backed commit returns and on nothing else, so a file's cleaning permission never triggers a whole-organization run. A commit that asks for cleaning reports the channels as REQUESTED and NOT PERFORMED by the commit. An import that is not preview-backed captures no cohort, so there is nothing for the cohort-scoped step to act on."
    },
    {
      "id": "gap.import-large-files",
      "title": "Large import files and staged uploads",
      "detail": "An MCP contact import travels base64-encoded inside one JSON-RPC body, and the request body cap is 4.5 MB, so the inline aggregate is bound near 2.6 MiB decoded. A larger file can be sent in bounded chunks with stage_contact_import_upload, which stores the bytes and returns a durable staging id. Pass that staging_id to preview_contact_import or commit_contact_import in a JSON body to import the staged file. The dashboard multipart import and the multipart REST route remain available for callers that prefer them."
    },
    {
      "id": "gap.form-intake-atomicity",
      "title": "Form-intake write atomicity",
      "detail": "A website form submission commits its contact mutation and its stored receipt in a single serialized transaction per organization. It checks for an existing same-key receipt before mutating, re-validates the prepared contact and email/phone matches at commit time, and writes the receipt in the same transaction. A same-key concurrent duplicate commits a single mutation and replays the first winner's receipt, and a receipt failure rolls the contact write back. A stored receipt records the outcome of the first accepted write, not proof that the current contact still matches that request, as later edits can update the contact. After an interrupted keyed request, read the stored receipt for that external_id before retrying. Submissions without an external_id perform the contact action without deduplication or replay."
    },
    {
      "id": "gap.member-credential-parity",
      "title": "Member and volunteer delegated credentials",
      "detail": "Operational portal credentials require an active owner or admin membership in the organization, matching the dashboard's access boundary. Eligible owners and admins can read and update their own personal profile and preferences, which apply across every organization they belong to. Members with calling access can use OAuth calling-workspace tools (calls:read and calls:operate), while broader member or volunteer credential delegation is not available. Do not present general member or volunteer operational access as available."
    },
    {
      "id": "gap.contact-field-typed-values",
      "title": "Contact-field value typing and advanced filter parity",
      "detail": "An agent can DISCOVER the contact-field registry (list_contact_fields) and SAVE an explicit key/label/type definition (upsert_contact_field), and the versioned route GET|PUT /api/v1/contacts/fields backs both. But saving a definition changes field METADATA only: it does NOT convert, clear, or repair any contact's stored value, and it does not change a stored value's type; a value that no longer matches the new type keeps its stored text. Writing a TYPED field VALUE onto a contact, and the advanced typed-filter surface (typed operators such as gt/gte/lt/lte/neq/contains over typed values), still have no tool and no Agent API operation. Only the flat text/number custom-value write on create_contact/update_contact is available. Do not present a definition save as a value conversion, and do not present registry discovery or a definition save as advanced filter parity."
    }
  ],
  "guidance": {
    "scopeFailures": [
      "A call the credential is not allowed to make returns a tool error naming missingScopes and does not run the action.",
      "Each tool publishes the exact scopes it needs in its metadata, so an agent can discover the complete required scope set for an action before requesting it. Those declared scopes are the complete set for the action, not a per-permission menu, and the manifest's tools[].scopes and scope inventory describe SUPPORTED scopes, not granted scopes.",
      "For an eligible active, full-access OAuth actor missing an in-role scope, the server returns the standard HTTP 403 insufficient-scope challenge, and the tool result may carry an optional matching hint that a capable host can use to begin a NEW, explicit, user-approved consent for the required permissions. The optional hint supplements and never replaces the standard signal. Interactive reauthorization requires host support; if the host does not start it automatically, use its connection settings to request new, explicit, user-approved consent.",
      "For an API-key credential, the fix for a missing scope is an intentional credential update by an authorized owner or admin, who mints a new key with that scope and stores it only in the host's secret settings, a vault, or the environment, never in chat, a URL, or source. An existing grant never widens silently, and the same call is never retried hoping it passes.",
      "Calls outside the role ceiling, or without eligible active/full actor state, remain plain refusals rather than an invitation to reconnect. Do not present a refusal as a fabricated consent, a permission prompt, or proof that an account is disconnected.",
      "Adding an email, text, mail, or voice send scope is an explicit grant; older keys keep only the scopes they originally had.",
      "An empty scope list on a tool (or an operation with no x-required-scopes) means any valid credential, not a public endpoint. A public operation is one whose route security is empty: that is a property of the route that answers without a credential, and it is separate from whether a tool needs a scope.",
      "A scope declared by a tool but absent from this document is a documentation defect, not a scope an agent may invent; treat unknown scopes as a stop, never as a silent no-op."
    ],
    "retry": [
      "Never retry a paid or irreversible action blindly. If the outcome is uncertain, read the resource back before acting again.",
      "Tools marked requiresConfirmation require confirm true and an idempotency_key. Generate a new key for a new operation; preserve that key and payload for retries of the same operation. Follow the tool's documented replay window and uncertain-outcome rules; a reused key is not an unlimited guarantee against another effect.",
      "Do not assume an action finished asynchronously. Read the stored status back; there is no invented background completion."
    ],
    "approvals": [
      "There is no blanket consent screen and no agent-only approval gate. Authentication and granted scopes authorize actions directly.",
      "A key whose owner configured a standing spend limit may route a paid action through the existing approval policy; without a configured limit the action runs directly under its scope."
    ]
  },
  "readiness": [
    "Connect through a host that supports remote MCP and OAuth 2.1. Sign in, choose your organization, and approve the requested scopes. After initialization and tool discovery, call get_capabilities to confirm your organization, granted scopes, and setup state. Configure the host connection once; tool access depends on your current role and granted scopes.",
    "The self-service MCP connection uses OAuth authorization-code flow with PKCE S256 and public-client registration (token_endpoint_auth_method=none). No manually supplied client secret or manually minted API key is required. Use the host's connection settings to complete sign-in and consent.",
    "Business access requires an organization-bound credential and eligible membership. Follow each tool's role, scope, setup, billing, and confirmation requirements; connecting does not grant additional permissions or complete business setup.",
    "An authorized owner/admin credential can read the organization roster and current pending invitations and manage ordinary (non-owner) team members. An invitee's own identity-plane actions and credential lifecycle actions (creating, listing, rotating, or revoking API keys and OAuth connected apps) have no MCP tool. Billing balance, ledger and existing Auto Top-Up configuration tools are available. Consult each capability gap for its remaining limitation.",
    "A registered texting brand alone is not SMS campaign approval or full texting readiness. Reading the stored campaign-registration readiness and running the free website-compliance preflight are available through get_texting_campaign_registration and check_texting_campaign_site; paid campaign filing is available through submit_texting_campaign_registration when the credential holds registration:submit. Check get_sms_readiness before treating a brand as ready to text.",
    "For hosts that support API-key authentication, configure an existing valid Campaign Commander API key only in the host's secret settings, vault or environment. Hosts that require OAuth should use the MCP OAuth connection flow; no API-key entry field is assumed. Never paste a credential into chat or a tool argument, and do not treat an unauthenticated 401 as proof that an account is disconnected.",
    "A campaign registration's already_filed flag means a carrier campaign reference is recorded, not approval, delivery, or a charge, and a missing reference does not prove the carrier received nothing. confirmation_required or submitting mean an existing attempt must not be filed again; reconcile it with support. None of these flags establish whether a review fee was charged, so a missing carrier identity is never permission to submit.",
    "Each tool publishes its complete required scope set. An eligible active/full-access OAuth actor missing an in-role scope receives the standard HTTP 403 insufficient-scope challenge and may receive an optional matching tool-result hint. The host must support interactive OAuth reauthorization to request new, explicit, user-approved consent; use its connection settings if it does not start that flow automatically. Security policy is published in _meta.securitySchemes, not a top-level securitySchemes field; hosts that require the top-level field must support this metadata location to use it."
  ]
}