Tools

Spur MCP tool catalog

Each tool below includes required inputs, operational notes, and JSON-RPC examples.

Meta spec-aligned template guidance

The guidance below aligns with Meta Business Messaging OpenAPI v23.0 template concepts and is adapted for Spur MCP draft payloads.

Reference: business-messaging-api_v23.0.yaml

  • Meta template objects require a name and language. Use deterministic language values such as en or en_US.
  • Meta runtime component types are commonly header, body, and button. Spur draft payloads use uppercase style such as HEADER, BODY, and BUTTONS.
  • For variable placeholders in body text ({{1}}, {{2}}), always include example.body_text sample values.
  • Supported parameter families include text, currency, date/time, and media. For media templates, provide explicit image/video/document structure. For template submission, media headers must include example.header_handle from whatsapp_template_media_upload. For broadcast launches, media headers must use provider-aware output from whatsapp_media_upload: INFOBIP uses Spur S3 links (image.link/video.link/document.link), while other providers use uploaded media IDs (image.id/video.id/document.id). PDF is the safe document default.
  • Button text and payload rules are strict. Keep quick replies and URL suffixes short and final before submission.
  • For MARKETING templates, add opt-out language in body/footer, for example: Send STOP to stop receiving marketing messages.

Recommended MCP workflow for complex templates

  1. Call template_search to find a close existing template by name/category.
  2. Call template_requirements to generate runtime payload requirements and sample contentMessage.
  3. If the template uses IMAGE/VIDEO/DOCUMENT headers, call whatsapp_template_media_upload and place returned headerHandle in example.header_handle.
  4. Build a new payload with template_create_draft using reviewed component structure.
  5. Call template_submit_for_review with the returned draft ID.
  6. If submission fails, read providerError fields (error_user_msg, error_subcode, fbtrace_id), adjust payload, and retry.
  7. Call template_refresh to sync latest review state before sending campaigns.

Broadcast lifecycle flow (recommended)

  1. Call segment_filters_catalog to discover valid filter fields/operators.
  2. Build and check ruleGroup via segment_validate before writing segments.
  3. Create or update audience segments with segment_create / segment_update and confirm validation.validForBroadcast=true.
  4. Use segment_search to select final include/exclude segment ids.
  5. For existing segments, run segment_size and confirm validation.audienceCount > 0 before launch.
  6. Call template_requirements before campaign creation, especially for carousel templates.
  7. For broadcast runtime media headers, call whatsapp_media_upload and place the provider-specific value in contentMessage header parameters: INFOBIP requires Spur S3 link, other providers require uploaded id.
  8. Create draft campaign using broadcast_create and a template reference.
  9. Update variables and audience operations with broadcast_update ( includeSet, includeAdd, excludeAdd, sendAt, deliveryBoost, splitAcrossDays ).
  10. Launch the campaign with broadcast_launch.
  11. Use uploaded media IDs at launch time. header_handle is for template submission examples and should not be used as runtime broadcast media input.
  12. Track performance via broadcast_analytics and broadcast_overview_stats.

CSV import flow (recommended)

  1. Upload CSV to an accessible URL (for example a signed S3 URL).
  2. Call customer_import_csv_validate to confirm mapping and row quality.
  3. Call customer_import_csv_start to queue import of valid rows only.
  4. Poll customer_import_csv_status until completed.
  5. Use customer_import_csv_clear to reset stored status before the next run.

template_create_draft

Creates a WhatsApp template draft inside Spur.

Required fields

  • name
  • channelId
  • category
  • format
  • language
  • components

Optional fields

  • subCategory

Behavior notes

  • category: MARKETING | UTILITY | AUTHENTICATION
  • format: SINGLE | CAROUSEL
  • Creates draft only and does not submit to Meta
  • Template name should be lowercase with underscores for reliable Meta acceptance
  • When BODY has variables ({{1}}, {{2}}), include example.body_text values to avoid review failures
  • For MARKETING templates, include opt-out language such as "Send STOP to stop receiving marketing messages."
  • WhatsApp text formatting is supported in BODY/FOOTER: *bold*, _italic_, ~strikethrough~, `inline code`, ```monospace```, numbered lists (1. item), bullets (- item), and quote lines (> text)
  • BUTTON text must exactly match what you intend to submit. Meta is strict about button text mismatches
  • For IMAGE/VIDEO/DOCUMENT headers, set example.header_handle from whatsapp_template_media_upload before submission

Example request

{
  "jsonrpc": "2.0",
  "id": 11,
  "method": "tools/call",
  "params": {
    "name": "template_create_draft",
    "arguments": {
      "name": "order_update_template",
      "channelId": 101,
      "category": "UTILITY",
      "format": "SINGLE",
      "language": "en",
      "components": {
        "body": "Your order update text"
      }
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "draftTemplateId": 873,
  "templateSummary": {
    "name": "order_update_template",
    "channelId": 101,
    "category": "UTILITY",
    "language": "en",
    "format": "SINGLE"
  }
}

template_create_draft (complex utility example)

Reference payload pattern for variable-rich utility templates with header and quick reply buttons.

Required fields

  • name
  • channelId
  • category
  • format
  • language
  • components

Optional fields

  • subCategory

Behavior notes

  • Use this shape when you need placeholders, deterministic button options, and production-safe examples
  • Quick replies should be concise and final before submitting for review
  • BODY text can include line breaks and WhatsApp formatting markers, but avoid malformed markdown patterns that Meta rejects

Example request

{
  "jsonrpc": "2.0",
  "id": 12,
  "method": "tools/call",
  "params": {
    "name": "template_create_draft",
    "arguments": {
      "name": "address_confirmation_v2",
      "channelId": 3,
      "category": "UTILITY",
      "format": "SINGLE",
      "language": "en",
      "subCategory": "CUSTOM",
      "components": [
        {
          "type": "HEADER",
          "format": "TEXT",
          "text": "Confirm Delivery Address"
        },
        {
          "type": "BODY",
          "text": "Thanks for placing your order with {{1}}. We will deliver to:\n\n{{2}}\n{{3}}\n{{4}}\n\nNeed any corrections before shipping?",
          "example": {
            "body_text": [["Spur Store", "221B Baker Street", "London", "NW1 6XE"]]
          }
        },
        {
          "type": "BUTTONS",
          "buttons": [
            { "type": "QUICK_REPLY", "text": "Address Is Correct" },
            { "type": "QUICK_REPLY", "text": "Edit My Address" }
          ]
        }
      ]
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "draftTemplateId": 910,
  "templateSummary": {
    "name": "address_confirmation_v2",
    "channelId": 3,
    "category": "UTILITY",
    "subCategory": "CUSTOM",
    "language": "en",
    "format": "SINGLE"
  }
}

template_update_draft

Updates an existing DRAFT template. Only templates in DRAFT status can be modified; approved or rejected templates must be recreated.

Required fields

  • templateId

Optional fields

  • name
  • category
  • subCategory
  • language
  • components
  • clickTrackingEnabled

Behavior notes

  • Only DRAFT status templates can be updated. Returns 404 if template does not exist in this workspace
  • Omitted fields are left unchanged; pass only the fields you want to modify
  • Components follow the same structure as template_create_draft
  • Use this to iterate on draft content before calling template_submit_for_review

Example request

{
  "jsonrpc": "2.0",
  "id": 13,
  "method": "tools/call",
  "params": {
    "name": "template_update_draft",
    "arguments": {
      "templateId": 873,
      "components": {
        "body": "Updated order text with {{1}} tracking link",
        "footer": "Reply STOP to opt out"
      }
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "updatedTemplateId": 873,
  "updatedFields": ["components"],
  "templateSummary": {
    "name": "order_update_template",
    "channelId": 101,
    "category": "UTILITY",
    "language": "en",
    "format": "SINGLE",
    "status": "DRAFT"
  }
}

template_submit_for_review

Submits an existing draft template for Meta review.

Required fields

  • templateId

Behavior notes

  • Returns provider-level error context when Meta rejects submission
  • Use this after template_create_draft
  • Media headers require example.header_handle from whatsapp_template_media_upload (URL/mediaId will fail)
  • warnings is reserved for non-fatal advisories
  • On failure, inspect providerError.error_user_msg, providerError.error_subcode, and traceId

Example request

{
  "jsonrpc": "2.0",
  "id": 12,
  "method": "tools/call",
  "params": {
    "name": "template_submit_for_review",
    "arguments": {
      "templateId": 873
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "submittedTemplate": {
    "id": 873,
    "status": "IN_REVIEW",
    "category": "UTILITY",
    "templateId": "1234567890"
  },
  "status": "IN_REVIEW",
  "category": "UTILITY",
  "warnings": []
	}

whatsapp_template_media_upload

Downloads media from a URL, uploads it through Meta resumable uploads, and returns header_handle for template IMAGE/VIDEO/DOCUMENT headers.

Required fields

  • channelId
  • mediaUrl

Optional fields

  • mediaType
  • filename

Behavior notes

  • Use this for template_create_draft media headers (example.header_handle)
  • Do not use this output in broadcast runtime payloads
  • Use whatsapp_media_upload for runtime broadcast/message media IDs (image.id/video.id/document.id)
  • mediaType: image | video | document (defaults to image)

Example request

{
  "jsonrpc": "2.0",
  "id": 13,
  "method": "tools/call",
  "params": {
    "name": "whatsapp_template_media_upload",
    "arguments": {
      "channelId": 12,
      "mediaUrl": "https://example.com/assets/template-banner.png",
      "mediaType": "image"
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "upload": {
    "channelId": 12,
    "headerHandle": "4::aW1hZ2UvcG5n:ARbx...",
    "mediaType": "image",
    "sourceUrl": "https://example.com/assets/template-banner.png",
    "mimeType": "image/png",
    "sizeBytes": 248321
  },
  "templateUsage": {
    "singleHeaderExample": {
      "type": "HEADER",
      "format": "IMAGE",
      "example": {
        "header_handle": ["4::aW1hZ2UvcG5n:ARbx..."]
      }
    },
    "carouselCardHeaderExample": {
      "card_index": 0,
      "components": [
        {
          "type": "HEADER",
          "format": "IMAGE",
          "example": {
            "header_handle": ["4::aW1hZ2UvcG5n:ARbx..."]
          }
        }
      ]
    }
  }
}

template_refresh

Refreshes template status/components from Meta by internal template id or metaTemplateId.

Required fields

  • one of: id | metaTemplateId

Behavior notes

  • Use id for a specific local template record
  • Use metaTemplateId when your flow stores Meta template IDs
  • Returns updated status and metadata after sync

Example request

{
  "jsonrpc": "2.0",
  "id": 14,
  "method": "tools/call",
  "params": {
    "name": "template_refresh",
    "arguments": {
      "metaTemplateId": "1208259160474051"
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "refreshedBy": "metaTemplateId",
  "template": {
    "id": 601,
    "name": "order_update_v1",
    "status": "APPROVED",
    "category": "UTILITY",
    "format": "SINGLE",
    "language": "en_US",
    "templateId": "1208259160474051"
  }
}

template_delete

Deletes a template by internal template id. MCP validates workspace ownership first, then attempts Meta deletion when a linked templateId exists.

Required fields

  • id

Behavior notes

  • Use template_search first to pick the correct internal id
  • metaDeletion.attempted=true means the deleted template had a linked Meta template id
  • metaDeletion.success can be null when no Meta deletion was attempted or provider response is unavailable

Example request

{
  "jsonrpc": "2.0",
  "id": 15,
  "method": "tools/call",
  "params": {
    "name": "template_delete",
    "arguments": {
      "id": 89
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "deleted": {
    "id": 89,
    "name": "mcp_marketing_simple_1771153651",
    "templateId": "1475797657492255",
    "status": "APPROVED",
    "category": "MARKETING",
    "subCategory": "CUSTOM",
    "format": "SINGLE",
    "language": "en",
    "channelId": 3
  },
  "metaDeletion": {
    "attempted": true,
    "success": true,
    "providerResponse": {
      "success": true
    }
  }
}

template_requirements

Returns runtime send requirements and a sample contentMessage payload for a template. Use this before carousel sends and variable-heavy templates.

Required fields

  • one of: id | name

Optional fields

  • channelId

Behavior notes

  • Use id whenever possible for deterministic lookups
  • When using name in a multi-channel workspace, pass channelId to avoid ambiguity
  • hasCarousel=true means contentMessage.components.carousel.cards is required for broadcast/message sends
  • For media headers, upload first via whatsapp_media_upload and use returned image.id/video.id/document.id
  • For dynamic URL buttons, runtime button text must be the URL suffix only (not a full URL)
  • URL button index must match the approved template button position (check requiredRuntimeComponents notes)
  • For single templates with multiple buttons, include runtime components for each supported button type (for example quick_reply + url)
  • sampleContentMessage is a scaffold, not a final production payload

Example request

{
  "jsonrpc": "2.0",
  "id": 16,
  "method": "tools/call",
  "params": {
    "name": "template_requirements",
    "arguments": {
      "id": 44
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "template": {
    "id": 44,
    "name": "test_template_carousel",
    "format": "CAROUSEL",
    "language": "en",
    "channelId": 3
  },
  "requirements": {
    "hasCarousel": true,
    "carouselCardCount": 2,
    "bodyVariableCount": 0,
    "hasDynamicButtons": true,
    "requiresContentMessage": true
  },
  "sampleContentMessage": {
    "name": "test_template_carousel",
    "language": { "code": "en" },
    "components": [
      {
        "type": "carousel",
        "cards": [
          {
            "card_index": 0,
            "components": [
              {
                "type": "header",
                "parameters": [{ "type": "image", "image": { "id": "<meta-media-id>" } }]
              },
	              {
	                "type": "button",
	                "sub_type": "url",
	                "index": "1",
	                "parameters": [{ "type": "text", "text": "url_suffix_value" }]
	              }
	            ]
	          }
	        ]
	      }
    ]
  }
}

channels_list

Lists connected channels in the workspace. Use this to discover valid sender handles before calling message_send.

Required fields

Optional fields

  • channelType

Behavior notes

  • channelType: whatsapp | instagram | facebook | live_chat | email
  • For WhatsApp, use channels[].from as options.from in message_send
  • Returns id, type, provider, handle, displayName, and isActive
  • Email channels report the mailbox address as from, handle, and displayName, and the mailbox provider as provider
  • isActive applies to email channels only: false when the mailbox is disconnected or the Email Add-on was lost. Non-email channels return null
  • For email, use channels[].from as options.from in message_send to pick the sending mailbox

Example request

{
  "jsonrpc": "2.0",
  "id": 13,
  "method": "tools/call",
  "params": {
    "name": "channels_list",
    "arguments": {}
  }
}

Example success structuredContent

{
  "ok": true,
  "count": 3,
  "channels": [
    {
      "id": 12,
      "type": "whatsapp",
      "provider": "AISENSY",
      "from": "919876543210",
      "handle": "919876543210",
      "displayName": "Spur Support",
      "isActive": null
    },
    {
      "id": 21,
      "type": "whatsapp",
      "provider": "AISENSY",
      "from": "918888777666",
      "handle": "918888777666",
      "displayName": "Spur Sales",
      "isActive": null
    },
    {
      "id": 8328,
      "type": "email",
      "provider": "GMAIL",
      "from": "support@spurnow.com",
      "handle": "support@spurnow.com",
      "displayName": "support@spurnow.com",
      "isActive": true
    }
  ]
}

whatsapp_media_upload

Downloads media from a URL and uploads it using provider-aware routing: INFOBIP channels upload to Spur S3 (link runtime), non-INFOBIP channels upload to Meta (id runtime).

Required fields

  • channelId
  • mediaUrl

Optional fields

  • mediaType
  • filename

Behavior notes

  • mediaType: image | video | document (defaults to image when omitted)
  • Call channels_list first if channel provider is unknown
  • Use this before broadcast_launch for any template with HEADER media
  • Not valid for template submission header_handle. Use whatsapp_template_media_upload for template drafts
  • INFOBIP launch payloads require Spur S3 link in image.link/video.link/document.link
  • Non-INFOBIP launch payloads require uploaded media id in image.id/video.id/document.id
  • Returned runtimeUsage snippets are copy-paste scaffolds for SINGLE and CAROUSEL templates

Example request

{
  "jsonrpc": "2.0",
  "id": 14,
  "method": "tools/call",
  "params": {
    "name": "whatsapp_media_upload",
    "arguments": {
      "channelId": 12,
      "mediaUrl": "https://example.com/assets/sale-banner.png",
      "mediaType": "image"
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "upload": {
    "channelId": 12,
    "provider": "CLOUD",
    "reference": {
      "kind": "meta_media_id",
      "mediaId": "1264783288392198"
    },
    "mediaId": "1264783288392198",
    "mediaType": "image",
    "sourceUrl": "https://example.com/assets/sale-banner.png",
    "mimeType": "image/png",
    "sizeBytes": 248321
  },
  "runtimeUsage": {
    "singleHeaderComponent": {
      "type": "header",
      "parameters": [
        { "type": "image", "image": { "id": "1264783288392198" } }
      ]
    },
    "carouselCardComponent": {
      "card_index": 0,
      "components": [
        {
          "type": "header",
          "parameters": [
            { "type": "image", "image": { "id": "1264783288392198" } }
          ]
        }
      ]
    }
  }
}

message_send

Sends a message to a single WhatsApp, Instagram, or Facebook contact, a WhatsApp group, or a single email from a connected mailbox.

Required fields

  • channel
  • content

Optional fields

  • to
  • options.from
  • options.conversationId
  • options.cc
  • options.bcc

Behavior notes

  • channel: whatsapp | instagram | facebook | email
  • to is required for whatsapp, instagram and facebook. It is optional only on an email reply, where the recipients are derived from the thread participants
  • to accepts a WhatsApp group as "group:<id>", for example "group:123". Groups are WhatsApp only
  • Single-recipient tool only. Do not use this for bulk campaigns, email included
  • content follows Spur send-message payload conventions
  • If multiple WhatsApp channels are connected, options.from is required
  • Call channels_list to discover valid options.from values
  • For bulk sends, use segment_create -> broadcast_create/broadcast_update -> broadcast_launch
  • When WhatsApp 24-hour window is expired, Spur may convert text to a configured template payload
  • WhatsApp sends can auto-create contact/inbox context if the number does not exist
  • For email, content is { subject, text, html } and needs at least one of text or html
  • Email reply: set options.conversationId to the "eml_<n>" thread id from conversation_search and omit to
  • Email new thread: omit options.conversationId and pass both to and content.subject
  • For email, options.from is the mailbox address (a mailbox channel id is also accepted) and is required when more than one mailbox is connected. An ambiguous send is rejected, never sent from a guessed mailbox
  • options.cc and options.bcc accept up to 20 addresses each. Email attachments are not supported over MCP
  • An email send returns the thread id as emailThreadId and conversationId, so the thread can be read straight back with conversation_messages
  • Email sends appear in the inbox as a normal agent reply, not as an AI agent message
  • warning can be returned for non-blocking conditions

Example request

{
  "jsonrpc": "2.0",
  "id": 16,
  "method": "tools/call",
  "params": {
    "name": "message_send",
    "arguments": {
      "channel": "whatsapp",
      "to": "919876543210",
      "content": {
        "type": "text",
        "text": {
          "body": "Hello from Spur MCP"
        }
      },
      "options": {
        "from": "919999999999"
      }
    }
  }
	}

Example success structuredContent

{
  "ok": true,
  "message": {
    "id": 9981,
    "status": "QUEUED"
  }
}

message_send (WhatsApp group example)

Same tool with a group recipient. Set to: "group:<id>" using the group id from the groups API. Text, media and approved templates are all supported.

Required fields

  • channel
  • to
  • content

Optional fields

  • options.from

Behavior notes

  • Groups are WhatsApp only. Any other channel with a group recipient is rejected
  • to must be "group:<id>" with a plain decimal id. Forms such as group:1e3 or group:0x7b are rejected rather than resolved to another group
  • content follows the same WhatsApp payload conventions as a contact send: text, image, video, audio, document, or template
  • Media must be a public https URL in content.<type>.link. A group cannot resolve an uploaded media id
  • A template sent to a group is billed by Meta as one conversation per delivered participant
  • Meta does not accept QUICK_REPLY, LIST, PRODUCT_LIST, CATALOG or FLOW components in groups. URL, phone number and copy code buttons are fine
  • A group is always messaged from the channel that owns it. options.from is only needed when several WhatsApp channels are connected, and it must be that owning channel
  • A group send never changes who the conversation is assigned to, so an automation will not take a group away from the agent handling it
  • The per-contact marketing opt-out is not applied to groups, because a group has many participants and no single opt-out state
  • A send that WhatsApp rejects is returned as an error carrying Meta's error code, not as a success

Example request

{
  "jsonrpc": "2.0",
  "id": 18,
  "method": "tools/call",
  "params": {
    "name": "message_send",
    "arguments": {
      "channel": "whatsapp",
      "to": "group:123",
      "content": {
        "type": "text",
        "text": {
          "body": "Call summary attached"
        }
      }
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "message": {
    "id": 112,
    "metaMessageId": "wamid.HBgSMTIwMzYzNDExMjAzNTk2MDI4FQgAERgSQjVFMjRFOTVBRDI2OEY2N0RGAA==",
    "sentDateTime": "2026-09-15T12:52:11.542Z",
    "content": {
      "type": "text",
      "text": {
        "body": "Call summary attached",
        "preview_url": true
      }
    }
  }
}

message_send (email example)

Same tool with channel: 'email'. Replies inside an existing thread using the "eml_<n>" id from conversation_search; omit options.conversationId and pass to + content.subject to start a new thread instead.

Required fields

  • channel
  • content

Optional fields

  • to
  • options.from
  • options.conversationId
  • options.cc
  • options.bcc

Behavior notes

  • On a reply, to and content.subject are omitted: recipients come from the thread participants and the subject is derived as a Re: of the thread subject
  • options.from is only needed when more than one mailbox is connected. With several connected and no options.from, the send is rejected with the list of mailbox addresses
  • A disconnected mailbox (channels_list isActive: false) rejects the send
  • A numeric chat conversation id in options.conversationId is rejected, not treated as a new thread
  • Sending a reply also pauses AI email replies on that thread for 30 minutes, the same as a reply sent from the Spur inbox

Example request

{
  "jsonrpc": "2.0",
  "id": 17,
  "method": "tools/call",
  "params": {
    "name": "message_send",
    "arguments": {
      "channel": "email",
      "content": {
        "text": "Your refund is on its way, it lands in 3-5 working days.",
        "html": "<p>Your refund is on its way, it lands in 3-5 working days.</p>"
      },
      "options": {
        "conversationId": "eml_8830",
        "from": "support@spurnow.com",
        "cc": ["billing@spurnow.com"]
      }
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "message": {
    "id": 48214,
    "channel": "email",
    "channelId": 12,
    "emailThreadId": "eml_8830",
    "conversationId": "eml_8830",
    "providerMessageId": "18fd2a91c4b7e001",
    "messageId": "<CAL=DK-abc123@mail.gmail.com>",
    "subject": "Re: Refund for order #1234",
    "from": "support@spurnow.com",
    "to": ["priya@example.com"],
    "cc": ["billing@spurnow.com"],
    "bcc": [],
    "sentAt": "2026-09-11T10:15:00.000Z",
    "sentViaSpur": true
  }
}

conversation_messages

Fetch messages in a chat conversation or an email thread, ordered by most recent first. Returns message content, sender info, timestamps, and delivery status.

Required fields

  • conversationId

Optional fields

  • cursor
  • limit
  • includeHtml

Behavior notes

  • conversationId is the id returned by conversation_search, passed verbatim: a number for a chat conversation, an "eml_<n>" string for an email thread
  • Returns messages and events in reverse chronological order
  • Cursor-based pagination using ISO datetime composite cursor: pass the nextCursor to fetch older messages. Email threads use the same '<ISO sentAt>|<messageId>' shape
  • Default limit is 20, max is 50
  • recordType indicates message type: 'message' (chat), 'event' (system event), 'ticketEvent', or 'emailMessage' (a message in an email thread)
  • content structure varies by message type (text, image, video, template, interactive, etc.)
  • For emailMessage, content is { subject, from[], to[], cc[], bcc[], snippet, text, html, isInternalNote, attachments[] }, and each attachment carries filename, mimeType, size, isInline, and url
  • Email bodies are capped: text and html are cut at 2000 characters and marked with a trailing truncation notice. snippet is never truncated
  • includeHtml applies to email threads only and is off by default because HTML email bodies are very large. The html key is omitted entirely unless includeHtml: true
  • isInternalNote: true marks an internal note that was never sent to the customer
  • messageReadTime and deliveredTime are always null on emailMessage rows: email tracks unread per thread, not per message
  • aiAgentId is set on an emailMessage when an AI agent authored the reply
  • sentViaSpur: true means the message was sent from Spur (outbound); false means inbound from contact
  • Reading an email thread never marks it as read
  • To reply into an email thread, pass this conversationId as options.conversationId to message_send with channel: 'email'

Example request

{
  "jsonrpc": "2.0",
  "id": 51,
  "method": "tools/call",
  "params": {
    "name": "conversation_messages",
    "arguments": {
      "conversationId": 4589,
      "limit": 20
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "conversationId": 4589,
  "count": 2,
  "nextCursor": "2026-04-04T09:00:00.000Z|9050",
  "messages": [
    {
      "id": "9102",
      "recordType": "message",
      "content": {
        "text": { "body": "Hi, when will my order ship?" }
      },
      "sentDateTime": "2026-04-04T10:15:00.000Z",
      "sentViaSpur": false,
      "senderId": "919876543210",
      "isDeleted": false,
      "messageReadTime": null,
      "deliveredTime": "2026-04-04T10:15:01.000Z",
      "errorStatusCode": null,
      "aiAgentId": null,
      "broadcastId": null
    },
    {
      "id": "9050",
      "recordType": "message",
      "content": {
        "text": { "body": "Your order #1234 has been shipped! Track here: https://track.example.com/1234" }
      },
      "sentDateTime": "2026-04-04T09:00:00.000Z",
      "sentViaSpur": true,
      "senderId": null,
      "isDeleted": false,
      "messageReadTime": "2026-04-04T09:02:00.000Z",
      "deliveredTime": "2026-04-04T09:00:02.000Z",
      "errorStatusCode": null,
      "aiAgentId": null,
      "broadcastId": 55
    }
  ]
}

conversation_messages (email thread example)

Same tool, called with an email thread id from conversation_search. Shows the emailMessage record shape.

Required fields

  • conversationId

Optional fields

  • cursor
  • limit
  • includeHtml

Behavior notes

  • Pass the "eml_<n>" conversationId from conversation_search exactly as returned
  • An eml_ id for a thread outside the API key workspace returns a not-found error
  • html is absent below because includeHtml was false. Set includeHtml: true to receive it, truncated at the same 2000 character cap as text
  • Inbound customer replies have sentViaSpur: false. Outbound Spur replies have sentViaSpur: true, and aiAgentId is set when an AI agent wrote them
  • Attachment urls are S3 links. isInline: true means the file is embedded in the body (signature images, tracking pixels) rather than a real attachment

Example request

{
  "jsonrpc": "2.0",
  "id": 52,
  "method": "tools/call",
  "params": {
    "name": "conversation_messages",
    "arguments": {
      "conversationId": "eml_8830",
      "limit": 2
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "conversationId": "eml_8830",
  "count": 2,
  "nextCursor": "2026-09-07T16:40:00.000Z|48210",
  "messages": [
    {
      "id": "48213",
      "recordType": "emailMessage",
      "content": {
        "subject": "Re: Refund for order #1234",
        "from": ["priya@example.com"],
        "to": ["support@spurnow.com"],
        "cc": ["billing@example.com"],
        "bcc": [],
        "snippet": "I still have not received the refund for order #1234.",
        "text": "I still have not received the refund for order #1234. Receipt attached.",
        "isInternalNote": false,
        "attachments": [
          {
            "filename": "receipt.pdf",
            "mimeType": "application/pdf",
            "size": 88214,
            "isInline": false,
            "url": "https://spur-media.s3.eu-central-1.amazonaws.com/email/8830/receipt.pdf"
          }
        ]
      },
      "sentDateTime": "2026-09-08T11:20:00.000Z",
      "sentViaSpur": false,
      "senderId": "priya@example.com",
      "isDeleted": null,
      "messageReadTime": null,
      "deliveredTime": null,
      "errorStatusCode": null,
      "aiAgentId": null,
      "broadcastId": null
    },
    {
      "id": "48210",
      "recordType": "emailMessage",
      "content": {
        "subject": "Refund for order #1234",
        "from": ["support@spurnow.com"],
        "to": ["priya@example.com"],
        "cc": [],
        "bcc": [],
        "snippet": "Thanks for waiting. Your refund for order #1234 has been issued.",
        "text": "Hi Priya, thanks for waiting. Your refund for order #1234 has been issued and should reach your card in 3 to 5 business days.… [truncated]",
        "isInternalNote": false,
        "attachments": []
      },
      "sentDateTime": "2026-09-07T16:40:00.000Z",
      "sentViaSpur": true,
      "senderId": "support@spurnow.com",
      "isDeleted": null,
      "messageReadTime": null,
      "deliveredTime": null,
      "errorStatusCode": null,
      "aiAgentId": 41,
      "broadcastId": null
    }
  ]
}

contact_create

Creates a contact in the workspace.

Required fields

  • firstName
  • lastName
  • number

Optional fields

  • email
  • name

Behavior notes

  • Uses strict REST semantics from POST /contacts
  • Returns conflict-style errors for duplicates
  • Phone format should be digits with country code

Example request

{
  "jsonrpc": "2.0",
  "id": 17,
  "method": "tools/call",
  "params": {
    "name": "contact_create",
    "arguments": {
      "firstName": "Aanya",
      "lastName": "Shah",
      "number": "919876543210",
      "email": "aanya@example.com"
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "contact": {
    "id": 4321,
    "firstName": "Aanya",
    "lastName": "Shah",
    "email": "aanya@example.com",
  "number": "919876543210"
  }
}

contact_update

Updates selected fields on an existing contact.

Required fields

  • id
  • at least one of firstName, lastName, email

Behavior notes

  • Uses strict REST semantics from PATCH /contacts/:id
  • Supported fields: firstName, lastName, email
  • Returns not-found when contact does not exist in workspace

Example request

{
  "jsonrpc": "2.0",
  "id": 19,
  "method": "tools/call",
  "params": {
    "name": "contact_update",
    "arguments": {
      "id": 4321,
      "email": "aanya.updated@example.com"
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "contact": {
    "id": 4321,
    "firstName": "Aanya",
    "lastName": "Shah",
    "email": "aanya.updated@example.com",
  "number": "919876543210"
  }
}

contact_delete

Deletes a contact by explicit contact ID or by WhatsApp number. Use ID when possible for deterministic deletion.

Required fields

  • one of: id | number

Optional fields

  • channel (only whatsapp when deleting by number)

Behavior notes

  • If number is provided, MCP resolves the WhatsApp contact using workspace-scoped number lookup
  • If multiple contacts match a number, MCP returns a conflict with candidate IDs
  • channel only narrows the number lookup. Deleting by id deletes the contact on whichever channel it belongs to
  • deleted.channel echoes that channel and is one of whatsapp, instagram, facebook, live_chat, or email
  • After deletion, sending to the same WhatsApp number can recreate contact context automatically

Example request

{
  "jsonrpc": "2.0",
  "id": 20,
  "method": "tools/call",
  "params": {
    "name": "contact_delete",
    "arguments": {
      "number": "919871739342",
      "channel": "whatsapp"
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "deleted": {
    "id": 10,
    "channel": "whatsapp",
    "number": "919871739342",
    "firstName": "Rohan",
    "lastName": "Rajpal",
    "name": "Rohan Rajpal"
  }
}

customer_import_csv_validate

Validates a contacts CSV with the same backend rules used by Spur UI import before queueing.

Required fields

  • url
  • columnMapping

Optional fields

  • tags

Behavior notes

  • Use this when AI needs a dry run before starting a large import
  • columnMapping entries use [spurField, { type, value }] tuples
  • Returns validRowIndices which are reused during start to skip invalid rows
  • invalidContactsCSV is present when invalid rows exist and includes row-level reasons

Example request

{
  "jsonrpc": "2.0",
  "id": 21,
  "method": "tools/call",
  "params": {
    "name": "customer_import_csv_validate",
    "arguments": {
      "url": "https://spur-uploads.s3.ap-south-1.amazonaws.com/imports/contacts.csv",
      "columnMapping": [
        ["phoneNumber", { "type": "column", "value": "Phone" }],
        ["firstName", { "type": "column", "value": "First Name" }],
        ["lastName", { "type": "column", "value": "Last Name" }],
        ["phoneCountryCode", { "type": "manual", "value": "91" }]
      ],
      "tags": [{ "id": 9, "title": "Imported-Feb-2026" }]
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "validation": {
    "valid": true,
    "totalRows": 1600,
    "invalidRows": 64,
    "validRows": 1536,
    "invalidContactsCSV": "https://spur-uploads.s3.ap-south-1.amazonaws.com/import-errors/invalid.csv",
    "validRowIndices": [0, 1, 2]
  }
}

customer_import_csv_start

Starts background import using the exact UI flow (validate first, then enqueue only valid rows).

Required fields

  • url
  • columnMapping

Optional fields

  • tags

Behavior notes

  • Use this for large imports instead of per-contact creation loops
  • If no valid rows exist, success=false and import is not queued
  • import.jobId maps to the workspace-scoped import queue job id
  • Follow with customer_import_csv_status for progress polling

Example request

{
  "jsonrpc": "2.0",
  "id": 22,
  "method": "tools/call",
  "params": {
    "name": "customer_import_csv_start",
    "arguments": {
      "url": "https://spur-uploads.s3.ap-south-1.amazonaws.com/imports/contacts.csv",
      "columnMapping": [
        ["phoneNumber", { "type": "column", "value": "Phone" }],
        ["firstName", { "type": "column", "value": "First Name" }]
      ],
      "tags": [{ "id": 9, "title": "Imported-Feb-2026" }]
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "success": true,
  "message": "Import started with 1536 valid contacts. 64 invalid contacts skipped.",
  "validation": {
    "valid": true,
    "totalRows": 1600,
    "invalidRows": 64,
    "validRows": 1536,
    "invalidContactsCSV": "https://spur-uploads.s3.ap-south-1.amazonaws.com/import-errors/invalid.csv",
    "validRowIndices": [0, 1, 2]
  },
  "import": {
    "id": "7-import-contacts",
    "name": "IMPORT_CONTACTS",
    "jobId": "7-import-contacts"
  }
}

customer_import_csv_status

Returns current import progress for the workspace import job.

Required fields

Behavior notes

  • Use this after customer_import_csv_start to track progress
  • status is null when no import job exists for the workspace
  • failedContacts can include a CSV URL for rows that failed during processing

Example request

{
  "jsonrpc": "2.0",
  "id": 23,
  "method": "tools/call",
  "params": {
    "name": "customer_import_csv_status",
    "arguments": {}
  }
}

Example success structuredContent

{
  "ok": true,
  "status": {
    "completed": 1200,
    "failed": 13,
    "total": 1500,
    "failedContacts": "https://spur-uploads.s3.ap-south-1.amazonaws.com/import-errors/failed-rows.csv"
  }
}

customer_import_csv_clear

Clears stored import job state when a run is complete or needs reset.

Required fields

Behavior notes

  • Returns cleared=true when a job record existed and was removed
  • Returns cleared=false when no import job exists
  • Use before restarting an import if stale status is blocking your workflow

Example request

{
  "jsonrpc": "2.0",
  "id": 24,
  "method": "tools/call",
  "params": {
    "name": "customer_import_csv_clear",
    "arguments": {}
  }
}

Example success structuredContent

{
  "ok": true,
  "cleared": true
}

segment_filters_catalog

Returns segment categories plus filter/operator metadata so AI can produce valid ruleGroup payloads.

Required fields

Optional fields

  • category

Behavior notes

  • Use this as the first step before segment_create or segment_update
  • category: customerInfo | messageActivity | websiteActivity | shoppingEvents
  • Operators include condition/type combinations required by backend SQL generation

Example request

{
  "jsonrpc": "2.0",
  "id": 30,
  "method": "tools/call",
  "params": {
    "name": "segment_filters_catalog",
    "arguments": {
      "category": "customerInfo"
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "count": 1,
  "categories": [
    {
      "type": "customerInfo",
      "name": "Customer Info",
      "filters": [
        {
          "label": "First Name",
          "value": "contact.firstName",
          "operators": [
            { "name": "Contains", "condition": "contains", "type": "STRING" }
          ]
        }
      ]
    }
  ]
}

segment_validate

Validates a SPUR ruleGroup without writing it and returns audience count + broadcast usability.

Required fields

  • ruleGroup

Optional fields

  • strictNonEmpty

Behavior notes

  • Run this before segment_create/segment_update
  • SPUR_TAGS names are normalized to tag ids in normalizedRuleGroup
  • DATETIME values should be YYYY-MM-DDTHH:mm; if seconds/timezone are provided they are normalized to minute format
  • For rolling windows like "not ordered in last 30 days", prefer RELATIVE_DATETIME values like "30:d" with condition "not_last"
  • Use validation.validForBroadcast=true as launch gate
  • strictNonEmpty defaults to true and treats 0-count audience as invalid for broadcast

Example request

{
  "jsonrpc": "2.0",
  "id": 31,
  "method": "tools/call",
  "params": {
    "name": "segment_validate",
    "arguments": {
      "ruleGroup": {
        "operator": "AND",
        "rules": [
          {
            "ruleType": "rule",
            "categorySelected": "customerInfo",
            "filter": { "field": "customSegment.spurTags" },
            "operator": { "condition": "true", "type": "SPUR_TAGS" },
            "value": "spur partner"
          }
        ]
      }
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "normalizedRuleGroup": {
    "operator": "AND",
    "rules": [
      {
        "ruleType": "rule",
        "categorySelected": "customerInfo",
        "filter": { "field": "customSegment.spurTags" },
        "operator": { "condition": "true", "type": "SPUR_TAGS" },
        "value": "30103"
      }
    ]
  },
  "validation": {
    "valid": true,
    "validForBroadcast": true,
    "isEmpty": false,
    "audienceCount": 124,
    "totalContacts": 3120,
    "audiencePercent": 3.97,
    "guidance": ["Segment currently matches 124 contact(s) and is eligible for broadcast include audiences."]
  }
}

segment_size

Returns audience size and broadcast usability for an existing segment id without modifying anything.

Required fields

  • segmentId

Optional fields

  • strictNonEmpty

Behavior notes

  • Read-only check for existing SPUR/SHOPIFY segments
  • Use this after segment_search when selecting include/exclude segment ids
  • Use validation.validForBroadcast=true as launch gate
  • strictNonEmpty defaults to true and treats 0-count audience as invalid for broadcast

Example request

{
  "jsonrpc": "2.0",
  "id": 32,
  "method": "tools/call",
  "params": {
    "name": "segment_size",
    "arguments": {
      "segmentId": 201
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "segment": {
    "id": 201,
    "name": "Recent WhatsApp Engagers",
    "type": "SPUR"
  },
  "validation": {
    "valid": true,
    "validForBroadcast": true,
    "isEmpty": false,
    "audienceCount": 84,
    "totalContacts": 3120,
    "audiencePercent": 2.69
  }
}

segment_create

Creates a SPUR segment with nested ruleGroup logic.

Required fields

  • name
  • ruleGroup

Optional fields

  • description

Behavior notes

  • Build ruleGroup using operator metadata from segment_filters_catalog
  • Use ruleType=rule for leaf predicates and ruleType=group for nesting
  • For DATETIME rules, send YYYY-MM-DDTHH:mm values (MCP normalizes seconds/timezone to this format)
  • For rolling windows, prefer RELATIVE_DATETIME (example: value="30:d", condition="not_last") instead of absolute dates
  • If you pass a shorthand numeric value for Last Order Date (example: value="30" with missing operator), MCP interprets it as older than 30 days and auto-adds Has Purchased = true
  • For NONE true/false operators, MCP auto-fills empty value as "true"/"false" for UI-safe segments
  • Response contains validation.audienceCount and validation.validForBroadcast
  • Do not use a segment in broadcasts when validation.validForBroadcast is false

Example request

{
  "jsonrpc": "2.0",
  "id": 33,
  "method": "tools/call",
  "params": {
    "name": "segment_create",
    "arguments": {
      "name": "Recent WhatsApp Engagers",
      "description": "Opened at least one message in last 30 days",
      "ruleGroup": {
        "operator": "AND",
        "rules": [
          {
            "ruleType": "rule",
            "categorySelected": "messageActivity",
            "filter": { "field": "customSegment.whatsappRead" },
            "operator": { "condition": "gtz", "type": "NONE" },
            "value": "1"
          }
        ]
      }
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "segment": {
    "id": 201,
    "name": "Recent WhatsApp Engagers",
    "type": "SPUR",
    "updatedAt": "2026-02-16T08:40:00.000Z"
  },
  "validation": {
    "valid": true,
    "validForBroadcast": true,
    "isEmpty": false,
    "audienceCount": 84,
    "totalContacts": 3120,
    "audiencePercent": 2.69
  }
}

segment_update

Updates an existing SPUR segment by replacing name/description/ruleGroup.

Required fields

  • id
  • name
  • ruleGroup

Optional fields

  • description

Behavior notes

  • Shopify segments are read-only and will return an explicit error
  • Send full ruleGroup payload for deterministic updates
  • Response validation tells if the updated segment is currently launch-safe

Example request

{
  "jsonrpc": "2.0",
  "id": 34,
  "method": "tools/call",
  "params": {
    "name": "segment_update",
    "arguments": {
      "id": 201,
      "name": "Recent WA Engagers (30d)",
      "ruleGroup": {
        "operator": "AND",
        "rules": [
          {
            "ruleType": "rule",
            "categorySelected": "messageActivity",
            "filter": { "field": "customSegment.whatsappRead" },
            "operator": { "condition": "gtz", "type": "NONE" },
            "value": "1"
          }
        ]
      }
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "segment": {
    "id": 201,
    "name": "Recent WA Engagers (30d)",
    "type": "SPUR"
  },
  "validation": {
    "valid": true,
    "validForBroadcast": true,
    "isEmpty": false,
    "audienceCount": 92,
    "totalContacts": 3120,
    "audiencePercent": 2.95
  }
}

segment_delete

Deletes a segment when it is not referenced by active broadcasts.

Required fields

  • id

Behavior notes

  • If segment is referenced by DRAFT/SCHEDULED/RUNNING broadcast, deletion fails with structured hints
  • Use broadcast_search + broadcast_update to remove segment references first

Example request

{
  "jsonrpc": "2.0",
  "id": 34,
  "method": "tools/call",
  "params": {
    "name": "segment_delete",
    "arguments": {
      "id": 201
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "deleted": {
    "id": 201,
    "name": "Recent WA Engagers (30d)",
    "type": "SPUR",
    "success": true
  }
}

shopify_segment_sync

Syncs Shopify customer segments from the connected store into Spur local segment records.

Required fields

Behavior notes

  • Use this after Shopify-side writes so segment_search(type=SHOPIFY) reflects latest state
  • Useful as a refresh checkpoint before assigning Shopify audiences to broadcasts

Example request

{
  "jsonrpc": "2.0",
  "id": 34,
  "method": "tools/call",
  "params": {
    "name": "shopify_segment_sync",
    "arguments": {}
  }
}

Example success structuredContent

{
  "ok": true,
  "synced": 13,
  "errors": []
}

shopify_segment_query_reference

Returns decision guidance for Shopify vs SPUR segments and practical Shopify query references/examples.

Required fields

Behavior notes

  • Call this when agent intent is ambiguous and it needs segment-system selection guidance
  • Includes official Shopify docs URLs and curated query examples
  • Pair with shopify_segment_validate before writes

Example request

{
  "jsonrpc": "2.0",
  "id": 35,
  "method": "tools/call",
  "params": {
    "name": "shopify_segment_query_reference",
    "arguments": {}
  }
}

Example success structuredContent

{
  "ok": true,
  "docs": [
    {
      "label": "Shopify Segment Query Language reference",
      "url": "https://shopify.dev/api/shopifyql/segment-query-language-reference"
    }
  ],
  "examples": [
    {
      "name": "Subscribed buyers",
      "query": "email_subscription_status = 'SUBSCRIBED' AND amount_spent > 0"
    }
  ]
}

shopify_segment_validate

Validates Shopify segment query syntax before create or update, without writing segment records.

Required fields

  • query

Behavior notes

  • Use this to fail fast on parse or field errors and reduce write retries
  • Returns valid=false with structured errors for invalid query text
  • Can return estimatedMemberCount when Shopify can evaluate synchronously

Example request

{
  "jsonrpc": "2.0",
  "id": 36,
  "method": "tools/call",
  "params": {
    "name": "shopify_segment_validate",
    "arguments": {
      "query": "email_subscription_status = 'SUBSCRIBED' AND amount_spent > 0"
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "valid": true,
  "query": "email_subscription_status = 'SUBSCRIBED' AND amount_spent > 0",
  "estimatedMemberCount": 12,
  "warnings": [],
  "errors": []
}

shopify_segment_create

Creates a Shopify-native customer segment using Shopify query syntax and optionally syncs it into Spur.

Required fields

  • name
  • query

Optional fields

  • syncAfterWrite

Behavior notes

  • Use this when audience logic must remain reusable in Shopify admin workflows
  • Call shopify_segment_validate first for safer writes
  • syncAfterWrite defaults to true

Example request

{
  "jsonrpc": "2.0",
  "id": 37,
  "method": "tools/call",
  "params": {
    "name": "shopify_segment_create",
    "arguments": {
      "name": "Shopify VIP Subscribers",
      "query": "email_subscription_status = 'SUBSCRIBED' AND amount_spent > 0",
      "syncAfterWrite": true
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "segment": {
    "id": 178,
    "name": "Shopify VIP Subscribers",
    "type": "SHOPIFY",
    "shopifySegmentId": "gid://shopify/Segment/649779282218"
  },
  "shopifySegment": {
    "id": "gid://shopify/Segment/649779282218",
    "name": "Shopify VIP Subscribers"
  },
  "sync": {
    "synced": 13,
    "errors": []
  }
}

shopify_segment_update

Updates a Shopify-native segment name or query by Shopify segment ID (numeric or gid).

Required fields

  • shopifySegmentId
  • at least one of: name | query

Optional fields

  • syncAfterWrite

Behavior notes

  • Use numeric id or gid://shopify/Segment/... format for shopifySegmentId
  • Run shopify_segment_validate before query changes
  • syncAfterWrite defaults to true

Example request

{
  "jsonrpc": "2.0",
  "id": 38,
  "method": "tools/call",
  "params": {
    "name": "shopify_segment_update",
    "arguments": {
      "shopifySegmentId": "649779282218",
      "query": "email_subscription_status = 'SUBSCRIBED' AND amount_spent > 250",
      "syncAfterWrite": true
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "segment": {
    "id": 178,
    "name": "Shopify VIP Subscribers",
    "type": "SHOPIFY"
  },
  "shopifySegment": {
    "id": "gid://shopify/Segment/649779282218",
    "query": "email_subscription_status = 'SUBSCRIBED' AND amount_spent > 250"
  },
  "sync": {
    "synced": 13,
    "errors": []
  }
}

shopify_segment_delete

Deletes a Shopify-native segment by Shopify segment ID and removes local synced records in Spur.

Required fields

  • shopifySegmentId

Optional fields

  • syncAfterWrite

Behavior notes

  • Use with caution in production because this deletes segment definition in Shopify
  • Supports numeric or gid segment IDs
  • syncAfterWrite defaults to true

Example request

{
  "jsonrpc": "2.0",
  "id": 39,
  "method": "tools/call",
  "params": {
    "name": "shopify_segment_delete",
    "arguments": {
      "shopifySegmentId": "gid://shopify/Segment/649779282218",
      "syncAfterWrite": true
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "deleted": {
    "shopifySegmentId": "gid://shopify/Segment/649779282218",
    "localDeletedCount": 1
  },
  "sync": {
    "synced": 12,
    "errors": []
  }
}

broadcast_create

Creates a broadcast draft with template reference, optional variable payload, and initial include/exclude segments. Use this (with segment tools) for bulk sends instead of looping message_send.

Required fields

  • name
  • one of: templateInternalId | template

Optional fields

  • channelId
  • contentMessage
  • includedSegmentIds
  • excludedSegmentIds

Behavior notes

  • When multiple WhatsApp channels exist, pass channelId explicitly
  • templateInternalId maps to internal Spur template id from template_search
  • contentMessage maps to broadcast content.message for template variables/components
  • For carousel templates, contentMessage is required. Call template_requirements first and copy the scaffold
  • For media headers: INFOBIP channels must use image.link/video.link/document.link from whatsapp_media_upload output; other providers must use image.id/video.id/document.id

Example request

{
  "jsonrpc": "2.0",
  "id": 35,
  "method": "tools/call",
  "params": {
    "name": "broadcast_create",
    "arguments": {
      "name": "Weekend Offer Blast",
      "templateInternalId": 873,
      "channelId": 12,
      "includedSegmentIds": [120],
      "excludedSegmentIds": [205]
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "broadcast": {
    "id": 890,
    "name": "Weekend Offer Blast",
    "status": "DRAFT",
    "channelId": 12,
    "includedSegmentIds": [120],
    "excludedSegmentIds": [205]
  }
}

broadcast_update

Updates a DRAFT broadcast with composable include/exclude operations, template/message changes, schedule, and delivery controls.

Required fields

  • id

Optional fields

  • name
  • templateInternalId
  • template
  • contentMessage
  • includeSet
  • includeAdd
  • includeRemove
  • excludeSet
  • excludeAdd
  • excludeRemove
  • sendAt
  • deliveryBoost
  • splitAcrossDays
  • dailyLimit
  • limitAudience
  • limitCount

Behavior notes

  • Only DRAFT broadcasts are editable
  • Delivery Boost and Split Broadcast cannot both be true
  • limitAudience=true requires limitCount
  • excludeAdd entries already present in include list are ignored

Example request

{
  "jsonrpc": "2.0",
  "id": 36,
  "method": "tools/call",
  "params": {
    "name": "broadcast_update",
    "arguments": {
      "id": 890,
      "includeAdd": [121],
      "includeRemove": [120],
      "excludeAdd": [205],
      "contentMessage": {
        "name": "weekend_offer_v1",
        "language": { "code": "en_US" },
        "components": [
          {
            "type": "body",
            "parameters": [{ "type": "text", "text": "20% OFF" }]
          }
        ]
      },
      "sendAt": "2026-02-20T09:00:00.000Z",
      "limitAudience": true,
      "limitCount": 8000
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "broadcast": {
    "id": 890,
    "status": "DRAFT",
    "includedSegmentIds": [121],
    "excludedSegmentIds": [205],
    "sendAt": "2026-02-20T09:00:00.000Z",
    "filterQuery": { "limit": true, "count": 8000 }
  }
}

broadcast_launch

Launches broadcast by calling setReady.

Required fields

  • id

Behavior notes

  • Template media headers must use uploaded media IDs. Direct links are rejected at launch
  • Returns launchedStatus RUNNING for immediate send
  • Returns launchedStatus SCHEDULED when sendAt is in the future

Example request

{
  "jsonrpc": "2.0",
  "id": 37,
  "method": "tools/call",
  "params": {
    "name": "broadcast_launch",
    "arguments": {
      "id": 890
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "launchedStatus": "SCHEDULED",
  "broadcast": {
    "id": 890,
    "name": "Weekend Offer Blast",
    "status": "SCHEDULED"
  }
}

broadcast_analytics

Returns per-broadcast performance metrics and delivery-boost metadata.

Required fields

  • id

Optional fields

  • cached

Behavior notes

  • Includes delivered/opened/clicked/replied counts and percentages
  • Includes revenue, orders, whatsappCost, and template metadata
  • Use cached=true for faster repeated polling

Example request

{
  "jsonrpc": "2.0",
  "id": 39,
  "method": "tools/call",
  "params": {
    "name": "broadcast_analytics",
    "arguments": {
      "id": 890,
      "cached": true
    }
  }
}

Example success structuredContent

{
  "ok": true,
  "analytics": {
    "broadcastId": 890,
    "messages": 10000,
    "delivered": 9300,
    "opened": 6120,
    "clicks": 830,
    "orders": 94,
    "revenue": 274580,
    "whatsappCost": 4810.2,
    "percentDelivered": 93,
    "percentOpened": 65.8,
    "percentClicked": 8.92
  }
}

broadcast_overview_stats

Returns workspace-level aggregate broadcast KPIs.

Required fields

Behavior notes

  • Use alongside broadcast_analytics for campaign-level + workspace-level reporting
  • Totals are currency-normalized to workspace currency

Example request

{
  "jsonrpc": "2.0",
  "id": 40,
  "method": "tools/call",
  "params": {
    "name": "broadcast_overview_stats",
    "arguments": {}
  }
}

Example success structuredContent

{
  "ok": true,
  "overview": {
    "totalCount": 124,
    "totalRevenue": 12495840,
    "totalOrders": 2611
  }
}